mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
let tool failures be observed by the model instead of killing the run
streamAgent executed tools with no try/catch around the call. A throw
from execTool or from a subflow agent streamed up through streamAgent,
out of trigger's inner catch (which rethrows non-abort errors), and
into the new top-level catch that the previous commit added. That
surfaces the failure — but it ends the run. One misbehaving tool took
down the whole conversation.
Wrap the tool-execution block in a try/catch. On abort, rethrow so the
existing AbortError path still fires. On any other error, convert the
exception into a tool-result payload ({ success: false, error, toolName })
and keep going. The model then sees a tool-result message saying the
tool failed with a specific message and can apologize, retry with
different arguments, pick a different tool, or explain to the user —
the normal recovery moves it already knows how to make.
No change to happy-path tool execution, no change to abort handling,
no change to subflow agent semantics (subflows that themselves error
are treated identically to regular tool errors at the call site).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c81d3cb27b
commit
15567cd1dd
1 changed files with 32 additions and 19 deletions
|
|
@ -955,27 +955,40 @@ export async function* streamAgent({
|
||||||
subflow: [],
|
subflow: [],
|
||||||
});
|
});
|
||||||
let result: unknown = null;
|
let result: unknown = null;
|
||||||
if (agent.tools![toolCall.toolName].type === "agent") {
|
try {
|
||||||
const subflowState = state.subflowStates[toolCallId];
|
if (agent.tools![toolCall.toolName].type === "agent") {
|
||||||
for await (const event of streamAgent({
|
const subflowState = state.subflowStates[toolCallId];
|
||||||
state: subflowState,
|
for await (const event of streamAgent({
|
||||||
idGenerator,
|
state: subflowState,
|
||||||
runId,
|
idGenerator,
|
||||||
messageQueue,
|
runId,
|
||||||
modelConfigRepo,
|
messageQueue,
|
||||||
signal,
|
modelConfigRepo,
|
||||||
abortRegistry,
|
signal,
|
||||||
})) {
|
abortRegistry,
|
||||||
yield* processEvent({
|
})) {
|
||||||
...event,
|
yield* processEvent({
|
||||||
subflow: [toolCallId, ...event.subflow],
|
...event,
|
||||||
});
|
subflow: [toolCallId, ...event.subflow],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!subflowState.getPendingAskHumans().length && !subflowState.getPendingPermissions().length) {
|
||||||
|
result = subflowState.finalResponse();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = await execTool(agent.tools![toolCall.toolName], toolCall.arguments, { runId, signal, abortRegistry });
|
||||||
}
|
}
|
||||||
if (!subflowState.getPendingAskHumans().length && !subflowState.getPendingPermissions().length) {
|
} catch (error) {
|
||||||
result = subflowState.finalResponse();
|
if ((error instanceof Error && error.name === "AbortError") || signal.aborted) {
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
} else {
|
const message = error instanceof Error ? (error.message || error.name) : String(error);
|
||||||
result = await execTool(agent.tools![toolCall.toolName], toolCall.arguments, { runId, signal, abortRegistry });
|
_logger.log('tool failed', message);
|
||||||
|
result = {
|
||||||
|
success: false,
|
||||||
|
error: message,
|
||||||
|
toolName: toolCall.toolName,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const resultPayload = result === undefined ? null : result;
|
const resultPayload = result === undefined ? null : result;
|
||||||
const resultMsg: z.infer<typeof ToolMessage> = {
|
const resultMsg: z.infer<typeof ToolMessage> = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue