139 lines
4.8 KiB
TypeScript
139 lines
4.8 KiB
TypeScript
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<MovementResult>;
|
||
|
||
/**
|
||
* 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 };
|
||
}
|