@evolvingmachines/evolve 0.0.55-project-sable.20260916.db0bc0bb

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +25 -0
  3. package/dist/chunk-5GVMBEYI.js +11 -0
  4. package/dist/chunk-HQJI3HHU.js +1 -0
  5. package/dist/chunk-JS2UTK2I.js +427 -0
  6. package/dist/cli/index.cjs +73 -0
  7. package/dist/cli/index.d.cts +270 -0
  8. package/dist/cli/index.d.ts +270 -0
  9. package/dist/cli/index.js +54 -0
  10. package/dist/index.cjs +669 -0
  11. package/dist/index.d.cts +5023 -0
  12. package/dist/index.d.ts +5023 -0
  13. package/dist/index.js +230 -0
  14. package/dist/managed-modal-IJW62SSK.js +4 -0
  15. package/dist/resumable-HFSICSH3.js +1 -0
  16. package/dist/tar-GVC4KE52.js +1 -0
  17. package/dist/types-DlpTxdR_.d.cts +4777 -0
  18. package/dist/types-DlpTxdR_.d.ts +4777 -0
  19. package/dist/upload-KPYDMWFJ.js +1 -0
  20. package/docs-agents/SKILL.md +138 -0
  21. package/docs-agents/index.md +81 -0
  22. package/docs-agents/python/01-getting-started.md +511 -0
  23. package/docs-agents/python/02-configuration.md +855 -0
  24. package/docs-agents/python/03-runtime.md +899 -0
  25. package/docs-agents/python/04-streaming.md +451 -0
  26. package/docs-agents/python/05-swarm-pipeline.md +953 -0
  27. package/docs-agents/python/index.md +122 -0
  28. package/docs-agents/typescript/01-getting-started.md +494 -0
  29. package/docs-agents/typescript/02-configuration.md +841 -0
  30. package/docs-agents/typescript/03-runtime.md +864 -0
  31. package/docs-agents/typescript/04-streaming.md +478 -0
  32. package/docs-agents/typescript/05-swarm-pipeline.md +957 -0
  33. package/docs-agents/typescript/index.md +121 -0
  34. package/docs-evals/SKILL.md +112 -0
  35. package/docs-evals/cli-reference/agent.mdx +67 -0
  36. package/docs-evals/cli-reference/analysis.mdx +91 -0
  37. package/docs-evals/cli-reference/analyze.mdx +74 -0
  38. package/docs-evals/cli-reference/auth.mdx +60 -0
  39. package/docs-evals/cli-reference/check.mdx +146 -0
  40. package/docs-evals/cli-reference/dataset.mdx +133 -0
  41. package/docs-evals/cli-reference/index.mdx +108 -0
  42. package/docs-evals/cli-reference/job.mdx +239 -0
  43. package/docs-evals/cli-reference/run.mdx +196 -0
  44. package/docs-evals/cli-reference/secrets.mdx +95 -0
  45. package/docs-evals/cli-reference/skill.mdx +51 -0
  46. package/docs-evals/cli-reference/skills.mdx +81 -0
  47. package/docs-evals/cli-reference/trial.mdx +112 -0
  48. package/docs-evals/cli-reference/upload.mdx +36 -0
  49. package/docs-evals/core-concepts/agents.mdx +106 -0
  50. package/docs-evals/core-concepts/analyze.mdx +86 -0
  51. package/docs-evals/core-concepts/check.mdx +51 -0
  52. package/docs-evals/core-concepts/datasets.mdx +138 -0
  53. package/docs-evals/core-concepts/index.mdx +42 -0
  54. package/docs-evals/core-concepts/jobs.mdx +136 -0
  55. package/docs-evals/core-concepts/models.mdx +68 -0
  56. package/docs-evals/core-concepts/sandboxes.mdx +52 -0
  57. package/docs-evals/core-concepts/secrets.mdx +41 -0
  58. package/docs-evals/core-concepts/skills.mdx +42 -0
  59. package/docs-evals/core-concepts/tasks.mdx +216 -0
  60. package/docs-evals/core-concepts/trial-outputs.mdx +103 -0
  61. package/docs-evals/core-concepts/trials.mdx +103 -0
  62. package/docs-evals/core-concepts/upload.mdx +45 -0
  63. package/docs-evals/dashboard/checks.mdx +15 -0
  64. package/docs-evals/dashboard/datasets.mdx +24 -0
  65. package/docs-evals/dashboard/jobs.mdx +65 -0
  66. package/docs-evals/dashboard/traces.mdx +15 -0
  67. package/docs-evals/dashboard/trial-viewer.mdx +30 -0
  68. package/docs-evals/docs.json +180 -0
  69. package/docs-evals/getting-started/installation.mdx +56 -0
  70. package/docs-evals/getting-started/quick-start.mdx +85 -0
  71. package/docs-evals/index.mdx +36 -0
  72. package/docs-evals/sdk/python.mdx +139 -0
  73. package/docs-evals/sdk/typescript.mdx +141 -0
  74. package/docs-evals/sdk-reference/agents.mdx +105 -0
  75. package/docs-evals/sdk-reference/analyses.mdx +125 -0
  76. package/docs-evals/sdk-reference/auth.mdx +76 -0
  77. package/docs-evals/sdk-reference/checks.mdx +166 -0
  78. package/docs-evals/sdk-reference/datasets.mdx +305 -0
  79. package/docs-evals/sdk-reference/errors.mdx +199 -0
  80. package/docs-evals/sdk-reference/index.mdx +58 -0
  81. package/docs-evals/sdk-reference/jobs.mdx +731 -0
  82. package/docs-evals/sdk-reference/meta.mdx +52 -0
  83. package/docs-evals/sdk-reference/skills.mdx +68 -0
  84. package/docs-evals/sdk-reference/trials.mdx +232 -0
  85. package/docs-evals/sdk-reference/types.mdx +272 -0
  86. package/docs-evals/snippets/global-options.mdx +13 -0
  87. package/docs-evals/snippets/list-flags.mdx +23 -0
  88. package/harness-capabilities.json +430 -0
  89. package/hosted-error-codes.json +120 -0
  90. package/package.json +163 -0
  91. package/skills/create-adapter/SKILL.md +287 -0
  92. package/skills/create-task/SKILL.md +491 -0
  93. package/skills/evolve/SKILL.md +39 -0
  94. package/skills/publish/SKILL.md +142 -0
  95. package/skills/rewardkit/SKILL.md +330 -0
  96. package/spec/atif/README.md +45 -0
  97. package/spec/atif/trajectory.schema.json +865 -0
  98. package/spec/openapi.yaml +13447 -0
