Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ on:
- main
paths:
- 'Wiki/**'
- 'docs/**'
- 'README.md'
- 'README.en.md'
- 'CHANGELOG.md'
- '.github/workflows/docs.yml'
pull_request:
paths:
- 'Wiki/**'
- 'docs/**'
- 'README.md'
- 'README.en.md'
- 'CHANGELOG.md'
- '.github/workflows/docs.yml'
workflow_dispatch:

Expand Down
2 changes: 1 addition & 1 deletion Wiki/content/docs/en/api/errors-events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Common stable codes include `idempotency_conflict`, `quota_exceeded`, and `join_
| --- | --- | --- |
| `run_terminal` | 409 | `/steer` is called against a run in a terminal status (`succeeded`/`failed`/`cancelled`/`timed_out`/`dead_letter`); no durable write is created |
| `run_superseded` | 409 | `/steer` or `/cancel` is called against an old `run_id` already superseded by a `resume`; call it against the new run's `id` instead (see `metadata.resumed_from_run_id`). For `/cancel`, this fires even though the old run's `status` is still `paused` -- the check reads the typed `superseded_by_run_id` column, not `status` and not user-writable `metadata`, so the historical run is never mutated to `cancelled` while its resumed descendant keeps running, and the check cannot be forged via a client-submitted `metadata` key (see the operations doc's "The `superseded_by_run_id` lineage marker") |
| `payload_too_large` | 413/422 | Serialized `payload` + `metadata` exceed the size cap (32KB, or the server's event size cap, whichever is smaller); no durable write is created |
| `payload_too_large` | 413 | Serialized `payload` + `metadata` exceed the size cap (32KB, or the server's event size cap, whichever is smaller); no durable write is created |
| `run_resume_conflict` | 409 | Concurrent resume calls raced against the same paused run; exactly one wins and creates the new run, every other caller gets this instead |
| `run_finalizing` | 409 | `/steer` is called after the run's graph has already finished executing but before its final status is committed (a short window while the worker durably flushes and finalizes); there is no safe point left to consume the event, so it is rejected rather than durably accepted with nobody to ever read it. A `paused` outcome is not affected -- steering a paused run remains normal |

Expand Down
6 changes: 5 additions & 1 deletion Wiki/content/docs/en/api/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Agent Server defaults to `http://localhost:8124`. Business resources use the ver
| GET | `/v1/runs/{run_id}/join` | Wait up to 300 seconds |
| GET | `/v1/runs/{run_id}/stream` | SSE event stream |
| POST | `/v1/runs/{run_id}/cancel` | Request cooperative cancellation |
| POST | `/v1/runs/{run_id}/steer` | Durably inject mid-run input |
| POST | `/v1/runs/{run_id}/resume` | Resume a paused run |
| POST | `/v1/runs/{run_id}/redrive` | Redrive a failed/dead-letter run |
| POST | `/v1/store/batch` | Batch Store operations |
Expand Down Expand Up @@ -63,9 +64,12 @@ stateDiagram-v2
paused --> cancelled: cancel (no live worker)
failed --> pending: redrive
dead_letter --> pending: redrive
paused --> pending: resume creates new run
state "new run: pending" as resumed_pending
paused --> resumed_pending: resume creates descendant run
```

`resume` does not mutate the old run from `paused` back to `pending`: the old run remains `paused` and is marked stale through the typed `superseded_by_run_id` field; the newly created descendant run is the one that starts in `pending`.

Stable status values are `pending`, `running`, `paused`, `succeeded`, `failed`, `cancelling`, `cancelled`, `timed_out`, and `dead_letter`.

<Cards>
Expand Down
21 changes: 11 additions & 10 deletions Wiki/content/docs/en/api/python-sdk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,21 @@ with LingxiGraphClient(
max_model_calls=8,
)

for event in client.runs.stream(run["id"]):
print(event["sequence"], event["kind"])

completed = client.runs.join(run["id"], timeout=120)
print(completed["output"])

# Durably inject mid-run input; success only means it was durably
# written -- not that the graph has processed it yet.
# Steering can be submitted immediately after creation: whether the
# run is still queued or already running, success only means durable acceptance.
accepted = client.runs.steer(
run["id"],
kind="user_input",
payload={"message": "switch to Spanish"},
idempotency_key="msg-123",
)
print(accepted["id"], accepted["status"]) # "pending"

for event in client.runs.stream(run["id"]):
print(event["sequence"], event["kind"])

completed = client.runs.join(run["id"], timeout=120)
print(completed["output"])
```

