mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge codex/goal-session into codex/commands
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-19-model-facing-goal-tools.md: 3d9a33566d3ae27f803cbe56b7464bd1da17c1eb
|
||||
2026-07-19-model-facing-goal-tools.zh.md: 45ca2dfdc3523a3598add845224cda6f58d2c738
|
||||
2026-07-19-model-facing-goal-tools.md: e37eaabecfd1984e1198c26a460e78a92375dac1
|
||||
2026-07-19-model-facing-goal-tools.zh.md: c0d289271d2a8053193299c16a2bc9477f2f1038
|
||||
|
||||
@@ -22,13 +22,13 @@ The prompt tells the model that it may infer goal intent from a direct human req
|
||||
|
||||
All three tools use exclusive execution so a model-ordered batch observes prior mutations and their new revisions. Results are compact JSON. ACP presentation is a pure function of arguments and uses generic read or mutation cards; activation is reported only as live observation and is never written into replay state.
|
||||
|
||||
A successful update that leaves the goal stopped contributes the existing terminal `agent/turn-stop` decision for that physical turn, preventing an unnecessary follow-up request after pause, block, or completion. A later successful resume in the same turn removes that contribution.
|
||||
An autonomous goal round that successfully reports completion or blocking contributes the existing terminal `agent/turn-stop` decision for that physical turn, preventing an unnecessary follow-up request. Direct-human mutations do not contribute a terminal stop: the assistant can acknowledge the change, and concurrent human steering remains available to ordinary continuation folding.
|
||||
|
||||
### Execution authority
|
||||
|
||||
Every call requires an `exec.agent` that is the exact running object in `AgentRegistry`, is the current inherited driver initiator, and has an open turn. These are execution-time checks and cannot be bypassed by prompt injection or hand-authored tool arguments.
|
||||
|
||||
Create, edit, pause, and resume additionally require an accepted user message or user steering event in the current turn of a runtime-root agent. Root ownership is derived from the live agent graph rather than durable fork ancestry: a resumed fork can receive direct human authority, while a live child remains a subagent and cannot mutate these states. The runtime proves provenance, not whether the human's wording semantically warrants creation or resumption; that interpretation remains with the model.
|
||||
Create, edit, pause, and resume additionally require an accepted user message or user steering event in the current turn of a runtime-root agent. Root ownership is derived from the live agent graph rather than durable fork ancestry: a resumed fork can receive direct human authority, while a live child remains a subagent and cannot mutate these states. User source is a host attestation: `Agent.send()` and `steer()` default an omitted source to `{ kind: 'user' }`, so non-human producers must label their own content. The runtime proves provenance, not whether the human's wording semantically warrants creation or resumption; that interpretation remains with the model.
|
||||
|
||||
Complete and blocked accept either direct-human authority or the exact current goal round. Goal-round authority requires a goal-sourced `user/message` whose goal id, revision, and round all equal the folded current goal. It grants only the two terminal reports. Direct human authority may stop a goal immediately.
|
||||
|
||||
@@ -38,7 +38,7 @@ Complete and blocked accept either direct-human authority or the exact current g
|
||||
|
||||
## Testing
|
||||
|
||||
Unit coverage pins registration and disposal, exclusive scheduling, generated prompt policy, generic presentation, direct-human creation in a non-English turn, exact live-agent and driver checks, root-versus-child authority, steering, mismatched initiators, read/create/edit/pause/resume behavior, rearming after a session-start edge, compare-and-set and argument failures, exact goal-round completion, the configured blocking threshold, and immediate human blocking. A keyless Loader/stdio process test mounts the real goal, tool, loop, and persistence plugins through `cordis.yml`, drives scripted model tool calls, and reads the JSONL externally to verify the model-visible create/pause snapshots, structured tool results, and configured prompt text.
|
||||
Unit coverage pins registration and disposal, exclusive scheduling, generated prompt policy, generic presentation, direct-human creation in a non-English turn, exact/stale/non-running agent and driver checks, live-child rejection, resumed-fork root authority, steering, mismatched initiators, read/create/edit/pause/resume behavior, rearming after a session-start edge, authority-before-conditional-argument failures, exact goal-round completion, autonomous-only terminal stopping, the configured blocking threshold, and immediate human blocking. A keyless Loader/stdio process test mounts the real goal, tool, loop, and persistence plugins through `cordis.yml`, drives scripted model tool calls through a human pause and assistant acknowledgment, and reads the JSONL externally to verify the model-visible create/pause snapshots, structured tool results, and configured prompt text.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -61,5 +61,6 @@ Unit coverage pins registration and disposal, exclusive scheduling, generated pr
|
||||
|
||||
- Semantic classification of a substantial goal, a request to continue, objective completion, and the same blocking condition remains model judgment. An independent evaluator or completion certificate is deferred.
|
||||
- These tools mutate goal state but do not schedule goal rounds, classify abnormal driver stops, or cancel an active turn; the same-session driver owns those behaviors.
|
||||
- Goal-round authority is dormant unless a separately mounted continuation driver admits goal-sourced user turns; this tool package never manufactures that authority itself.
|
||||
- Human slash-command discovery and rendering are deferred to the command-surface layer.
|
||||
- A scope can hide tool registrations while leaving the independently registered prompt section visible unless the deployment scopes both together.
|
||||
|
||||
@@ -22,13 +22,13 @@ Status: implemented
|
||||
|
||||
三个工具都采用独占执行,使模型排序的批次可以观察此前变更及其新修订号。结果为紧凑 JSON。ACP 展示是参数的纯函数,使用通用读取或变更卡片;激活态仅作为实时观察返回,绝不会写入回放状态。
|
||||
|
||||
成功更新后若目标处于停止状态,插件会为该物理轮次贡献现有的终止型 `agent/turn-stop` 决策,避免在暂停、阻塞或完成后再发起一次不必要的模型请求。同一轮次中后续成功的恢复会移除该贡献。
|
||||
自主目标回合成功报告完成或阻塞后,插件会为该物理轮次贡献现有的终止型 `agent/turn-stop` 决策,避免再发起一次不必要的模型请求。直接人类发起的变更不会贡献终止决策:智能体可以确认该变更,并且并发的人类 steering(转向)仍可参与普通的继续执行折叠。
|
||||
|
||||
### 执行权限
|
||||
|
||||
每次调用都要求存在 `exec.agent`,且它必须是 `AgentRegistry` 中完全相同的运行中对象、当前继承的驱动发起者,并处于开放轮次内。这些检查在执行时进行,不能通过提示词注入或手写工具参数绕过。
|
||||
|
||||
创建、编辑、暂停与恢复还要求运行时根智能体的当前轮次已经接纳一条用户消息或用户 steering(转向)事件。根所有权来自实时智能体图,而非持久的 fork 祖先关系:恢复后的派生会话可以接收新的直接人类权限,实时子智能体则仍然是子智能体,不能改变这些状态。运行时证明来源,而不判断人类措辞在语义上是否足以创建或恢复目标;该解释仍由模型完成。
|
||||
创建、编辑、暂停与恢复还要求运行时根智能体的当前轮次已经接纳一条用户消息或用户 steering(转向)事件。根所有权来自实时智能体图,而非持久的 fork 祖先关系:恢复后的派生会话可以接收新的直接人类权限,实时子智能体则仍然是子智能体,不能改变这些状态。用户来源是宿主的证明:`Agent.send()` 和 `steer()` 会把省略的来源默认为 `{ kind: 'user' }`,因此非人类生产者必须标注自己的内容。运行时证明来源,而不判断人类措辞在语义上是否足以创建或恢复目标;该解释仍由模型完成。
|
||||
|
||||
完成与阻塞既接受直接人类权限,也接受准确的当前目标回合。目标回合权限要求存在一条来源为目标的 `user/message`,其中目标 id、修订号和回合都与折叠后的当前目标相等。它只授予这两种终止报告权限。直接人类权限可以立即停止目标。
|
||||
|
||||
@@ -38,7 +38,7 @@ Status: implemented
|
||||
|
||||
## 测试
|
||||
|
||||
单元测试固定注册与释放、独占调度、生成的提示词策略、通用展示、非英语轮次中的直接人类创建、精确实时智能体与驱动检查、根与子智能体权限、steering、发起者不匹配、读取/创建/编辑/暂停/恢复行为、会话启动边沿后的重新激活、比较并交换与参数失败、准确目标回合的完成、可配置阻塞阈值,以及人类立即阻塞。无密钥 Loader/stdio 进程测试通过 `cordis.yml` 挂载真实的目标、工具、循环和持久化插件,驱动脚本化模型工具调用,并从外部读取 JSONL,以验证模型可见的创建/暂停快照、结构化工具结果和配置后的提示词文本。
|
||||
单元测试固定注册与释放、独占调度、生成的提示词策略、通用展示、非英语轮次中的直接人类创建、精确/陈旧/非运行中智能体与驱动检查、实时子智能体拒绝、恢复后派生根的权限、steering、发起者不匹配、读取/创建/编辑/暂停/恢复行为、会话启动边沿后的重新激活、权限先于条件参数失败、准确目标回合的完成、仅自主回合触发终止、可配置阻塞阈值,以及人类立即阻塞。无密钥 Loader/stdio 进程测试通过 `cordis.yml` 挂载真实的目标、工具、循环和持久化插件,驱动脚本化模型工具调用经过人类暂停与智能体确认,并从外部读取 JSONL,以验证模型可见的创建/暂停快照、结构化工具结果和配置后的提示词文本。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -61,5 +61,6 @@ Status: implemented
|
||||
|
||||
- 是否属于重大目标、是否要求继续、目标是否完成以及阻塞条件是否相同,仍由模型进行语义分类。独立评估器或完成证书予以延期。
|
||||
- 这些工具会改变目标状态,但不调度目标回合、不分类异常驱动停止,也不取消活跃轮次;这些行为由同会话驱动器负责。
|
||||
- 除非另行挂载的继续执行驱动器接纳了目标来源的用户轮次,否则目标回合权限路径处于休眠状态;本工具包本身不会制造这种权限。
|
||||
- 面向人类的斜杠命令发现与渲染延期到命令表面层。
|
||||
- 若部署没有同时设定两个注册项的作用域,某个作用域可能隐藏工具注册,却保留独立注册的提示词段。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-19-persisted-same-session-goal-domain.md: 40db008e28fb2e1b2c392bf9759bc1007926c971
|
||||
2026-07-19-persisted-same-session-goal-domain.zh.md: fa81bf1a399119c627d1e8d5a2ee990212f13e0c
|
||||
2026-07-19-persisted-same-session-goal-domain.md: 744d55aee275b8dd4c71f628f685372f62c818bf
|
||||
2026-07-19-persisted-same-session-goal-domain.zh.md: 3e00d7af53abf801f8b92e8c7de879d91a62d503
|
||||
|
||||
@@ -22,7 +22,7 @@ Every non-clear mutation uses `Agent.inject()` to append a raw, model-visible `c
|
||||
|
||||
The replay fold validates JSON shape, source attribution, rendered content, fresh ids, revision continuity, lifecycle transitions, counters, and monotonic per-goal timestamps. Goal rounds are positive sequential `user/message` source numbers for the current active revision and cannot exceed `maxGoalRounds`; ordinary session turns do not affect the counter. A malformed current-format record fails replay rather than being ignored or repaired.
|
||||
|
||||
When `Agent.inject()` defers a mutation inside an active tool batch, the service overlays the accepted payload in process memory so a later mutation can use its new revision. Reconciliation removes only an exact matching payload when the FIFO append becomes visible; the durable log remains authoritative after restart.
|
||||
When `Agent.inject()` defers a mutation inside an active tool batch, the service overlays the accepted payload in process memory so a later mutation can use its new revision. Reconciliation removes only an exact matching payload when the FIFO append becomes visible; reentrant append observers project each mutation exactly once. Incremental replay advances its cursor after each valid event and remains positioned at the first corrupt event, so later reads report the same durable fault. The durable log remains authoritative after restart.
|
||||
|
||||
### Lifecycle and live activation
|
||||
|
||||
@@ -36,7 +36,7 @@ The service accepts only the exact live `Agent` object registered under its id.
|
||||
|
||||
## Testing
|
||||
|
||||
Unit coverage pins creation defaults, exact-live-agent checks, compare-and-set rejection, every lifecycle transition, cap enforcement, clear/replacement, seeded replay and `SessionStore.fork()` inheritance, session-start and lifecycle-owner disarming, active-goal rearming, FIFO deferred mutation reconciliation, listener containment, backward-clock clamping, strict record decoding, lifecycle continuity, source/content agreement, and sequential round attribution. A keyless Loader/stdio process test mounts the service and a lifecycle consumer through test-only `cordis.yml`, then reads the persisted JSONL externally to verify the model-visible snapshot and absence of an unrequested goal round. The package source is held to the repository's per-file 100% coverage gate.
|
||||
Unit coverage pins creation defaults, exact-live-agent checks, compare-and-set rejection, every lifecycle transition, cap enforcement, clear/replacement, seeded replay and `SessionStore.fork()` inheritance, session-start and lifecycle-owner disarming, active-goal rearming, FIFO deferred mutation reconciliation, reentrant append observation, rejected-injection rollback, stable corrupt-event replay, service/listener disposal, listener containment, backward-clock clamping, strict record decoding, lifecycle continuity, source/content agreement, and sequential round attribution. A keyless Loader/stdio process test mounts the service and a lifecycle consumer through test-only `cordis.yml`, then reads the persisted JSONL externally to verify the model-visible snapshot and absence of an unrequested goal round. The package source is held to the repository's per-file 100% coverage gate.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -59,4 +59,5 @@ Unit coverage pins creation defaults, exact-live-agent checks, compare-and-set r
|
||||
- This domain records state but does not schedule goal rounds, cancel active turns, or classify abnormal stops.
|
||||
- The actor that records `complete` or `blocked` is authoritative; an independent evaluator or completion certificate is deferred to a policy consumer.
|
||||
- There is one current goal per session; parallel objective graphs and cross-session goal storage are absent.
|
||||
- Plugins share one trusted process boundary. Direct session writers can counterfeit goal records; strict replay detects inconsistency and fails goal access at the offending record, but does not isolate plugins or repair the log.
|
||||
- `GOAL_CHANGE_VERSION` has no pre-release compatibility promise or migration path.
|
||||
|
||||
@@ -22,7 +22,7 @@ Status: implemented
|
||||
|
||||
回放折叠会校验 JSON 形状、来源归属、渲染内容、新 id、修订连续性、生命周期转换、计数器以及单个目标内单调递增的时间戳。目标回合是当前活跃修订上带正数且连续编号的 `user/message` 来源,且不能超过 `maxGoalRounds`;普通会话轮次不会影响该计数器。当前格式的畸形记录会使回放失败,而不会被忽略或修复。
|
||||
|
||||
当 `Agent.inject()` 在活跃工具批次中延迟变更时,服务会在进程内叠加已接受的载荷,使后续变更可以使用新的修订号。FIFO 追加可见后,协调过程只移除完全匹配的载荷;重启后仍以持久日志为准。
|
||||
当 `Agent.inject()` 在活跃工具批次中延迟变更时,服务会在进程内叠加已接受的载荷,使后续变更可以使用新的修订号。FIFO 追加可见后,协调过程只移除完全匹配的载荷;重入的追加观察器对每次变更只投影一次。增量回放会在每个有效事件后推进游标,并停留在首个损坏事件处,因此后续读取会报告同一个持久故障。重启后仍以持久日志为准。
|
||||
|
||||
### 生命周期与实时激活态
|
||||
|
||||
@@ -36,7 +36,7 @@ Status: implemented
|
||||
|
||||
## 测试
|
||||
|
||||
单元测试固定创建默认值、精确实时 agent 校验、比较并交换拒绝、所有生命周期转换、上限执行、清除与替换、种子回放和 `SessionStore.fork()` 继承、会话启动与生命周期所有者解除激活、活跃目标重新激活、FIFO 延迟变更协调、监听器隔离、挂钟后退钳制、严格记录解码、生命周期连续性、来源与内容一致性,以及连续目标回合归属。无密钥 Loader/stdio 进程测试通过测试专用 `cordis.yml` 挂载服务与生命周期消费者,再从外部读取持久 JSONL,以验证模型可见快照以及不存在未经请求的目标回合。包源码受仓库逐文件 100% 覆盖率门禁约束。
|
||||
单元测试固定创建默认值、精确实时 agent 校验、比较并交换拒绝、所有生命周期转换、上限执行、清除与替换、种子回放和 `SessionStore.fork()` 继承、会话启动与生命周期所有者解除激活、活跃目标重新激活、FIFO 延迟变更协调、重入追加观察、注入拒绝回滚、损坏事件的稳定回放、服务与监听器销毁、监听器隔离、挂钟后退钳制、严格记录解码、生命周期连续性、来源与内容一致性,以及连续目标回合归属。无密钥 Loader/stdio 进程测试通过测试专用 `cordis.yml` 挂载服务与生命周期消费者,再从外部读取持久 JSONL,以验证模型可见快照以及不存在未经请求的目标回合。包源码受仓库逐文件 100% 覆盖率门禁约束。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -59,4 +59,5 @@ Status: implemented
|
||||
- 本领域记录状态,但不调度目标回合、不取消活跃轮次,也不分类异常停止。
|
||||
- 记录 `complete` 或 `blocked` 的参与者具有最终权威;独立评估器或完成证书延期到策略消费者中实现。
|
||||
- 每个会话只有一个当前目标;不存在并行目标图和跨会话目标存储。
|
||||
- 插件共享同一个受信任的进程边界。直接写入会话的插件可以伪造目标记录;严格回放会检测不一致并在违规记录处使目标访问失败,但不会隔离插件或修复日志。
|
||||
- `GOAL_CHANGE_VERSION` 在首次发布前不承诺兼容性,也不提供迁移路径。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-19-same-session-goal-round-driver.md: 2fc27ab28bff5e192e43d30afc6cd796fd3394e1
|
||||
2026-07-19-same-session-goal-round-driver.zh.md: e9610c94eca48fb4d06be505a8cdfffb6c272012
|
||||
2026-07-19-same-session-goal-round-driver.md: 94c0a870c46d6b7f618faf3f0a4b01c141bec358
|
||||
2026-07-19-same-session-goal-round-driver.zh.md: 9fdff05ffb722ef683bc06acee606f7897e5afc6
|
||||
|
||||
@@ -39,7 +39,7 @@ The driver classifies one closed goal-owned turn as follows:
|
||||
| Turn result | Action |
|
||||
|---|---|
|
||||
| durable `completed` | continue while active/armed and under cap |
|
||||
| broad cancellation / `aborted` | pause and disarm |
|
||||
| cancellation of a reserved/admitted goal round, or its `aborted` result | pause and disarm |
|
||||
| `error` with code `RATE_LIMIT` | mark `usage-limited` |
|
||||
| other `error` | block |
|
||||
| `max-tokens` | block |
|
||||
@@ -52,9 +52,9 @@ No abnormal outcome requests an automatic retry. A later human prompt can ask to
|
||||
|
||||
### Durability and cancellation seam
|
||||
|
||||
Every `goal/changed` notification creates a checkpoint obligation. The driver awaits `ctx.sessions.flush(session)` before reserving work, then checks for a newer mutation, agent lifecycle change, or competing prompt. Turn-end flush failure is reported by the existing `agent/error` notification after `turn/end`; the driver associates it with the exact attempt and disarms before the next idle decision.
|
||||
Every `goal/changed` notification creates a checkpoint obligation. The driver awaits `ctx.sessions.flush(session)` before reserving work, then checks for a newer mutation, agent lifecycle change, or competing prompt. Turn-end flush failure is reported by the existing `agent/error` notification after `turn/end`; the driver finds that exact closed turn even when a concurrent one-shot injection appended a later turn, associates the failure with the exact attempt, and disarms before the next idle decision.
|
||||
|
||||
Broad cancellation previously exposed only its effects after queues were cleared or the request aborted. The public agent vocabulary now includes observe-only `agent/cancel-requested(agent, reason)`. The concrete loop emits it for effective cancellation before either action; fused notification containment means a broken listener cannot veto cancellation. The goal driver uses this edge to clear its reservation and pause an active armed goal before the loop destroys the queued-work evidence.
|
||||
Broad cancellation previously exposed only its effects after queues were cleared or the request aborted. The public agent vocabulary now includes observe-only `agent/cancel-requested(agent, reason)`. The concrete loop emits it for effective cancellation before either action; fused notification containment means a broken listener cannot veto cancellation. The goal driver uses this edge to clear its reservation before the loop destroys the queued-work evidence. When that reservation is a queued or admitted goal attempt, cancellation durably pauses the goal; when cancellation belongs to unrelated human work with no goal attempt, it only removes process-local activation. If the pause mutation throws, the driver falls back to disarming rather than allowing cancelled automatic work to restart.
|
||||
|
||||
This is a coordination notification, not a second stop API. `Agent.cancel()` remains the only public broad cancellation verb, idle calls remain no-ops, and custom `Agent` implementations that claim the interface must honor the event ordering if consumers depend on it.
|
||||
|
||||
@@ -68,7 +68,7 @@ An inbox acceptance can win the microtask race immediately before plugin unload
|
||||
|
||||
## Testing
|
||||
|
||||
The unit suite uses the real agent loop and session service with only the model scripted. It covers exact sequential admission and cap enforcement, load/resume inertness, every outcome classification, rate limiting, request errors, max tokens, downstream prompt veto, pre-admission and in-flight cancellation, human-input ordering, queued and downstream revision races, forged goal attribution, failed mutation and turn checkpoints, scheduler and custom-agent failures, session-start reset, exact lifecycle retirement, and queued/running plugin teardown. The new driver source has per-file 100% statement, branch, function, and line coverage.
|
||||
The unit suite uses the real agent loop and session service with only the model scripted. It covers exact sequential admission and cap enforcement, load/resume inertness, every outcome classification, rate limiting, request errors, max tokens, downstream prompt veto, pre-admission and in-flight cancellation, unrelated-human cancellation, failed-pause fallback, human-input ordering, queued and downstream revision races, forged goal attribution, failed mutation and turn checkpoints including a later one-shot injection, scheduler and custom-agent failures, session-start reset, exact lifecycle retirement, and queued/running plugin teardown. The new driver source has per-file 100% statement, branch, function, and line coverage.
|
||||
|
||||
A keyless Loader/stdio process test mounts the real goal domain, goal tools, goal driver, agent loop, persistence, and deterministic adapter through `cordis.yml`. One human turn creates a two-round goal; round one stops normally; round two reads the exact ref and completes it. The external JSONL assertion proves one session, round sources `1, 2`, unchanged round revision, final complete revision, five model steps, and no extra request after the terminal completion tool.
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ Status: implemented
|
||||
| 轮次结果 | 动作 |
|
||||
|---|---|
|
||||
| 持久的 `completed` | 目标仍 active/armed 且未到上限时继续 |
|
||||
| 广义取消 / `aborted` | 暂停并解除激活 |
|
||||
| 取消已预留/接纳的目标回合,或该回合产生 `aborted` 结果 | 暂停并解除激活 |
|
||||
| 代码为 `RATE_LIMIT` 的 `error` | 标记为 `usage-limited` |
|
||||
| 其他 `error` | 阻塞 |
|
||||
| `max-tokens` | 阻塞 |
|
||||
@@ -52,9 +52,9 @@ Status: implemented
|
||||
|
||||
### 持久性与取消接缝
|
||||
|
||||
每次 `goal/changed` 通知都会产生一个检查点义务。驱动器在预留工作前等待 `ctx.sessions.flush(session)`,随后检查是否出现了更新的变更、agent 生命周期变化或竞争提示词。轮次结束时的 flush 失败会在 `turn/end` 之后通过现有 `agent/error` 通知报告;驱动器把它关联到精确尝试,并在下一次空闲决策前解除激活。
|
||||
每次 `goal/changed` 通知都会产生一个检查点义务。驱动器在预留工作前等待 `ctx.sessions.flush(session)`,随后检查是否出现了更新的变更、agent 生命周期变化或竞争提示词。轮次结束时的 flush 失败会在 `turn/end` 之后通过现有 `agent/error` 通知报告;即使并发的一次性注入已追加后续轮次,驱动器仍会找到该精确的已关闭轮次,把失败关联到精确尝试,并在下一次空闲决策前解除激活。
|
||||
|
||||
广义取消此前只在队列已清除或请求已中止后暴露结果。公共 agent 词汇现在新增只观察的 `agent/cancel-requested(agent, reason)`。具体循环仅在取消有效时发出该事件,并且发生在清除队列和中止步骤之前;融合通知会隔离失败,因此损坏的监听器不能否决取消。目标驱动器利用该边沿清除预留并暂停 active 且 armed 的目标,之后循环才销毁排队工作证据。
|
||||
广义取消此前只在队列已清除或请求已中止后暴露结果。公共 agent 词汇现在新增只观察的 `agent/cancel-requested(agent, reason)`。具体循环仅在取消有效时发出该事件,并且发生在清除队列和中止步骤之前;融合通知会隔离失败,因此损坏的监听器不能否决取消。目标驱动器利用该边沿在循环销毁排队工作证据前清除预留。若该预留是排队中或已接纳的目标尝试,取消会持久暂停目标;若取消属于没有目标尝试的无关人类工作,则只移除进程内激活态。若暂停变更抛错,驱动器会回退到解除激活,避免已取消的自动工作重新启动。
|
||||
|
||||
该通知是协调事件,不是第二个停止 API。`Agent.cancel()` 仍是唯一的公共广义取消动词,空闲调用仍是无操作;若消费者依赖此接缝,自定义 `Agent` 实现就必须满足该事件顺序。
|
||||
|
||||
@@ -68,7 +68,7 @@ Status: implemented
|
||||
|
||||
## 测试
|
||||
|
||||
单元测试使用真实 agent loop 与会话服务,只对模型编写脚本。覆盖内容包括精确连续接纳和上限执行、加载与恢复的惰性、所有结果分类、限流、请求错误、最大 token、下游提示词否决、接纳前与执行中取消、人类输入排序、排队时与下游修订竞争、伪造目标来源、变更与轮次检查点失败、调度器与自定义 agent 失败、会话启动重置、精确生命周期退出,以及排队中和运行中的插件卸载。新驱动器源码达到逐文件 100% 语句、分支、函数和行覆盖率。
|
||||
单元测试使用真实 agent loop 与会话服务,只对模型编写脚本。覆盖内容包括精确连续接纳和上限执行、加载与恢复的惰性、所有结果分类、限流、请求错误、最大 token、下游提示词否决、接纳前与执行中取消、无关人类工作取消、暂停失败回退、人类输入排序、排队时与下游修订竞争、伪造目标来源、变更与轮次检查点失败(包括后续一次性注入)、调度器与自定义 agent 失败、会话启动重置、精确生命周期退出,以及排队中和运行中的插件卸载。新驱动器源码达到逐文件 100% 语句、分支、函数和行覆盖率。
|
||||
|
||||
无密钥 Loader/stdio 进程测试通过 `cordis.yml` 挂载真实目标领域、目标工具、目标驱动器、agent loop、持久化和确定性适配器。一个人类轮次创建两回合目标;第一回合正常停止;第二回合读取精确引用并完成目标。测试从外部检查 JSONL,证明只有一个会话、回合来源依次为 `1, 2`、回合修订号不变、最终完成修订正确、共有五个模型步骤,并且终止性的完成工具后没有额外请求。
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ Effective broad cancellation was requested, before queued/steering work is clear
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:186`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:189`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/created` — emit
|
||||
|
||||
@@ -54,7 +54,7 @@ A fully configured agent and live session were published. Setup is composition-o
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:148`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:151`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/disposed` — emit
|
||||
|
||||
@@ -74,7 +74,7 @@ An agent left the registry; AgentLoop emits this after driver quiescence but bef
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:157`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:160`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/error` — emit
|
||||
|
||||
@@ -96,7 +96,7 @@ A step or turn errored. The loop reports a failure here (plus the logger) even w
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:322`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:325`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/post-step` — serial
|
||||
|
||||
@@ -119,7 +119,7 @@ Awaited serial checkpoint after the response, real or synthetic tool results, in
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:275`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:278`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/pre-step` — serial
|
||||
|
||||
@@ -142,7 +142,7 @@ Awaited serial checkpoint before `step/start`; appends land outside the pending
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:215`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:218`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/prompt-submit` — waterfall
|
||||
|
||||
@@ -163,7 +163,7 @@ Allow, rewrite, or block one drained prompt before it becomes a user message. Ca
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) · [PromptDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:225`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:228`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/queued` — emit
|
||||
|
||||
@@ -184,7 +184,7 @@ Detached, frozen content entered the agent's inbox. Source defaults have already
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:176`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:179`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/request` — waterfall
|
||||
|
||||
@@ -207,7 +207,7 @@ Replace the frozen call configuration. Model-visible content must use logged cha
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [LlmCallConfig](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:237`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:240`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/request-error` — waterfall
|
||||
|
||||
@@ -232,7 +232,7 @@ Recover a model-request failure after its failed step has closed. `retry` opens
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [RequestError](../core-data-structures/core.md) · [RequestErrorDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:289`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:292`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/session-prefix` — waterfall
|
||||
|
||||
@@ -258,7 +258,7 @@ Compose request-only messages placed before derived history. The frozen result i
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:252`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:255`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/session-start` — emit
|
||||
|
||||
@@ -280,7 +280,7 @@ The session lifecycle began, once before the first turn. Use `agent.inject()` to
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [SessionStartSource](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:199`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:202`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/status` — emit
|
||||
|
||||
@@ -300,7 +300,7 @@ Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does no
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [AgentStatus](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:166`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:169`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/step-result` — waterfall
|
||||
|
||||
@@ -322,7 +322,7 @@ Waterfall: post-process the assembled assistant Message before tool dispatch (va
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:263`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:266`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/turn-continuation` — waterfall
|
||||
|
||||
@@ -343,7 +343,7 @@ Override whether the turn continues. The default continues after tool calls or s
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [ContinuationDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:299`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:302`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/turn-stop` — serial
|
||||
|
||||
@@ -364,7 +364,7 @@ Monotonic terminal-stop checkpoint after continuation and steering are folded; a
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [ContinuationStop](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:309`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:312`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
## `agent-loop/*`
|
||||
|
||||
|
||||
@@ -630,7 +630,7 @@ clear(agent: Agent, ref: GoalRef): GoalRef
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [CreateGoalRequest](../core-data-structures/goal.md) · [CreateGoalSpec](../core-data-structures/goal.md) · [EditGoalRequest](../core-data-structures/goal.md) · [GoalRef](../core-data-structures/goal.md) · [GoalView](../core-data-structures/goal.md)
|
||||
|
||||
Source: [`packages/goal/goal/src/index.ts:97`](../../packages/goal/goal/src/index.ts)
|
||||
Source: [`packages/goal/goal/src/index.ts:104`](../../packages/goal/goal/src/index.ts)
|
||||
|
||||
## `ctx.llm` — `LlmService`
|
||||
|
||||
|
||||
@@ -8,22 +8,22 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| Event | Mode | Declared in | Dispatchers | Listeners |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:362`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
|
||||
| `agent/cancel-requested` | `emit` | [`packages/core/agent/src/types.ts:186`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:148`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:157`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:322`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) |
|
||||
| `agent/post-step` | `serial` | [`packages/core/agent/src/types.ts:275`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic) |
|
||||
| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:215`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`time-context`](../packages/context/time-context), [`user-approval`](../packages/ui/user-approval) |
|
||||
| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:225`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) |
|
||||
| `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:176`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:237`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:289`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic) |
|
||||
| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:252`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:199`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`stdio`](../packages/ui/stdio) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:166`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session), [`invariants`](../packages/support/invariants), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
|
||||
| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:263`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
|
||||
| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:299`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `agent/turn-stop` | `serial` | [`packages/core/agent/src/types.ts:309`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`tool-goal`](../packages/goal/tool-goal) |
|
||||
| `agent/cancel-requested` | `emit` | [`packages/core/agent/src/types.ts:189`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:151`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:160`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:325`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) |
|
||||
| `agent/post-step` | `serial` | [`packages/core/agent/src/types.ts:278`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic) |
|
||||
| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:218`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`time-context`](../packages/context/time-context), [`user-approval`](../packages/ui/user-approval) |
|
||||
| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:228`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) |
|
||||
| `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:179`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:240`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:292`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic) |
|
||||
| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:255`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill), [`workspace-context`](../packages/context/workspace-context) |
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:202`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`stdio`](../packages/ui/stdio) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:169`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session), [`invariants`](../packages/support/invariants), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) |
|
||||
| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:266`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - |
|
||||
| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:302`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `agent/turn-stop` | `serial` | [`packages/core/agent/src/types.ts:312`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`tool-goal`](../packages/goal/tool-goal) |
|
||||
| `approval/request` | `waterfall` | [`packages/ui/user-approval/src/index.ts:31`](../packages/ui/user-approval/src/index.ts) | [`user-approval`](../packages/ui/user-approval) (`waterfall`) | [`acp`](../packages/ui/acp) |
|
||||
| `commands/change` | `emit` | [`packages/ui/commands/src/index.ts:93`](../packages/ui/commands/src/index.ts) | [`commands`](../packages/ui/commands) (`emit`) | [`acp`](../packages/ui/acp), [`tui`](../packages/ui/tui) |
|
||||
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:61`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) |
|
||||
|
||||
@@ -32,4 +32,4 @@ FIXME(glossary-completeness): Expand this glossary before the first release so i
|
||||
|
||||
- **turn** — one drain of admitted input in a session, ending after the model and its tools stop or a terminal policy intervenes. <a id="turn"></a>
|
||||
- **step** — one model request plus the tool executions caused by its response; a turn contains one or more steps. <a id="step"></a>
|
||||
- **round** — an outer policy iteration containing a turn, such as a [goal round](#goal-round) or one fresh-agent Ralph attempt. Round counters belong to that policy and do not count every turn in a session. <a id="round"></a>
|
||||
- **round** — an outer policy iteration containing a turn, such as a [goal round](#goal-round). Round counters belong to that policy and do not count every turn in a session. <a id="round"></a>
|
||||
|
||||
@@ -410,7 +410,7 @@ Create one persisted same-session completion goal when the current direct human
|
||||
},
|
||||
"max_goal_rounds": {
|
||||
"type": "number",
|
||||
"description": "Optional positive safe-integer cap; omission uses the goal-domain deployment default."
|
||||
"description": "Optional positive safe-integer limit on automatic continuation rounds."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -423,7 +423,7 @@ Source: [`packages/goal/tool-goal/src/index.ts`](../packages/goal/tool-goal/src/
|
||||
|
||||
### `get_goal`
|
||||
|
||||
Read the current same-session goal, including its exact id/revision, durable phase, admitted round count, cap, and live process-local activation. Call this before updating a goal.
|
||||
Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, and whether another continuation is armed. Call this before updating a goal.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -436,7 +436,7 @@ Source: [`packages/goal/tool-goal/src/index.ts`](../packages/goal/tool-goal/src/
|
||||
|
||||
### `update_goal`
|
||||
|
||||
Update the exact current goal revision. edit, pause, and resume require a direct top-level human turn. complete and blocked additionally accept the exact admitted goal round. blocked is rejected before the configured minimum round count; the model remains responsible for judging that the same condition persisted across those rounds.
|
||||
Update the exact current goal revision. edit, pause, and resume require a direct top-level human request. During an automatic continuation of the current goal, complete and blocked are also allowed. blocked is rejected before the configured minimum round count; the model remains responsible for judging that the same condition persisted across those rounds.
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Deterministic adapter that creates, reads, then pauses one goal. */
|
||||
/** Deterministic adapter that creates, reads, pauses, then acknowledges one goal. */
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
@@ -75,7 +75,7 @@ class GoalScriptAdapter extends LlmAdapter {
|
||||
if (goal === undefined) throw new Error('scripted goal state missing')
|
||||
return toolCall('update_goal', { goal_id: goal.id, revision: goal.revision, action: 'pause' })
|
||||
}
|
||||
if (prompt.text === 'pause') return textReply('UNEXPECTED CONTINUATION AFTER PAUSE')
|
||||
if (prompt.text === 'pause') return textReply('GOAL PAUSED')
|
||||
return textReply('UNEXPECTED PROMPT')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ Turn and step boundaries and the model token stream are durable `session/event`
|
||||
|
||||
The handle every plugin programs against:
|
||||
|
||||
- `agent.send(content, options?)` — queue a message; starts a turn when idle. Content and resolved source become one detached, deeply frozen lossless-JSON record before `agent/queued` and enqueue; invalid data throws synchronously, and caller or notification-listener in-place mutation cannot change the log or model input (`agent/prompt-submit` still rewrites by returning replacement content).
|
||||
- `agent.send(content, options?)` — queue a message; starts a turn when idle. Omitting `options.source` attests direct human input as `{ kind: 'user' }` and may authorize policy consumers, so plugins, schedulers, and other non-human producers provide their own source. Content and resolved source become one detached, deeply frozen lossless-JSON record before `agent/queued` and enqueue; invalid data throws synchronously, and caller or notification-listener in-place mutation cannot change the log or model input (`agent/prompt-submit` still rewrites by returning replacement content).
|
||||
- `agent.steer(content, options?)` — steer a running turn (inject between steps); uses the same owned acceptance boundary and behaves like `send` when idle
|
||||
- `agent.inject(content, options?)` — accept detached in-session context without running the model; the next request sees its `context/message`. `options.envelope` defaults to the canonical `<context>` framing and may be `'raw'` when the caller owns a complete familiar frame; `options.meta` persists opaque JSON state without rendering it. While a turn is open it joins that turn, deferring FIFO while the current tool batch executes and draining before turn close if execution is interrupted; while idle it is wrapped in a one-shot `injection` turn and durability checkpoint ([the turn-enclosure invariant](../../../.agents/notes/implemented/architecture/2026-06-15-turn-enclosure-invariant.md)).
|
||||
- `agent.cancel(reason?)` — cancel ALL pending work: an effective call emits `agent/cancel-requested` before it clears the queued + steering FIFOs, aborts the in-flight step, and drops a turn about to start (the pre-step window). Observers may synchronize their own state but cannot veto cancellation. A UI/ACP `session/cancel` maps to this. The single public stop primitive. Idle with nothing pending → a safe no-op with no notification.
|
||||
|
||||
@@ -25,7 +25,10 @@ export interface AgentOptions {
|
||||
model?: string
|
||||
}
|
||||
|
||||
/** Message options; an omitted source resolves to `{ kind: 'user' }`, so plugins must label their own content. */
|
||||
/**
|
||||
* Message options. An omitted source attests direct human input as `{ kind: 'user' }`
|
||||
* and may authorize policy consumers, so non-human producers must label their content.
|
||||
*/
|
||||
export interface SendOptions {
|
||||
source?: MessageSource
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ The retained prompt names the JSON-quoted objective and `round/maxGoalRounds`, t
|
||||
| Durable turn outcome | Goal action | Automatic retry |
|
||||
|---|---|---|
|
||||
| `completed` with goal still active and armed | admit the next round, or mark `budget-limited` at the cap | yes |
|
||||
| broad cancellation / `aborted` | `paused` | no |
|
||||
| cancellation of a reserved/admitted goal round, or its `aborted` outcome | `paused` | no |
|
||||
| cancellation with no goal-round attempt | keep durable phase; disarm activation | no |
|
||||
| `error` with `RATE_LIMIT` | `usage-limited` | no |
|
||||
| other `error`, `max-tokens`, or a non-stale prompt rejection | `blocked` | no |
|
||||
| durability failure, disposal, interruption, or unknown future outcome | disarm or block for inspection | no |
|
||||
@@ -39,11 +40,11 @@ A goal mutation made during its round supersedes settlement of the older revisio
|
||||
|
||||
## Lifecycle and durability
|
||||
|
||||
`goal/changed` creates a durability obligation. Before queuing work, the driver awaits `ctx.sessions.flush()` and rechecks both the goal revision and competing input after the await. A closing flush failure arrives through `agent/error`; the driver disarms before another round can start.
|
||||
`goal/changed` creates a durability obligation. Before queuing work, the driver awaits `ctx.sessions.flush()` and rechecks both the goal revision and competing input after the await. A closing flush failure arrives through `agent/error`; the driver associates it with the exact closed turn even if a later one-shot injection has appended another turn, then disarms before another round can start.
|
||||
|
||||
Activation is never inherited when this plugin loads over an existing agent. `GoalService.disarm()` removes process-local authority without changing durable phase, revision, or history; explicit human-authorized resume records the later reactivation. The same rule applies after session resume and fork through the goal domain's `agent/session-start` handling.
|
||||
|
||||
Cancellation is observe-before-act: the concrete loop emits `agent/cancel-requested` before clearing queues or aborting a step, allowing this plugin to pause and disarm the exact active goal. Plugin teardown closes admission, disarms every live goal, cancels an admitted round, and awaits the driver plus agent quiescence while its event fence remains installed.
|
||||
Cancellation is observe-before-act: the concrete loop emits `agent/cancel-requested` before clearing queues or aborting a step. The plugin durably pauses an active goal only when the cancellation owns a reserved or admitted goal attempt; cancellation of unrelated human work merely disarms process-local continuation. If the pause mutation fails, the driver falls back to disarming. Plugin teardown closes admission, disarms every live goal, cancels an admitted round, and awaits the driver plus agent quiescence while its event fence remains installed.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -276,8 +276,8 @@ export function apply(ctx: Context): void {
|
||||
/** Mark a post-turn persistence failure before idle scheduling can run. */
|
||||
ctx.on('agent/error', (agent, turn) => {
|
||||
const state = stateFor(agent)
|
||||
const last = agent.session.events.at(-1)
|
||||
if (last?.type !== 'turn/end' || last.data.turn !== turn) return
|
||||
const closed = agent.session.events.some(event => event.type === 'turn/end' && event.data.turn === turn)
|
||||
if (!closed) return
|
||||
if (state.attempt?.turn === turn) state.flushFailedTurns.add(turn)
|
||||
disarm(state)
|
||||
})
|
||||
@@ -312,11 +312,21 @@ export function apply(ctx: Context): void {
|
||||
})
|
||||
ctx.on('agent/cancel-requested', (agent, reason) => {
|
||||
const state = stateFor(agent)
|
||||
const attempt = state.attempt
|
||||
state.attempt = undefined
|
||||
state.competingQueued = false
|
||||
const goal = currentGoal(state)
|
||||
if (goal?.phase === 'active' && goal.activation === 'armed') {
|
||||
applyOutcome(state, goal, { kind: 'pause', reason })
|
||||
if (attempt === undefined) {
|
||||
disarm(state)
|
||||
return
|
||||
}
|
||||
try {
|
||||
applyOutcome(state, goal, { kind: 'pause', reason })
|
||||
} catch (error: unknown) {
|
||||
ctx.logger.warn(`goal-session: could not pause cancelled goal for agent "${agent.id}": ${renderThrown(error)}`)
|
||||
disarm(state)
|
||||
}
|
||||
}
|
||||
})
|
||||
ctx.on('goal/changed', (agent) => {
|
||||
|
||||
@@ -402,12 +402,17 @@ describe('same-session goal driving', () => {
|
||||
expect(test.adapter.requests).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('disarms an admitted round whose closing durability checkpoint fails', async () => {
|
||||
it('disarms an admitted round when a later injection hides its failed closing checkpoint', async () => {
|
||||
const test = await harness([textResponse('not durable')])
|
||||
let injected = false
|
||||
test.ctx.on('session/flush', (session) => {
|
||||
const lastStart = session.events.findLast(event => event.type === 'turn/start')
|
||||
if (lastStart?.type === 'turn/start' && lastStart.data.trigger.kind === 'message'
|
||||
&& lastStart.data.trigger.source.kind === 'goal') {
|
||||
&& lastStart.data.trigger.source.kind === 'goal' && !injected) {
|
||||
injected = true
|
||||
test.agent.inject([{ type: 'text', text: 'concurrent completion notice' }], {
|
||||
source: { kind: 'plugin', plugin: 'test' },
|
||||
})
|
||||
return Promise.reject(new Error('round flush failed'))
|
||||
}
|
||||
})
|
||||
@@ -421,6 +426,10 @@ describe('same-session goal driving', () => {
|
||||
|
||||
expect(goal?.phase).toBe('active')
|
||||
expect(test.adapter.requests).toHaveLength(1)
|
||||
const turns = test.agent.session.events.filter(event => event.type === 'turn/start')
|
||||
const goalTurn = turns.findIndex(event => event.data.trigger.source.kind === 'goal')
|
||||
const injectedTurn = turns.findIndex(event => event.data.trigger.source.kind === 'plugin')
|
||||
expect(injectedTurn).toBeGreaterThan(goalTurn)
|
||||
})
|
||||
|
||||
it('blocks the goal when a custom agent rejects the otherwise valid send', async () => {
|
||||
@@ -559,6 +568,42 @@ describe('same-session goal driving', () => {
|
||||
expect(test.adapter.requests).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('disarms without durably pausing when cancellation belongs to unrelated human work', async () => {
|
||||
const test = await harness(['hang'])
|
||||
test.agent.send([{ type: 'text', text: 'inspect something first' }])
|
||||
await waitForRequests(test.adapter, 1)
|
||||
const created = test.ctx.goals.create(test.agent, { objective: 'continue after inspection' })
|
||||
|
||||
test.agent.cancel('cancel the inspection')
|
||||
await test.agent.whenIdle()
|
||||
|
||||
expect(test.ctx.goals.get(test.agent)).toMatchObject({
|
||||
id: created.id,
|
||||
revision: created.revision,
|
||||
phase: 'active',
|
||||
activation: 'disarmed',
|
||||
roundsStarted: 0,
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to disarming when a cancelled reservation cannot be paused', async () => {
|
||||
const test = await harness([])
|
||||
const cancel = test.ctx.on('agent/queued', (agent, _content, info) => {
|
||||
if (agent !== test.agent || info.source.kind !== 'goal') return
|
||||
cancel()
|
||||
vi.spyOn(test.ctx.goals, 'pause').mockImplementationOnce(() => {
|
||||
throw new Error('pause failed')
|
||||
})
|
||||
agent.cancel('cancel the reserved goal round')
|
||||
})
|
||||
test.ctx.goals.create(test.agent, { objective: 'fail closed after cancellation' })
|
||||
|
||||
const goal = await waitForGoal(test.ctx, test.agent, current => current?.activation === 'disarmed')
|
||||
|
||||
expect(goal).toMatchObject({ phase: 'active', revision: 1, roundsStarted: 0 })
|
||||
expect(test.adapter.requests).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('blocks admission when downstream cancellation clears the reservation', async () => {
|
||||
const test = await harness([])
|
||||
let cancelled = false
|
||||
|
||||
@@ -21,7 +21,7 @@ At most one goal is current. Creation produces an active revision-one goal and a
|
||||
|
||||
Every non-clear mutation appends a complete versioned snapshot through `agent.inject()`; clear appends a revisioned tombstone. The raw `context/message`, its `{ kind: 'goal' }` source, and its metadata must agree exactly. Replay rejects malformed shapes, source/content drift, discontinuous revisions, illegal lifecycle transitions, non-monotonic per-goal timestamps, and non-sequential goal rounds. Mutation timestamps clamp against the preceding goal update when wall time moves backward.
|
||||
|
||||
Injection may append immediately or wait in an active tool-batch FIFO. The service overlays accepted pending changes in memory and reconciles each exact payload when it enters the log, so consecutive model-tool mutations see their own latest revisions without treating an unlogged cache as durable state. `goal/changed` fires after the append or enqueue succeeds; listener failures are contained.
|
||||
Injection may append immediately or wait in an active tool-batch FIFO. The service overlays accepted pending changes in memory and reconciles each exact payload when it enters the log, so consecutive model-tool mutations see their own latest revisions without treating an unlogged cache as durable state. Reentrant append observers see each accepted mutation exactly once, and incremental replay retains its cursor at the first corrupt event. `goal/changed` fires after the append or enqueue succeeds; listener failures are contained.
|
||||
|
||||
Activation is never persisted. A fresh cache and every `agent/session-start` edge disarm it even when replay finds an active durable phase. A continuation driver also calls `disarm()` before unload or after durability uncertainty. Session resume, fork, and driver replacement therefore retain the objective, phase, revisions, and admitted-round count without initiating work; a later explicit resume mutation must arm continuation.
|
||||
|
||||
@@ -51,3 +51,4 @@ Append-only within an epoch: each mutation follows the reusable request prefix a
|
||||
- **Round-count budget only** — `maxGoalRounds` does not meter tokens, currency, wall time, or provider quotas.
|
||||
- **No independent evaluator** — the caller that records completion or blocking is authoritative; evaluator-backed certification is deferred to a separate policy layer.
|
||||
- **One current goal** — parallel objectives and a separate goal database are intentionally absent; history remains available in the session log after replacement or clear.
|
||||
- **Trusted in-process producers** — a plugin with direct `Session` access can append counterfeit goal metadata. Strict replay detects malformed or inconsistent records and leaves goal access failed at that record until the log is repaired; this is integrity detection, not plugin isolation.
|
||||
|
||||
@@ -64,12 +64,19 @@ export interface ResolvedConfig {
|
||||
defaultMaxGoalRounds: number
|
||||
}
|
||||
|
||||
/** One accepted mutation waiting to enter or be observed in the session log. */
|
||||
interface PendingGoalChange {
|
||||
readonly change: GoalChangeMeta
|
||||
readonly activation: GoalActivation
|
||||
applied: boolean
|
||||
}
|
||||
|
||||
/** Process-local cache plus mutations waiting in the active tool-batch FIFO. */
|
||||
interface GoalCache {
|
||||
readonly state: GoalFoldState
|
||||
activation: GoalActivation
|
||||
observedSeq: number
|
||||
readonly pending: GoalChangeMeta[]
|
||||
readonly pending: PendingGoalChange[]
|
||||
}
|
||||
|
||||
/** Validate a caller-visible positive safe-integer round cap. */
|
||||
@@ -373,15 +380,21 @@ export class GoalService extends Service {
|
||||
const change = decodeGoalEvent(event)
|
||||
if (change !== undefined) {
|
||||
const pending = cache.pending[0]
|
||||
if (pending !== undefined && sameChange(pending, change)) {
|
||||
if (pending !== undefined && sameChange(pending.change, change)) {
|
||||
if (!pending.applied) {
|
||||
applyGoalChange(cache.state, change)
|
||||
cache.activation = pending.activation
|
||||
pending.applied = true
|
||||
}
|
||||
cache.pending.shift()
|
||||
cache.observedSeq += 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
applyGoalEvent(cache.state, event)
|
||||
cache.observedSeq += 1
|
||||
}
|
||||
cache.observedSeq = session.seq
|
||||
}
|
||||
|
||||
/** Build a new revision with one replacement phase. */
|
||||
@@ -479,14 +492,26 @@ export class GoalService extends Service {
|
||||
const meta = snapshotJsonValue(change) as JsonValue | undefined
|
||||
/* v8 ignore next -- validated goal changes contain only finite JSON primitives and records */
|
||||
if (meta === undefined) throw new Error('goal change is not losslessly JSON-serializable')
|
||||
agent.inject(renderGoalChange(change), {
|
||||
source: { kind: 'goal', goalId: ref.id, revision: ref.revision, round: 0 },
|
||||
envelope: 'raw',
|
||||
meta,
|
||||
})
|
||||
cache.pending.push(change)
|
||||
applyGoalChange(cache.state, change)
|
||||
cache.activation = activation
|
||||
const pending: PendingGoalChange = { change, activation, applied: false }
|
||||
cache.pending.push(pending)
|
||||
try {
|
||||
agent.inject(renderGoalChange(change), {
|
||||
source: { kind: 'goal', goalId: ref.id, revision: ref.revision, round: 0 },
|
||||
envelope: 'raw',
|
||||
meta,
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
const index = cache.pending.indexOf(pending)
|
||||
/* v8 ignore next -- a committed goal append cannot reject after its contained observers run */
|
||||
if (index < 0) throw new Error('goal injection failed after its pending mutation was reconciled', { cause: error })
|
||||
cache.pending.splice(index, 1)
|
||||
throw error
|
||||
}
|
||||
if (!pending.applied) {
|
||||
applyGoalChange(cache.state, change)
|
||||
cache.activation = activation
|
||||
pending.applied = true
|
||||
}
|
||||
this.sync(agent.session, cache)
|
||||
const goal = this.view(cache)
|
||||
const notification: GoalChanged = {
|
||||
|
||||
@@ -249,6 +249,25 @@ describe('GoalService creation and replay', () => {
|
||||
expect(ctx.goals.resume(agent, goal)).toMatchObject({ revision: 2, activation: 'armed' })
|
||||
})
|
||||
|
||||
it('removes the service and its session-start listener with the providing fiber', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(AgentRegistry)
|
||||
const fiber = await ctx.plugin(GoalService)
|
||||
const first = ctx.goals
|
||||
const stub = stubAgent('goal-hmr')
|
||||
ctx.agents.register(stub.agent)
|
||||
const goal = first.create(stub.agent, { objective: 'survive service reload' })
|
||||
|
||||
await fiber.dispose()
|
||||
expect(ctx.get('goals')).toBeUndefined()
|
||||
agentEvents(ctx, stub.agent).emit('agent/session-start', 'resume')
|
||||
expect(first.get(stub.agent)).toMatchObject({ id: goal.id, activation: 'armed' })
|
||||
|
||||
await ctx.plugin(GoalService)
|
||||
expect(ctx.goals).not.toBe(first)
|
||||
expect(ctx.goals.get(stub.agent)).toMatchObject({ id: goal.id, activation: 'disarmed' })
|
||||
})
|
||||
|
||||
it('requires the exact live registry instance for reads and mutations', async () => {
|
||||
const { ctx, agent } = await harness()
|
||||
const impostor = { ...agent, session: new Session(agent.id) }
|
||||
@@ -418,6 +437,46 @@ describe('GoalService mutations', () => {
|
||||
expect(foldGoal(session.events)).toMatchObject({ goal: { revision: 3, phase: 'paused' } })
|
||||
})
|
||||
|
||||
it('publishes a mutation consistently to a reentrant session observer', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(GoalService)
|
||||
const stub = stubAgentForSession(ctx.sessions.create(SessionId('goal-reentrant-observer')))
|
||||
ctx.agents.register(stub.agent)
|
||||
let observed: ReturnType<GoalService['get']>
|
||||
ctx.on('session/event', (session, event) => {
|
||||
if (session === stub.session && event.type === 'context/message') observed = ctx.goals.get(stub.agent)
|
||||
})
|
||||
|
||||
const created = ctx.goals.create(stub.agent, { objective: 'publish once' })
|
||||
|
||||
expect(observed).toEqual(created)
|
||||
expect(ctx.goals.get(stub.agent)).toEqual(created)
|
||||
expect(foldGoal(stub.session.events)).toMatchObject({ goal: { id: created.id, revision: 1 } })
|
||||
})
|
||||
|
||||
it('rolls back a pending mutation when injection rejects before append', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(GoalService)
|
||||
const stub = stubAgent('goal-rejected-injection')
|
||||
const append = stub.agent.inject.bind(stub.agent)
|
||||
let reject = true
|
||||
stub.agent.inject = (content, options) => {
|
||||
if (reject) throw new Error('injection rejected')
|
||||
append(content, options)
|
||||
}
|
||||
ctx.agents.register(stub.agent)
|
||||
|
||||
expect(() => ctx.goals.create(stub.agent, { objective: 'first attempt' })).toThrow('injection rejected')
|
||||
reject = false
|
||||
expect(ctx.goals.create(stub.agent, { objective: 'second attempt' })).toMatchObject({
|
||||
objective: 'second attempt',
|
||||
revision: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects deferred goal mutations that enter the log out of FIFO order', async () => {
|
||||
const test = await harness()
|
||||
test.setDeferred(true)
|
||||
@@ -461,6 +520,39 @@ describe('GoalService mutations', () => {
|
||||
activation: 'disarmed',
|
||||
})
|
||||
})
|
||||
|
||||
it('reports the same corrupt unseen event after committing its valid prefix', async () => {
|
||||
const { ctx, agent, session } = await harness()
|
||||
expect(ctx.goals.get(agent)).toBeUndefined()
|
||||
const change: GoalSnapshotChangeMeta = {
|
||||
kind: 'goal/change',
|
||||
version: 1,
|
||||
operation: 'create',
|
||||
goal: {
|
||||
id: GoalId('goal-valid-prefix'),
|
||||
revision: 1,
|
||||
objective: 'valid prefix',
|
||||
phase: 'active',
|
||||
maxGoalRounds: 4,
|
||||
},
|
||||
roundsStarted: 0,
|
||||
createdAt: 12,
|
||||
updatedAt: 12,
|
||||
}
|
||||
appendInjection(session, renderGoalChange(change), {
|
||||
source: { kind: 'goal', goalId: change.goal.id, revision: 1, round: 0 },
|
||||
envelope: 'raw',
|
||||
meta: change as never,
|
||||
})
|
||||
appendInjection(session, [{ type: 'text', text: 'corrupt' }], {
|
||||
source: { kind: 'goal', goalId: change.goal.id, revision: 2, round: 0 },
|
||||
envelope: 'raw',
|
||||
meta: { ...change, operation: 'edit', extra: true } as never,
|
||||
})
|
||||
|
||||
expect(() => ctx.goals.get(agent)).toThrow('invalid shape')
|
||||
expect(() => ctx.goals.get(agent)).toThrow('invalid shape')
|
||||
})
|
||||
})
|
||||
|
||||
describe('goal replay validation', () => {
|
||||
|
||||
@@ -10,12 +10,14 @@ The model-facing control surface for [`ctx.goals`](../goal/README.md): `get_goal
|
||||
|
||||
All calls are exclusive, so a model-ordered batch observes earlier mutations and their new revisions. ACP and other clients receive pure generic cards: read for `get_goal`, other for mutations.
|
||||
|
||||
A successful mutation that leaves the goal stopped contributes the existing terminal `agent/turn-stop` decision for that physical turn. A later same-turn resume clears the contribution. This avoids an extra model request after pause, block, or completion without changing ordinary loop continuation.
|
||||
An autonomous goal round that successfully reports `complete` or `blocked` contributes the existing terminal `agent/turn-stop` decision for that physical turn. Direct-human mutations never contribute this stop: the assistant may acknowledge the change and concurrent human steering remains available to the loop.
|
||||
|
||||
## Authority
|
||||
|
||||
Execution requires the exact live `exec.agent`, its inherited `AgentRegistry` initiator, running status, and an open turn. Create, edit, pause, and resume additionally require an accepted `{ kind: 'user' }` message or steering event in a runtime-root agent's current turn. Durable fork lineage does not demote a resumed root; live subagent ownership does.
|
||||
|
||||
`{ kind: 'user' }` is a host attestation. `Agent.send()` and `steer()` assign it when their caller omits a source, so plugins, schedulers, and other non-human producers must pass their own source rather than inheriting human authority.
|
||||
|
||||
Complete and blocked also accept the exact current goal round: a goal-sourced `user/message` whose id, revision, and round equal the folded current goal. A goal-round blocked call is mechanically rejected until `blockedAfterConsecutiveRounds`; the model judges whether the same condition actually persisted. Direct human authority may stop a goal immediately.
|
||||
|
||||
## Config
|
||||
@@ -70,4 +72,5 @@ Schemas are prefix-stable while their definitions and visibility are unchanged.
|
||||
- **Semantic intent remains model judgment** — execution can prove direct human provenance, not whether a request is substantial enough to merit a goal.
|
||||
- **Same-condition blocking remains model judgment** — the runtime enforces distinct admitted-round count, not semantic equivalence of obstacles; an independent evaluator is deferred.
|
||||
- **No scheduling or UI commands** — these tools mutate state only; the same-session driver and human command surfaces are separate stack layers.
|
||||
- **Goal-round authority requires a driver** — the autonomous `complete`/`blocked` path is dormant unless a continuation driver admits goal-sourced user turns; mounting this tool package alone does not create them.
|
||||
- **Prompt registration is independent of filtering** — a scope may hide the tools while retaining their guidance unless the deployment scopes both registrations together.
|
||||
|
||||
@@ -62,7 +62,11 @@ export function goalToolExecution(ctx: Context, exec: ToolRunContext): GoalToolE
|
||||
return { agent, ...openTurn(agent) }
|
||||
}
|
||||
|
||||
/** Whether an accepted human message appears in the current root-agent turn. */
|
||||
/**
|
||||
* Whether host-attested human input appears in the current root-agent turn.
|
||||
* An omitted `Agent.send()` / `steer()` source resolves to `user`, so non-human
|
||||
* producers must supply their own source rather than inheriting this authority.
|
||||
*/
|
||||
function hasDirectHumanInput(ctx: Context, execution: GoalToolExecution): boolean {
|
||||
if (!ctx.agents.roots().includes(execution.agent)) return false
|
||||
return execution.events.some(event =>
|
||||
|
||||
@@ -50,8 +50,8 @@ const CREATE_DESCRIPTION =
|
||||
+ 'trivial single-turn work. Execution rejects non-human and subagent authority.'
|
||||
|
||||
const GET_DESCRIPTION =
|
||||
'Read the current same-session goal, including its exact id/revision, durable phase, admitted '
|
||||
+ 'round count, cap, and live process-local activation. Call this before updating a goal.'
|
||||
'Read the current same-session goal, including its exact id/revision, objective, phase, completed '
|
||||
+ 'continuation rounds, round limit, and whether another continuation is armed. Call this before updating a goal.'
|
||||
|
||||
/** Render policy guidance with its deployment-selected blocked threshold. */
|
||||
function guidance(blockedAfter: number): string {
|
||||
@@ -107,13 +107,14 @@ function present(title: string, kind: 'read' | 'other', rawInput?: unknown): Gen
|
||||
return { card: 'generic', title, kind, ...rawInput === undefined ? {} : { rawInput } }
|
||||
}
|
||||
|
||||
/** Remember whether one successful mutation makes this turn terminal. */
|
||||
/** Remember whether one autonomous terminal report should stop this turn. */
|
||||
function observeMutation(
|
||||
terminalTurns: WeakMap<Agent, number>,
|
||||
execution: GoalToolExecution,
|
||||
goal: GoalView,
|
||||
autonomousTerminal: boolean,
|
||||
): void {
|
||||
if (goal.phase === 'active' && goal.activation === 'armed') {
|
||||
if (!autonomousTerminal || (goal.phase === 'active' && goal.activation === 'armed')) {
|
||||
terminalTurns.delete(execution.agent)
|
||||
return
|
||||
}
|
||||
@@ -123,6 +124,8 @@ function observeMutation(
|
||||
/** Register the three Codex-shaped goal tools and their shared policy section. */
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
const resolved = resolveConfig(config)
|
||||
// A stale entry cannot match a later loop turn because turn numbers increase
|
||||
// monotonically within the agent's fixed session.
|
||||
const terminalTurns = new WeakMap<Agent, number>()
|
||||
ctx.on('agent/turn-stop', (agent, turn) => {
|
||||
if (terminalTurns.get(agent) !== turn) return undefined
|
||||
@@ -160,7 +163,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
},
|
||||
max_goal_rounds: {
|
||||
type: 'number',
|
||||
description: 'Optional positive safe-integer cap; omission uses the goal-domain deployment default.',
|
||||
description: 'Optional positive safe-integer limit on automatic continuation rounds.',
|
||||
},
|
||||
},
|
||||
execute(args, exec) {
|
||||
@@ -170,7 +173,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
objective: args.objective,
|
||||
...args.max_goal_rounds === undefined ? {} : { maxGoalRounds: args.max_goal_rounds },
|
||||
})
|
||||
observeMutation(terminalTurns, execution, goal)
|
||||
observeMutation(terminalTurns, execution, goal, false)
|
||||
return Promise.resolve([{ type: 'text', text: renderGoal(goal) }])
|
||||
},
|
||||
presentCall: args => present('Create goal', 'other', args.objective),
|
||||
@@ -179,8 +182,8 @@ export function apply(ctx: Context, config: Config): void {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'update_goal',
|
||||
description: 'Update the exact current goal revision. edit, pause, and resume require a direct '
|
||||
+ 'top-level human turn. complete and blocked additionally accept the exact admitted goal '
|
||||
+ 'round. blocked is rejected before the configured minimum round count; the model remains '
|
||||
+ 'top-level human request. During an automatic continuation of the current goal, complete '
|
||||
+ 'and blocked are also allowed. blocked is rejected before the configured minimum round count; the model remains '
|
||||
+ 'responsible for judging that the same condition persisted across those rounds.',
|
||||
parameters: {
|
||||
goal_id: { type: 'string', required: true, description: 'Exact id returned by get_goal.' },
|
||||
@@ -204,27 +207,33 @@ export function apply(ctx: Context, config: Config): void {
|
||||
if (args.action === 'edit') {
|
||||
requireDirectHuman(ctx, execution)
|
||||
const goal = ctx.goals.edit(execution.agent, ref, replacements)
|
||||
observeMutation(terminalTurns, execution, goal)
|
||||
observeMutation(terminalTurns, execution, goal, false)
|
||||
return Promise.resolve([{
|
||||
type: 'text',
|
||||
text: renderGoal(goal),
|
||||
}])
|
||||
}
|
||||
if (args.action === 'pause' || args.action === 'resume') {
|
||||
requireDirectHuman(ctx, execution)
|
||||
if (args.objective !== undefined || args.max_goal_rounds !== undefined) {
|
||||
throw new HarnessError(
|
||||
'objective and max_goal_rounds are valid only with action edit',
|
||||
'GOAL_TOOL_INVALID_UPDATE',
|
||||
)
|
||||
}
|
||||
const goal = args.action === 'pause'
|
||||
? ctx.goals.pause(execution.agent, ref)
|
||||
: ctx.goals.resume(execution.agent, ref)
|
||||
observeMutation(terminalTurns, execution, goal, false)
|
||||
return Promise.resolve([{ type: 'text', text: renderGoal(goal) }])
|
||||
}
|
||||
const authority = completionAuthority(ctx, execution)
|
||||
if (args.objective !== undefined || args.max_goal_rounds !== undefined) {
|
||||
throw new HarnessError(
|
||||
'objective and max_goal_rounds are valid only with action edit',
|
||||
'GOAL_TOOL_INVALID_UPDATE',
|
||||
)
|
||||
}
|
||||
if (args.action === 'pause' || args.action === 'resume') {
|
||||
requireDirectHuman(ctx, execution)
|
||||
const goal = args.action === 'pause'
|
||||
? ctx.goals.pause(execution.agent, ref)
|
||||
: ctx.goals.resume(execution.agent, ref)
|
||||
observeMutation(terminalTurns, execution, goal)
|
||||
return Promise.resolve([{ type: 'text', text: renderGoal(goal) }])
|
||||
}
|
||||
const authority = completionAuthority(ctx, execution)
|
||||
if (args.action === 'blocked' && authority.kind === 'goal-round'
|
||||
&& authority.goal.roundsStarted < resolved.blockedAfterConsecutiveRounds) {
|
||||
throw new HarnessError(
|
||||
@@ -236,7 +245,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
const goal = args.action === 'complete'
|
||||
? ctx.goals.complete(execution.agent, ref)
|
||||
: ctx.goals.block(execution.agent, ref)
|
||||
observeMutation(terminalTurns, execution, goal)
|
||||
observeMutation(terminalTurns, execution, goal, authority.kind === 'goal-round')
|
||||
return Promise.resolve([{ type: 'text', text: renderGoal(goal) }])
|
||||
},
|
||||
presentCall: args => present(
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('goal tools through a real Loader, app, and stdio process', () => {
|
||||
expect(stdout).toContain('goal-tools e2e ready.')
|
||||
expect(stdout).toContain('GOAL CREATED')
|
||||
expect(stdout).toContain(PAUSED_RESULT)
|
||||
expect(stdout).not.toContain('UNEXPECTED CONTINUATION AFTER PAUSE')
|
||||
expect(stdout).toContain('GOAL PAUSED')
|
||||
|
||||
const logs = await jsonlFiles(join(workdir as string, '.sessions'))
|
||||
expect(logs).toHaveLength(1)
|
||||
|
||||
@@ -7,7 +7,7 @@ import GoalService, { GoalId } from '@deepseek-ai/dsh-goal'
|
||||
import type { GoalRef } from '@deepseek-ai/dsh-goal'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||
import type { ToolExecutionResult } from '@deepseek-ai/dsh-tools'
|
||||
@@ -20,8 +20,8 @@ interface StubAgent {
|
||||
}
|
||||
|
||||
/** Build one registry-compatible live agent whose injections append in place. */
|
||||
function stubAgent(rawId: string): StubAgent {
|
||||
const session = new Session(SessionId(rawId))
|
||||
function stubAgent(rawId: string, supplied?: Session): StubAgent {
|
||||
const session = supplied ?? new Session(SessionId(rawId))
|
||||
let status: AgentStatus = 'running'
|
||||
const agent: Agent = {
|
||||
id: session.id,
|
||||
@@ -219,6 +219,42 @@ describe('goal tool execution authority', () => {
|
||||
expect(childResult.error?.code).toBe('GOAL_TOOL_AUTHORITY_REQUIRED')
|
||||
})
|
||||
|
||||
it('rejects stale agent objects and agents outside running status through the executor', async () => {
|
||||
const { ctx, root } = await harness()
|
||||
openTurn(root, { kind: 'user' })
|
||||
const stale = { ...root.agent }
|
||||
const staleResult = await execute(ctx, 'get_goal', {}, stale, stale)
|
||||
expect(staleResult.error?.code).toBe('GOAL_TOOL_DRIVER_REQUIRED')
|
||||
|
||||
root.setStatus('idle')
|
||||
const idleResult = await execute(ctx, 'get_goal', {}, root.agent)
|
||||
expect(idleResult.error?.code).toBe('GOAL_TOOL_DRIVER_REQUIRED')
|
||||
})
|
||||
|
||||
it('treats a fork resumed as a runtime root as direct-human authority', async () => {
|
||||
const { ctx, root } = await harness()
|
||||
const originalTurn = openTurn(root, { kind: 'user' })
|
||||
const created = ctx.goals.create(root.agent, { objective: 'resume the fork' })
|
||||
closeTurn(root, originalTurn)
|
||||
const forkId = SessionId('goal-tool-resumed-fork')
|
||||
const forkSession = new Session(forkId, root.session.events, {
|
||||
version: SESSION_FORMAT_VERSION,
|
||||
id: forkId,
|
||||
createdAt: Date.now(),
|
||||
parentSession: root.session.id,
|
||||
seedLength: root.session.seq,
|
||||
})
|
||||
const fork = stubAgent(forkId, forkSession)
|
||||
ctx.agents.register(fork.agent)
|
||||
expect(ctx.goals.get(fork.agent)).toMatchObject({ id: created.id, activation: 'disarmed' })
|
||||
|
||||
openTurn(fork, { kind: 'user' }, '继续这个目标')
|
||||
const resumed = await execute(ctx, 'update_goal', {
|
||||
goal_id: created.id, revision: created.revision, action: 'resume',
|
||||
}, fork.agent)
|
||||
expect(resultGoal(resumed)).toMatchObject({ id: created.id, revision: 2, phase: 'active' })
|
||||
})
|
||||
|
||||
it('rejects calls before a turn and after its end boundary', async () => {
|
||||
const { ctx, root } = await harness()
|
||||
const before = await execute(ctx, 'get_goal', {}, root.agent)
|
||||
@@ -237,6 +273,10 @@ describe('goal tool execution authority', () => {
|
||||
goal_id: 'goal-missing', revision: 1, action: 'complete',
|
||||
}, root.agent)
|
||||
expect(result.error?.code).toBe('GOAL_TOOL_AUTHORITY_REQUIRED')
|
||||
const malformed = await execute(ctx, 'update_goal', {
|
||||
goal_id: 'goal-missing', revision: 1, action: 'pause', objective: 'probe',
|
||||
}, root.agent)
|
||||
expect(malformed.error?.code).toBe('GOAL_TOOL_AUTHORITY_REQUIRED')
|
||||
})
|
||||
|
||||
it('accepts direct human steering in a goal-sourced root turn', async () => {
|
||||
@@ -290,16 +330,29 @@ describe('goal tool state transitions', () => {
|
||||
expect(await agentEvents(ctx, root.agent).serial('agent/turn-stop', 1)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('stops the current turn after a successful stopped-state mutation', async () => {
|
||||
it('terminal-stops an autonomous completion but leaves a human pause interactive', async () => {
|
||||
const { ctx, root } = await harness()
|
||||
openTurn(root, { kind: 'user' })
|
||||
const humanTurn = openTurn(root, { kind: 'user' })
|
||||
const created = ctx.goals.create(root.agent, { objective: 'pause cleanly' })
|
||||
const paused = await execute(ctx, 'update_goal', {
|
||||
goal_id: created.id, revision: created.revision, action: 'pause',
|
||||
}, root.agent)
|
||||
expect(resultGoal(paused)).toMatchObject({ phase: 'paused' })
|
||||
expect(await agentEvents(ctx, root.agent).serial('agent/turn-stop', 1)).toEqual({ action: 'stop' })
|
||||
expect(await agentEvents(ctx, root.agent).serial('agent/turn-stop', 1)).toBeUndefined()
|
||||
expect(await agentEvents(ctx, root.agent).serial('agent/turn-stop', humanTurn)).toBeUndefined()
|
||||
const resumed = resultGoal(await execute(ctx, 'update_goal', {
|
||||
goal_id: created.id, revision: 2, action: 'resume',
|
||||
}, root.agent))
|
||||
closeTurn(root, humanTurn)
|
||||
|
||||
const roundTurn = openTurn(root, {
|
||||
kind: 'goal', goalId: created.id, revision: resumed['revision'] as number, round: 1,
|
||||
})
|
||||
const complete = await execute(ctx, 'update_goal', {
|
||||
goal_id: created.id, revision: resumed['revision'], action: 'complete',
|
||||
}, root.agent)
|
||||
expect(resultGoal(complete)).toMatchObject({ phase: 'complete' })
|
||||
expect(await agentEvents(ctx, root.agent).serial('agent/turn-stop', roundTurn)).toEqual({ action: 'stop' })
|
||||
expect(await agentEvents(ctx, root.agent).serial('agent/turn-stop', roundTurn)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rearms a restored active goal only after a new direct human prompt', async () => {
|
||||
|
||||
@@ -28,7 +28,7 @@ Effective broad cancellation was requested, before queued/steering work is clear
|
||||
- `agent` — the agent whose current work is being cancelled.
|
||||
- `reason` — resolved cancellation reason, including the default. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L186)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L189)
|
||||
|
||||
### agent/created
|
||||
|
||||
@@ -52,7 +52,7 @@ A fully configured agent and live session were published. Setup is composition-o
|
||||
|
||||
- `agent` — the newly registered agent with its live session and completed setup. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L148)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L151)
|
||||
|
||||
### agent/disposed
|
||||
|
||||
@@ -74,7 +74,7 @@ An agent left the registry; AgentLoop emits this after driver quiescence but bef
|
||||
|
||||
- `agent` — the exact agent removed from the registry. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L157)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L160)
|
||||
|
||||
### agent/error
|
||||
|
||||
@@ -101,7 +101,7 @@ A step or turn errored. The loop reports a failure here (plus the logger) even w
|
||||
- `step` — the step at which the failure surfaced.
|
||||
- `error` — the failure, verbatim. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L322)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L325)
|
||||
|
||||
### agent/post-step
|
||||
|
||||
@@ -129,7 +129,7 @@ Awaited serial checkpoint after the response, real or synthetic tool results, in
|
||||
- `step` — the open step number.
|
||||
- `signal` — the turn abort signal. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L275)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L278)
|
||||
|
||||
### agent/pre-step
|
||||
|
||||
@@ -157,7 +157,7 @@ Awaited serial checkpoint before `step/start`; appends land outside the pending
|
||||
- `step` — the pending step number.
|
||||
- `signal` — the turn abort signal.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L215)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L218)
|
||||
|
||||
### agent/prompt-submit
|
||||
|
||||
@@ -182,7 +182,7 @@ Allow, rewrite, or block one drained prompt before it becomes a user message. Ca
|
||||
- `content` — the drained message's blocks, as queued.
|
||||
- `source` — the message's resolved source. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L225)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L228)
|
||||
|
||||
### agent/queued
|
||||
|
||||
@@ -207,7 +207,7 @@ Detached, frozen content entered the agent's inbox. Source defaults have already
|
||||
- `content` — the accepted content blocks retained by the inbox.
|
||||
- `info` — the accepted source plus whether it entered as steering. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L176)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L179)
|
||||
|
||||
### agent/request
|
||||
|
||||
@@ -235,7 +235,7 @@ Replace the frozen call configuration. Model-visible content must use logged cha
|
||||
- `step` — the step whose request this is.
|
||||
- `config` — the config the loop would use (frozen); return a replacement to switch. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L237)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L240)
|
||||
|
||||
### agent/request-error
|
||||
|
||||
@@ -267,7 +267,7 @@ Recover a model-request failure after its failed step has closed. `retry` opens
|
||||
- `retryAttempt` — zero-based number of prior recovery retries.
|
||||
- `signal` — the turn abort signal. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L289)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L292)
|
||||
|
||||
### agent/session-prefix
|
||||
|
||||
@@ -297,7 +297,7 @@ Compose request-only messages placed before derived history. The frozen result i
|
||||
- `prefix` — the frozen seed; return an extended replacement.
|
||||
- `signal` — aborts composition when the step is torn down.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L252)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L255)
|
||||
|
||||
### agent/session-start
|
||||
|
||||
@@ -322,7 +322,7 @@ The session lifecycle began, once before the first turn. Use `agent.inject()` to
|
||||
- `agent` — the agent whose session lifecycle began.
|
||||
- `source` — why the session started (fresh startup, resume, …). Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L199)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L202)
|
||||
|
||||
### agent/status
|
||||
|
||||
@@ -345,7 +345,7 @@ Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does no
|
||||
- `agent` — the agent whose status flipped.
|
||||
- `status` — the status just entered (the transition's destination). Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L166)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L169)
|
||||
|
||||
### agent/step-result
|
||||
|
||||
@@ -372,7 +372,7 @@ Waterfall: post-process the assembled assistant Message before tool dispatch (va
|
||||
- `step` — the step that produced the message.
|
||||
- `message` — the assistant message as assembled from the stream. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L263)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L266)
|
||||
|
||||
### agent/turn-continuation
|
||||
|
||||
@@ -397,7 +397,7 @@ Override whether the turn continues. The default continues after tool calls or s
|
||||
- `turn` — the turn being continued or stopped.
|
||||
- `defaultDecision` — what the loop would do absent an override. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L299)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L302)
|
||||
|
||||
### agent/turn-stop
|
||||
|
||||
@@ -421,7 +421,7 @@ Monotonic terminal-stop checkpoint after continuation and steering are folded; a
|
||||
- `agent` — the agent whose composed continuation outcome may be stopped.
|
||||
- `turn` — the turn at its terminal-stop checkpoint. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L309)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L312)
|
||||
|
||||
## agent-loop/*
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
Goal service (`ctx.goals`) backed exclusively by the owning session log.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L97)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L104)
|
||||
|
||||
### ctx.goals.resolveCreate(request)
|
||||
|
||||
@@ -25,7 +25,7 @@ Materialize deployment defaults and validate one create request.
|
||||
|
||||
**Returns** detached, fully resolved create specification.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L122)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L129)
|
||||
|
||||
### ctx.goals.get(agent)
|
||||
|
||||
@@ -45,7 +45,7 @@ Read the current goal for one exact live agent.
|
||||
|
||||
**Returns** a fresh view or `undefined` when no goal is current.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L135)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L142)
|
||||
|
||||
### ctx.goals.disarm(agent)
|
||||
|
||||
@@ -66,7 +66,7 @@ Remove process-local continuation authority without changing durable goal phase
|
||||
|
||||
**Returns** a fresh disarmed view, or `undefined` when no goal is current.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L149)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L156)
|
||||
|
||||
### ctx.goals.create(agent, request)
|
||||
|
||||
@@ -88,7 +88,7 @@ Create and arm a goal. A completed goal may be replaced; every other current pha
|
||||
|
||||
**Returns** the created live view.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L164)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L171)
|
||||
|
||||
### ctx.goals.edit(agent, ref, request)
|
||||
|
||||
@@ -111,7 +111,7 @@ Edit objective and/or round cap without changing phase.
|
||||
|
||||
**Returns** the edited view.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L189)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L196)
|
||||
|
||||
### ctx.goals.pause(agent, ref)
|
||||
|
||||
@@ -132,7 +132,7 @@ Pause an active goal and disarm automatic continuation.
|
||||
|
||||
**Returns** the paused view.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L210)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L217)
|
||||
|
||||
### ctx.goals.resume(agent, ref)
|
||||
|
||||
@@ -154,7 +154,7 @@ Resume and arm a stopped goal, or rearm an active goal after a session-start edg
|
||||
|
||||
**Returns** the active view.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L221)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L228)
|
||||
|
||||
### ctx.goals.complete(agent, ref)
|
||||
|
||||
@@ -175,7 +175,7 @@ Mark a current non-complete goal complete and disarm it.
|
||||
|
||||
**Returns** the completed view.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L246)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L253)
|
||||
|
||||
### ctx.goals.block(agent, ref)
|
||||
|
||||
@@ -196,7 +196,7 @@ Mark an active goal blocked and disarm it.
|
||||
|
||||
**Returns** the blocked view.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L263)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L270)
|
||||
|
||||
### ctx.goals.markUsageLimited(agent, ref)
|
||||
|
||||
@@ -217,7 +217,7 @@ Mark an active goal stopped by an external usage limit.
|
||||
|
||||
**Returns** the usage-limited view.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L273)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L280)
|
||||
|
||||
### ctx.goals.markBudgetLimited(agent, ref)
|
||||
|
||||
@@ -238,7 +238,7 @@ Mark an active goal stopped at its configured round cap.
|
||||
|
||||
**Returns** the budget-limited view.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L283)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L290)
|
||||
|
||||
### ctx.goals.clear(agent, ref)
|
||||
|
||||
@@ -259,4 +259,4 @@ Clear the current goal while retaining a durable tombstone and history.
|
||||
|
||||
**Returns** the tombstone ref whose revision is one past the cleared snapshot.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L310)
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/goal/goal/src/index.ts#L317)
|
||||
|
||||
Reference in New Issue
Block a user