maestro/src/engine/agent-loop/delegate-runner.ts
oss-sync 63d34d7cf6
All checks were successful
CI / build-and-test (push) Successful in 6m22s
sync: update from private repo (061c701c)
2026-07-09 03:01:06 +00:00

139 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 };
}