@@ -0,0 +1,451 @@
1
+ # Streaming Events
2
+
3
+ Real-time output from `run()` and `execute_command()`. For basic usage, see [Getting Started](./01-getting-started.md#streaming).
4
+
5
+ ---
6
+
7
+ ## Event Listeners
8
+
9
+ Both `run()` and `execute_command()` stream output in real-time:
10
+
11
+ ```python
12
+ from evolve import Evolve, AgentConfig
13
+
14
+ evolve = Evolve(config=AgentConfig(type='claude'))
15
+
16
+ # Parsed events (recommended)
17
+ evolve.on('content', lambda event: print(event['update']['sessionUpdate']))
18
+ evolve.on('lifecycle', lambda event: print(event['reason'], event['sandbox']))
19
+
20
+ # Raw output (debugging)
21
+ evolve.on('stdout', lambda data: print(data, end=''))
22
+ evolve.on('stderr', lambda data: print(f'[ERR] {data}', end=''))
23
+
24
+ await evolve.run(prompt='Hello')
25
+ ```
26
+
27
+ | Event | Type | Description |
28
+ |-------|------|-------------|
29
+ | `content` | `OutputEvent` | Parsed ACP-style events (recommended) |
30
+ | `lifecycle` | `dict` (`LifecycleEvent` shape below) | Sandbox and agent state transitions |
31
+ | `stdout` | `str` | Raw JSONL output |
32
+ | `stderr` | `str` | Error output |
33
+
34
+ `evolve.on(...)` supports only: `stdout`, `stderr`, `content`, `lifecycle`.
35
+ Passing any other event name raises `ValueError`.
36
+
37
+ ---
38
+
39
+ ## LifecycleEvent (TypedDict shape)
40
+
41
+ ```python
42
+ class LifecycleEvent(TypedDict):
43
+ sandbox_id: str | None
44
+ sandbox: Literal["booting", "error", "ready", "running", "paused", "stopped"]
45
+ agent: Literal["idle", "running", "interrupted", "error"]
46
+ timestamp: str
47
+ browser: NotRequired[dict[str, str]] # live_url/session_id/session_tag
48
+ reason: Literal[
49
+ "browser_ready",
50
+ "sandbox_boot",
51
+ "sandbox_ready",
52
+ "sandbox_connected",
53
+ "sandbox_pause",
54
+ "sandbox_resume",
55
+ "sandbox_killed",
56
+ "sandbox_error",
57
+ "run_start",
58
+ "run_complete",
59
+ "run_interrupted",
60
+ "run_failed",
61
+ "run_background_complete",
62
+ "run_background_failed",
63
+ "command_start",
64
+ "command_complete",
65
+ "command_interrupted",
66
+ "command_failed",
67
+ "command_background_complete",
68
+ "command_background_failed",
69
+ ]
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Type Definitions
75
+
76
+ Use these `TypedDict` definitions for type hints:
77
+
78
+ ```python
79
+ from typing import Any, TypedDict, Literal, Union, NotRequired
80
+
81
+ # =============================================================================
82
+ # Content Types
83
+ # =============================================================================
84
+
85
+ class TextContent(TypedDict):
86
+ type: Literal["text"]
87
+ text: str
88
+
89
+ class ImageContent(TypedDict):
90
+ type: Literal["image"]
91
+ data: str # Base64-encoded
92
+ mimeType: str # "image/png", "image/jpeg"
93
+ uri: NotRequired[str]
94
+
95
+ ContentBlock = Union[TextContent, ImageContent]
96
+
97
+ class DiffContent(TypedDict):
98
+ type: Literal["diff"]
99
+ path: str
100
+ oldText: str | None # None for new files
101
+ newText: str
102
+
103
+ class WrappedContent(TypedDict):
104
+ type: Literal["content"]
105
+ content: ContentBlock
106
+
107
+ ToolCallContent = Union[WrappedContent, DiffContent]
108
+
109
+ # =============================================================================
110
+ # Tool Types
111
+ # =============================================================================
112
+
113
+ ToolKind = Literal[
114
+ "read", # Read, NotebookRead
115
+ "edit", # Edit, Write, NotebookEdit
116
+ "delete", # (future)
117
+ "move", # (future)
118
+ "search", # Glob, Grep, LS
119
+ "execute", # Bash, BashOutput, KillShell
120
+ "think", # Task (subagent)
121
+ "fetch", # WebFetch, WebSearch
122
+ "switch_mode", # ExitPlanMode
123
+ "other", # Unknown or third-party MCP tools
124
+ ]
125
+
126
+ ToolCallStatus = Literal["pending", "in_progress", "completed", "failed"]
127
+
128
+ class ToolCallLocation(TypedDict):
129
+ path: str
130
+ line: NotRequired[int]
131
+
132
+ # =============================================================================
133
+ # Session Update Types
134
+ # =============================================================================
135
+
136
+ class AgentMessageChunk(TypedDict):
137
+ sessionUpdate: Literal["agent_message_chunk"]
138
+ content: ContentBlock
139
+
140
+ class AgentThoughtChunk(TypedDict):
141
+ sessionUpdate: Literal["agent_thought_chunk"]
142
+ content: ContentBlock
143
+
144
+ class UserMessageChunk(TypedDict):
145
+ sessionUpdate: Literal["user_message_chunk"]
146
+ content: ContentBlock
147
+
148
+ class ToolCall(TypedDict):
149
+ sessionUpdate: Literal["tool_call"]
150
+ toolCallId: str
151
+ title: str
152
+ toolName: NotRequired[str] # harness-native tool name, e.g. "mcp__mcp-server__get_secret"
153
+ kind: ToolKind
154
+ status: ToolCallStatus
155
+ rawInput: NotRequired[dict]
156
+ content: NotRequired[list[ToolCallContent]]
157
+ locations: NotRequired[list[ToolCallLocation]]
158
+
159
+ class ToolCallUpdate(TypedDict):
160
+ sessionUpdate: Literal["tool_call_update"]
161
+ toolCallId: str
162
+ status: NotRequired[ToolCallStatus]
163
+ title: NotRequired[str]
164
+ content: NotRequired[list[ToolCallContent]]
165
+ locations: NotRequired[list[ToolCallLocation]]
166
+ rawOutput: NotRequired[Any] # the harness's own structured record of the result, verbatim
167
+
168
+ PlanEntryStatus = Literal["pending", "in_progress", "completed"]
169
+
170
+ class PlanEntry(TypedDict):
171
+ content: str
172
+ status: PlanEntryStatus
173
+ priority: Literal["high", "medium", "low"]
174
+
175
+ class Plan(TypedDict):
176
+ sessionUpdate: Literal["plan"]
177
+ entries: list[PlanEntry]
178
+
179
+ class AgentError(TypedDict):
180
+ sessionUpdate: Literal["error"]
181
+ message: str # the harness's own message, verbatim
182
+ fatal: bool # True when the harness treated it as terminal for the turn
183
+
184
+ class TokenUsage(TypedDict, total=False):
185
+ promptTokens: int # input INCLUDING the cached and cache-written shares
186
+ completionTokens: int
187
+ cachedTokens: int # the cache-read share of promptTokens
188
+ costUsd: float # only when the harness priced it
189
+ extra: dict[str, Any] # the harness's other counters, its own key names verbatim
190
+
191
+ class AgentUsage(TypedDict):
192
+ sessionUpdate: Literal["usage"]
193
+ scope: Literal["call", "run"] # one LLM inference, or the harness's whole-run total
194
+ usage: TokenUsage
195
+
196
+ SessionUpdate = Union[
197
+ AgentMessageChunk,
198
+ AgentThoughtChunk,
199
+ UserMessageChunk,
200
+ ToolCall,
201
+ ToolCallUpdate,
202
+ Plan,
203
+ AgentError,
204
+ AgentUsage,
205
+ ]
206
+
207
+ # =============================================================================
208
+ # Top-Level Event
209
+ # =============================================================================
210
+
211
+ class OutputEvent(TypedDict):
212
+ sessionId: NotRequired[str]
213
+ update: SessionUpdate
214
+ timestamp: NotRequired[str] # the harness's own clock for this line, ISO 8601
215
+ model: NotRequired[str] # the model the harness named for this line
216
+ messageId: NotRequired[str] # the harness's id for the LLM message this line belongs to
217
+ parentToolCallId: NotRequired[str] # on a SUBAGENT's line: the parent's tool call that delegated to it
218
+ extra: NotRequired[dict[str, Any]] # other facts of the line, the harness's own key names (e.g. stop_reason)
219
+ ```
220
+
221
+ Everything beyond `update` is optional and comes straight from the wire line the update was parsed
222
+ from — a field the harness did not print is absent, never guessed. `timestamp` is the harness's
223
+ clock (claude, gemini, opencode and droid stamp every line; qwen and kimi stamp none); `model` is
224
+ the model named on the line, or on the harness's init line for gemini and droid; `messageId` lets
225
+ you tell which lines belong to one LLM message (claude prints one line per content block, all with
226
+ the same `message.id`); `parentToolCallId` is set only on a subagent's lines and names the
227
+ `toolCallId` of the `Task`/`agent` call that spawned it.
228
+
229
+ `toolName` is the harness-native tool name, verbatim — `Bash`, `Read`, or the joined `mcp__<server>__<tool>` an MCP call carries. Prefer it over parsing `title`, which is formatted per tool for people to read and is not round-trippable; `toolName` is the identifier the model actually called. It is a deliberate addition to the ACP shape, which names no tool and whose `kind` collapses every MCP tool to `other`, and it is optional — absent on traces recorded before the SDK carried it, and on the occasional call a harness cannot name, so fall back to `kind` there.
230
+
231
+ `content` is the result text exactly as the harness sent it — a failed call's error text is not
232
+ wrapped in a code fence or prefixed; frame it in your own UI. `rawOutput` is the harness's
233
+ structured record of the same result when it prints one beyond the text: claude's
234
+ `tool_use_result` (`stdout`, `stderr`, `exitCode`, `interrupted`, or the file it wrote), codex's
235
+ completed item (`aggregated_output`, `exit_code`, `status`), opencode's tool state (`output`,
236
+ `metadata` with the exit code, `time`). Read an exit code from there rather than from prose.
237
+
238
+ ---
239
+
240
+ ## Browser Automation Streaming
241
+
242
+ The full browser guide is [Configuration → Browser Automation](./02-configuration.md#browser-automation).
243
+ This section only documents the streaming fields for browser live view.
244
+
245
+ | Need | API | Use |
246
+ |------|-----|-----|
247
+ | Show live browser during a run | `lifecycle` event with `reason == "browser_ready"` | `event["browser"]["live_url"]` |
248
+ | Save the browser/session id | same lifecycle event | `event["browser"]["session_id"]` |
249
+
250
+ ### Managed Browser
251
+
252
+ Managed browser sessions emit the live-view URL as soon as the browser is ready:
253
+
254
+ ```python
255
+ def on_lifecycle(event):
256
+ if event['reason'] == 'browser_ready' and event.get('browser'):
257
+ open_live_view(event['browser']['live_url'])
258
+ remember_session_id(event['browser']['session_id'])
259
+
260
+ evolve.on('lifecycle', on_lifecycle)
261
+ ```
262
+
263
+ The same URL is also stored in trace metadata for replay or embedding after the trace exists:
264
+
265
+ ```python
266
+ TraceMetadata = {
267
+ "browser_session_id": "...",
268
+ "dashboard_session_id": "...",
269
+ "browser_session_tag": "...",
270
+ "browser_live_url": "...",
271
+ }
272
+ ```
273
+
274
+ Use `event["browser"]["live_url"]` or `result.browser["live_url"]` for immediate
275
+ UI display. For replay after cleanup, use the `session_id` with
276
+ `sessions().browser_replay()`; the full example lives in
277
+ [Configuration → Browser Automation](./02-configuration.md#browser-automation).
278
+
279
+ ---
280
+
281
+ ## Event Types Summary
282
+
283
+ | Type | `sessionUpdate` | Description |
284
+ |------|-----------------|-------------|
285
+ | `AgentMessageChunk` | `"agent_message_chunk"` | Text/image streaming from agent |
286
+ | `AgentThoughtChunk` | `"agent_thought_chunk"` | Reasoning (Codex) or thinking (Claude) |
287
+ | `UserMessageChunk` | `"user_message_chunk"` | User message echo (Gemini) |
288
+ | `ToolCall` | `"tool_call"` | Tool execution started |
289
+ | `ToolCallUpdate` | `"tool_call_update"` | Tool execution finished |
290
+ | `Plan` | `"plan"` | TodoWrite updates (replaces entire list) |
291
+ | `AgentError` | `"error"` | A failure the HARNESS reported. **Not agent work** — see below |
292
+ | `AgentUsage` | `"usage"` | Token accounting the HARNESS reported. **Not agent work** — see below |
293
+
294
+ ---
295
+
296
+ ## ToolKind Reference
297
+
298
+ | Kind | Tools | Icon |
299
+ |------|-------|------|
300
+ | `read` | Read, NotebookRead | :page_facing_up: |
301
+ | `edit` | Edit, Write, NotebookEdit | :pencil2: |
302
+ | `search` | Glob, Grep, LS | :mag: |
303
+ | `execute` | Bash, BashOutput, KillShell | :zap: |
304
+ | `think` | Task (subagent) | :brain: |
305
+ | `fetch` | WebFetch, WebSearch | :globe_with_meridians: |
306
+ | `switch_mode` | ExitPlanMode | :twisted_rightwards_arrows: |
307
+ | `other` | MCP tools, unknown | :grey_question: |
308
+
309
+ ---
310
+
311
+ ## UI Integration Example
312
+
313
+ ```python
314
+ from typing import cast
315
+
316
+ def handle_event(event: OutputEvent) -> None:
317
+ update = event["update"]
318
+ event_type = update["sessionUpdate"]
319
+
320
+ if event_type == "agent_message_chunk":
321
+ msg = cast(AgentMessageChunk, update)
322
+ if msg["content"]["type"] == "text":
323
+ ui.append_message(msg["content"]["text"])
324
+ else:
325
+ img = cast(ImageContent, msg["content"])
326
+ ui.append_image(img["data"], img["mimeType"])
327
+
328
+ elif event_type == "agent_thought_chunk":
329
+ thought = cast(AgentThoughtChunk, update)
330
+ ui.append_thought(thought["content"])
331
+
332
+ elif event_type == "user_message_chunk":
333
+ # Gemini echo - typically ignored
334
+ pass
335
+
336
+ elif event_type == "tool_call":
337
+ tool = cast(ToolCall, update)
338
+ ui.add_tool(
339
+ id=tool["toolCallId"],
340
+ title=tool["title"],
341
+ kind=tool["kind"],
342
+ status=tool["status"],
343
+ locations=tool.get("locations"),
344
+ )
345
+
346
+ elif event_type == "tool_call_update":
347
+ update_data = cast(ToolCallUpdate, update)
348
+ ui.update_tool(
349
+ update_data["toolCallId"],
350
+ status=update_data.get("status"),
351
+ content=update_data.get("content"),
352
+ )
353
+
354
+ elif event_type == "plan":
355
+ plan = cast(Plan, update)
356
+ ui.render_plan(plan["entries"])
357
+
358
+ evolve.on("content", handle_event)
359
+ ```
360
+
361
+ ---
362
+
363
+ ## Key Patterns
364
+
365
+ 1. **Handle all 6 event types** — Don't silently drop unknown events
366
+ 2. **Match tools by ID** — `tool_call` and `tool_call_update` share `toolCallId`
367
+ 3. **Handle out-of-order** — `tool_call_update` may arrive before `tool_call`
368
+ 4. **Concatenate chunks** — Message text arrives incrementally
369
+ 5. **Support images** — `ContentBlock` includes `ImageContent`
370
+ 6. **Use `kind` for icons** — Categorize tools visually (read, edit, execute, etc.)
371
+ 7. **Identify tools by `toolName`** — The harness-native name, not the human-readable `title`; fall back to `kind` when it is absent
372
+ 8. **Track `locations`** — Show affected file paths in UI
373
+ 9. **Use `cast()` for narrowing** — TypedDict unions need explicit casting after checking `sessionUpdate`
374
+
375
+ ---
376
+
377
+
378
+ ## Harness-reported failures (`error`)
379
+
380
+ A harness can fail without the process dying, and it reports that on the same stream it uses for
381
+ output. Codex, for example, writes `{"type": "error"}` while it retries and
382
+ `{"type": "turn.failed"}` when a turn gives up — on **stdout**, while stderr says only
383
+ `Reading prompt from stdin...`. Those are surfaced as their own update so a transcript shows what
384
+ actually happened:
385
+
386
+ ```python
387
+ {
388
+ "update": {
389
+ "sessionUpdate": "error",
390
+ "message": "stream disconnected before completion: ...", # the harness's own words
391
+ "fatal": False, # True when the harness treated it as terminal for the turn
392
+ }
393
+ }
394
+ ```
395
+
396
+ Python receives events as plain dicts (there is no typed union to import, unlike TypeScript), so
397
+ this arrives as `event["update"]["sessionUpdate"] == "error"`.
398
+
399
+ **It is deliberately not a message chunk.** If you are counting "did the agent do any work", an
400
+ error must not count — otherwise a run that never reached the model looks like a run that produced
401
+ output:
402
+
403
+ ```python
404
+ def did_work(events):
405
+ return any(e.get("update", {}).get("sessionUpdate") not in ("error", "usage") for e in events)
406
+ ```
407
+
408
+ ## Harness-reported usage (`usage`)
409
+
410
+ Every harness prints its own token accounting on the stream, and it arrives as its own update so
411
+ you can meter a run without reading the raw JSON: claude and qwen print each LLM message's usage,
412
+ opencode prints each step's tokens and cost, and codex, gemini, claude, qwen and droid print a
413
+ whole-run total on their terminal line. Kimi's stream-json prints no usage at all, so a kimi run
414
+ simply has no `usage` events.
415
+
416
+ ```python
417
+ {
418
+ "messageId": "msg_01...", # claude and qwen: the LLM message this line belongs to
419
+ "update": {
420
+ "sessionUpdate": "usage",
421
+ "scope": "call", # "call": one LLM inference; "run": the harness's total for the whole run
422
+ "usage": {
423
+ "promptTokens": 150, # input INCLUDING the cached and cache-written shares
424
+ "completionTokens": 7,
425
+ "cachedTokens": 30, # the cache-read share of promptTokens
426
+ "extra": {"cache_creation_input_tokens": 20, "service_tier": "standard"},
427
+ },
428
+ },
429
+ }
430
+ ```
431
+
432
+ The names are Harbor's ATIF `Metrics` fields, so a trajectory copies them without renaming. A
433
+ counter the harness did not print is absent, never `0`. Two things to know when you sum:
434
+
435
+ - A `"call"` event repeats for every line of the same `messageId` (claude prints one line per
436
+ content block, each with the message's running usage) — keep the **last** one per `messageId`,
437
+ then add across messages.
438
+ - A `"run"` event is the harness's own total, reported once at the end; it is not another call.
439
+
440
+ ```python
441
+ per_message: dict[str, dict] = {}
442
+ for e in events:
443
+ update = e.get("update", {})
444
+ if update.get("sessionUpdate") != "usage" or update.get("scope") != "call":
445
+ continue
446
+ per_message[e.get("messageId", f"line-{len(per_message)}")] = update["usage"]
447
+ prompt_tokens = sum(u.get("promptTokens", 0) for u in per_message.values())
448
+ ```
449
+
450
+ Like `error`, `usage` is **not agent work**: a stream that carries only accounting still counts as a
451
+ run that did nothing.