@evolvingmachines/evolve 0.0.55-project-sable.20260916.db0bc0bb → 0.0.56
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.
- package/dist/cli/index.cjs +32 -32
- package/dist/cli/index.js +27 -27
- package/package.json +4 -6
- package/skills/evolve/SKILL.md +4 -3
- package/skills/evolve-evals/SKILL.md +112 -0
- package/{docs-evals → skills/evolve-evals/references}/cli-reference/run.mdx +1 -1
- package/{docs-evals → skills/evolve-evals/references}/cli-reference/skills.mdx +4 -4
- package/{docs-evals → skills/evolve-evals/references}/core-concepts/agents.mdx +2 -2
- package/{docs-evals → skills/evolve-evals/references}/core-concepts/analyze.mdx +1 -1
- package/{docs-evals → skills/evolve-evals/references}/core-concepts/jobs.mdx +3 -3
- package/{docs-evals → skills/evolve-evals/references}/core-concepts/models.mdx +1 -1
- package/{docs-evals → skills/evolve-evals/references}/core-concepts/sandboxes.mdx +2 -2
- package/{docs-evals → skills/evolve-evals/references}/core-concepts/secrets.mdx +1 -1
- package/{docs-evals → skills/evolve-evals/references}/core-concepts/trial-outputs.mdx +1 -1
- package/{docs-evals → skills/evolve-evals/references}/getting-started/quick-start.mdx +1 -1
- package/{docs-evals → skills/evolve-evals/references}/index.mdx +1 -1
- package/{docs-evals → skills/evolve-evals/references}/sdk/python.mdx +1 -1
- package/{docs-evals → skills/evolve-evals/references}/sdk/typescript.mdx +1 -1
- package/{docs-evals → skills/evolve-evals/references}/sdk-reference/errors.mdx +2 -2
- package/{docs-evals → skills/evolve-evals/references}/sdk-reference/jobs.mdx +2 -2
- package/docs-agents/SKILL.md +0 -138
- package/docs-agents/index.md +0 -81
- package/docs-agents/python/01-getting-started.md +0 -511
- package/docs-agents/python/02-configuration.md +0 -855
- package/docs-agents/python/03-runtime.md +0 -899
- package/docs-agents/python/04-streaming.md +0 -451
- package/docs-agents/python/05-swarm-pipeline.md +0 -953
- package/docs-agents/python/index.md +0 -122
- package/docs-agents/typescript/01-getting-started.md +0 -494
- package/docs-agents/typescript/02-configuration.md +0 -841
- package/docs-agents/typescript/03-runtime.md +0 -864
- package/docs-agents/typescript/04-streaming.md +0 -478
- package/docs-agents/typescript/05-swarm-pipeline.md +0 -957
- package/docs-agents/typescript/index.md +0 -121
- package/docs-evals/SKILL.md +0 -112
- package/docs-evals/docs.json +0 -180
- /package/{docs-evals → skills/evolve-evals/references}/cli-reference/agent.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/cli-reference/analysis.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/cli-reference/analyze.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/cli-reference/auth.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/cli-reference/check.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/cli-reference/dataset.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/cli-reference/index.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/cli-reference/job.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/cli-reference/secrets.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/cli-reference/skill.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/cli-reference/trial.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/cli-reference/upload.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/core-concepts/check.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/core-concepts/datasets.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/core-concepts/index.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/core-concepts/skills.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/core-concepts/tasks.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/core-concepts/trials.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/core-concepts/upload.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/dashboard/checks.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/dashboard/datasets.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/dashboard/jobs.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/dashboard/traces.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/dashboard/trial-viewer.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/getting-started/installation.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/sdk-reference/agents.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/sdk-reference/analyses.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/sdk-reference/auth.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/sdk-reference/checks.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/sdk-reference/datasets.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/sdk-reference/index.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/sdk-reference/meta.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/sdk-reference/skills.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/sdk-reference/trials.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/sdk-reference/types.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/snippets/global-options.mdx +0 -0
- /package/{docs-evals → skills/evolve-evals/references}/snippets/list-flags.mdx +0 -0
|
@@ -1,451 +0,0 @@
|
|
|
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.
|