Available resource methods:
Expand Down Expand Up @@ -96,8 +96,6 @@ async with AsyncLingxiGraphClient(base_url, token=token) as client:
thread_id=thread["id"],
input={"request": "hello"},
)
async for event in client.runs.stream(run["id"]):
print(event)

accepted = await client.runs.steer(
run["id"],
Expand All @@ -106,6 +104,9 @@ async with AsyncLingxiGraphClient(base_url, token=token) as client:
idempotency_key="msg-123",
)
print(accepted["id"], accepted["status"])

async for event in client.runs.stream(run["id"]):
print(event)
```

Async resources mirror sync resources. Do not use the synchronous client inside an async event loop.
Expand Down
2 changes: 1 addition & 1 deletion Wiki/content/docs/en/api/threads-runs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ events on whatever run eventually consumes it — no need to track every interme
### Payload size limits

Serialized `payload` + `metadata` size is bounded (roughly 32KB by default, or the server's
event byte cap, whichever is smaller); exceeding it returns `413`/`422` with error code
event byte cap, whichever is smaller); exceeding it returns HTTP `413` with error code
`payload_too_large` and creates no durable write. JSON values must be safely serializable
plain types (string, number, boolean, null, array, object) — no binary data, cyclic
references, or non-JSON-serializable custom types.
Expand Down
38 changes: 18 additions & 20 deletions Wiki/content/docs/en/operations/steering-operations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ description: "Migration rollout order, PostgreSQL-as-source-of-truth, latency, r

## Migration rollout order

Mid-run steering (issue #16) ships as four forward-only Alembic revisions on top of the base
schema:
Mid-run steering ships as four forward-only Alembic revisions on top of the base schema:

| Revision | Adds |
| --- | --- |
| `0002_steering` | `run_steering_events` table, its indexes, and the worker/server code paths that read/write it |
| `0003_steering_source_event` | `source_event_id` column + partial index on `run_steering_events`, used to correlate a steering event across repeated pause/resume migrations |
| `0004_steering_closed` | `runs.steering_closed` — the atomic finalization admission gate (review round 6/7); see "The `steering_closed` finalization gate" below |
| `0005_superseded_by_run_id` | `runs.superseded_by_run_id` — the typed control-plane lineage marker for resumed runs (review round 14, BLOCKER); moves this out of user-writable `metadata` so it cannot be forged. See "The `superseded_by_run_id` lineage marker" below |
| `0004_steering_closed` | `runs.steering_closed` — the atomic finalization admission gate; see "The `steering_closed` finalization gate" below |
| `0005_superseded_by_run_id` | `runs.superseded_by_run_id` — the typed control-plane lineage marker for resumed runs; moves this out of user-writable `metadata` so it cannot be forged. See "The `superseded_by_run_id` lineage marker" below |

Apply them with the standard Alembic path:

Expand Down Expand Up @@ -47,7 +46,7 @@ run, and it must reach `0005_superseded_by_run_id`, not stop at `0002_steering`,

## The `steering_closed` finalization gate

Once a worker's graph execution reaches its terminal/paused outcome, the run may still read
Once a worker's graph execution reaches a genuinely terminal outcome, the run may still read
`running`/`cancelling` for a little while longer while the worker durably flushes whatever
steering the graph already drained and commits the final status — there is no more graph safe
point left to consume anything new. `runs.steering_closed` is a boolean the owning worker sets
Expand Down Expand Up @@ -96,19 +95,18 @@ still carry a `pending` steering row, as deliberate durable history, picked back
`/redrive` like any other queued steering. `/redrive` only accepts `failed`/`dead_letter` runs,
though — an ordinary client-initiated **cancel** is not covered by that recovery path, so steering
left `pending` on a `cancelled` run has no automatic future delivery attempt today. See the
steering concept page's "Finalization and the steering
admission boundary" section for the full policy.
steering concept page's "Finalization and the steering admission boundary" section for the full
policy.

## The `superseded_by_run_id` lineage marker

When `/resume` creates a descendant run from a `paused` run, the old run needs a durable way to
say "I have been superseded — steer/cancel/resume the new run instead". Before `0005`, that
marker lived in `metadata.superseded_by_run_id` — ordinary, user-writable `Run` metadata.
say "I have been superseded — steer/cancel/resume the new run instead". An earlier implementation
stored that marker in `metadata.superseded_by_run_id` — ordinary, user-writable `Run` metadata.
Because `/steer`, `/cancel`, and the concurrent-resume conflict gate all treated that key as
authoritative, a caller could forge it at run-creation time (`POST /v1/runs` with
`metadata: {"superseded_by_run_id": "..."}`) to falsely trigger `409 run_superseded` /
`409 run_resume_conflict` on a run that was never actually resumed (issue #16 PR #17 review
round 14, BLOCKER).
`409 run_resume_conflict` on a run that was never actually resumed.

`runs.superseded_by_run_id` (added by `0005_superseded_by_run_id`) is a typed, nullable column
that replaces that key as the sole source of truth:
Expand All @@ -123,10 +121,10 @@ that replaces that key as the sole source of truth:
- A caller submitting `metadata.superseded_by_run_id` on an ordinary `RunCreate` now has no
effect on control flow at all — the key is inert, stored (if at all) as plain user metadata,
and never copied into the typed column.
- Upgrade compatibility: a pre-existing run from before this PR that happens to carry a user
metadata key with this exact name is never reinterpreted as system lineage after upgrading —
the typed column starts `NULL` for every existing row and is populated only by a future
resume.
- Upgrade compatibility: a pre-existing run from before the current version that happens to
carry a user metadata key with this exact name is never reinterpreted as system lineage after
upgrading — the typed column starts `NULL` for every existing row and is populated only by a
future resume.

## Idempotent replay is safe across every admission gate

Expand Down Expand Up @@ -238,11 +236,11 @@ runs against a schema the new binaries no longer expect. Practical implications:
revision simply ignore the extra table/columns and do not fail to start.
- **Schema compatibility is not the same as semantic compatibility.** An older binary running
against the new schema will not crash, but it also does not understand `steering_closed` or
`superseded_by_run_id` — it will not enforce the finalization admission gate or the
resume-lineage cancel/steer rejection this PR adds. Treat the new steering/lineage control
semantics as available only once every control-plane instance is running the new binary, not
the moment the schema migration lands. Rolling a binary back after it has already exercised
these semantics (e.g. after a resume has set `superseded_by_run_id`) does not un-happen that
`superseded_by_run_id` — it will not enforce the finalization admission gate or the current
resume-lineage cancel/steer rejection. Treat the new steering/lineage control semantics as
available only once every control-plane instance is running the new binary, not the moment
the schema migration lands. Rolling a binary back after it has already exercised these
semantics (e.g. after a resume has set `superseded_by_run_id`) does not un-happen that
transition; the old binary simply stops enforcing it going forward. If you must roll back
binaries while steering is in active use, quiesce or let in-flight runs finish first rather
than assuming the old binary preserves the same guarantees.
Expand Down
2 changes: 1 addition & 1 deletion Wiki/content/docs/zh/api/errors-events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ description: "处理 problem details、run 失败、事件续传与客户端重
| --- | --- | --- |
| `run_terminal` | 409 | 对处于终止态(`succeeded`/`failed`/`cancelled`/`timed_out`/`dead_letter`)的 run 调用 `/steer`;不产生任何 durable 写入 |
| `run_superseded` | 409 | 对已经被 `resume` 取代的旧 `run_id` 调用 `/steer` 或 `/cancel`;应改用新 run 的 `id`(见 `metadata.resumed_from_run_id`)。对 `/cancel` 而言,即使旧 run 的 `status` 仍是 `paused` 也会触发此错误——检查的是类型化的 `superseded_by_run_id` 列,而非 `status`,也不是可被用户写入的 `metadata`,因此历史 run 绝不会被误改为 `cancelled`,而其被 resume 出来的新 run 却仍在继续执行;该检查也无法通过客户端提交的 `metadata` key 伪造(详见运维文档"`superseded_by_run_id` 血缘标记"一节) |
| `payload_too_large` | 413/422 | `payload` + `metadata` 序列化后超过大小上限(32KB,或服务端事件大小上限,取两者较小值);不产生任何 durable 写入 |
| `payload_too_large` | 413 | `payload` + `metadata` 序列化后超过大小上限(32KB,或服务端事件大小上限,取两者较小值);不产生任何 durable 写入 |
| `run_resume_conflict` | 409 | 并发 `resume` 调用同一个已暂停的 run;只有一个调用会成功创建新 run,其余调用都会收到该错误 |
| `run_finalizing` | 409 | run 的图已经执行结束、但最终状态尚未提交(worker 正在持久化刷新并收尾的短暂窗口)时调用 `/steer`;此时已经没有任何安全点可以消费该事件,因此直接拒绝,而不是接受一条永远不会被消费的记录。`paused` 结局不受影响——对已暂停的 run 发起 `/steer` 仍然正常 |

Expand Down
6 changes: 5 additions & 1 deletion Wiki/content/docs/zh/api/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Agent Server 的默认地址为 `http://localhost:8124`。业务资源使用版
| GET | `/v1/runs/{run_id}/join` | 最长 300 秒等待 run |
| GET | `/v1/runs/{run_id}/stream` | SSE 事件流 |
| POST | `/v1/runs/{run_id}/cancel` | 请求协作式取消 |
| POST | `/v1/runs/{run_id}/steer` | durably 注入运行中输入 |
| POST | `/v1/runs/{run_id}/resume` | 恢复 paused run |
| POST | `/v1/runs/{run_id}/redrive` | 重驱 failed/dead-letter run |
| POST | `/v1/store/batch` | 批量 Store 操作 |
Expand Down Expand Up @@ -63,9 +64,12 @@ stateDiagram-v2
paused --> cancelled: cancel(无存活 worker)
failed --> pending: redrive
dead_letter --> pending: redrive
paused --> pending: resume creates new run
state "new run: pending" as resumed_pending
paused --> resumed_pending: resume creates descendant run
```

`resume` 不会把旧 run 的 `paused` 状态改成 `pending`:旧 run 仍保持 `paused`,并通过类型化的 `superseded_by_run_id` 标记已被取代;新创建的后继 run 才从 `pending` 开始执行。

稳定状态值为 `pending`、`running`、`paused`、`succeeded`、`failed`、`cancelling`、`cancelled`、`timed_out`、`dead_letter`。

<Cards>
Expand Down
20 changes: 11 additions & 9 deletions Wiki/content/docs/zh/api/python-sdk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,21 @@ with LingxiGraphClient(
max_model_calls=8,
)

for event in client.runs.stream(run["id"]):
print(event["sequence"], event["kind"])

completed = client.runs.join(run["id"], timeout=120)
print(completed["output"])

# Durably 注入一条运行中输入;成功只代表已 durably 写入,不代表已被图消费。
# Run 创建后即可提交 steering;无论它仍在排队还是已经 running,
# 成功都只代表已 durably 写入,不代表图已经消费。
accepted = client.runs.steer(
run["id"],
kind="user_input",
payload={"message": "改成用中文回复"},
idempotency_key="msg-123",
)
print(accepted["id"], accepted["status"]) # "pending"

for event in client.runs.stream(run["id"]):
print(event["sequence"], event["kind"])

completed = client.runs.join(run["id"], timeout=120)
print(completed["output"])
```

