Update — scope clarification (post-investigation)
Same as for #2654 (cross-link): exposing allowed_tools on the ai.action form requires extending AgentActionArgs in packages/tracecat-ee/tracecat_ee/agent/schemas.py, which is in the EE module. The custom-source-level part is fully OSS.
We don't intend to ship code into the EE module ourselves. The OSS PR will include a defensive getattr so the cascade is forward-compatible — when EE exposes the field, the action-level override starts taking effect automatically with no further OSS change. Maintainers can apply the EE one-liner at merge time, or guide us towards a different layout before the PR opens.
TL;DR
ai.action always inlines the JSON schemas of the full Claude Code built-in toolset (Bash, Read, Write, Edit, Grep, Glob, MultiEdit, etc.) in the system prompt — even though the action is documented as a "simple LLM call, no tools" and runs with max_tool_calls=0.
That's ~3 940 input tokens of pure overhead per call (96 % of the system context), measured below.
This issue proposes exposing one optional field (allowed_tools: list[str] | None) at both the custom-source and action levels, mapping directly onto ClaudeAgentOptions.tools which already supports the full range (None = SDK default, [] = disable all built-ins, ["Bash","Read"] = whitelist).
Default None everywhere → zero behaviour change for existing workflows.
Measured impact
Tested directly against the bundled CLI 2.1.85 inside sysdio2_tracecat_agent_executor, ANTHROPIC_BASE_URL pointed at a remote Ollama running qwen3.5:9b. User prompt: "Reply: hi" (3 tokens). The CLI's --output-format json returns a usage.input_tokens field that captures system prompt + tool definitions + user prompt cumulatively.
| # |
Configuration |
input_tokens |
Delta vs A |
| A |
CLI defaults (no flags) |
4 096 |
reference |
| B |
--system-prompt "You are Qwen, by Alibaba." |
4 096 |
0 |
| C |
--system-prompt "..." --tools "" |
158 |
−96 % |
| D |
--bare --system-prompt "..." --tools "" |
157 |
−96 % |
So roughly 96 % of the per-call overhead comes from the inlined JSON tool definitions. For an ai.action that's just doing a one-shot LLM completion (classification, extraction, summarization), this is pure waste — and on top of that, smaller models often hallucinate around tools they "see" but cannot actually call.
Capability already in the SDK
ClaudeAgentOptions.tools natively supports:
tools=None → no --tools flag → CLI applies its default (full toolset).
tools=[] → --tools "" → all built-ins disabled.
tools=["Bash","Read"] → --tools "Bash,Read" → whitelist.
The proposal is to surface this in the Tracecat schema.
Proposed change
Custom source (default)
| Field |
Type |
Default |
allowed_tools |
list[str] | None |
None |
ai.action schema (per-call override)
Same field. When set, takes precedence over the source-level value.
Cascade rule
if action.allowed_tools is not None:
resolved = action.allowed_tools # action overrides source (even if [])
elif source.allowed_tools is not None:
resolved = source.allowed_tools # source default if no action value
else:
resolved = None # SDK default (full toolset)
The runtime maps resolved directly onto ClaudeAgentOptions.tools.
Backward compatibility
- New field defaults to
None → zero behaviour change for existing workflows or sources.
disallowed_tools and existing built-in filtering logic in Tracecat are untouched.
- MCP servers (
tracecat-registry) are not affected by this PR.
Note on pydantic_ai runtime
The pydantic-ai harness doesn't have the same notion of "implicit built-in tools" — it only sees the tools you give it. So allowed_tools would be a no-op there. We'd log a WARNING if it's set on a workflow that resolves to the pydantic-ai runtime, and document this clearly.
Scope of implementation
tracecat/db/models.py: allowed_tools JSONB NULL on agent_custom_provider.
- Alembic migration.
tracecat/agent/provider/schemas.py: extend Create/Update/Read.
- Registry action schema for
ai.action: add allowed_tools.
tracecat/dsl/action.py:build_agent_args_activity: cascade.
tracecat/agent/runtime/claude_code/runtime.py: pass tools=resolved to ClaudeAgentOptions.
tracecat/agent/runtime/pydantic_ai/runtime.py: warn if set, ignore.
- Frontend: types regenerated via
just gen-client-ci. Custom source modal: add the field under "Agent settings". Action form picks it up automatically through schema generation.
- Tests:
- Unit: verify the
cmd array produced by subprocess_cli._build_command includes --tools "" when tools=[], --tools "Bash,Read" when whitelist, and no --tools flag when None.
- Cascade: matrix of source × action × None.
- Token-overhead regression: integration test asserting
input_tokens for tools=[] is well below the default.
- Docs: update
docs/agents/ai-action.mdx with a "Reducing token overhead" section.
Open question for maintainers
Two design alternatives we considered but rejected:
- Auto-disable tools when
model_provider == "custom-model-provider". Pro: zero UX. Con: silent behaviour change for existing custom-source users — some might rely on tools being there.
- Auto-disable tools when
max_tool_calls == 0. Same trade-off, with the bonus of fitting the ai.action semantics. Still a silent behaviour change though.
The opt-in design proposed here is more conservative and matches the spirit of Passthrough mode (you choose when to skip default behaviour). Happy to discuss if maintainers prefer one of the auto-disable variants.
Happy to send the PR. Splitting source-level and action-level fields into two PRs is doable.
Update — scope clarification (post-investigation)
Same as for #2654 (cross-link): exposing
allowed_toolson theai.actionform requires extendingAgentActionArgsinpackages/tracecat-ee/tracecat_ee/agent/schemas.py, which is in the EE module. The custom-source-level part is fully OSS.We don't intend to ship code into the EE module ourselves. The OSS PR will include a defensive
getattrso the cascade is forward-compatible — when EE exposes the field, the action-level override starts taking effect automatically with no further OSS change. Maintainers can apply the EE one-liner at merge time, or guide us towards a different layout before the PR opens.TL;DR
ai.actionalways inlines the JSON schemas of the full Claude Code built-in toolset (Bash, Read, Write, Edit, Grep, Glob, MultiEdit, etc.) in the system prompt — even though the action is documented as a "simple LLM call, no tools" and runs withmax_tool_calls=0.That's ~3 940 input tokens of pure overhead per call (96 % of the system context), measured below.
This issue proposes exposing one optional field (
allowed_tools: list[str] | None) at both the custom-source and action levels, mapping directly ontoClaudeAgentOptions.toolswhich already supports the full range (None= SDK default,[]= disable all built-ins,["Bash","Read"]= whitelist).Default
Noneeverywhere → zero behaviour change for existing workflows.Measured impact
Tested directly against the bundled CLI 2.1.85 inside
sysdio2_tracecat_agent_executor,ANTHROPIC_BASE_URLpointed at a remote Ollama runningqwen3.5:9b. User prompt:"Reply: hi"(3 tokens). The CLI's--output-format jsonreturns ausage.input_tokensfield that captures system prompt + tool definitions + user prompt cumulatively.input_tokens--system-prompt "You are Qwen, by Alibaba."--system-prompt "..." --tools ""--bare --system-prompt "..." --tools ""So roughly 96 % of the per-call overhead comes from the inlined JSON tool definitions. For an
ai.actionthat's just doing a one-shot LLM completion (classification, extraction, summarization), this is pure waste — and on top of that, smaller models often hallucinate around tools they "see" but cannot actually call.Capability already in the SDK
ClaudeAgentOptions.toolsnatively supports:tools=None→ no--toolsflag → CLI applies its default (full toolset).tools=[]→--tools ""→ all built-ins disabled.tools=["Bash","Read"]→--tools "Bash,Read"→ whitelist.The proposal is to surface this in the Tracecat schema.
Proposed change
Custom source (default)
allowed_toolslist[str] | NoneNoneai.actionschema (per-call override)Same field. When set, takes precedence over the source-level value.
Cascade rule
The runtime maps
resolveddirectly ontoClaudeAgentOptions.tools.Backward compatibility
None→ zero behaviour change for existing workflows or sources.disallowed_toolsand existing built-in filtering logic in Tracecat are untouched.tracecat-registry) are not affected by this PR.Note on
pydantic_airuntimeThe pydantic-ai harness doesn't have the same notion of "implicit built-in tools" — it only sees the tools you give it. So
allowed_toolswould be a no-op there. We'd log aWARNINGif it's set on a workflow that resolves to the pydantic-ai runtime, and document this clearly.Scope of implementation
tracecat/db/models.py:allowed_tools JSONB NULLonagent_custom_provider.tracecat/agent/provider/schemas.py: extend Create/Update/Read.ai.action: addallowed_tools.tracecat/dsl/action.py:build_agent_args_activity: cascade.tracecat/agent/runtime/claude_code/runtime.py: passtools=resolvedtoClaudeAgentOptions.tracecat/agent/runtime/pydantic_ai/runtime.py: warn if set, ignore.just gen-client-ci. Custom source modal: add the field under "Agent settings". Action form picks it up automatically through schema generation.cmdarray produced bysubprocess_cli._build_commandincludes--tools ""whentools=[],--tools "Bash,Read"when whitelist, and no--toolsflag whenNone.input_tokensfortools=[]is well below the default.docs/agents/ai-action.mdxwith a "Reducing token overhead" section.Open question for maintainers
Two design alternatives we considered but rejected:
model_provider == "custom-model-provider". Pro: zero UX. Con: silent behaviour change for existing custom-source users — some might rely on tools being there.max_tool_calls == 0. Same trade-off, with the bonus of fitting theai.actionsemantics. Still a silent behaviour change though.The opt-in design proposed here is more conservative and matches the spirit of
Passthrough mode(you choose when to skip default behaviour). Happy to discuss if maintainers prefer one of the auto-disable variants.Happy to send the PR. Splitting source-level and action-level fields into two PRs is doable.