maestro/src/engine/agent-loop/llm-iteration.ts
oss-sync d41ff0f658
Some checks failed
CI / build-and-test (push) Failing after 6m23s
sync: update from private repo (d3780b00)
2026-07-09 00:12:24 +00:00

149 lines
5.7 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 type { OpenAICompatClient, Message, ToolDef } from '../../llm/openai-compat.js';
import { consumeLlmStream, type ConsumedLLMResponse } from '../llm-stream.js';
import type { EventLogger } from '../../progress/event-log.js';
import { logger } from '../../logger.js';
import { TRANSITION_TOOL_NAME, COMPLETE_TOOL_NAME } from './terminal-control.js';
import type { AgentLoopCallbacks } from './types.js';
import type { MovementWatchdogs } from './watchdogs.js';
export interface LlmIterationResult extends ConsumedLLMResponse {
/** Stream wall-clock time from request send to last chunk (ms). */
llmDurationMs: number;
}
export interface RunLlmIterationArgs {
client: OpenAICompatClient;
messages: Message[];
tools: ToolDef[];
cancelSignal?: AbortSignal;
callbacks?: AgentLoopCallbacks;
/**
* Mutated in place: the regular (non flow-control) tool names used so far in
* this movement. The onToolUse hook appends newly-seen names, matching the
* original inline behaviour.
*/
toolsUsed: string[];
/** Movement watchdogs; markToolUse is fired for every tool the LLM invokes. */
watchdogs: MovementWatchdogs;
movementName: string;
iteration: number;
/** Movement-scoped EventLogger (movementEvents). */
events: EventLogger;
userId?: string;
}
/**
* Run one LLM call for the current iteration: emit the start/end trace events,
* stream the response through consumeLlmStream (wiring the public callbacks and
* filtering the hidden transition/complete control tools out of the UI-facing
* channels), fire onLLMCall, and log the usage / text-preview summary.
*
* Extracted verbatim from executeMovement (agent-loop.ts) to slim the loop
* body. Returns the consumed stream plus the call's wall-clock duration.
*/
export async function runLlmIteration(args: RunLlmIterationArgs): Promise<LlmIterationResult> {
const {
client,
messages,
tools,
cancelSignal,
callbacks,
toolsUsed,
watchdogs,
movementName,
iteration,
events,
userId,
} = args;
logger.info(`[agent-loop] movement=${movementName} sending LLM request (iteration=${iteration})`);
// provider.timeoutMinutes に連動デフォルト10分。チャンク間の無応答がこの時間を超えたら接続断とみなす
const idleTimeoutMs = client.timeoutMs > 0 ? client.timeoutMs : 10 * 60 * 1000;
const llmStartedAt = Date.now();
events.emit('llm_call_start', {
iteration,
messageCount: messages.length,
});
callbacks?.onLlmRequestStart?.({ movementName, iteration, messageCount: messages.length });
const consumed = await consumeLlmStream(
client,
messages,
tools,
cancelSignal,
idleTimeoutMs,
{
onText: callbacks?.onText,
onToolUse: (name, input, callId) => {
if (name !== TRANSITION_TOOL_NAME && name !== COMPLETE_TOOL_NAME) {
callbacks?.onToolUse?.(name, input, callId);
if (!toolsUsed.includes(name)) toolsUsed.push(name);
}
watchdogs.markToolUse(name);
},
onToolCallDelta: (_index, callId, name, chunk) => {
// Hidden control tools never stream to the UI.
if (name && (name === TRANSITION_TOOL_NAME || name === COMPLETE_TOOL_NAME)) {
return;
}
callbacks?.onToolCallDelta?.(callId, name, chunk);
},
onPromptProgress: (progress) => {
callbacks?.onPromptProgress?.(progress);
},
// Phase A: surface proxy backend identity to the worker. Only
// fires for proxy-mode clients that received x-litellm-model-id.
onBackend: (backendId, cacheKey) => {
callbacks?.onBackendResolved?.({ backendId, cacheKey });
},
onRetry: (info) => {
events.emit('llm_call_retry', {
iteration,
attempt: info.attempt,
maxAttempts: info.maxAttempts,
reason: info.reason.slice(0, 300),
errorClass: info.errorClass,
httpStatus: info.httpStatus,
delayMs: info.delayMs,
});
callbacks?.onLlmRetry?.(info);
},
onThinking: (totalChars) => {
callbacks?.onThinking?.({ chars: totalChars });
},
},
`movement=${movementName} `,
{ userId },
);
const llmDurationMs = Date.now() - llmStartedAt;
const { accumulatedText, pendingToolCalls, hadError, lastUsage } = consumed;
// 診断カラム分類・リトライ数・thinking 量・担当バックエンドを1呼び出し
// 1行の llm_call_end に集約する。TraceTab の LLM 呼び出しログの一次データ。
const callInfo = {
iteration,
durationMs: llmDurationMs,
promptTokens: lastUsage?.prompt_tokens,
completionTokens: lastUsage?.completion_tokens,
toolCalls: pendingToolCalls.length,
textChars: accumulatedText.length,
hadError,
errorClass: consumed.errorClass,
httpStatus: consumed.httpStatus,
retries: consumed.retries,
thinkingChars: consumed.thinkingChars,
backendId: consumed.backendId,
};
events.emit('llm_call_end', callInfo);
callbacks?.onLLMCall?.(callInfo);
logger.info(`[agent-loop] movement=${movementName} LLM stream ended (iteration=${iteration}, hadError=${hadError}${consumed.errorClass ? ` class=${consumed.errorClass}` : ''}, ${llmDurationMs}ms${lastUsage ? ` in=${lastUsage.prompt_tokens} out=${lastUsage.completion_tokens}` : ''})`);
// LLM 応答のサマリーログ
logger.info(`[agent-loop] movement=${movementName} response: text=${accumulatedText.length}chars toolCalls=${pendingToolCalls.length} tools=[${pendingToolCalls.map((t) => t.function.name).join(',')}]`);
if (accumulatedText.length > 0) {
logger.info(`[agent-loop] movement=${movementName} text preview: ${accumulatedText.substring(0, 300)}`);
callbacks?.onTextPreview?.(movementName, accumulatedText);
}
return { ...consumed, llmDurationMs };
}