basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen4 * SPDX-License-Identifier: Apache-2.05 */6 7/**8 * @fileoverview TaskStop tool — lets the model stop a background task.9 */10 11import type { Config } from '../config/config.js';12import { ToolErrorType } from './tool-error.js';13import { ToolNames, ToolDisplayNames } from './tool-names.js';14import {15 BaseDeclarativeTool,16 BaseToolInvocation,17 Kind,18 type ToolInvocation,19 type ToolResult,20} from './tools.js';21 22export interface TaskStopParams {23 /** The ID of the background task to stop. */24 task_id: string;25}26 27class TaskStopInvocation extends BaseToolInvocation<28 TaskStopParams,29 ToolResult30> {31 constructor(32 private readonly config: Config,33 params: TaskStopParams,34 ) {35 super(params);36 }37 38 getDescription(): string {39 return `Stop background task ${this.params.task_id}`;40 }41 42 async execute(_signal: AbortSignal): Promise<ToolResult> {43 const taskId = this.params.task_id;44 45 // Subagent registry first (Phase A control plane). Agent IDs follow the46 // pattern `<subagentName>-<suffix>`, so they cannot collide with shell47 // IDs (which are `bg_<8 hex chars>` from the background shell pool).48 const agentRegistry = this.config.getBackgroundTaskRegistry();49 const agentEntry = agentRegistry.get(taskId);50 if (agentEntry) {51 if (agentEntry.status === 'paused') {52 const abandoned = this.config.abandonBackgroundAgent(taskId);53 if (!abandoned) {54 return {55 llmContent:56 `Error: Background agent "${taskId}" could not be cancelled ` +57 `from paused state.`,58 returnDisplay: 'Task could not be cancelled.',59 error: {60 message: `Task could not be cancelled: ${taskId}`,61 type: ToolErrorType.TASK_STOP_NOT_RUNNING,62 },63 };64 }65 66 const desc = agentEntry.description;67 return {68 llmContent:69 `Cancelled paused background agent "${taskId}".\n` +70 `Description: ${desc}`,71 returnDisplay: `Cancelled: ${desc}`,72 };73 }74 if (agentEntry.status !== 'running') {75 return notRunningError('agent', taskId, agentEntry.status);76 }77 agentRegistry.cancel(taskId);78 // The terminal task-notification is emitted by the agent's own handler79 // (via registry.complete/fail) rather than cancel(), so the parent80 // model still receives the agent's real partial/final result — not just81 // a bare "cancelled" message — once the reasoning loop unwinds.82 const desc = agentEntry.description;83 return {84 llmContent:85 `Cancellation requested for background agent "${taskId}". ` +86 `A final task-notification carrying the agent's last result will ` +87 `follow.\nDescription: ${desc}`,88 returnDisplay: `Cancelled: ${desc}`,89 };90 }91 92 // Background shell registry (Phase B). Settles asynchronously when the93 // child process exits in response to the AbortController; the registry94 // entry's terminal state (`cancelled`) and final exit code/output stay95 // observable via `/tasks` (text), the interactive Background tasks96 // dialog (focus the footer Background tasks pill, then Enter), and97 // the on-disk output file.98 const shellRegistry = this.config.getBackgroundShellRegistry();99 const shellEntry = shellRegistry.get(taskId);100 if (shellEntry) {101 if (shellEntry.status !== 'running') {102 return notRunningError('shell', taskId, shellEntry.status);103 }104 // requestCancel triggers the AbortController only — the registry's105 // settle path records the real terminal status + endTime once the106 // process actually drains. Calling cancel(id, Date.now()) here would107 // mark the entry terminal immediately and lose the real exit info.108 shellRegistry.requestCancel(taskId);109 return {110 llmContent:111 `Cancellation requested for background shell "${taskId}". ` +112 `Final status will be visible via /tasks (text) or the interactive Background tasks dialog (focus the footer Background tasks pill, then Enter) once the process drains; ` +113 `captured output remains at ${shellEntry.outputPath}.\n` +114 `Command: ${shellEntry.command}`,115 returnDisplay: `Cancelled shell: ${shellEntry.command}`,116 };117 }118 119 const monitorRegistry = this.config.getMonitorRegistry();120 const monitorEntry = monitorRegistry.get(taskId);121 if (monitorEntry) {122 if (monitorEntry.status !== 'running') {123 return notRunningError('monitor', taskId, monitorEntry.status);124 }125 monitorRegistry.cancel(taskId);126 return {127 llmContent:128 // Unlike background shells (which settle asynchronously when the129 // child process exits), `monitorRegistry.cancel()` settles the130 // entry synchronously — the cancelled state is observable right131 // now, no drain phrasing.132 `Monitor "${taskId}" cancelled. ` +133 `Status is visible via /tasks (text) or the interactive Background tasks dialog (focus the footer Background tasks pill, then Enter).\n` +134 `Command: ${monitorEntry.command}`,135 returnDisplay: `Cancelled monitor: ${monitorEntry.description}`,136 };137 }138 139 // MemoryManager memory tasks (dream + extract). Memory tasks live140 // outside the registry trio (MemoryManager owns its own task map).141 // Only `dream` is cancellable — extract is short-lived and runs on142 // the request loop, so cancelling it would interfere with the143 // user's own turn. Surface a distinct error for known-but-not-144 // cancellable records so the model doesn't conclude the id was145 // never valid (which would happen if we fell through to NOT_FOUND).146 const memoryManager = this.config.getMemoryManager();147 const memoryRecord = memoryManager.getTask(taskId);148 if (memoryRecord) {149 if (memoryRecord.taskType !== 'dream') {150 return {151 llmContent:152 `Error: Memory task "${taskId}" (${memoryRecord.taskType}) is ` +153 `not cancellable. Only dream consolidation tasks support ` +154 `cancellation; extract tasks run on the request loop and ` +155 `complete in milliseconds.`,156 returnDisplay: `Task not cancellable (${memoryRecord.taskType}).`,157 error: {158 message: `task is not cancellable: ${taskId} (${memoryRecord.taskType})`,159 type: ToolErrorType.TASK_STOP_NOT_CANCELLABLE,160 },161 };162 }163 if (memoryRecord.status !== 'running') {164 return notRunningError('dream', taskId, memoryRecord.status);165 }166 // cancelTask returns false if the AbortController is missing for167 // a running record (logic-level invariant violation; see168 // MemoryManager.cancelTask). Surface that explicitly so the model169 // sees the cancel didn't take and doesn't claim success.170 const cancelled = memoryManager.cancelTask(taskId);171 if (!cancelled) {172 // Distinct from TASK_STOP_NOT_RUNNING (the task IS running)173 // and TASK_STOP_NOT_CANCELLABLE (the kind supports cancel,174 // we just couldn't deliver it). INTERNAL_ERROR signals that175 // this is unexpected and worth filing — the abort controller176 // should have been registered alongside status='running' in177 // scheduleDream.178 return {179 llmContent:180 `Error: Dream task "${taskId}" could not be cancelled ` +181 `(internal state inconsistency — abort controller missing).`,182 returnDisplay: 'Dream cancellation failed (internal state).',183 error: {184 message: `dream cancel failed: ${taskId}`,185 type: ToolErrorType.TASK_STOP_INTERNAL_ERROR,186 },187 };188 }189 return {190 llmContent:191 `Cancellation requested for dream task "${taskId}". ` +192 `The fork agent is being aborted; the consolidation lock will ` +193 `be released as the agent unwinds. Status is visible via the ` +194 `interactive Background tasks dialog (focus the footer Background ` +195 `tasks pill, then Enter).`,196 returnDisplay: `Cancelled dream: ${taskId}`,197 };198 }199 200 return {201 llmContent: `Error: No background task found with ID "${taskId}".`,202 returnDisplay: 'Task not found.',203 error: {204 message: `Task not found: ${taskId}`,205 type: ToolErrorType.TASK_STOP_NOT_FOUND,206 },207 };208 }209}210 211function notRunningError(212 kind: 'agent' | 'shell' | 'monitor' | 'dream',213 taskId: string,214 status: string,215): ToolResult {216 return {217 llmContent: `Error: Background ${kind} "${taskId}" is not running (status: ${status}).`,218 returnDisplay: `Task not running (${status}).`,219 error: {220 message: `${kind} is ${status}: ${taskId}`,221 type: ToolErrorType.TASK_STOP_NOT_RUNNING,222 },223 };224}225 226export class TaskStopTool extends BaseDeclarativeTool<227 TaskStopParams,228 ToolResult229> {230 static readonly Name = ToolNames.TASK_STOP;231 232 constructor(private readonly config: Config) {233 super(234 TaskStopTool.Name,235 ToolDisplayNames.TASK_STOP,236 'Stop a background task by its ID. Running agents and shells are cancelled; paused recovered agents are abandoned without resuming them.',237 Kind.Other,238 {239 type: 'object',240 properties: {241 task_id: {242 type: 'string',243 description:244 'The ID of the background task to stop (from the launch response or notification).',245 },246 },247 required: ['task_id'],248 additionalProperties: false,249 },250 true, // isOutputMarkdown251 false, // canUpdateOutput252 true, // shouldDefer — stopping tasks is infrequent253 false, // alwaysLoad254 'task stop cancel kill background',255 );256 }257 258 protected createInvocation(259 params: TaskStopParams,260 ): ToolInvocation<TaskStopParams, ToolResult> {261 return new TaskStopInvocation(this.config, params);262 }263}264 