Skip to content

feat(ai): execute_tool dispatcher — drop 29 non-core tool schemas from model context - #1310

Merged
2witstudios merged 6 commits into
masterfrom
pu/execute-tool-dispatch
May 12, 2026
Merged

2witstudios merged 6 commits into
masterfrom
pu/execute-tool-dispatch

Conversation

@2witstudios

@2witstudios 2witstudios commented May 12, 2026 •

Copy link
Copy Markdown
Owner

Summary

Removes all 34 non-core tool names, descriptions, and schemas from the model's visible tool list. Reduces model context from ~37 tools to 10 (~5–12k token savings per request). The model gains a complete awareness map of what exists via a compact grouped tool name section in the system prompt (~120 tokens).

Architecture

Before (stub approach): model saw name + description + `{}` schema for all 37 tools
After: model sees only ~10 tools directly:

Slot What How accessed
8 core tools Full schemas Called directly
`tool_search` Full schema Discover any tool's schema
`execute_tool` Full schema Dispatch to any non-core tool

Non-core tool names appear as a compact grouped list in the system prompt:
```
NON-CORE TOOLS (use execute_tool to call; use tool_search("select:tool_name") for parameter schemas):
drive: create_drive, rename_drive, update_drive_context
pages: list_trash, list_conversations, read_conversation, rename_page, trash, restore, move_page, edit_sheet_cells
search: glob_search, web_search
tasks: update_task, get_assigned_tasks
agents: update_agent_config, list_agents, multi_drive_list_agents, ask_agent
activity: get_activity
calendar: list_calendar_events, get_calendar_event, check_calendar_availability, create_calendar_event, update_calendar_event, delete_calendar_event, rsvp_calendar_event, invite_calendar_attendees, remove_calendar_attendee
channels: send_channel_message
```

Changes

  • `execute-tool.ts` (new) — `createExecuteTool(allowedTools)` factory: validates args against real Zod schema, guards missing execute, forwards options through
  • `stub-tools.ts` — stripped to `CORE_TOOL_NAMES` Set only (stub functions removed)
  • `ai-tools.ts` — removes `pageSpaceToolsStubbed`, adds `corePageSpaceTools`
  • `system-prompt.ts` — adds `buildNonCoreToolNamesPrompt` with `CATEGORY_MAP`, updates `TOOL_DISCOVERY_PROMPT`
  • `route.ts` — builds core/non-core split, injects names prompt conditionally, wires `execute_tool`; passes `finalSystemPrompt` to `calculateTotalContextSize` for accurate token accounting

Tests

  • `execute-tool.test.ts` (new) — 6 tests: unknown tool, no-execute guard, invalid params, valid dispatch, empty registry
  • `stream-socket-events.test.ts` — 33/33 pass
  • `system-prompt.test.ts` — 23/23 pass (incl. 4 new `buildNonCoreToolNamesPrompt` tests)
  • `stub-tools.test.ts` — updated (removed obsolete `buildStubbedTools` tests)
  • `ai-tools.test.ts` — updated (`pageSpaceToolsStubbed` → `corePageSpaceTools` tests)

🤖 Generated with Claude Code

Drops non-core tool names+descriptions from the model context by removing
all 29 non-core tools from the tool list. Core tools (list/read/search/create)
are sent with full schemas as before. The model uses execute_tool to call
any non-core tool after discovering its schema via tool_search, shrinking
the visible tool list from ~37 down to ~10.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 12, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Split Global Assistant tools into core vs non-core: core tools are exposed directly to the model; non-core tools are discovered via tool_search and invoked through a new execute_tool dispatcher. Changes touch prompt text, tool catalog filtering, a dispatcher factory, route wiring, and tests/mocks.

Changes

Tool Dispatch Model Refactor

