Skip to content

feat(schema): carry a vendor-neutral tool kind on ToolCallInfo (AI-2538) - #63

Merged
alexeyzimarev merged 1 commit into
mainfrom
alexeyzimarev/ai-2538-carry-the-vendor-neutral-tool-kind-on-the-persisted
Sep 7, 2026
Merged

alexeyzimarev merged 1 commit into
mainfrom
alexeyzimarev/ai-2538-carry-the-vendor-neutral-tool-kind-on-the-persisted

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Member

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:

Scope item Repo Status
Schema — add the tool-kind field to ToolCallInfo kurrent-agents this PR
Populate — ClaudeTranscriptEvents / CodexRolloutEvents kcap-cli unblocked by this PR

ToolCallInfo is defined in schema/proto/kurrent/agent/v2/value_types.proto and dual-published as Kurrent.Agent.Schema (NuGet) and kurrent-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; on ToolCallInfo. tool_name stays raw vendor fidelity; tool_kind carries the vendor-neutral ACP ToolKind classification (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 as TOOL_KIND_SWITCH_MODE, not switch_mode.

Absent vs other

The one semantic the issue calls out is enforced, not merely documented:

  • absent — nobody classified this call (no mapping table for that framework yet, or an ACP agent that sent no kind). "We don't know."
  • 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=False writer settings make absence the JSON default — a producer has to go out of its way to emit "".

Two guards keep it that way:

  1. ToolKindPresenceTests (.NET) and test_tool_kind_presence.py (Python) pin absent ≠ "" ≠ "other" in both languages, so a future writer-settings change cannot collapse them silently.
  2. The AssistantToolCallsGenerated fixture 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-arguments case, which had no fixture coverage before.

Cross-language dump, confirming both writers agree and that the unclassified call carries no tool_kind key at all:

py-out  [{"call_id":"call-001",...,"tool_kind":"search"}, {"call_id":"call-002","tool_name":"unmapped_vendor_tool","arguments":{}}]
net-out [{"call_id":"call-001",...,"tool_kind":"search"}, {"call_id":"call-002","tool_name":"unmapped_vendor_tool","arguments":{}}]

Also in here

  • SCHEMA_v2.md §3.4.1 — vocabulary table, producer rules, unrecognised-value handling, and the note that tool_kind classifies the call and not its outcome. §3.4 no longer claims conversation events are "unchanged" from v1, and §1 gains item 8.
  • Both CHANGELOGs; packages to 0.5.0, which realigns .NET (was 0.4.1) and Python (was 0.4.0). Both READMEs' version lines were already stale at 0.4.0.
  • A CLAUDE.md convention line, per that file's own promotion rule for a new canonical field.

Verification

Gate Result
buf lint pass
buf breaking vs main pass — purely additive
buf generate drift none; codegen idempotent
.NET tests 36 passed
Python tests 27 passed
Cross-language parity 19/19 fixtures structurally equal
MAF .NET (only ProjectReference consumer) builds clean

Notes for the reviewer

  • Nothing in this repo populates the field. Deliberate — that is exactly the documented "absent" case. Capacitor's import lanes are the first producers.
  • Publishing stays manual. The release workflows are tag-gated (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.
  • The ten uv.lock files each record the editable schema dependency's version, and uv sync --frozen rejects a stale lock, so the bump cannot be skipped. Those lines are edited surgically rather than by re-running uv lock, which re-resolves upstream deps in four of them (~6700 lines of unrelated churn in the adk runner alone).
  • Pre-existing, untouched: google-adk/python/uv.lock and claude-agent-sdk/python/uv.lock already reported stale on main before this branch (they recorded schema 0.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

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>
@linear-code

linear-code Bot commented Sep 7, 2026

Copy link
Copy Markdown

AI-2538

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add vendor-neutral tool kind to ToolCallInfo

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Add optional ACP tool classification while preserving raw vendor tool names.
• Preserve and test the distinction between unclassified calls and explicit other.
• Publish synchronized .NET and Python schema packages at version 0.5.0.
Diagram

graph TD
  P["Proto schema"] --> D[".NET binding"] --> DT[".NET tests"]
  P --> Y["Python binding"] --> YT["Python tests"]
  F["Shared fixture"] --> DT
  F --> YT
  D --> J["Canonical JSON"]
  Y --> J
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a protobuf enum
  • ➕ Provides generated symbolic constants and compile-time discoverability.
  • ➕ Restricts producers to a schema-declared vocabulary.
  • ➖ Produces protobuf-style JSON enum tokens instead of ACP’s lowercase wire values.
  • ➖ Requires schema releases for new ACP values and complicates unknown-value handling.
  • ➖ Would diverge from the schema’s existing open-string convention.
2. Keep classification outside the schema
  • ➕ Avoids changing and republishing the canonical schema packages.
  • ➕ Allows each consumer to define domain-specific classifications.
  • ➖ Forces consumers to maintain duplicate per-vendor mapping tables.
  • ➖ Prevents producers from preserving classifications supplied by ACP-native agents.
  • ➖ Creates inconsistent semantics across trace, evaluation, and policy consumers.

Recommendation: The PR’s optional string field is the best fit: it is additive, matches existing schema conventions, preserves ACP token parity, and uses Edition 2024 presence to distinguish unknown classification from explicit other. A protobuf enum or consumer-local mapping would sacrifice wire compatibility or interoperability.

Files changed (25) +299 / -29

Enhancement (4) +87 / -11
ValueTypes.csGenerate .NET ToolKind schema support +69/-6

Generate .NET ToolKind schema support

• Adds generated ToolKind storage, presence and clearing APIs, serialization, parsing, cloning, equality, hashing, merging, and descriptor metadata for field 4.

schema/dotnet/Kurrent.Agent.Schema/Generated/ValueTypes.cs

value_types.protoAdd optional tool_kind to ToolCallInfo +11/-0

Add optional tool_kind to ToolCallInfo

• Adds string field 4 for ACP-compatible, vendor-neutral tool classification. Comments define explicit presence and distinguish an omitted value from 'other'.

schema/proto/kurrent/agent/v2/value_types.proto

value_types_pb2.pyGenerate Python ToolCallInfo descriptor changes +3/-3

Generate Python ToolCallInfo descriptor changes

• Regenerates the protobuf descriptor so ToolCallInfo includes string field 4, 'tool_kind'.

schema/python/kurrent_agent_schema/_generated/kurrent/agent/v2/value_types_pb2.py

value_types_pb2.pyiExpose tool_kind in Python type stubs +4/-2

Expose tool_kind in Python type stubs

• Adds the field slot, field-number constant, attribute annotation, and constructor parameter for 'tool_kind'.

schema/python/kurrent_agent_schema/_generated/kurrent/agent/v2/value_types_pb2.pyi

Tests (3) +120 / -1
ToolKindPresenceTests.csTest .NET tool-kind presence semantics +57/-0

Test .NET tool-kind presence semantics

• Adds coverage for omitted values, explicit 'other', JSON round trips, and explicitly present empty strings. The tests verify protobuf presence remains observable through the .NET serializer.

schema/dotnet/Kurrent.Agent.Schema.Tests/ToolKindPresenceTests.cs

AssistantToolCallsGenerated.jsonExercise classified and unclassified tool calls +7/-1

Exercise classified and unclassified tool calls

• Marks the search call with 'tool_kind: search' and adds an unclassified vendor tool without the field. The empty arguments object is retained for fixture round-trip coverage.

schema/fixtures/events/AssistantToolCallsGenerated.json

test_tool_kind_presence.pyTest Python tool-kind presence semantics +56/-0

Test Python tool-kind presence semantics

• Adds coverage for omitted values, explicit 'other', JSON round trips, and explicitly present empty strings. The tests mirror the .NET presence suite.

schema/python/tests/test_tool_kind_presence.py

Documentation (6) +80 / -5
CLAUDE.mdDocument tool-kind producer conventions +1/-0

Document tool-kind producer conventions

• Adds the repository-wide rule that integrations must preserve raw tool names and omit unclassified tool kinds rather than emitting an empty string or substituting 'other'.

CLAUDE.md

SCHEMA_v2.mdSpecify the ToolCallInfo tool-kind contract +43/-3

Specify the ToolCallInfo tool-kind contract

• Documents the optional 'tool_kind' field, ACP vocabulary, use cases, forward compatibility, and producer/reader behavior. Defines absent as unclassified and 'other' as explicitly classified outside the standard categories.

schema/SCHEMA_v2.md

CHANGELOG.mdDocument the .NET 0.5.0 schema release +17/-0

Document the .NET 0.5.0 schema release

• Records the new ToolKind property, ACP vocabulary, explicit-presence contract, and additive compatibility guarantees.

schema/dotnet/Kurrent.Agent.Schema/CHANGELOG.md

README.mdReport .NET schema package version 0.5.0 +1/-1

Report .NET schema package version 0.5.0

• Updates the documented current NuGet package version.

schema/dotnet/README.md

CHANGELOG.mdDocument the Python 0.5.0 schema release +17/-0

Document the Python 0.5.0 schema release

• Records the new 'tool_kind' field, ACP vocabulary, protobuf presence behavior, and backward compatibility.

schema/python/CHANGELOG.md

README.mdReport Python schema package version 0.5.0 +1/-1

Report Python schema package version 0.5.0

• Updates the documented current PyPI package version.

schema/python/README.md

Other (12) +12 / -12
uv.lockAlign AG-UI interop tests with schema 0.5.0 +1/-1

Align AG-UI interop tests with schema 0.5.0

• Updates the editable 'kurrent-agent-schema' lock entry to version 0.5.0.

ag-ui/interop-tests/uv.lock

uv.lockAlign Claude SDK integration with schema 0.5.0 +1/-1

Align Claude SDK integration with schema 0.5.0

• Updates the local editable schema package lock entry from 0.1.1 to 0.5.0.

claude-agent-sdk/python/uv.lock

uv.lockAlign ADK demo runner with schema 0.5.0 +1/-1

Align ADK demo runner with schema 0.5.0

• Updates the demo runner’s editable schema package lock entry to version 0.5.0.

demo/ag-ui-showcase/runners/adk/uv.lock

uv.lockAlign MAF demo runner with schema 0.5.0 +1/-1

Align MAF demo runner with schema 0.5.0

• Updates the demo runner’s editable schema package lock entry to version 0.5.0.

demo/ag-ui-showcase/runners/maf/uv.lock

uv.lockAlign Strands demo runner with schema 0.5.0 +1/-1

Align Strands demo runner with schema 0.5.0

• Updates the demo runner’s editable schema package lock entry to version 0.5.0.

demo/ag-ui-showcase/runners/strands/uv.lock

uv.lockAlign Google ADK integration with schema 0.5.0 +1/-1

Align Google ADK integration with schema 0.5.0

• Updates the local editable schema package lock entry from 0.1.2 to 0.5.0.

google-adk/python/uv.lock

uv.lockAlign Microsoft Agent Framework with schema 0.5.0 +1/-1

Align Microsoft Agent Framework with schema 0.5.0

• Updates the editable schema package lock entry from 0.3.1 to 0.5.0.

microsoft-agent-framework/python/uv.lock

uv.lockAlign OpenAI Agents integration with schema 0.5.0 +1/-1

Align OpenAI Agents integration with schema 0.5.0

• Updates the local editable schema package lock entry to version 0.5.0.

openai-agents/python/uv.lock

Kurrent.Agent.Schema.csprojBump the .NET schema package to 0.5.0 +1/-1

Bump the .NET schema package to 0.5.0

• Changes the NuGet package version from 0.4.1 to 0.5.0 for the additive schema release.

schema/dotnet/Kurrent.Agent.Schema/Kurrent.Agent.Schema.csproj

pyproject.tomlBump the Python schema package to 0.5.0 +1/-1

Bump the Python schema package to 0.5.0

• Changes the PyPI project version from 0.4.0 to 0.5.0.

schema/python/pyproject.toml

uv.lockLock the Python schema package at 0.5.0 +1/-1

Lock the Python schema package at 0.5.0

• Updates the package’s editable lock entry to match the new project version.

schema/python/uv.lock

uv.lockAlign Strands integration with schema 0.5.0 +1/-1

Align Strands integration with schema 0.5.0

• Updates the local editable schema package lock entry to version 0.5.0.

strands/python/uv.lock

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Empty tool kinds break .NET lookups 🐞 Bug ≡ Correctness
Description
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.
Code

schema/dotnet/Kurrent.Agent.Schema/Generated/ValueTypes.cs[921]

+      if (HasToolKind) hash ^= ToolKind.GetHashCode();
Evidence
The new getter returns the same empty string for absence as for an explicitly assigned empty value,
and the added equality comparison therefore cannot distinguish those states. The added hash
calculation is conditional on presence, while the new test confirms that an explicitly empty value
is a supported, serialized present state, creating inconsistent equality and hash behavior.

schema/dotnet/Kurrent.Agent.Schema/Generated/ValueTypes.cs[854-890]
schema/dotnet/Kurrent.Agent.Schema/Generated/ValueTypes.cs[900-921]
schema/dotnet/Kurrent.Agent.Schema.Tests/ToolKindPresenceTests.cs[47-56]
schema/proto/kurrent/agent/v2/value_types.proto[41-46]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


Grey Divider

Context sources
⚠️ Tickets: not configured — ticket URL found in PR but could not be fetched — check ticket provider credentials
✅ Compliance rules (platform): 15 rules
Review mode: ⚖️ Balanced: This is a public schema and generated-package contract change spanning .NET, Python, protobuf serialization, JSON presence semantics, fixtures, and parity tests, so it carries genuine compatibility and behavioral risk but is not unusually defect-dense enough to require redundant extended review.

Grey Divider

Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

if (HasCallId) hash ^= CallId.GetHashCode();
if (HasToolName) hash ^= ToolName.GetHashCode();
if (arguments_ != null) hash ^= Arguments.GetHashCode();
if (HasToolKind) hash ^= ToolKind.GetHashCode();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

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

@alexeyzimarev
alexeyzimarev merged commit 8bf78d4 into main Sep 7, 2026
6 checks passed
@alexeyzimarev
alexeyzimarev deleted the alexeyzimarev/ai-2538-carry-the-vendor-neutral-tool-kind-on-the-persisted branch September 7, 2026 10:31
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