basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2026 Qwen4 * SPDX-License-Identifier: Apache-2.05 */6 7import type { ToolInvocation, ToolResult } from './tools.js';8import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';9import { ToolDisplayNames, ToolNames } from './tool-names.js';10import type { Config } from '../config/config.js';11import type { PermissionDecision } from '../permissions/types.js';12import {13 WAKEUP_MAX_SECONDS,14 WAKEUP_MIN_SECONDS,15 clampWakeupSeconds,16} from '../services/cronScheduler.js';17import { getErrorMessage } from '../utils/errors.js';18 19export interface LoopWakeupParams {20 delaySeconds: number;21 prompt: string;22 reason?: string;23}24 25function formatRequested(delaySeconds: number): string {26 return Number.isFinite(delaySeconds) ? `${delaySeconds}s` : `${delaySeconds}`;27}28 29class LoopWakeupInvocation extends BaseToolInvocation<30 LoopWakeupParams,31 ToolResult32> {33 constructor(34 private readonly config: Config,35 params: LoopWakeupParams,36 ) {37 super(params);38 }39 40 getDescription(): string {41 const clamped = clampWakeupSeconds(this.params.delaySeconds);42 const roundedDelaySeconds = Number.isFinite(this.params.delaySeconds)43 ? Math.round(this.params.delaySeconds)44 : this.params.delaySeconds;45 const prefix =46 clamped === roundedDelaySeconds47 ? `${clamped}s`48 : `${clamped}s (requested ${formatRequested(this.params.delaySeconds)})`;49 return `${prefix}: ${this.params.prompt}`;50 }51 52 /**53 * Scheduling future model input is side-effectful: the continuation runs54 * against the agent with full tool access at fire time. Returning 'ask'55 * (never 'allow') keeps it out of AUTO mode's L4 short-circuit so the56 * classifier still vets it — same reasoning as CronCreate.57 */58 override async getDefaultPermission(): Promise<PermissionDecision> {59 return 'ask';60 }61 62 async execute(): Promise<ToolResult> {63 const prompt = this.params.prompt.trim();64 if (!prompt) {65 const message = 'Loop wakeup prompt must not be empty.';66 return {67 llmContent: message,68 returnDisplay: message,69 error: { message },70 };71 }72 73 try {74 const scheduler = this.config.getCronScheduler();75 if (scheduler.disabled) {76 const message =77 'Loop wakeups are disabled for the rest of this session ' +78 '(token limit reached). Restart the session to re-enable.';79 return {80 llmContent: message,81 returnDisplay: message,82 error: { message },83 };84 }85 const { id, scheduledFor, clampedDelaySeconds, wasClamped, replacedId } =86 scheduler.scheduleWakeup(this.params.delaySeconds, prompt);87 const reason = this.params.reason?.trim();88 89 const llmContent = [90 `Scheduled loop wakeup ${id}.`,91 replacedId ? `Replaced pending wakeup ${replacedId}.` : null,92 `Scheduled for: ${scheduledFor} (in ${clampedDelaySeconds}s).`,93 wasClamped94 ? `Requested ${formatRequested(this.params.delaySeconds)} was clamped to the [${WAKEUP_MIN_SECONDS}, ${WAKEUP_MAX_SECONDS}] s range.`95 : null,96 reason ? `Reason: ${reason}.` : null,97 'Session-only one-shot; not persisted. Call LoopWakeup again before ' +98 'ending the turn to keep the loop alive; omit it to end the loop.',99 ]100 .filter(Boolean)101 .join('\n');102 const returnDisplay = `Loop wakeup ${id} scheduled for ${scheduledFor}${103 reason ? ` — ${reason}` : ''104 }`;105 106 return { llmContent, returnDisplay };107 } catch (error) {108 const message = getErrorMessage(error);109 return {110 llmContent: `Error scheduling loop wakeup: ${message}`,111 returnDisplay: message,112 error: { message },113 };114 }115 }116}117 118export class LoopWakeupTool extends BaseDeclarativeTool<119 LoopWakeupParams,120 ToolResult121> {122 static readonly Name = ToolNames.LOOP_WAKEUP;123 124 constructor(private readonly config: Config) {125 super(126 LoopWakeupTool.Name,127 ToolDisplayNames.LOOP_WAKEUP,128 'Schedule when to resume work in a self-paced loop iteration (always pass the `prompt` arg). Call this before ending the turn to keep the loop alive; omit the call to end the loop. Session-only and one-shot — it does not persist or recur. A self-paced wakeup chain may run for at most 24h. When a background task you started will wake you on its own — a backgrounded agent or a Monitor sends a terminal `<task-notification>` on exit, failure, cancellation, or monitor auto-stop — keep this wakeup as a long fallback heartbeat rather than a poll; see `delaySeconds`.',129 Kind.Other,130 {131 type: 'object',132 properties: {133 delaySeconds: {134 type: 'number',135 description: `Seconds from now to wake up. Clamped to [${WAKEUP_MIN_SECONDS}, ${WAKEUP_MAX_SECONDS}]. Use 60-270s only when actively polling external state that nothing else reports (a CI run, a remote queue) — staying inside the ~5-min prompt-cache window. When a background task you started will wake you via a \`<task-notification>\` once it finishes, that is the real wake signal — use 1200-1800s here as a fallback for when it never arrives (the task hangs, a Monitor auto-stops on idle or max-events, or another agent owns it). With no specific signal to watch, default to 1200s+.`,136 },137 prompt: {138 type: 'string',139 maxLength: 10000,140 description:141 'Continuation prompt to enqueue when the wakeup fires. Prefix with `/loop` so the next firing re-invokes the loop skill, e.g. `/loop check the deploy`.',142 },143 reason: {144 type: 'string',145 description:146 'One short sentence explaining the chosen delay. Shown to the user. Be specific.',147 },148 },149 required: ['delaySeconds', 'prompt'],150 additionalProperties: false,151 },152 true, // isOutputMarkdown153 false, // canUpdateOutput154 true, // shouldDefer — scheduling is infrequent155 false, // alwaysLoad156 'loop wakeup continuation follow-up self-pace',157 );158 }159 160 protected createInvocation(161 params: LoopWakeupParams,162 ): ToolInvocation<LoopWakeupParams, ToolResult> {163 return new LoopWakeupInvocation(this.config, params);164 }165 166 /**167 * Forward the continuation prompt and cadence to the AUTO classifier —168 * it is enqueued and executed against the agent at fire time, so it169 * needs the same scrutiny as a direct command (mirrors CronCreate).170 */171 override toAutoClassifierInput(172 params: LoopWakeupParams,173 ): Record<string, unknown> {174 return {175 delaySeconds: clampWakeupSeconds(params.delaySeconds),176 prompt: params.prompt,177 reason: params.reason ?? '',178 };179 }180}181 