Skip to content

feat: expose operation usage metadata - #2530

Merged
shrey150 merged 8 commits into
v4-spikefrom
shrey/stg-2670-json-rpc-usage
Aug 3, 2026
Merged

feat: expose operation usage metadata#2530
shrey150 merged 8 commits into
v4-spikefrom
shrey/stg-2670-json-rpc-usage

Conversation

@shrey150

@shrey150 shrey150 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • expose required per-operation LLM usage metadata on act, observe, and extract results
  • default every usage counter to 0, so deterministic actions and cache hits return a complete zero-valued aggregate instead of omitting usage
  • aggregate usage across every model inference performed by an operation
  • regenerate the canonical JSON Schema and generated Python and Go models
  • verify snake_case JSON-RPC transport and camelCase TypeScript results through the real browser runtime

Result shape

result.metadata.usage: {
  inputTokens: number;
  outputTokens: number;
  reasoningTokens: number;
  cachedInputTokens: number;
  inferenceTimeMs: number;
};

The raw JSON-RPC response uses input_tokens, output_tokens, reasoning_tokens, cached_input_tokens, and inference_time_ms. All five fields are always present. Operations that do not run inference return 0 for all five fields.

Usage is scoped to one operation and aggregates every model call made by that operation. Session-wide aggregation through stagehand.metrics() is intentionally outside this PR.

E2E Test Matrix

Command / flow Observed output Confidence / sufficiency
Local built SDK and server with headless Chromium and a real Groq model: extractobserveact against https://example.com All three operations succeeded and returned metadata.usage; extract returned 811 input / 237 output tokens, observe returned 680 / 376, and act returned 1149 / 313 and navigated to the expected public destination Proves real provider inference reaches each operation result through the locally built runtime. It covers one provider/model, not every supported provider.
node_modules/.bin/vitest run --root . packages/sdk-ts/tests/browser-runtime/stagehand-launch-connect-smoke.test.ts 1 browser-runtime file and 13 tests passed in real headless Chromium; deterministic act returned five zero-valued camelCase SDK fields and five zero-valued snake_case wire fields Proves the required zero aggregate crosses the actual extension/service-worker JSON-RPC transport and is decoded by the public SDK. The test model is deterministic so exact aggregation can be asserted.
<Corepack PATH> node_modules/.bin/turbo run test:unit 9 tasks passed: protocol 321, TypeScript SDK 117, server 197 with 11 todo, root 72, docs 18, and evals 403 Covers schema defaults, required result fields, multi-inference aggregation, self-healing actions, cache hits, deterministic actions, generated models, and package installation.
node_modules/.bin/turbo run build && node_modules/.bin/turbo run fmt:check lint typecheck Build completed; all 8 check tasks passed with no errors Covers package builds, formatting, lint, type checking, and documentation validation.
uv --directory packages/sdk-python run --locked pytest plus Ruff, Ty, and generator checks 208 tests passed; formatting, lint, type, and generated-model checks passed Proves the generated Python model exposes the complete required usage result and remains synchronized with the protocol.
go list ./... package tests plus generator tests/check All non-example Go packages and the generator passed Proves the generated Go model and embedded extension match the merged protocol/server source.

Follow-up

A stacked PR will implement the existing stagehand.metrics() RPC using V3-style session totals built from these per-operation usage values.

@changeset-bot

changeset-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 99d58ca

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@shrey150 shrey150 changed the title feat(protocol): add operation usage metadata feat: expose operation usage metadata Jul 30, 2026
@shrey150
shrey150 marked this pull request as ready for review July 30, 2026 22:58
@shrey150
shrey150 requested a review from a team as a code owner July 30, 2026 22:58

@cubic-dev-ai cubic-dev-ai 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.

cubic analysis

No issues found across 16 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Linked issue analysis

Linked issue: STG-2670: figure out what we are doing with .metrics & token tracking