可用资源方法:
Expand Down Expand Up @@ -93,8 +94,6 @@ async with AsyncLingxiGraphClient(base_url, token=token) as client:
thread_id=thread["id"],
input={"request": "hello"},
)
async for event in client.runs.stream(run["id"]):
print(event)

accepted = await client.runs.steer(
run["id"],
Expand All @@ -103,6 +102,9 @@ async with AsyncLingxiGraphClient(base_url, token=token) as client:
idempotency_key="msg-123",
)
print(accepted["id"], accepted["status"])

async for event in client.runs.stream(run["id"]):
print(event)
```

异步资源与同步资源一一对应。不要在 async 事件循环中使用同步客户端。
Expand Down
2 changes: 1 addition & 1 deletion Wiki/content/docs/zh/api/threads-runs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ steering 必须已经全部就位——不存在"新 Run 已可被执行,但
### payload 大小限制

`payload` + `metadata` 序列化后大小受限(默认约 32KB,或服务端事件字节上限,两者先
到者生效);超限返回 `413`/`422`,错误码 `payload_too_large`,不会产生任何 durable
到者生效);超限返回 HTTP `413`,错误码 `payload_too_large`,不会产生任何 durable
写入。JSON 值必须是可安全序列化的普通类型(字符串、数字、布尔、null、数组、对象);
不支持二进制、循环引用或不可 JSON 化的自定义类型。

Expand Down
Loading
Loading