Skip to content

Commit 377acb5

Browse files
grypezclaude
andcommitted
fix(kernel-agents): report invalid-argument tool calls as tool errors
The chat strategy catches a capability invocation failure and reports it as an `Error calling …` tool message, but it assumed the thrown value was an `Error` and read `.message` directly. An exo interface guard can reject with a non-`Error` value, so an invalid-argument tool call made the catch itself throw and crash the whole task instead of recovering. Extract the message defensively (`error instanceof Error ? error.message : String(error)`), matching the idiom already used elsewhere in the file, and add a regression test that an invalid-argument tool call comes back as an `Error calling …` tool message — and that the capability never runs with the bad arguments — instead of crashing the task. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1ab9888 commit 377acb5

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

packages/kernel-agents/src/strategies/chat-agent.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,48 @@ describe('makeChatAgent', () => {
155155
).toBe(true);
156156
});
157157

158+
it('injects a tool error for an invalid-argument tool call and continues', async () => {
159+
const add = vi.fn(async (a: number, b: number) => a + b);
160+
const addCap = makeMethodCapability(
161+
'Math',
162+
'add',
163+
add,
164+
S.method(
165+
'Add two numbers',
166+
[S.arg('a', S.number()), S.arg('b', S.number())],
167+
S.number(),
168+
),
169+
);
170+
171+
const recorded: ChatMessage[][] = [];
172+
let call = 0;
173+
const chat: BoundChat = async ({ messages }) => {
174+
recorded.push([...messages]);
175+
call += 1;
176+
if (call === 1) {
177+
// `b` is missing, so the exo's interface guard rejects the call.
178+
return makeToolCallResponse('0', [makeToolCall('c1', 'add', { a: 3 })]);
179+
}
180+
return makeTextResponse('recovered');
181+
};
182+
183+
const agent = makeChatAgent({ chat, capabilities: { add: addCap } });
184+
const result = await agent.task('add 3 and ?');
185+
186+
// The guard rejection surfaces as a tool error rather than crashing the
187+
// task, and the implementation never runs with the bad arguments.
188+
expect(result).toBe('recovered');
189+
expect(add).not.toHaveBeenCalled();
190+
const secondTurn = recorded[1] ?? [];
191+
expect(
192+
secondTurn.some(
193+
(message) =>
194+
message.role === 'tool' &&
195+
message.content.startsWith('Error calling add'),
196+
),
197+
).toBe(true);
198+
});
199+
158200
it('throws when invocation budget is exceeded', async () => {
159201
const ping = makeMethodCapability(
160202
'Server',

packages/kernel-agents/src/strategies/chat-agent.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,11 @@ export const makeChatAgent = ({
182182
// here and is reported as the tool error below.
183183
toolResult = await spec.func(args as never);
184184
} catch (error) {
185-
const errorContent = `Error calling ${name}: ${(error as Error).message}`;
185+
// The guard rejection (or any thrown value) becomes a tool error
186+
// rather than crashing the task; it may not be an `Error`.
187+
const message =
188+
error instanceof Error ? error.message : String(error);
189+
const errorContent = `Error calling ${name}: ${message}`;
186190
const toolMsg: ChatMessage = {
187191
role: 'tool',
188192
tool_call_id: toolCall.id,

0 commit comments

Comments
 (0)