Skip to content

RFC 012: Formalize tool.call payload extension for accounting detail - #18

Open
brettin wants to merge 1 commit into
mainfrom
rfc/012-tool-call-extensibility
Open

RFC 012: Formalize tool.call payload extension for accounting detail#18
brettin wants to merge 1 commit into
mainfrom
rfc/012-tool-call-extensibility

Conversation

@brettin

@brettin brettin commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds RFC 012, which requests extending tool.call payload accounting detail beyond a single tokenCost integer—optional fields such as inputTokens, outputTokens, modelId, provider, modelTier, spendUsd, and uncertainty (Option A recommended), or a sibling tool.call.detail event (Option B). Blocked on RFC 011 and RFC 004. Disposition: Hold.

RFC document

Full text of contracts/rfcs/012-tool-call-extensibility.md (also in this PR's Files tab):

# RFC 012: Formalize `tool.call` payload extension surface for accounting detail

**Gap IDs:** `provisional-gap-003` (provisional — replace before opening upstream)

## Problem

[`tool-call-event.schema.json`](../schemas/events/tool-call-event.schema.json)
`payload` has `additionalProperties: false` and admits five fields:

runId | toolName | status | latencyMs | tokenCost


`tokenCost` is a **single integer** — total tokens for the call. The schema
forbids:

- per-call input vs. output token split (`inputTokens`, `outputTokens`)
- model identifier (`modelId`)
- provider name (`provider` — e.g., `vertex`, `bedrock`, `openai`)
- per-call USD spend (`spendUsd`)
- routing tier used (`modelTier` per
  [`model-routing-policy.schema.json`](../schemas/common/model-routing-policy.schema.json))
- output uncertainty / confidence (`uncertainty` per
  [`uncertainty.schema.json`](../schemas/common/uncertainty.schema.json))

All six are operationally required:

- **Accounting** (`getTaskUsage`, `allocateBudget`, `enforceBudget`,
  related to [RFC 004](004-budgets-allocate-enforce.md)) needs input/output
  split *and* model id *and* spend to compute meaningful cost. Sonnet and
  Haiku at the same `tokenCost` integer cost ~10× apart.
- **Reasoning observability** (`runEval`, `getEvalResult`) needs the routing
  tier to interpret eval scores (a Haiku eval result is not directly comparable
  to an Opus eval result on the same suite).
- **Per-call uncertainty** is `GAP-002` in
  [`GAP_DISPOSITION_REGISTER.md`](../GAP_DISPOSITION_REGISTER.md), already
  dispositioned as `spine_patch` on `eval-result` and reasoning payloads — but
  the same data attached to a `tool.call` is currently homeless.

The standard workaround today is a sibling extension event (e.g.,
`x-gmp.tool.call.tokens`) with `relatedEventId` pointing back to the strict
`tool.call`'s `eventId`. Both events share `correlationId` + `runId` for
joins. This works but:

- Every consumer must learn a two-event join pattern.
- The sibling event isn't part of any canonical schema, so its shape is
  per-implementer — implementations will reinvent it differently.
- `GET /v3/events?eventType=tool.call` returns the strict event only; pulling
  accounting detail requires a separate filter or a payload-aware read model.
  `enforceBudget` callers need both halves of the data, which is exactly the
  kind of split the strict envelope was meant to prevent.

This RFC is the `tool.call`-specific follow-on to [RFC 011](011-run-document-extensibility.md);
the precedent set there should propagate to all event payload schemas in a
later sweep.

## Proposal

Two options. Option A is recommended for ergonomic alignment with the
already-dispositioned `GAP-002` (uncertainty as `spine_patch`, not as a
separate canonical event). Option B is the strict-schema-preserving
alternative.

### Option A — Add optional accounting fields to `tool.call.payload`

```diff
 "payload": {
   "type": "object",
   "required": ["runId", "toolName", "status", "latencyMs", "tokenCost"],
   "properties": {
     "runId": { "type": "string", "minLength": 1 },
     "toolName": { "type": "string", "minLength": 1 },
     "status": { "type": "string", "enum": ["started", "succeeded", "failed"] },
     "latencyMs": { "type": "integer", "minimum": 0 },
-    "tokenCost": { "type": "integer", "minimum": 0 }
+    "tokenCost": { "type": "integer", "minimum": 0 },
+    "inputTokens": { "type": "integer", "minimum": 0 },
+    "outputTokens": { "type": "integer", "minimum": 0 },
+    "modelId": { "type": "string", "minLength": 1 },
+    "provider": { "type": "string", "minLength": 1 },
+    "modelTier": { "$ref": "../common/model-routing-policy.schema.json#/properties/defaultTier" },
+    "spendUsd": { "type": "number", "minimum": 0 },
+    "uncertainty": { "$ref": "../common/uncertainty.schema.json" }
   },
   "additionalProperties": false
 }

Semantics

  • All seven new fields are optional. Implementations that cannot or do not
    want to populate them keep emitting the minimal five-field payload unchanged.
  • tokenCost stays required as the total. inputTokens + outputTokens is
    not required to equal tokenCost — providers report differently;
    implementations should clamp negative values to zero.
  • spendUsd is the provider-reported billable amount, not a normalized
    cross-provider estimate (deliberately leaves cost-modeling to consumers).
  • uncertainty exists in the schema today
    (uncertainty.schema.json) but
    has no carrier on tool.call; this adds the carrier.

Backward-compatible — existing fixtures and emitters remain valid.

Option B — Canonical tool-call-detail-event.schema.json sibling

Keep tool-call-event.schema.json strict. Add a new event type
tool.call.detail with the additional fields plus a required
relatedEventId pointing back to the parent tool.call. Add it to the
event-envelope.schema.json
oneOf.

Trade-offs

  • Pros: zero change to the strict tool.call event; explicit two-event
    contract every implementer can rely on.
  • Cons: every consumer that needs accounting detail does a two-event read;
    join discipline is the implementer's responsibility; emit-path failures
    become two-stage (the parent succeeds, the detail can fail silently).

Recommendation

Option A. It collapses the join, keeps the schema honest about what the
field set actually represents, and matches the already-merged spirit of
GAP-002. Option B is a viable fallback if backward compatibility on the
existing five-field shape is paramount.

Coordination with RFC 004

RFC 004 is the agenda item for the budget
allocate/enforce contract. If RFC 004 lands first and chooses field names like
spentUsd or inputTokenCount, this RFC's field names should follow that
choice. We propose coordinating the naming pass at PR-review time.

Deferred (out of scope for this RFC)

  • Generalizing the rule to every event payload. The same
    additionalProperties: false vs. extensionPolicy tension exists on
    policy-check-event, reasoning-node-event, classification-shift-event.
    RFC 011 sets the precedent on Run;
    this RFC follows for tool.call; a later RFC should sweep the rest.
  • Cross-provider cost normalization. This RFC adds spendUsd as the
    provider-reported number; whether ARIA standardizes a normalization function
    (or a cost-conversion companion schema) is a separate question.
  • Streaming-call accounting. Anthropic and Vertex streaming SDKs do not
    populate usage at the same iteration point as non-streaming calls. Whether
    tool.call should distinguish stream vs. non-stream emission is a separate
    accounting RFC.

Platform v0 alignment (out of scope)

Item Current drift Target (Option A)
Per-call accounting Sibling extension event (x-gmp.tool.call.tokens) Optional fields on the canonical tool.call.payload
Provider/model identification Carried in metadata or sibling event First-class provider, modelId, modelTier
Per-call spend Reconstructed from token counts × pricing Provider-reported spendUsd
Per-call uncertainty Has no carrier on tool.call; lives on related reasoning/eval payloads First-class uncertainty ref on tool.call.payload

Platform reference

ARIAPlatform platform-v0-implementation-profile.json
ToolCallEvent, BudgetAllocate, EnforceBudget. ARIAPlatform v0 sketches
the budget side of accounting but does not yet ground the per-call data the
budget endpoints need to do their job; this RFC closes that loop.


## Test plan

- [ ] Review proposed contract change against cited schemas/profiles
- [ ] Confirm disposition (Ship / Issue / Hold) and whether to proceed to an impl PR
- [ ] Approve or request changes on this RFC PR (do not merge until implementation decision is clear)


Made with [Cursor](https://cursor.com)

…ail.

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

2 participants