electric-sql / electric-sql/electric

Typing tool call execute parameters.

Open
#4,484 0 comments 0 reactions 0 assignees View on GitHub
feature request triage
Dominant language
TypeScript
Stars
10.4k
Forks
375
Avg merge
3d 1h
Merged PRs (30d)
18

Description

I have a tool definition in my example code that requires `params: unknown` and then casting to the real type:

```ts
import { Type, type Static } from '@sinclair/typebox'

const taskParameters = Type.Object({
task: Type.String({ description: `The task for the assistant.` }),
})
type TaskParams = Static

function createSpawnAssistantTool(ctx: HandlerContext) {
return {
name: `spawn_assistant`,
label: `Spawn Assistant`,
description: `Spawn an assistant sub-agent to perform a task.`,
parameters: taskParameters,
execute: async (_toolCallId: string, params: unknown) => {
const { task } = params as TaskParams
...
},
...
}
}
```

My initial reaction reading the code was "why can't the type of `params` be inferred from the `parameters: taskParameters`". I asked Claude and this was his verdict:

````
P1 — the tools array fixes the schema generic at its default. AgentConfig.tools is typed Array
(entity-stream-db-…d.ts:847), and AgentTool's execute is (id, params: Static, …). With TParameters left at its
default TSchema, Static is unknown. So every execute slot demands params: unknown, and strictFunctionTypes checks
function params contravariantly — only a supertype of unknown (i.e. unknown or any) is accepted. TaskParams is a subtype, so it's
rejected. That's your error verbatim.

P2 — the public AgentTool export isn't even generic. The rolled-up .d.ts has type AgentTool$1 = AgentTool (line 841) and
re-exports AgentTool$1 as AgentTool. That alias collapses the type params to their defaults, so importing AgentTool from
@electric-ax/agents-runtime and writing AgentTool fails with "Type 'AgentTool$1' is not generic." (I hit
this directly.) It's almost certainly a dts-bundler artifact (the $1 rename), not intentional — a consumer can't parameterize the
tool type at all through the public name.

Runtime-side fixes

Fix A — one token, unblocks typed execute with zero call-site changes. Widen the array element to any:
interface AgentConfig {
tools: Array> // was Array
}
Static is any, and any params are bivariant, so your original execute: async (_id, params: TaskParams) => … slots in with no
cast. Verified. This is also exactly what pi-agent-core already does for Agent.tools and AgentContext.tools (both
AgentTool[]), so it's consistency, not a hack. Trade-off: any won't flag a wrong param shape — but that's no worse than the
cast you have now.

Fix B — best DX + actual soundness: ship a defineTool helper. TypeScript won't infer a type parameter across the fields of a bare
object literal, so the only way to get parameters → execute(params) inference is a wrapper call:
export function defineTool

(def: AgentTool): AgentTool {
return def
}
Then params is inferred from parameters — no annotation, no cast. Consuming the result still needs Fix A (or making the config
generic over a tuple, []>). This is the Vercel AI SDK tool() pattern. Verified (params
inferred to { task: string }, proven narrowed not any).

Fix P2 regardless: re-export the generic properly, e.g. export type { AgentTool } from '@mariozechner/pi-agent-core', so the name
keeps its parameters for downstream code. Fix A alone resolves your error without this; P2 only bites consumers who want to name
AgentTool themselves.

Without touching the runtime

Write your own structural helper — it doesn't reference the runtime's (non-generic) AgentTool, so P2 doesn't block it, and the
single cast is hidden in one place instead of one per tool:

import type { AgentTool } from '@electric-ax/agents-runtime'
import { type Static, type TSchema } from '@sinclair/typebox'

function tool

(def: {
name: string
label: string
description: string
parameters: P
execute: (id: string, params: Static

, signal?: AbortSignal) => Promise<{
content: Array<{ type: `text`; text: string }>
details?: unknown
terminate?: boolean
}>
}): AgentTool {
return def as unknown as AgentTool
}
const spawnAssistant = tool({
parameters: taskParameters,
execute: async (_id, params) => { // params: { task: string }, inferred
const { task } = params
...
},
// name/label/description...
})
Verified against the real runtime types — it slots straight into AgentConfig['tools']. The downside is you hand-maintain that
structural shape to match the runtime's AgentToolResult.

Bottom line

- Smallest real library fix for the error you hit: Array → Array> in AgentConfig.
- Proper library fix: that plus a generic defineTool helper plus fixing the non-generic re-export (P2).
- For this example specifically, the inline params as TaskParams you've already got is the clearest thing for a reader — the helper only pays off across many tools. Worth filing P1/P2 against the runtime though, since P2 in particular looks like an unintended packaging regression.
````

Does this make sense? Should we:

- [ ] update `Array` to `Array>` in AgentConfig?
- [ ] define a generic `defineTool` helper
- [ ] fix the non-generic re-export

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.