docs: restructure docs into diataxis (#121)
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
# Codex `exec --json` event cheatsheet
|
||||
|
||||
`codex exec --json` writes **one JSON object per line** (JSONL) to stdout. Each
|
||||
line is a top-level **thread event** with a `type` field.
|
||||
|
||||
Below: **required + commonly emitted fields** for every line type plus a
|
||||
**full-line example** for each shape that can be emitted. Fields noted as
|
||||
optional may be omitted (or `null`) depending on Codex version and lifecycle.
|
||||
Unknown fields may appear; ignore what you don't use.
|
||||
|
||||
## Top-level event lines (non-item)
|
||||
|
||||
### `thread.started`
|
||||
|
||||
Fields:
|
||||
- `type`
|
||||
- `thread_id`
|
||||
|
||||
Example:
|
||||
```json
|
||||
{"type":"thread.started","thread_id":"0199a213-81c0-7800-8aa1-bbab2a035a53"}
|
||||
```
|
||||
|
||||
### `turn.started`
|
||||
|
||||
Fields:
|
||||
- `type`
|
||||
|
||||
Example:
|
||||
```json
|
||||
{"type":"turn.started"}
|
||||
```
|
||||
|
||||
### `turn.completed`
|
||||
|
||||
Fields:
|
||||
- `type`
|
||||
- `usage.input_tokens`
|
||||
- `usage.cached_input_tokens`
|
||||
- `usage.output_tokens`
|
||||
|
||||
Example:
|
||||
```json
|
||||
{"type":"turn.completed","usage":{"input_tokens":24763,"cached_input_tokens":24448,"output_tokens":122}}
|
||||
```
|
||||
|
||||
### `turn.failed`
|
||||
|
||||
Fields:
|
||||
- `type`
|
||||
- `error.message`
|
||||
|
||||
Example:
|
||||
```json
|
||||
{"type":"turn.failed","error":{"message":"model response stream ended unexpectedly"}}
|
||||
```
|
||||
|
||||
### `error`
|
||||
|
||||
Fields:
|
||||
- `type`
|
||||
- `message`
|
||||
|
||||
Example:
|
||||
```json
|
||||
{"type":"error","message":"stream error: broken pipe"}
|
||||
```
|
||||
|
||||
Note: Codex may emit transient reconnect notices as `type="error"` with messages
|
||||
like `"Reconnecting... 1/5"` while it retries a dropped stream. Treat those as
|
||||
non-fatal progress updates (the turn continues).
|
||||
|
||||
## Item event lines (`item.*`)
|
||||
|
||||
Every item line includes:
|
||||
- `type` (`item.started`, `item.updated`, or `item.completed`)
|
||||
- `item.id`
|
||||
- `item.type`
|
||||
- fields for the specific `item.type` below
|
||||
|
||||
`item.id` is stable for the item; updates/completion reuse the same id.
|
||||
|
||||
### `agent_message` (only `item.completed`)
|
||||
|
||||
Fields:
|
||||
- `item.text`
|
||||
|
||||
Example:
|
||||
```json
|
||||
{"type":"item.completed","item":{"id":"item_3","type":"agent_message","text":"Done. I updated the docs and added examples."}}
|
||||
```
|
||||
|
||||
### `reasoning` (only `item.completed`, if enabled)
|
||||
|
||||
Fields:
|
||||
- `item.text`
|
||||
|
||||
Example:
|
||||
```json
|
||||
{"type":"item.completed","item":{"id":"item_0","type":"reasoning","text":"**Scanning docs for exec JSON schema**"}}
|
||||
```
|
||||
|
||||
### `command_execution` (`item.started` and `item.completed`)
|
||||
|
||||
Fields:
|
||||
- `item.command`
|
||||
- `item.aggregated_output`
|
||||
- `item.exit_code` (null or omitted until completion)
|
||||
- `item.status` (`in_progress`, `completed`, `failed`)
|
||||
|
||||
Example (started):
|
||||
```json
|
||||
{"type":"item.started","item":{"id":"item_1","type":"command_execution","command":"bash -lc ls","aggregated_output":"","exit_code":null,"status":"in_progress"}}
|
||||
```
|
||||
|
||||
Example (completed, success):
|
||||
```json
|
||||
{"type":"item.completed","item":{"id":"item_1","type":"command_execution","command":"bash -lc ls","aggregated_output":"docs\nsrc\n","exit_code":0,"status":"completed"}}
|
||||
```
|
||||
|
||||
Example (completed, failure):
|
||||
```json
|
||||
{"type":"item.completed","item":{"id":"item_2","type":"command_execution","command":"bash -lc false","aggregated_output":"","exit_code":1,"status":"failed"}}
|
||||
```
|
||||
|
||||
Note: `aggregated_output` is truncated to **64 KiB**; truncated output ends with
|
||||
`\n...(truncated)`.
|
||||
|
||||
### `file_change` (only `item.completed`)
|
||||
|
||||
Fields:
|
||||
- `item.changes[].path`
|
||||
- `item.changes[].kind` (`add`, `delete`, `update`)
|
||||
- `item.status` (`completed`, `failed`)
|
||||
|
||||
Example:
|
||||
```json
|
||||
{"type":"item.completed","item":{"id":"item_4","type":"file_change","changes":[{"path":"docs/exec-json-cheatsheet.md","kind":"add"},{"path":"docs/exec.md","kind":"update"}],"status":"completed"}}
|
||||
```
|
||||
|
||||
### `mcp_tool_call` (`item.started` and `item.completed`)
|
||||
|
||||
Fields:
|
||||
- `item.server`
|
||||
- `item.tool`
|
||||
- `item.arguments` (JSON value; defaults to `null` if absent)
|
||||
- `item.result` (object or `null`; may be omitted)
|
||||
- `item.result.content` (array of MCP content blocks)
|
||||
- `item.result.structured_content` (JSON value or `null`)
|
||||
- `item.error` (object or `null`; may be omitted)
|
||||
- `item.error.message` (if `error` is present)
|
||||
- `item.status` (`in_progress`, `completed`, `failed`)
|
||||
|
||||
Example (started):
|
||||
```json
|
||||
{"type":"item.started","item":{"id":"item_5","type":"mcp_tool_call","server":"docs","tool":"search","arguments":{"q":"exec --json"},"result":null,"error":null,"status":"in_progress"}}
|
||||
```
|
||||
|
||||
Example (completed, success):
|
||||
```json
|
||||
{"type":"item.completed","item":{"id":"item_5","type":"mcp_tool_call","server":"docs","tool":"search","arguments":{"q":"exec --json"},"result":{"content":[{"type":"text","text":"Found 3 matches.","annotations":{"audience":["assistant"],"lastModified":"2025-01-01T00:00:00Z","priority":0.5}}],"structured_content":{"matches":3}},"error":null,"status":"completed"}}
|
||||
```
|
||||
|
||||
Example (completed, failure):
|
||||
```json
|
||||
{"type":"item.completed","item":{"id":"item_6","type":"mcp_tool_call","server":"docs","tool":"search","arguments":{"q":"exec --json"},"result":null,"error":{"message":"tool timeout"},"status":"failed"}}
|
||||
```
|
||||
|
||||
### `web_search` (only `item.completed`)
|
||||
|
||||
Fields:
|
||||
- `item.query`
|
||||
|
||||
Example:
|
||||
```json
|
||||
{"type":"item.completed","item":{"id":"item_7","type":"web_search","query":"codex exec --json schema"}}
|
||||
```
|
||||
|
||||
### `todo_list` (`item.started`, `item.updated`, and `item.completed`)
|
||||
|
||||
Fields:
|
||||
- `item.items[].text`
|
||||
- `item.items[].completed`
|
||||
|
||||
Example (started):
|
||||
```json
|
||||
{"type":"item.started","item":{"id":"item_8","type":"todo_list","items":[{"text":"Scan docs","completed":false},{"text":"Write cheatsheet","completed":false}]}}
|
||||
```
|
||||
|
||||
Example (updated):
|
||||
```json
|
||||
{"type":"item.updated","item":{"id":"item_8","type":"todo_list","items":[{"text":"Scan docs","completed":true},{"text":"Write cheatsheet","completed":false}]}}
|
||||
```
|
||||
|
||||
Example (completed):
|
||||
```json
|
||||
{"type":"item.completed","item":{"id":"item_8","type":"todo_list","items":[{"text":"Scan docs","completed":true},{"text":"Write cheatsheet","completed":true}]}}
|
||||
```
|
||||
|
||||
### `error` (non-fatal warning as an item; only `item.completed`)
|
||||
|
||||
Fields:
|
||||
- `item.message`
|
||||
|
||||
Example:
|
||||
```json
|
||||
{"type":"item.completed","item":{"id":"item_9","type":"error","message":"command output truncated"}}
|
||||
```
|
||||
|
||||
## MCP content block shapes (`mcp_tool_call.result.content`)
|
||||
|
||||
`result.content` is an array of **content blocks**. Each block is one of the
|
||||
types below; all optional fields may appear depending on the server.
|
||||
|
||||
### Text content
|
||||
|
||||
Fields:
|
||||
- `type`
|
||||
- `text`
|
||||
- `annotations.audience` (optional)
|
||||
- `annotations.lastModified` (optional)
|
||||
- `annotations.priority` (optional)
|
||||
|
||||
Example block:
|
||||
```json
|
||||
{"type":"text","text":"Hello","annotations":{"audience":["assistant"],"lastModified":"2025-01-01T00:00:00Z","priority":0.5}}
|
||||
```
|
||||
|
||||
### Image content
|
||||
|
||||
Fields:
|
||||
- `type`
|
||||
- `data` (base64)
|
||||
- `mimeType`
|
||||
- `annotations.*` (same as above, optional)
|
||||
|
||||
Example block:
|
||||
```json
|
||||
{"type":"image","data":"<base64>","mimeType":"image/png","annotations":{"audience":["assistant"]}}
|
||||
```
|
||||
|
||||
### Audio content
|
||||
|
||||
Fields:
|
||||
- `type`
|
||||
- `data` (base64)
|
||||
- `mimeType`
|
||||
- `annotations.*` (optional)
|
||||
|
||||
Example block:
|
||||
```json
|
||||
{"type":"audio","data":"<base64>","mimeType":"audio/wav","annotations":{"audience":["assistant"]}}
|
||||
```
|
||||
|
||||
### Resource link
|
||||
|
||||
Fields:
|
||||
- `type`
|
||||
- `name`
|
||||
- `uri`
|
||||
- `description` (optional)
|
||||
- `mimeType` (optional)
|
||||
- `size` (optional)
|
||||
- `title` (optional)
|
||||
- `annotations.*` (optional)
|
||||
|
||||
Example block:
|
||||
```json
|
||||
{"type":"resource_link","name":"docs/exec.md","uri":"file:///repo/docs/exec.md","description":"Exec docs","mimeType":"text/markdown","size":1234,"title":"exec.md","annotations":{"audience":["assistant"]}}
|
||||
```
|
||||
|
||||
### Embedded resource
|
||||
|
||||
Fields:
|
||||
- `type`
|
||||
- `resource` (either text or blob contents)
|
||||
- `annotations.*` (optional)
|
||||
|
||||
Example block (embedded text):
|
||||
```json
|
||||
{"type":"resource","resource":{"uri":"file:///repo/README.md","text":"Hello","mimeType":"text/markdown"},"annotations":{"audience":["assistant"]}}
|
||||
```
|
||||
|
||||
Example block (embedded blob):
|
||||
```json
|
||||
{"type":"resource","resource":{"uri":"file:///repo/image.png","blob":"<base64>","mimeType":"image/png"},"annotations":{"audience":["assistant"]}}
|
||||
```
|
||||
|
||||
## Consumer considerations (rendering + success/failure)
|
||||
|
||||
Use this section to decide what to surface to end users vs. what to treat as
|
||||
machine-only metadata.
|
||||
|
||||
### What to render for users
|
||||
|
||||
- **Final answer:** render `item.completed` where `item.type = "agent_message"` as
|
||||
the main response.
|
||||
- **Progress updates (optional):**
|
||||
- `item.completed` with `item.type = "reasoning"` can be shown as brief
|
||||
activity breadcrumbs (only if you want to expose reasoning summaries).
|
||||
- `item.started` / `item.completed` with `item.type = "command_execution"` can
|
||||
be shown as “running command …” status lines without printing full output.
|
||||
- `item.completed` with `item.type = "file_change"` can be rendered as a list
|
||||
of changed paths and kinds (add/update/delete).
|
||||
- `item.*` with `item.type = "todo_list"` can be shown as a progress checklist.
|
||||
- **Errors:** render `type = "error"` and `item.type = "error"` as user-visible
|
||||
warnings or failures.
|
||||
|
||||
### Fields you can safely skip for UX
|
||||
|
||||
- `command_execution.aggregated_output` is often noisy; many consumers omit or
|
||||
truncate it, and rely on `command_execution.status` + `exit_code` instead.
|
||||
- `mcp_tool_call.result.content` can be large and tool-specific; consider showing
|
||||
only high-level status unless you know the tool’s schema.
|
||||
- `usage` fields (`turn.completed.usage.*`) are typically telemetry-only.
|
||||
|
||||
### Success and failure signals
|
||||
|
||||
- **Turn success:** `type = "turn.completed"` indicates overall success.
|
||||
- **Turn failure:** `type = "turn.failed"` with `error.message` indicates failure.
|
||||
- **Item success/failure:** use `item.status` on the item payload:
|
||||
- `command_execution.status`: `completed` = success, `failed` = failure.
|
||||
- `file_change.status`: `completed` = patch applied, `failed` = patch failed.
|
||||
- `mcp_tool_call.status`: `completed` = tool succeeded, `failed` = tool failed.
|
||||
- **Fatal stream errors:** `type = "error"` means the JSONL stream itself hit an
|
||||
unrecoverable error (except transient `"Reconnecting... X/Y"` notices, which
|
||||
are non-fatal).
|
||||
|
||||
### Suggested minimal rendering
|
||||
|
||||
If you want a compact UI, the following is usually enough:
|
||||
- Thread/turn lifecycle: `thread.started`, `turn.started`, `turn.completed` or
|
||||
`turn.failed`
|
||||
- Final answer: `item.completed` with `item.type = "agent_message"`
|
||||
- Optional progress: `item.started` / `item.completed` for `command_execution`
|
||||
and `file_change`
|
||||
|
||||
### Optional/conditional emission notes
|
||||
|
||||
- `turn.failed` only appears on failure; otherwise `turn.completed` is emitted.
|
||||
- `reasoning` items only appear when reasoning summaries are enabled.
|
||||
- `todo_list` items only appear when the plan tool is active; they are the
|
||||
primary source of `item.updated`.
|
||||
- `file_change` and `web_search` items are emitted only as `item.completed`
|
||||
in the current `codex exec --json` stream.
|
||||
@@ -0,0 +1,432 @@
|
||||
# Codex -> Takopi event mapping
|
||||
|
||||
This document describes how Codex exec --json events are translated to Takopi's normalized event model.
|
||||
|
||||
> **Authoritative source:** The schema definitions are in `src/takopi/schemas/codex.py` and the translation logic is in `src/takopi/runners/codex.py`. When in doubt, refer to the code.
|
||||
|
||||
## The 3-event Takopi schema
|
||||
|
||||
The Takopi event model uses 3 event types. The `action` event includes a `phase` field to represent started/updated/completed lifecycles.
|
||||
|
||||
### 1) `started`
|
||||
|
||||
Emitted once **as soon as you know the resume token** (Codex: `thread.started.thread_id`).
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "started",
|
||||
"engine": "codex",
|
||||
"resume": { "engine": "codex", "value": "0199..." },
|
||||
"title": "Codex", // optional
|
||||
"meta": { "raw": { ... } } // optional: for debugging
|
||||
}
|
||||
```
|
||||
|
||||
### 2) `action`
|
||||
|
||||
Emitted for **everything that is progress / updates / warnings / per-item lifecycle**.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "action",
|
||||
"engine": "codex",
|
||||
"action": {
|
||||
"id": "item_5",
|
||||
"kind": "tool", // command | tool | file_change | web_search | subagent | note | turn | warning | telemetry
|
||||
"title": "docs.search", // short label for renderer
|
||||
"detail": { ... } // structured payload (freeform)
|
||||
},
|
||||
"phase": "started", // started | updated | completed
|
||||
"ok": true, // optional; present when phase=completed (or warnings)
|
||||
"message": "optional text", // optional; logs/warnings can use this
|
||||
"level": "info" // optional: debug|info|warning|error
|
||||
}
|
||||
```
|
||||
|
||||
### 3) `completed`
|
||||
|
||||
Emitted once at end-of-run with the **final answer** (from `agent_message`) and final status.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "completed",
|
||||
"engine": "codex",
|
||||
"resume": { "engine": "codex", "value": "0199..." }, // if known
|
||||
"ok": true,
|
||||
"answer": "Done. I updated the docs...",
|
||||
"error": null,
|
||||
"usage": { "input_tokens": 24763, "cached_input_tokens": 24448, "output_tokens": 122 } // optional
|
||||
}
|
||||
```
|
||||
|
||||
Why this fits Takopi cleanly:
|
||||
|
||||
* Your `started` corresponds to the old “session.started” concept (runner learns resume token; bridge can now safely serialize per thread).
|
||||
* Your `action` is “everything that would have been action.started/action.completed/log/error” collapsed into one stream.
|
||||
* Your `completed` corresponds to final `RunResult` + status, using Codex’s `agent_message` as the answer source.
|
||||
|
||||
---
|
||||
|
||||
## How everything fits together (end-to-end)
|
||||
|
||||
From the bridge/runner point of view:
|
||||
|
||||
1. **Bridge receives Telegram prompt**
|
||||
2. Bridge tries to extract a resume line (`codex resume <uuid>`) from the message/reply (runner-owned parsing).
|
||||
3. Bridge calls `runner.run(prompt, resumeTokenOrNone)`
|
||||
4. Codex runner spawns `codex exec --json ...` and reads JSONL line-by-line.
|
||||
5. The *first moment the runner can know thread identity* is:
|
||||
|
||||
* `thread.started` → contains `thread_id` (this is your resume value)
|
||||
6. Runner must (per Takopi’s concurrency invariant) **acquire the per-thread lock as soon as the new thread token is known**, before emitting `started`.
|
||||
7. Runner translates subsequent Codex JSONL lines into `action` events for progress rendering.
|
||||
8. Runner captures the final answer from `item.completed` where `item.type="agent_message"`.
|
||||
9. Runner emits exactly one `completed` event when the run ends (`turn.completed` or failure), including the captured final answer.
|
||||
|
||||
---
|
||||
|
||||
## Direct translation: every Codex `exec --json` line → your 3-event schema
|
||||
|
||||
Codex emits two categories: **top-level lines** and **item lines**.
|
||||
|
||||
### A) Top-level lines
|
||||
|
||||
#### `thread.started`
|
||||
|
||||
Codex:
|
||||
|
||||
```json
|
||||
{"type":"thread.started","thread_id":"0199..."}
|
||||
```
|
||||
|
||||
→ Takopi:
|
||||
|
||||
* emit **`started`**:
|
||||
|
||||
* `resume.value = thread_id`
|
||||
|
||||
This is exactly the “learn resume tag” moment you described.
|
||||
|
||||
---
|
||||
|
||||
#### `turn.started`
|
||||
|
||||
Codex:
|
||||
|
||||
```json
|
||||
{"type":"turn.started"}
|
||||
```
|
||||
|
||||
→ Takopi (recommended):
|
||||
|
||||
* emit **`action`** with a synthetic action id, e.g. `"turn_0"`
|
||||
|
||||
* `kind="turn"`, `phase="started"`, `title="turn started"`
|
||||
|
||||
You *can* also drop it if your UI doesn’t care, but if you want “every codex type translates”, this maps cleanly into `action`.
|
||||
|
||||
---
|
||||
|
||||
#### `turn.completed`
|
||||
|
||||
Codex includes usage:
|
||||
|
||||
```json
|
||||
{"type":"turn.completed","usage":{...}}
|
||||
```
|
||||
|
||||
→ Takopi:
|
||||
|
||||
* emit **`completed`**
|
||||
|
||||
* `ok=true`
|
||||
* `answer = last seen agent_message text` (or `""` if none)
|
||||
* `usage = usage` (optional)
|
||||
|
||||
This is your authoritative “run succeeded” boundary.
|
||||
|
||||
---
|
||||
|
||||
#### `turn.failed`
|
||||
|
||||
Codex:
|
||||
|
||||
```json
|
||||
{"type":"turn.failed","error":{"message":"..."}}
|
||||
```
|
||||
|
||||
→ Takopi:
|
||||
|
||||
* emit **`completed`**
|
||||
|
||||
* `ok=false`
|
||||
* `error = error.message`
|
||||
* `answer = last seen agent_message` (if any; usually empty)
|
||||
|
||||
This is “run ended, but failed”.
|
||||
|
||||
---
|
||||
|
||||
#### Top-level `error` (stream error)
|
||||
|
||||
Codex:
|
||||
|
||||
```json
|
||||
{"type":"error","message":"stream error: broken pipe"}
|
||||
```
|
||||
|
||||
Cheatsheet meaning: this is a **fatal stream failure** (not just a tool failure).
|
||||
However, Codex may also emit transient reconnect notices as `type="error"` with
|
||||
messages like `"Reconnecting... 1/5"` while it retries a dropped stream. Treat
|
||||
those as non-fatal progress updates (do **not** end the run).
|
||||
|
||||
→ Takopi:
|
||||
|
||||
* if you haven’t emitted `completed` yet: emit **`completed`** with `ok=false` and `error=message`
|
||||
* if you *already* emitted `completed`, treat it as an extra warning (or ignore; it’s “post-mortem noise”)
|
||||
|
||||
---
|
||||
|
||||
### B) Item lines: `item.started`, `item.updated`, `item.completed`
|
||||
|
||||
All item lines include `item.id` and it is stable across updates/completion.
|
||||
That means your `action.action.id` should just be `item.id` — perfect match to “stable within a run”.
|
||||
|
||||
#### General rule (for any item.* line)
|
||||
|
||||
* `action.action.id = item.id`
|
||||
* `action.phase = started | updated | completed`
|
||||
* `action.action.kind` derived from `item.type`
|
||||
* `action.action.detail` contains the relevant item fields (possibly trimmed)
|
||||
|
||||
Now, map each `item.type`:
|
||||
|
||||
---
|
||||
|
||||
## Item-type mapping: `item.type` → `action.kind/title/detail/ok`
|
||||
|
||||
Below is a “complete coverage” mapping for all item types listed in the cheatsheet.
|
||||
|
||||
### 1) `agent_message` (only `item.completed`)
|
||||
|
||||
Codex:
|
||||
|
||||
```json
|
||||
{"type":"item.completed","item":{"id":"item_3","type":"agent_message","text":"..."}}
|
||||
```
|
||||
|
||||
→ Takopi:
|
||||
|
||||
* **do not emit an `action`** (recommended)
|
||||
* instead: **store** `final_answer = item.text`
|
||||
* final answer will be surfaced by the eventual `completed` event
|
||||
|
||||
Reason: you want `completed` to be “final answer delivery”, and you probably don’t want the answer duplicated in progress rendering.
|
||||
|
||||
(If you *do* want to render it as it arrives, you can emit an `action` too, but then your renderer must avoid showing it twice.)
|
||||
|
||||
---
|
||||
|
||||
### 2) `reasoning` (only `item.completed`, if enabled)
|
||||
|
||||
Codex gives a text breadcrumb.
|
||||
|
||||
→ Takopi `action`:
|
||||
|
||||
* `kind="note"`
|
||||
* `title="reasoning"` (or “thought”)
|
||||
* `phase="completed"`
|
||||
* `message=item.text` (or put it under `detail.text`)
|
||||
|
||||
This is usually safe to show as a short “what it’s doing” line (or ignore if you don’t want to surface it).
|
||||
|
||||
---
|
||||
|
||||
### 3) `command_execution` (`item.started` and `item.completed`)
|
||||
|
||||
Codex fields include `command`, `status`, `aggregated_output` (often noisy), and
|
||||
`exit_code` (null or omitted until completion).
|
||||
|
||||
→ Takopi `action`:
|
||||
|
||||
* `kind="command"`
|
||||
* `title=item.command` (or a shortened version like `pytest`)
|
||||
* `detail={ command, exit_code, status }` (optionally include output tail)
|
||||
* `phase="started"` on `item.started`
|
||||
* `phase="completed"` on `item.completed`
|
||||
* `ok = (item.status == "completed")` (and `exit_code == 0` when present)
|
||||
|
||||
Note: “failed” command becomes `ok=false` but it’s still just an `action` completion — the overall run might still succeed later, depending on agent behavior.
|
||||
|
||||
---
|
||||
|
||||
### 4) `file_change` (only `item.completed`)
|
||||
|
||||
Codex contains `changes[]` and `status`.
|
||||
|
||||
→ Takopi `action`:
|
||||
|
||||
* `kind="file_change"`
|
||||
* `title="file changes"`
|
||||
* `detail={ changes }`
|
||||
* `phase="completed"`
|
||||
* `ok = (item.status == "completed")`
|
||||
|
||||
This is a great progress line for your UI (“updated docs/…, added …”).
|
||||
|
||||
---
|
||||
|
||||
### 5) `mcp_tool_call` (`item.started` and `item.completed`)
|
||||
|
||||
Codex contains server/tool/arguments/status and may include result/error on
|
||||
completion. Result can be large; may include base64 in content blocks.
|
||||
|
||||
→ Takopi `action`:
|
||||
|
||||
* `kind="tool"`
|
||||
* `title=f"{item.server}.{item.tool}"`
|
||||
* `detail={ server, tool, arguments, status }`
|
||||
* on completion, include *summary* of result:
|
||||
|
||||
* e.g. `detail.result_summary = { content_blocks: N, has_structured: bool }`
|
||||
* include `detail.error_message` if failed
|
||||
* `phase="started"` or `"completed"`
|
||||
* `ok = (item.status == "completed")`
|
||||
|
||||
Recommendation: **do not dump** full `result.content` into `detail` if it can contain large blobs; keep a summary and optionally stash full raw elsewhere for debugging.
|
||||
|
||||
---
|
||||
|
||||
### 6) `web_search` (only `item.completed`)
|
||||
|
||||
Codex includes `query`.
|
||||
|
||||
→ Takopi `action`:
|
||||
|
||||
* `kind="web_search"`
|
||||
* `title="web search"`
|
||||
* `detail={ query }`
|
||||
* `phase="completed"`
|
||||
* `ok=true` (this is just “it did a search”; success/failure is typically not expressed here)
|
||||
|
||||
---
|
||||
|
||||
### 7) `todo_list` (`item.started`, `item.updated`, `item.completed`)
|
||||
|
||||
Codex includes checklist items with `completed` booleans.
|
||||
|
||||
→ Takopi `action`:
|
||||
|
||||
* `kind="note"` (or `"todo"`)
|
||||
* `title="plan"`
|
||||
* `detail={ items, done: count_done, total: count_total }`
|
||||
* `phase` maps 1:1 to started/updated/completed
|
||||
* `ok=true` when phase completed (optional)
|
||||
|
||||
This is the one case where `item.updated` is common; your unified `action` event is exactly the right shape for it.
|
||||
|
||||
---
|
||||
|
||||
### 8) Item `error` (non-fatal warning as an item; only `item.completed`)
|
||||
|
||||
Codex:
|
||||
|
||||
```json
|
||||
{"type":"item.completed","item":{"id":"item_9","type":"error","message":"command output truncated"}}
|
||||
```
|
||||
|
||||
Cheatsheet: this is a **non-fatal warning** (different from top-level fatal `error`).
|
||||
|
||||
→ Takopi `action`:
|
||||
|
||||
* `kind="warning"` (or `"note"`)
|
||||
* `title="warning"`
|
||||
* `message=item.message`
|
||||
* `level="warning"`
|
||||
* `phase="completed"`
|
||||
* `ok=true` (because it’s informational) **or** omit `ok`
|
||||
|
||||
---
|
||||
|
||||
## Suggested “single-pass” translator logic (pseudocode)
|
||||
|
||||
This shows how to implement it without needing more than one pass or complicated buffering:
|
||||
|
||||
```python
|
||||
final_answer = None
|
||||
resume = None
|
||||
did_emit_started = False
|
||||
did_emit_completed = False
|
||||
turn_index = 0
|
||||
|
||||
def emit(evt): yield evt # emit to the output event stream
|
||||
|
||||
for line in codex_jsonl_stream:
|
||||
t = line["type"]
|
||||
|
||||
if t == "thread.started":
|
||||
resume = {"engine": "codex", "value": line["thread_id"]}
|
||||
# acquire per-thread lock here (for new sessions) before emitting started
|
||||
emit({"type":"started","engine":"codex","resume":resume,"title":"Codex"})
|
||||
did_emit_started = True
|
||||
continue
|
||||
|
||||
if t == "turn.started":
|
||||
emit({"type":"action","engine":"codex",
|
||||
"action":{"id":f"turn_{turn_index}","kind":"turn","title":"turn started","detail":{}},
|
||||
"phase":"started"})
|
||||
continue
|
||||
|
||||
if t == "item.started" or t == "item.updated" or t == "item.completed":
|
||||
item = line["item"]
|
||||
item_type = item["type"]
|
||||
item_id = item["id"]
|
||||
|
||||
if t == "item.completed" and item_type == "agent_message":
|
||||
final_answer = item.get("text","")
|
||||
continue
|
||||
|
||||
# map item_type -> kind/title/detail/ok
|
||||
action_evt = map_item_to_action(item, phase=t.split(".")[1])
|
||||
emit(action_evt)
|
||||
continue
|
||||
|
||||
if t == "turn.completed":
|
||||
emit({"type":"completed","engine":"codex","resume":resume,
|
||||
"ok":True,"answer":final_answer or "",
|
||||
"error":None,"usage":line.get("usage")})
|
||||
did_emit_completed = True
|
||||
continue
|
||||
|
||||
if t == "turn.failed":
|
||||
emit({"type":"completed","engine":"codex","resume":resume,
|
||||
"ok":False,"answer":final_answer or "",
|
||||
"error":line["error"]["message"]})
|
||||
did_emit_completed = True
|
||||
continue
|
||||
|
||||
if t == "error": # fatal stream error
|
||||
if not did_emit_completed:
|
||||
emit({"type":"completed","engine":"codex","resume":resume,
|
||||
"ok":False,"answer":final_answer or "",
|
||||
"error":line.get("message")})
|
||||
did_emit_completed = True
|
||||
continue
|
||||
|
||||
# Optional: if stream ends without turn.completed/failed,
|
||||
# emit completed with ok=False and error="unexpected EOF"
|
||||
```
|
||||
|
||||
This design preserves the Takopi ordering/serialization principles: `started` happens as soon as resume token is known, actions stream in order, and exactly one `completed` closes the run.
|
||||
|
||||
---
|
||||
|
||||
## One practical note: what “completed” should mean
|
||||
|
||||
Even though you *learn* the final answer at `agent_message`, you generally want `completed` to be emitted at the **turn boundary** (`turn.completed` / `turn.failed`), because:
|
||||
|
||||
* you can attach usage (`turn.completed.usage`) only there,
|
||||
* you guarantee `completed` is truly the last event,
|
||||
* you still use `agent_message` as the authoritative answer payload.
|
||||
|
||||
That still matches your intent (“completed is when we get final answer”) because the answer comes from `agent_message`; you just *publish* it at the terminal boundary.
|
||||
Reference in New Issue
Block a user