google / google/adk-js

A throwing tool ends the invocation in adk-python and is recovered from in adk-js

Open
#857 3 comments 0 reactions 1 assignee Claimed by @Varun-S10 View on GitHub
bug help wanted
Dominant language
TypeScript
Stars
1.4k
Forks
205
Avg merge
3d 16h
Merged PRs (30d)
92

Description

A tool that throws ends the invocation in adk-python and is recovered from in adk-js. Both runtimes reach the same decision point through the same hook, and take opposite branches when no plugin handles the error.

`core/src/agents/functions.ts:558-585`:

```ts
try {
functionResponse = await callToolAsync(tool, functionArgs, toolContext);
} catch (e: unknown) {
const onToolErrorResponse = await invocationContext.pluginManager.runOnToolErrorCallback({...});
if (onToolErrorResponse != null) {
functionResponse = normalizeCallbackResponse(onToolErrorResponse);
} else {
functionResponseError = e.message; // continue with an error response
}
}
```

`adk-python`, `src/google/adk/flows/llm_flows/_tool_caller.py:776-790`:

```python
try:
function_response = await tool_runner()
except Exception as tool_error:
error_response = await _tool_error_handler.run_on_tool_error_callbacks(...)
if error_response is not None:
function_response = error_response
else:
raise tool_error # invocation ends here
```

### Reproduction

A scripted model, one tool that raises, no plugins. TypeScript:

```ts
const raises = new FunctionTool({
name: 'raises', description: 'Always fails.',
parameters: z.object({x: z.number()}),
execute: () => { throw new Error('tool blew up'); },
});
// model script: [ functionCall('raises', {x: 1}), 'done' ]
```

```
call:raises | response:raises(error) | text:done ← runAsync completes
```

Python, same script and tool through `InMemoryRunner.run_async`:

```
call:raises | ValueError: tool blew up ← run_async raises
```

### Where the two agree, and where they do not

Seven scripted-model scenarios, no plugins registered:

| scenario | adk-python | adk-js |
| --- | --- | --- |
| model calls an unregistered tool | error response, turn continues | error response, turn continues |
| unregistered tool beside a registered one | both answered, turn continues | both answered, turn continues |
| function call with an empty name | error response, turn continues | error response, turn continues |
| required argument missing | error response, turn continues | error response, turn continues |
| **tool raises** | **invocation raises** | error response, turn continues |
| **tool raises beside a succeeding tool** | **invocation raises** | error response, turn continues |
| **argument of the wrong type** | **invocation raises** (`TypeError` from the tool body) | error response, turn continues |

The last row differs for a second reason: `FunctionTool` validates arguments against the parameter schema in TypeScript, so a wrongly typed argument never reaches the function. In Python it does, and whatever the body raises propagates.

### Which one is intended

I could not tell from the code, so I have not sent a patch.

Python recovering from a tool it cannot find but not from a tool that fails reads as an inconsistency inside one runtime — `_tool_caller.py:707-711` builds a not-found response for the lookup failure a few lines above the `raise`. Against that, `raise tool_error` is explicit rather than incidental, and the callback is documented as an opportunity to "handle tool errors gracefully", which reads as: unhandled means not handled.

The difference matters either way. An agent that recovers gives the model the error and a chance to correct itself; an agent that raises surfaces the failure to the caller. Code written against one runtime does the wrong thing on the other, and a plugin registered only to keep an invocation alive is a no-op in TypeScript and load-bearing in Python.

Happy to send the patch once you say which side should move.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.