Layer / File(s) Summary
Core tool set and filtering
apps/web/src/lib/ai/core/ai-tools.ts, apps/web/src/lib/ai/core/stub-tools.ts
Add corePageSpaceTools computed by filtering pageSpaceTools against CORE_TOOL_NAMES. stub-tools now only exports CORE_TOOL_NAMES; previous stub-wrapping and buildStubbedTools were removed.
System prompt & non-core helper
apps/web/src/lib/ai/core/system-prompt.ts
Update TOOL_DISCOVERY_PROMPT and buildNonCoreToolNamesPrompt to instruct that core tools may be called directly while non-core tools must be invoked via execute_tool({tool_name, parameters}) after using tool_search("select:tool_name").
Execute-tool dispatcher factory
apps/web/src/lib/ai/tools/execute-tool.ts, apps/web/src/lib/ai/tools/__tests__/execute-tool.test.ts
Add createExecuteTool(allowedTools): Tool which accepts {tool_name, parameters} (Zod-validated), resolves the target tool from allowedTools, returns structured errors for unknown tools / missing execute / parameter validation failures, and forwards validated parameters to the tool's execute. Tests cover unknown tool, missing execute, invalid params, valid execution, and empty registry behavior.
Global messages route — tool assembly & wiring
apps/web/src/app/api/ai/global/[id]/messages/route.ts
On POST, build filteredAllTools from pageSpaceTools (respecting isReadOnly/webSearchEnabled), split into coreTools and nonCoreTools via CORE_TOOL_NAMES, extend finalSystemPrompt with non-core tool names, expose tool_search over the full filtered catalog, and wire execute_tool (via createExecuteTool) to dispatch only nonCoreTools while the model receives only coreTools. The pre-stream context size now uses finalSystemPrompt.
Tests, mocks and expectations updated
apps/web/src/app/api/ai/global/[id]/messages/__tests__/stream-socket-events.test.ts, apps/web/src/lib/ai/core/__tests__/*
Adjust mocks to replace pageSpaceToolsStubbed with corePageSpaceTools, add buildNonCoreToolNamesPrompt and createExecuteTool mocks, remove buildStubbedTools-centric tests, and add/modify tests for CORE_TOOL_NAMES, corePageSpaceTools, and buildNonCoreToolNamesPrompt.
sequenceDiagram
  participant Client
  participant Route as GlobalRoute
  participant LLM
  participant ToolSearch as tool_search
  participant ExecuteTool as execute_tool
  participant Tools as ToolCatalog

  Client->>Route: POST /api/ai/global/[id]/messages
  Route->>Tools: build filteredAllTools (pageSpaceTools + flags)
  Route->>Tools: split coreTools / nonCoreTools (CORE_TOOL_NAMES)
  Route->>LLM: send system prompt + coreTools + tool_search
  LLM->>ToolSearch: tool_search("select:tool_name")
  ToolSearch->>Route: return schema/info from filteredAllTools
  LLM->>ExecuteTool: execute_tool({tool_name, parameters})
  ExecuteTool->>Tools: lookup non-core tool in allowedTools
  ExecuteTool->>Tools: validate parameters -> call tool.execute
  Tools-->>ExecuteTool: tool result
  ExecuteTool-->>LLM: execution response / error
  LLM-->>Client: stream responses
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I hop through code with careful paws,
I split the tools and learn the laws,
Search for schemas, then execute true,
Send the model core, dispatch non-core through,
A carrot for every passing test.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title precisely describes the main change: introducing an execute_tool dispatcher to reduce non-core tool schemas in model context from ~37 to ~10 tools, saving significant tokens.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pu/execute-tool-dispatch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c839609e6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/web/src/lib/ai/core/ai-tools.ts
Comment thread apps/web/src/lib/ai/core/stub-tools.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/web/src/lib/ai/tools/execute-tool.ts (1)

22-29: ⚖️ Poor tradeoff

Type assertions in dispatch logic are pragmatic but limit type safety.

Lines 22 and 29 use type assertions to work around TypeScript's inability to preserve precise types through the dynamic dispatch pattern. This is a reasonable tradeoff for the flexibility of runtime tool routing, but it does mean:

  • Line 22: The actual schema type might be more specific than z.ZodType
  • Line 29: Return type is erased to unknown

If stricter typing is needed in the future, consider using generics or a registry pattern with mapped types.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/lib/ai/tools/execute-tool.ts` around lines 22 - 29, The dynamic
dispatch currently uses broad type assertions (realSchema = t.inputSchema as
z.ZodType and t.execute as (args: unknown, opts: unknown) => unknown) which
erases useful type information; fix by updating the tool registry/type
definitions so t.inputSchema is declared as z.ZodTypeAny (or a generic
z.ZodType<unknown>) and t.execute has a typed signature that accepts the
inferred input (use z.infer on the schema in the registry or make ToolDefinition
generic), then remove the local type assertions in execute-tool.ts and rely on
the stronger ToolDefinition types for realSchema, parsed.data, and the execute
call (referencing symbols: realSchema, t.inputSchema, parsed, t.execute,
tool_name).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/web/src/lib/ai/tools/execute-tool.ts`:
- Around line 22-29: The dynamic dispatch currently uses broad type assertions
(realSchema = t.inputSchema as z.ZodType and t.execute as (args: unknown, opts:
unknown) => unknown) which erases useful type information; fix by updating the
tool registry/type definitions so t.inputSchema is declared as z.ZodTypeAny (or
a generic z.ZodType<unknown>) and t.execute has a typed signature that accepts
the inferred input (use z.infer on the schema in the registry or make
ToolDefinition generic), then remove the local type assertions in
execute-tool.ts and rely on the stronger ToolDefinition types for realSchema,
parsed.data, and the execute call (referencing symbols: realSchema,
t.inputSchema, parsed, t.execute, tool_name).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 145369b4-cb2d-4787-90ca-ea1308bd77b3

📥 Commits

Reviewing files that changed from the base of the PR and between f1090de and 3c83960.

📒 Files selected for processing (6)
  • apps/web/src/app/api/ai/global/[id]/messages/__tests__/stream-socket-events.test.ts
  • apps/web/src/app/api/ai/global/[id]/messages/route.ts
  • apps/web/src/lib/ai/core/ai-tools.ts
  • apps/web/src/lib/ai/core/stub-tools.ts
  • apps/web/src/lib/ai/core/system-prompt.ts
  • apps/web/src/lib/ai/tools/execute-tool.ts
💤 Files with no reviewable changes (1)
  • apps/web/src/lib/ai/core/stub-tools.ts

2witstudios and others added 2 commits May 12, 2026 12:58
The model now sees a compact grouped list of all 34 non-core tool names
in the system prompt (~100-150 tokens), giving it a complete map of
what is callable without schema overhead. Previously non-core tools
were completely invisible after the execute_tool dispatcher change.

Also removes the now-obsolete buildStubbedTools tests from stub-tools.test.ts
and adds unit tests for buildNonCoreToolNamesPrompt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pageSpaceToolsStubbed was removed in the execute_tool dispatcher
refactor. Replace its test block with equivalent corePageSpaceTools
assertions covering export existence, key containment in CORE_TOOL_NAMES,
and object reference identity with pageSpaceTools.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@2witstudios

Copy link
Copy Markdown
Owner Author

Re: CodeRabbit nitpick on execute-tool.ts lines 22-29 (type assertions)

The assertions are intentional and correct for dynamic dispatch:

  • t.inputSchema as z.ZodType — The AI SDK's Tool type declares inputSchema as Schema<PARAMETERS> where PARAMETERS is a generic. We intentionally widen to z.ZodType so we can call .safeParse() uniformly across all tools. Using z.ZodTypeAny would be equivalent (ZodType<any, any, any>) but no stricter.
  • t.execute as (args: unknown, opts: unknown) => unknown — The execute signature is generic too; we must erase it to call it with the dynamically-parsed data. This is the same pattern used in the existing stub-tools.ts before this PR.

The tradeoff is explicitly acceptable here — runtime correctness is ensured by the safeParse validation before the call. No change made.

…te-tool tests

- execute-tool.ts: add guard for tools with no execute function (Tool.execute
  is optional in AI SDK; calling undefined throws TypeError)
- route.ts: store buildNonCoreToolNamesPrompt result before concatenation so
  empty nonCoreTools doesn't append a stray '\n\n' to finalSystemPrompt
- execute-tool.test.ts: 6 unit tests covering unknown tool, no-execute tool,
  invalid params, valid dispatch, and empty registry cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/web/src/lib/ai/tools/__tests__/execute-tool.test.ts (1)

7-10: ⚡ Quick win

Assert the full dispatched payload.

The happy-path test only proves startDate survived dispatch. If createExecuteTool accidentally drops or reshapes endDate, this still passes. Have the fake tool echo both inputs, or capture the args and assert the full object.

Suggested test tightening
 const calendarTool: Tool = {
   description: 'List calendar events',
   inputSchema: z.object({ startDate: z.string(), endDate: z.string() }),
-  execute: async ({ startDate }: { startDate: string; endDate: string }) => ({ events: [], startDate }),
+  execute: async ({ startDate, endDate }: { startDate: string; endDate: string }) => ({
+    events: [],
+    startDate,
+    endDate,
+  }),
 };
-    ) as { events: unknown[]; startDate: string };
+    ) as { events: unknown[]; startDate: string; endDate: string };
     assert({
       given: 'valid parameters for list_calendar_events',
-      should: 'return the real execute result',
-      actual: result.startDate,
-      expected: '2024-01-01',
+      should: 'forward the full parsed parameter object to the real execute',
+      actual: { startDate: result.startDate, endDate: result.endDate },
+      expected: { startDate: '2024-01-01', endDate: '2024-01-31' },
     });

Also applies to: 67-78

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/lib/ai/tools/__tests__/execute-tool.test.ts` around lines 7 -
10, The test's fake tool (calendarTool) only returns startDate so the test
doesn't verify endDate survives dispatch; update the fake tool used in
execute-tool.test.ts (the calendarTool object: inputSchema and execute) to echo
both startDate and endDate (or capture the execute args) and then assert the
full dispatched payload in the happy-path test (and the related test at lines
67-78) to ensure createExecuteTool doesn't drop or reshape endDate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/app/api/ai/global/`[id]/messages/route.ts:
- Around line 711-715: The context-size accounting is underreporting because
calculateTotalContextSize(...) is still passed systemPrompt while the actual
prompt sent is finalSystemPrompt (which includes nonCoreToolNamesPrompt and
agentAwarenessPrompt additions); update the call to
calculateTotalContextSize(...) to use finalSystemPrompt instead of systemPrompt
and re-run tests/logging to ensure the reported token usage matches the
constructed prompt (refer to calculateTotalContextSize, finalSystemPrompt,
systemPrompt in the route handler).

---

Nitpick comments:
In `@apps/web/src/lib/ai/tools/__tests__/execute-tool.test.ts`:
- Around line 7-10: The test's fake tool (calendarTool) only returns startDate
so the test doesn't verify endDate survives dispatch; update the fake tool used
in execute-tool.test.ts (the calendarTool object: inputSchema and execute) to
echo both startDate and endDate (or capture the execute args) and then assert
the full dispatched payload in the happy-path test (and the related test at
lines 67-78) to ensure createExecuteTool doesn't drop or reshape endDate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9f0b5418-4c9e-4bd3-bd85-ae51cabbc9dd

📥 Commits

Reviewing files that changed from the base of the PR and between 23974bf and 448d20f.

📒 Files selected for processing (3)
  • apps/web/src/app/api/ai/global/[id]/messages/route.ts
  • apps/web/src/lib/ai/tools/__tests__/execute-tool.test.ts
  • apps/web/src/lib/ai/tools/execute-tool.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/lib/ai/tools/execute-tool.ts

Comment thread apps/web/src/app/api/ai/global/[id]/messages/route.ts
2witstudios and others added 2 commits May 12, 2026 13:18
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rate token accounting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@2witstudios
2witstudios merged commit 3429bd2 into master May 12, 2026
10 checks passed
@2witstudios
2witstudios deleted the pu/execute-tool-dispatch branch May 12, 2026 19:07
2witstudios added a commit that referenced this pull request May 15, 2026
…m model context (#1310)

* feat(ai): replace stub tool schemas with execute_tool dispatcher

Drops non-core tool names+descriptions from the model context by removing
all 29 non-core tools from the tool list. Core tools (list/read/search/create)
are sent with full schemas as before. The model uses execute_tool to call
any non-core tool after discovering its schema via tool_search, shrinking
the visible tool list from ~37 down to ~10.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ai): inject non-core tool names into system prompt

The model now sees a compact grouped list of all 34 non-core tool names
in the system prompt (~100-150 tokens), giving it a complete map of
what is callable without schema overhead. Previously non-core tools
were completely invisible after the execute_tool dispatcher change.

Also removes the now-obsolete buildStubbedTools tests from stub-tools.test.ts
and adds unit tests for buildNonCoreToolNamesPrompt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tests): update ai-tools.test.ts to use corePageSpaceTools

pageSpaceToolsStubbed was removed in the execute_tool dispatcher
refactor. Replace its test block with equivalent corePageSpaceTools
assertions covering export existence, key containment in CORE_TOOL_NAMES,
and object reference identity with pageSpaceTools.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ai): guard t.execute undefined + fix trailing newline + add execute-tool tests

- execute-tool.ts: add guard for tools with no execute function (Tool.execute
  is optional in AI SDK; calling undefined throws TypeError)
- route.ts: store buildNonCoreToolNamesPrompt result before concatenation so
  empty nonCoreTools doesn't append a stray '\n\n' to finalSystemPrompt
- execute-tool.test.ts: 6 unit tests covering unknown tool, no-execute tool,
  invalid params, valid dispatch, and empty registry cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ai): use z.record(z.string(), z.unknown()) for Zod v4 compatibility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ai): pass finalSystemPrompt to calculateTotalContextSize for accurate token accounting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2witstudios added a commit that referenced this pull request Jul 27, 2026
The previous note said only "belongs in its own change", which reads as an
unexamined punt. The actual reason is a trade-off worth recording, because the
constant's own definition implies web_fetch should be in the set:

- Page chat: genuinely broken. An agent whose saved allowlist omits web_fetch but
  who toggles web search on gets it rejected at dispatch.
- Global Assistant: not broken. That route sets no allowlist, so web_fetch still
  dispatches through execute_tool. Adding it there only spends context on the one
  surface deliberately engineered to keep upfront schemas small (#1310 dropped 29
  schemas for ~5-12k tokens/turn).

So promoting it in the SHARED set would charge the Global Assistant for a
page-chat-only fix. The likely correct fix is page-chat-specific, which this
design already allows without further change: alwaysUpfront remains a per-call
parameter, so that route can pass
`new Set([...ALWAYS_UPFRONT_TOOLS, 'web_fetch'])` in isolation.

Comment-only change; no behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ukUtoKKTgQDH35docNcE6
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