Status Acceptance criteria Notes
Add optional per-operation usage metadata to act/observe/extract results with fields inputTokens, outputTokens, reasoningTokens, cachedInputTokens, inferenceTimeMs Schema, docs, and runtime all add the usage object and the specified fields; tests assert presence/shape in SDK and server results.
Populate usage from real model inference and aggregate multi-call operations (e.g., two-step actions) Server code converts inference responses into usage, aggregates multi-call usage, and tests verify aggregation and numeric totals.
Omit usage metadata when an operation did not run inference (deterministic actions / cache hits) Runtime preserves omission for operations without inference and tests assert usage is undefined for cache hits or purely deterministic actions.
Ensure JSON-RPC wire uses snake_case and SDK surfaces camelCase fields (round-trip casing + verify through browser runtime) Wire-casing tests and browser-runtime smoke test capture raw JSON-RPC messages and assert snake_case on the wire and camelCase in SDK results; test instruments transport to check raw messages.
Regenerate canonical JSON Schema and update generated Python and Go models to include the usage shape The canonical JSON schema and generated model files were updated to include StagehandResultUsage and the checked-in generated models reflect the new fields.
Architecture diagram
sequenceDiagram
    participant Client as Client SDK
    participant Server as RPC Server
    participant ActSvc as Act Service
    participant LLM as LLM Service
    participant Cache as Cache Store

    Note over Client,Cache: 🆕 Per-operation LLM usage metadata

    Client->>Server: act({ instruction, page })
    Server->>ActSvc: act(params)

    alt instruction is Action (deterministic)
        ActSvc->>ActSvc: performAction(deterministic)
        Note over ActSvc: No LLM inference
        ActSvc-->>Server: { data: { success, ... }, metadata: {} }
    else instruction is a string
        ActSvc->>Cache: getCachedActions(instruction)
        alt Cache HIT
            Cache-->>ActSvc: cached actions
            Note over ActSvc: No LLM inference
            ActSvc-->>Server: { data: { ... }, metadata: {} }
        else Cache MISS
            ActSvc->>LLM: generate(instruction, model)
            LLM-->>ActSvc: { action, twoStep, usage: { prompt_tokens, ... } }
            ActSvc->>ActSvc: aggregateUsage(initial)
            alt twoStep is true
                ActSvc->>LLM: generate(second_action)
                LLM-->>ActSvc: { action, usage: { ... } }
                ActSvc->>ActSvc: aggregateUsage(additional)
            end
            ActSvc->>ActSvc: performAction()
            ActSvc->>Cache: setCachedActions(actions)
            ActSvc-->>Server: { data: { ... }, metadata: { usage: { inputTokens, ... } } }
        end
    end

    Note over Server,Client: JSON-RPC transport converts to snake_case (input_tokens, etc.)
    Server-->>Client: act result (with optional usage)
    Client->>Client: decode to camelCase
Loading

Re-trigger cubic

Comment thread packages/protocol/schemas.ts
Comment thread packages/protocol/schemas.ts Outdated
@shrey150
shrey150 force-pushed the shrey/stg-2670-json-rpc-usage branch from a5d1d8f to c1c2f8b Compare July 31, 2026 06:41
@shrey150
shrey150 force-pushed the shrey/stg-2670-json-rpc-usage branch from c1c2f8b to 8a4f4f3 Compare July 31, 2026 20:18
Comment thread packages/server/services/actService.ts Outdated
@shrey150
shrey150 merged commit 6bd2647 into v4-spike Aug 3, 2026
26 checks passed
shrey150 added a commit that referenced this pull request Aug 3, 2026
## Summary

- add a runtime-scoped metrics accumulator for `act`, `observe`, and
`extract`
- record each returned operation's already-aggregated `metadata.usage`
exactly once
- implement the existing `stagehand.metrics()` JSON-RPC method as a
detached session snapshot
- preserve V3-style per-method and overall prompt, completion,
reasoning, cached-input, and inference-time totals

## Semantics

Each Stagehand instance starts with zeroed metrics. Every successful
operation returns a complete `metadata.usage` object. Deterministic
actions and cache hits return zero-valued usage, so recording them
leaves the counters unchanged. Calls that throw before returning a
result are not recorded. Reading `stagehand.metrics()` does not mutate
or reset the accumulated values.

This PR is stacked on
[#2530](#2530), which
introduces and populates the per-operation `metadata.usage` values
consumed here.

## E2E Test Matrix

| Command / flow | Observed output | Confidence / sufficiency |
| --- | --- | --- |
| `corepack pnpm exec turbo run build --force` | 4 build tasks completed
successfully with cache bypassed; the protocol, server extension,
TypeScript SDK, and eval package outputs were rebuilt | Proves the exact
combined source builds without relying on a stale cached extension
artifact. |
| `pnpm exec vitest run
packages/sdk-ts/tests/browser-runtime/stagehand-launch-connect-smoke.test.ts`
| 1 browser-runtime file and 14 tests passed against headless Chrome |
Proves the public SDK reaches the merged server through the real
extension/service-worker transport, deterministic actions return
complete zero usage, inferred operations update session metrics exactly
once, and repeated snapshots are read-only. The model adapter is
deterministic rather than a live provider. |
| `pnpm exec vitest run packages/server/tests/metrics.test.ts
packages/server/tests/rpc-router.test.ts` | 2 files and 8 tests passed |
Proves zero initialization, per-method and total arithmetic, zero-valued
usage leaves counters unchanged, snapshots are detached, and the metrics
RPC routes successfully. |
| `pnpm --filter ./packages/server test` | Server build passed; 24 files
passed with 183 tests passing and 11 existing TODOs | Covers the full
server unit suite around the changed controller, runtime, and
accumulator behavior. |
| `pnpm check` | 8 tasks completed successfully | Covers formatting,
lint, type checking, package builds, and documentation validation. |
| `go -C packages/sdk-go run ./internal/extensionpack --check` | Exited
successfully with no output | Proves the committed Go extension archive
exactly matches the rebuilt server extension. |

## Scope

This preserves the existing public `StagehandMetrics` result shape and
SDK methods; it only implements the previously stubbed server behavior.
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.

3 participants