Repository navigation
feat(schema): carry a vendor-neutral tool kind on ToolCallInfo (AI-2538) - #63
Conversation
Adds `ToolCallInfo.tool_kind` (field 4, string) to the canonical v2 schema — the schema half of AI-2538. `tool_name` stays raw vendor fidelity; `tool_kind` carries the vendor-neutral ACP `ToolKind` classification, so a consumer that needs to know what a call *did* reads one closed set instead of maintaining its own per-vendor tool-name table. String rather than enum, matching house style: this schema uses documented open strings for every closed set (`InterruptResolved.outcome`, `SubagentCompleted.outcome`). A string also keeps exact token parity with ACP on the JSON wire — a proto enum would serialise as `TOOL_KIND_SWITCH_MODE` rather than `switch_mode`. The load-bearing semantic is that ABSENT stays distinguishable from `other`: absent means nobody classified the call, `other` means classified and none of the above. Edition-2024 explicit presence plus the existing `formatDefaultValues: false` / `always_print_fields_with_no_presence=False` writer settings make absence the JSON default. `ToolKindPresenceTests` and `test_tool_kind_presence.py` pin the distinction in both languages so a future writer-settings change cannot collapse it silently, and the AssistantToolCallsGenerated fixture now carries both a classified and an unclassified call so the cross-language parity job exercises it too. Purely additive: `buf breaking` passes, existing payloads parse unchanged, and no integration in this repo populates the field yet — that is exactly the documented "absent" case. Capacitor's Claude/Codex import lanes (kcap-cli, the other half of AI-2538) are the first producers. Packages go to 0.5.0, realigning .NET (0.4.1) and Python (0.4.0). The version bump invalidates the recorded editable-dependency version in ten uv.lock files; those lines are edited surgically rather than by re-running `uv lock`, which would drag unrelated upstream churn into four of them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR Summary by QodoAdd vendor-neutral tool kind to ToolCallInfo
AI Description
Diagram
High-Level Assessment
Files changed (25)
|
Code Review by Qodo
1. Empty tool kinds break .NET lookups
|
| if (HasCallId) hash ^= CallId.GetHashCode(); | ||
| if (HasToolName) hash ^= ToolName.GetHashCode(); | ||
| if (arguments_ != null) hash ^= Arguments.GetHashCode(); | ||
| if (HasToolKind) hash ^= ToolKind.GetHashCode(); |
There was a problem hiding this comment.
1. Empty tool kinds break .net lookups 🐞 Bug ≡ Correctness
ToolCallInfo.Equals treats absent and explicitly empty ToolKind values as equal because both getters return "", while GetHashCode includes the value only when HasToolKind is true. A dictionary or hash set keyed by these messages can therefore fail to find an equal entry when a parsed payload explicitly contains an empty string, which the public JSON codec preserves.
Agent Prompt
## Issue description
The generated .NET `ToolCallInfo` considers absent and explicitly empty `ToolKind` values equal, but computes their hashes differently. This violates the equality/hash contract and also conflicts with the documented distinction between an absent field and a present empty value.
## Issue Context
The getter maps absence to the empty default string, `Equals` compares only getter values, and `GetHashCode` conditionally includes the value based on `HasToolKind`. Update the protobuf representation or generation approach—not only the generated file—so presence participates consistently, retain wire field number 4, regenerate artifacts, and add a regression test covering equality and hash-based lookup.
## Fix Focus Areas
- schema/proto/kurrent/agent/v2/value_types.proto[41-46]
- schema/dotnet/Kurrent.Agent.Schema/Generated/ValueTypes.cs[907-921]
- schema/dotnet/Kurrent.Agent.Schema.Tests/ToolKindPresenceTests.cs[47-56]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Closes the schema half of AI-2538 / kcap-cli#794.
That issue is an auto-import of a kcap-cli issue, so its "Here" bullet means kcap-cli. It splits across two repos:
ToolCallInfoClaudeTranscriptEvents/CodexRolloutEventsToolCallInfois defined inschema/proto/kurrent/agent/v2/value_types.protoand dual-published asKurrent.Agent.Schema(NuGet) andkurrent-agent-schema(PyPI), so the NuGet package the issue describes as "not repo code" is in fact built from here.What this adds
string tool_kind = 4;onToolCallInfo.tool_namestays raw vendor fidelity;tool_kindcarries the vendor-neutral ACPToolKindclassification (read,edit,delete,move,search,execute,think,fetch,switch_mode,other), so a consumer that needs to know what a call did — a trace UI picking an icon, an eval counting file writes, a policy engine gating execution — reads one closed set instead of maintaining its own per-vendor name table.String rather than enum. This schema uses documented open strings for every closed set already (
InterruptResolved.outcome,SubagentCompleted.outcome) and declares no enums at all. A string also keeps exact token parity with ACP on the JSON wire: a proto enum would serialise asTOOL_KIND_SWITCH_MODE, notswitch_mode.Absent vs
otherThe one semantic the issue calls out is enforced, not merely documented:
other— classified, and deliberately none of the above (a subagent, a skill, an MCP tool). "We know, and it's none of these."Edition-2024 explicit presence plus the existing
formatDefaultValues: false/always_print_fields_with_no_presence=Falsewriter settings make absence the JSON default — a producer has to go out of its way to emit"".Two guards keep it that way:
ToolKindPresenceTests(.NET) andtest_tool_kind_presence.py(Python) pinabsent ≠ "" ≠ "other"in both languages, so a future writer-settings change cannot collapse them silently.AssistantToolCallsGeneratedfixture now carries both a classified call and an unclassified one, so the fixture-drift and cross-language parity jobs exercise the omission on every run. It also picks up the empty-argumentscase, which had no fixture coverage before.Cross-language dump, confirming both writers agree and that the unclassified call carries no
tool_kindkey at all:Also in here
SCHEMA_v2.md §3.4.1— vocabulary table, producer rules, unrecognised-value handling, and the note thattool_kindclassifies the call and not its outcome. §3.4 no longer claims conversation events are "unchanged" from v1, and §1 gains item 8.0.4.0.Verification
buf lintbuf breakingvsmainbuf generatedriftProjectReferenceconsumer)Notes for the reviewer
schema-python-v*/schema-dotnet-v*), so merging this does not publish; cutting the tags is a separate deliberate step, and kcap-cli needs 0.5.0 on the feeds before it can take the other half.uv.lockfiles each record the editable schema dependency's version, anduv sync --frozenrejects a stale lock, so the bump cannot be skipped. Those lines are edited surgically rather than by re-runninguv lock, which re-resolves upstream deps in four of them (~6700 lines of unrelated churn in the adk runner alone).google-adk/python/uv.lockandclaude-agent-sdk/python/uv.lockalready reported stale onmainbefore this branch (they recorded schema0.1.2/0.1.1). The version line is corrected here; the rest of that drift is left alone as unrelated scope.🤖 Generated with Claude Code