Build the initial system message with full context.
(
userPrompt: string,
mode: ToolExecutionMode['mode'],
modelId: string,
/** Sub-agent role name. Affects system prompt selection. */
role?: string,
/** Tools actually available to this run; if provided, drives the "Available tools:"
* line so a restricted sub-agent doesn't think it has tools the registry filters out. */
allowedTools?: string[],
)
| 365 | name: string, |
| 366 | result: { isError?: boolean }, |
| 367 | durationMs: number, |
| 368 | ): void { |
| 369 | this.insightsFor(sessionId).recordTool({ name, ok: !result.isError, durationMs }); |
| 370 | appendAudit({ type: 'tool', tool: name, ok: !result.isError, durationMs, sessionId }, this.cwd); |
| 371 | } |
| 372 | |
| 373 | /** Allow slash commands to toggle features at runtime without restart. */ |
| 374 | setSubagentMode(mode: 'off' | 'sequential' | 'parallel'): void { |
| 375 | (this.config as any).subagents = { ...(this.config as any).subagents, mode }; |
| 376 | } |
| 377 | setAutoSnapshot(enabled: boolean): void { |
| 378 | if (enabled && !this.snapshotService) { |
| 379 | this.snapshotService = new SnapshotService(this.cwd, 'runtime-enabled', { |
| 380 | retentionTurns: (this.config as any).safety?.snapshotRetentionTurns ?? 50, |
| 381 | }); |
| 382 | } |
| 383 | if (!enabled && this.snapshotService) { |
| 384 | // Don't destroy outstanding snapshots — user may still want to restore. |
| 385 | // Just stop taking NEW ones by nulling the service reference for new tool calls. |
| 386 | this.snapshotService = undefined; |
| 387 | } |
| 388 | (this.config as any).safety = { ...(this.config as any).safety, autoSnapshot: enabled }; |
| 389 | } |
| 390 | |
| 391 | /** |
| 392 | * Queue a mid-task steering note (from `/btw …`). Picked up at the top of the next |
| 393 | * run-loop iteration and injected into the conversation. Safe to call while a run |
| 394 | * is in flight; no-ops on an empty/whitespace note. |
| 395 | */ |
| 396 | pushSteer(note: string): void { |
| 397 | if (typeof note === 'string' && note.trim().length > 0) { |
| 398 | this.steerQueue.push(note.trim()); |
| 399 | } |
| 400 | } |
| 401 | |
| 402 | /** True when there are steering notes waiting to be injected. */ |
| 403 | hasPendingSteer(): boolean { |
| 404 | return this.steerQueue.length > 0; |
| 405 | } |
| 406 | |
| 407 | /** |
| 408 | * Run a sub-agent inline. Used by the `task` tool. |
| 409 | * |
| 410 | * Architecture: sub-agent gets a fresh AgentLoop with the same router/registry/perms |
| 411 | * but a new session id and an isolated message history. Tools run in 'subagent' mode |
| 412 | * (task and present_plan filtered). We drain the event stream and collect just the |
| 413 | * final text + tool-call count. |
| 414 | * |
| 415 | * The sub-agent's events ARE persisted to the session store under its sub-session id — |
| 416 | * so `qx sessions` will list it as a child, debuggable independently. We do NOT |
| 417 | * pipe sub-agent UI events to the parent's UI; the parent just sees the eventual |
| 418 | * tool result. |
| 419 | */ |
| 420 | async runSubagent( |
| 421 | prompt: string, |
| 422 | opts: { |
| 423 | maxIterations: number; |
| 424 | signal?: AbortSignal; |
no test coverage detected