feat(agent): add terminating tool result hints closes #3525
This commit is contained in:
@@ -199,11 +199,13 @@ async function runLoop(
|
||||
|
||||
// Check for tool calls
|
||||
const toolCalls = message.content.filter((c) => c.type === "toolCall");
|
||||
hasMoreToolCalls = toolCalls.length > 0;
|
||||
|
||||
const toolResults: ToolResultMessage[] = [];
|
||||
if (hasMoreToolCalls) {
|
||||
toolResults.push(...(await executeToolCalls(currentContext, message, config, signal, emit)));
|
||||
hasMoreToolCalls = false;
|
||||
if (toolCalls.length > 0) {
|
||||
const executedToolBatch = await executeToolCalls(currentContext, message, config, signal, emit);
|
||||
toolResults.push(...executedToolBatch.messages);
|
||||
hasMoreToolCalls = !executedToolBatch.terminate;
|
||||
|
||||
for (const result of toolResults) {
|
||||
currentContext.messages.push(result);
|
||||
@@ -339,7 +341,7 @@ async function executeToolCalls(
|
||||
config: AgentLoopConfig,
|
||||
signal: AbortSignal | undefined,
|
||||
emit: AgentEventSink,
|
||||
): Promise<ToolResultMessage[]> {
|
||||
): Promise<ExecutedToolCallBatch> {
|
||||
const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
|
||||
const hasSequentialToolCall = toolCalls.some(
|
||||
(tc) => currentContext.tools?.find((t) => t.name === tc.name)?.executionMode === "sequential",
|
||||
@@ -350,6 +352,11 @@ async function executeToolCalls(
|
||||
return executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit);
|
||||
}
|
||||
|
||||
type ExecutedToolCallBatch = {
|
||||
messages: ToolResultMessage[];
|
||||
terminate: boolean;
|
||||
};
|
||||
|
||||
async function executeToolCallsSequential(
|
||||
currentContext: AgentContext,
|
||||
assistantMessage: AssistantMessage,
|
||||
@@ -357,8 +364,9 @@ async function executeToolCallsSequential(
|
||||
config: AgentLoopConfig,
|
||||
signal: AbortSignal | undefined,
|
||||
emit: AgentEventSink,
|
||||
): Promise<ToolResultMessage[]> {
|
||||
const results: ToolResultMessage[] = [];
|
||||
): Promise<ExecutedToolCallBatch> {
|
||||
const finalizedCalls: FinalizedToolCallOutcome[] = [];
|
||||
const messages: ToolResultMessage[] = [];
|
||||
|
||||
for (const toolCall of toolCalls) {
|
||||
await emit({
|
||||
@@ -391,10 +399,14 @@ async function executeToolCallsSequential(
|
||||
await emitToolExecutionEnd(finalized, emit);
|
||||
const toolResultMessage = createToolResultMessage(finalized);
|
||||
await emitToolResultMessage(toolResultMessage, emit);
|
||||
results.push(toolResultMessage);
|
||||
finalizedCalls.push(finalized);
|
||||
messages.push(toolResultMessage);
|
||||
}
|
||||
|
||||
return results;
|
||||
return {
|
||||
messages,
|
||||
terminate: shouldTerminateToolBatch(finalizedCalls),
|
||||
};
|
||||
}
|
||||
|
||||
async function executeToolCallsParallel(
|
||||
@@ -404,7 +416,7 @@ async function executeToolCallsParallel(
|
||||
config: AgentLoopConfig,
|
||||
signal: AbortSignal | undefined,
|
||||
emit: AgentEventSink,
|
||||
): Promise<ToolResultMessage[]> {
|
||||
): Promise<ExecutedToolCallBatch> {
|
||||
const finalizedCalls: FinalizedToolCallEntry[] = [];
|
||||
|
||||
for (const toolCall of toolCalls) {
|
||||
@@ -445,14 +457,17 @@ async function executeToolCallsParallel(
|
||||
const orderedFinalizedCalls = await Promise.all(
|
||||
finalizedCalls.map((entry) => (typeof entry === "function" ? entry() : Promise.resolve(entry))),
|
||||
);
|
||||
const results: ToolResultMessage[] = [];
|
||||
const messages: ToolResultMessage[] = [];
|
||||
for (const finalized of orderedFinalizedCalls) {
|
||||
const toolResultMessage = createToolResultMessage(finalized);
|
||||
await emitToolResultMessage(toolResultMessage, emit);
|
||||
results.push(toolResultMessage);
|
||||
messages.push(toolResultMessage);
|
||||
}
|
||||
|
||||
return results;
|
||||
return {
|
||||
messages,
|
||||
terminate: shouldTerminateToolBatch(orderedFinalizedCalls),
|
||||
};
|
||||
}
|
||||
|
||||
type PreparedToolCall = {
|
||||
@@ -481,6 +496,10 @@ type FinalizedToolCallOutcome = {
|
||||
|
||||
type FinalizedToolCallEntry = FinalizedToolCallOutcome | (() => Promise<FinalizedToolCallOutcome>);
|
||||
|
||||
function shouldTerminateToolBatch(finalizedCalls: FinalizedToolCallOutcome[]): boolean {
|
||||
return finalizedCalls.length > 0 && finalizedCalls.every((finalized) => finalized.result.terminate === true);
|
||||
}
|
||||
|
||||
function prepareToolCallArguments(tool: AgentTool<any>, toolCall: AgentToolCall): AgentToolCall {
|
||||
if (!tool.prepareArguments) {
|
||||
return toolCall;
|
||||
@@ -612,6 +631,7 @@ async function finalizeExecutedToolCall(
|
||||
result = {
|
||||
content: afterResult.content ?? result.content,
|
||||
details: afterResult.details ?? result.details,
|
||||
terminate: afterResult.terminate ?? result.terminate,
|
||||
};
|
||||
isError = afterResult.isError ?? isError;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface BeforeToolCallResult {
|
||||
* - `content`: if provided, replaces the tool result content array in full
|
||||
* - `details`: if provided, replaces the tool result details value in full
|
||||
* - `isError`: if provided, replaces the tool result error flag
|
||||
* - `terminate`: if provided, replaces the early-termination hint
|
||||
*
|
||||
* Omitted fields keep the original executed tool result values.
|
||||
* There is no deep merge for `content` or `details`.
|
||||
@@ -64,6 +65,11 @@ export interface AfterToolCallResult {
|
||||
content?: (TextContent | ImageContent)[];
|
||||
details?: unknown;
|
||||
isError?: boolean;
|
||||
/**
|
||||
* Hint that the agent should stop after the current tool batch.
|
||||
* Early termination only happens when every finalized tool result in the batch sets this to true.
|
||||
*/
|
||||
terminate?: boolean;
|
||||
}
|
||||
|
||||
/** Context passed to `beforeToolCall`. */
|
||||
@@ -209,6 +215,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
||||
* - `content` replaces the full content array
|
||||
* - `details` replaces the full details payload
|
||||
* - `isError` replaces the error flag
|
||||
* - `terminate` replaces the early-termination hint
|
||||
*
|
||||
* Any omitted fields keep their original values. No deep merge is performed.
|
||||
* The hook receives the agent abort signal and is responsible for honoring it.
|
||||
@@ -286,6 +293,11 @@ export interface AgentToolResult<T> {
|
||||
content: (TextContent | ImageContent)[];
|
||||
/** Arbitrary structured details for logs or UI rendering. */
|
||||
details: T;
|
||||
/**
|
||||
* Hint that the agent should stop after the current tool batch.
|
||||
* Early termination only happens when every finalized tool result in the batch sets this to true.
|
||||
*/
|
||||
terminate?: boolean;
|
||||
}
|
||||
|
||||
/** Callback used by tools to stream partial execution updates. */
|
||||
|
||||
Reference in New Issue
Block a user