Skip to content

feat: native tool calling, end to end - #17

Merged
ardada2468 merged 2 commits into
mainfrom
feat/native-tool-calling
Aug 22, 2026
Merged

ardada2468 merged 2 commits into
mainfrom
feat/native-tool-calling

Conversation

@ardada2468

Copy link
Copy Markdown
Owner

RespAct implemented ReAct purely by text prompting — regex-extracting Action: / Action Input: from raw completions and appending Observation: strings to a growing conversation string. Every tool took exactly one string argument, there were no parallel tool calls, and the loop was at the mercy of the model formatting its output correctly.

Meanwhile ToolCall and ChatMessage.toolCalls were declared and publicly exported from @ts-dspy/core with zero readers anywhere in the repo, and all three providers reported supportsFunctionCalling: true while implementing nothing. This makes the flag honest.

Provider layer

  • LLMCallOptions gains tools and toolChoice. ILanguageModel gains chatWithTools, which returns text, tool calls, and a normalised finish reason from one turn. BaseLM supplies a text-only default, so the capability flag — not feature detection — is what callers branch on.
  • Per-SDK translation: OpenAI tools: [{type:'function', function:{…}}] → message.tool_calls; Anthropic tools: [{name, description, input_schema}] → ToolUseBlock; Gemini config.tools: [{functionDeclarations}] → Part.functionCall → Part.functionResponse.
  • Fixes the silent role collapse in all three converters. tool and function turns were downgraded to user text — toOpenAIMessages even carried a comment saying the core ChatMessage had no tool_call_id. Anthropic could then merge a tool result into the preceding user turn, destroying the correlation the API needs.
  • Two Anthropic-specific traps closed: textOf() filters to type: 'text' blocks, so tool_use blocks were silently discarded; and chatStream only forwarded text_delta, ignoring input_json_delta.

Module layer

  • RespAct takes the native path when the model supports it and tools are declared, keeping the text-parsing loop as the fallback for local models and providers without native tool calling. Both paths run the same tools, honour the same repeat guard, and emit the same RespActEvents.
  • Tools may declare a JSON Schema or Zod schema for their arguments and receive a validated object instead of one string. Bare functions and { description, function } keep working unchanged.
  • Parallel tool calls in one turn are executed and reported individually.
  • forceTextMode: true pins a tool-capable model to the text loop.

Breaking change

ToolCall is reshaped. It was a copy of OpenAI's encoding — a required id, a type: 'function' literal, and a nested function.arguments JSON string — which no other provider can populate faithfully (Anthropic sends input already parsed, Gemini sends args already parsed and has no id at all). It is now:

interface ToolCall {
    id?: string;                        // absent on Gemini
    name: string;
    arguments: Record<string, unknown>; // always parsed
    rawArguments?: string;              // OpenAI's original bytes
}

The dead ChatMessage.functionCall field is removed; ChatMessage gains toolCallId.

Verification

npm run build, lint, typecheck, format:check, test (218 passing, up from 164), and verify:packaging all green. A throwaway consumer script drove RespAct with a multi-argument tool through the native path against the built dist/, then flipped supportsFunctionCalling: false on the same fake and confirmed the text fallback completed the same task identically.

packages/core/src/modules/respact.test.ts stays green unmodified — the text path is unchanged behaviour.

Docs: section 06 of site/docs.html documents native vs. text-mode tool calling and typed arguments (and fixes a stale fn: key that should always have been function:); the README "Tools" section gains typed arguments and the two-path explanation.

🤖 Generated with Claude Code

ardada2468 and others added 2 commits August 22, 2026 13:35
All three providers reported `supportsFunctionCalling: true` while
implementing nothing, and `RespAct` ran ReAct purely by text prompting.
This makes the flag honest and gives the loop a native path.

Provider layer:
- `LLMCallOptions` gains `tools` and `toolChoice`; `ILanguageModel` gains
  `chatWithTools`, returning text, tool calls, and a normalised finish
  reason from one turn. `BaseLM` supplies a text-only default.
- Each provider translates declarations into its own request shape and
  reads the calls back: OpenAI `tools`/`tool_calls`, Anthropic
  `input_schema`/`tool_use`, Gemini `functionDeclarations`/`functionCall`.
- Fixes the silent role collapse in all three converters: `tool` and
  `function` turns were downgraded to `user` text, and Anthropic could
  then merge a tool result into the preceding user turn.
- Anthropic also dropped `tool_use` blocks on the floor and ignored
  `input_json_delta` while streaming; both are now surfaced.

Module layer:
- `RespAct` uses the native path when the model supports it and tools are
  declared, keeping the text-parsing loop as the fallback. Both paths run
  the same tools and emit the same events.
- Tools may declare a JSON Schema or Zod schema for their arguments and
  receive a validated object; bare functions keep working unchanged.
- Parallel tool calls in one turn are executed and reported individually.
- `forceTextMode` pins a tool-capable model to the text loop.

BREAKING: `ToolCall` is reshaped for cross-provider use — it was a copy
of OpenAI's encoding that no other provider could populate faithfully.
It is now `{ id?, name, arguments, rawArguments? }` with `arguments`
always a parsed object. The dead `ChatMessage.functionCall` is removed;
`ChatMessage` gains `toolCallId`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	packages/anthropic/src/anthropic-lm.test.ts
#	packages/anthropic/src/anthropic-lm.ts
#	packages/core/src/modules/respact.ts
#	packages/core/src/types/language-model.ts
#	packages/gemini/src/gemini-lm.ts
#	packages/openai/src/openai-lm.ts
@ardada2468
ardada2468 merged commit 7a51069 into main Aug 22, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant