import { randomUUID } from 'node:crypto'; import type { OpenAICompatClient } from '../../llm/openai-compat.js'; import type { SafetyConfig } from '../../config.js'; import { summarizeToolInput } from '../../progress/log-format.js'; import { ContextManager } from '../context-manager.js'; import { Conversation } from '../context/conversation.js'; import { ToolResultCache } from '../context/tool-result-cache.js'; import type { ToolContext } from '../tools/index.js'; import type { AgentLoopCallbacks, ExecuteMovementOptions, Movement, MovementResult, } from './types.js'; type ExecuteSubMovement = ( movement: Movement, taskInstruction: string, client: OpenAICompatClient, ctx: ToolContext, options: ExecuteMovementOptions, ) => Promise; /** * Run an isolated sub-agent for the `delegate` tool. * * Each sub-agent gets its own Conversation, ContextManager, ToolResultCache, * and mcpQuotaState — none of those are shared with the parent. The parent's * conversation.messages are never touched by the sub-run (isolation invariant). */ export async function runDelegateSubAgent( params: { description: string; prompt: string }, opts: { client: OpenAICompatClient; parentCtx: ToolContext; parentMovement: Movement; depth: number; maxDelegationDepth: number; maxIterations?: number; safetyConfig?: SafetyConfig; cancelSignal?: AbortSignal; cancelCheck?: () => boolean; contextLimit: number; parentCallbacks?: AgentLoopCallbacks; executeSubMovement: ExecuteSubMovement; }, ): Promise<{ result: string; status: 'success' | 'aborted' | 'needs_user_input' }> { const { client, parentCtx, parentMovement, depth, maxDelegationDepth, maxIterations, safetyConfig, cancelSignal, cancelCheck, contextLimit, parentCallbacks, executeSubMovement, } = opts; const atLeafDepth = depth >= maxDelegationDepth; const subAllowedTools = atLeafDepth ? parentMovement.allowedTools.filter((t) => t !== 'delegate') : parentMovement.allowedTools; const subMovement: Movement = { ...parentMovement, name: `delegate:${params.description.slice(0, 40)}`, instruction: params.prompt, rules: [], allowedTools: subAllowedTools, defaultNext: undefined, }; const parentRunId = parentCtx.delegateRunId ?? null; const delegateRunId = randomUUID(); const delegateCallbacks: AgentLoopCallbacks | undefined = parentCallbacks && { onText: (text) => parentCallbacks.onDelegateText?.(delegateRunId, text), onToolUse: (name, input) => parentCallbacks.onDelegateTool?.(delegateRunId, name, summarizeToolInput(name, input)), onDelegateLifecycle: parentCallbacks.onDelegateLifecycle, onDelegateText: parentCallbacks.onDelegateText, onDelegateTool: parentCallbacks.onDelegateTool, }; const subConversation = new Conversation(undefined); const subContextManager = new ContextManager({ limitTokens: contextLimit }); const subCache = new ToolResultCache(); const subEvents = parentCtx.eventLogger?.child({ movement: `delegate:${params.description.slice(0, 40)}`, // delegateRunId: 実行帰属タグ。tool_call が correlationId を上書きしても // 保持されるので、reconstructDelegateRuns がツールイベントを確実に束ねられる。 // correlationId: 後方互換(correlationId 上書きしないイベントの既定値)。 correlationId: delegateRunId, delegateRunId, }); const subCtx: ToolContext = { ...parentCtx, delegationDepth: depth, delegateRunId, runDelegate: undefined, contextManager: subContextManager, mcpQuotaState: { files: 0, bytes: 0 }, eventLogger: subEvents ?? parentCtx.eventLogger, }; parentCtx.eventLogger?.emit('delegate_start', { delegateRunId, parentRunId, description: params.description, depth, }); parentCallbacks?.onDelegateLifecycle?.({ delegateRunId, parentRunId, depth, description: params.description, status: 'running', }); const res = await executeSubMovement(subMovement, params.prompt, client, subCtx, { maxIterations, safetyConfig, contextManager: subContextManager, cancelSignal, cancelCheck, toolResultCache: subCache, conversation: subConversation, visitCount: 1, maxVisits: 1, callbacks: delegateCallbacks, }); const status: 'success' | 'aborted' | 'needs_user_input' = res.next === 'COMPLETE' ? 'success' : res.next === 'ASK' ? 'needs_user_input' : 'aborted'; const resultPreview = (res.output ?? '').slice(0, 2000); parentCtx.eventLogger?.emit('delegate_complete', { delegateRunId, parentRunId, description: params.description, depth, next: res.next, status, resultPreview, }); parentCallbacks?.onDelegateLifecycle?.({ delegateRunId, parentRunId, depth, description: params.description, status, }); return { result: res.output ?? '', status }; }