refactor(agent): expose resolved input acceptance
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-22-unified-send-and-coalesced-user-messages.md: 9f6b29b4e1d90d75dbb88ab770c7393cdb859627
|
||||
2026-07-22-unified-send-and-coalesced-user-messages.zh.md: 4469d211da23a24f61017770b29bc983a2abb5f1
|
||||
2026-07-22-unified-send-and-coalesced-user-messages.md: f9196b2170bdcdc45325abc1666d0c1545d475ac
|
||||
2026-07-22-unified-send-and-coalesced-user-messages.zh.md: b984d0706a6758e37df312d0b59bba3afb602258
|
||||
|
||||
@@ -12,7 +12,7 @@ Separately, `context/message` and `user/message` had converged: the surface proj
|
||||
|
||||
## Decision
|
||||
|
||||
**One private mechanism, four public intents.** The concrete loop resolves `send`, `queue`, `steer`, and `inject` into one private (`target` × `wakeup`) delivery mechanism. `send` is `next-turn`/wakeup, `queue` is `next-turn`/no-wakeup, `steer` is `next-step`/wakeup, and `inject` is `next-step`/no-wakeup. The public structural interface exposes no routing fields or abstract base; the [intent-named delivery decision](2026-07-24-intent-named-agent-delivery.md) owns that superseding interface choice. Internally, `wakeup` means “make the model run”: wake a parked driver for an ordinary item or force a continuation for running steering.
|
||||
**One acceptance mechanism, four intent helpers.** The concrete loop resolves `send`, `queue`, `steer`, and `inject` into one (`target` × `wakeup`) acceptance mechanism. `send` is `next-turn`/wakeup, `queue` is `next-turn`/no-wakeup, `steer` is `next-step`/wakeup, and `inject` is `next-step`/no-wakeup. The public structural interface also exposes that mechanism as `acceptInput(ResolvedAgentInput)` for callers that already have fully resolved routing; every field is mandatory, and the discriminated input type excludes attached contexts from injection. The [intent-named delivery decision](2026-07-24-intent-named-agent-delivery.md) owns that superseding interface choice. Internally, `wakeup` means “make the model run”: wake a parked driver for an ordinary item or force a continuation for running steering.
|
||||
|
||||
**inject keeps its mechanism.** `inject` appends durable model-facing context at the current log position (deferred behind an executing tool batch), or opens a one-shot `injection` turn when idle. It bypasses the FIFOs entirely, accepts no attached contexts, and defaults its source to `{ kind: 'plugin', plugin: '' }`, never `{ kind: 'user' }`.
|
||||
|
||||
@@ -43,4 +43,4 @@ Internally, `wakeup` is the “should the model run” signal, so the inbox dist
|
||||
- [one-send-one-turn](../simplification/2026-07-17-one-send-one-turn.md) — the one-claimed-message-per-turn rule this builds on.
|
||||
- [remove-agent-steering-mirror](../simplification/2026-07-04-remove-agent-steering-mirror.md) — the precedent for collapsing a mirrored live event.
|
||||
- [explicit-turn-cancellation](2026-07-16-explicit-turn-cancellation.md) — the cancel-cause signal `keepInbox` extends.
|
||||
- [intent-named-agent-delivery](2026-07-24-intent-named-agent-delivery.md) — the public interface and private routing placement.
|
||||
- [intent-named-agent-delivery](2026-07-24-intent-named-agent-delivery.md) — the public helpers and fully resolved acceptance interface.
|
||||
|
||||
@@ -12,7 +12,7 @@ agent 的对外驱动接口逐渐长出三个近乎平行的动词——`send`
|
||||
|
||||
## 决策
|
||||
|
||||
**一个私有机制,四种公开意图。** 具体循环把 `send`、`queue`、`steer` 和 `inject` 解析到同一个私有的(`target` × `wakeup`)投递机制中。`send` 是 `next-turn`/wakeup,`queue` 是 `next-turn`/no-wakeup,`steer` 是 `next-step`/wakeup,`inject` 是 `next-step`/no-wakeup。公开的结构化接口不暴露路由字段或抽象基类;取代旧接口的选择由[按意图命名的投递决策](2026-07-24-intent-named-agent-delivery.md)负责说明。内部的 `wakeup` 表示「让模型运行」:为一条普通消息唤醒处于停泊状态的驱动器,或强制运行中的 steering 继续执行。
|
||||
**一种接受机制,四种意图辅助方法。** 具体循环把 `send`、`queue`、`steer` 和 `inject` 解析到同一个(`target` × `wakeup`)接受机制中。`send` 是 `next-turn`/wakeup,`queue` 是 `next-turn`/no-wakeup,`steer` 是 `next-step`/wakeup,`inject` 是 `next-step`/no-wakeup。公开的结构化接口还将该机制暴露为 `acceptInput(ResolvedAgentInput)`;调用方若已持有完全解析的路由信息,即可使用该方法。使用时必须提供所有字段,可辨识输入类型也不允许注入携带附加上下文。取代旧接口的选择由[按意图命名的投递决策](2026-07-24-intent-named-agent-delivery.md)负责说明。内部的 `wakeup` 表示「让模型运行」:为一条普通消息唤醒处于停泊状态的驱动器,或强制运行中的 steering 继续执行。
|
||||
|
||||
**inject 保留其机制。** `inject` 在当前日志位置追加持久、面向模型的上下文(在执行中的工具批处理之后延迟处理),或在空闲时开启一个一次性的 `injection` 轮次。它完全绕过 FIFO,不接受附加上下文,并把来源默认设为 `{ kind: 'plugin', plugin: '' }`,绝不是 `{ kind: 'user' }`。
|
||||
|
||||
@@ -43,4 +43,4 @@ agent 的对外驱动接口逐渐长出三个近乎平行的动词——`send`
|
||||
- [one-send-one-turn](../simplification/2026-07-17-one-send-one-turn.md)——本决策所依托的“每轮次只认领一条消息”规则。
|
||||
- [remove-agent-steering-mirror](../simplification/2026-07-04-remove-agent-steering-mirror.md)——折叠镜像实时事件的先例。
|
||||
- [explicit-turn-cancellation](2026-07-16-explicit-turn-cancellation.md)——`keepInbox` 所扩展的取消原因信号。
|
||||
- [intent-named-agent-delivery](2026-07-24-intent-named-agent-delivery.md)——公开接口和私有路由的归属位置。
|
||||
- [intent-named-agent-delivery](2026-07-24-intent-named-agent-delivery.md)——公开辅助方法以及接受完全解析输入的接口。
|
||||
|
||||
@@ -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-24-intent-named-agent-delivery.md: 62a05ed7601ca3dd2f05fae609341ab3fa37671a
|
||||
2026-07-24-intent-named-agent-delivery.zh.md: e559b106ca54f901a18f2936142770d6306ef757
|
||||
2026-07-24-intent-named-agent-delivery.md: 54ea6353a516830795d51c395706241a4570260d
|
||||
2026-07-24-intent-named-agent-delivery.zh.md: 0d903a0d5dd383a154ade02ee8bed607d1f986b1
|
||||
|
||||
@@ -6,13 +6,13 @@ English | [中文](2026-07-24-intent-named-agent-delivery.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A public `send(content, { target, wakeup, ... })` makes every caller learn the loop's routing matrix, its defaults, and the interaction between active-turn targeting and model activation. Optional routing fields also let advanced-looking calls silently become ordinary sends. The mechanism is useful inside the concrete driver, but exposing it gives plugins implementation knowledge without leverage.
|
||||
A configurable `send(content, { target?, wakeup?, ... })` makes every caller learn the loop's routing matrix, its defaults, and the interaction between active-turn targeting and model activation. Optional routing fields also let advanced-looking calls silently become ordinary sends. Most callers have one semantic intent, while some adapters already possess exact routing facts and should not have to reverse-map them into a helper name.
|
||||
|
||||
Sharing helper implementations through an abstract `Agent` class also makes the public seam nominal in practice. Object-literal adapters and tests must inherit prototype methods even though the package promises a swappable structural handle. The shared base exists only to forward fixed arguments, while the concrete loop remains the sole production adapter.
|
||||
|
||||
## Decision
|
||||
|
||||
`Agent` is a structural interface with four intent-named delivery methods:
|
||||
`Agent` is a structural interface with four intent-named delivery helpers:
|
||||
|
||||
- `send()` queues an ordinary turn and wakes the driver.
|
||||
- `queue()` queues an ordinary turn without waking an idle driver.
|
||||
@@ -21,15 +21,17 @@ Sharing helper implementations through an abstract `Agent` class also makes the
|
||||
|
||||
`send`, `queue`, and `steer` accept `SendOptions`; `inject` accepts `InjectOptions`, which omits attached contexts because injection has no inbox item to own them. `followup` is absent: ordinary `send` already names the established common operation, and “follow-up” is false for a session's first message.
|
||||
|
||||
`ReactLoopAgent` resolves each public call into one module-private `ResolvedAgentInput` and passes it to native-private `#acceptInput`. Every internal field is mandatory: content, source, contexts, metadata (possibly `undefined`), target, and wakeup. Injection resolves contexts to the empty tuple. The private name says what the synchronous boundary guarantees: acceptance can still lead to later dequeue, discard, or durable injection rather than eventual delivery.
|
||||
`Agent` also exposes `acceptInput(ResolvedAgentInput)` for callers that already hold the complete route. Every field is mandatory: content, source, contexts, metadata (possibly `undefined`), target, and wakeup. The discriminated union requires the empty context tuple for non-waking next-step injection. `ReactLoopAgent` implements this method once, and all four helpers resolve their defaults before delegating to it. The name says what the synchronous boundary guarantees: acceptance can still lead to later dequeue, discard, or durable injection rather than eventual delivery.
|
||||
|
||||
The target/wakeup matrix remains an implementation mechanism in `dsh-agent-loop`. It is not exported, protected, or represented by a base class. With one concrete adapter, a subclass delivery seam would be hypothetical; callers and tests use the same public `Agent` interface.
|
||||
The target/wakeup matrix is an explicit advanced part of the structural `Agent` interface, not the ordinary helper options and not a base-class implementation seam. With one concrete adapter, a protected subclass seam would be hypothetical; callers and tests use the same public interface.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep a public configurable primitive.** Mandatory routing arguments remove defaulting mistakes but still require every caller to learn the matrix and allow invalid intent combinations such as attached contexts on injection. The concrete loop needs that flexibility; ordinary plugins do not.
|
||||
**Keep the resolved primitive private.** This minimizes the public method count, but forces adapters that already hold exact target/wakeup facts to reverse-map them into helper calls and removes the reusable type for that resolved state.
|
||||
|
||||
**Rename the primitive to `sendInternal` or `addMessageAdvanced`.** Visibility belongs in the type and runtime boundary, not a warning in a public name. `addMessageAdvanced` is also inaccurate because acceptance may wake, queue, steer, inject, or later discard work.
|
||||
**Use configurable `send` as the primitive.** Even mandatory routing arguments would make the common method carry advanced concerns. Keeping `send` semantic preserves its simple defaulted call shape; the separate discriminated input type rejects attached contexts on injection.
|
||||
|
||||
**Rename the primitive to `sendInternal` or `addMessageAdvanced`.** A public method must not describe itself as internal. `addMessageAdvanced` is also inaccurate because acceptance may wake, queue, steer, inject, or later discard work; `acceptInput` names the synchronous guarantee instead.
|
||||
|
||||
**Keep `followup` as the waking-turn helper.** Existing production callers use `send`, while `followup` has no TypeScript caller and does not describe the first ordinary message. Reusing `send` preserves the familiar intent without retaining an alias.
|
||||
|
||||
@@ -37,13 +39,13 @@ The target/wakeup matrix remains an implementation mechanism in `dsh-agent-loop`
|
||||
|
||||
## Verification
|
||||
|
||||
Focused agent-loop coverage exercises waking sends, quiet queues, active and idle steering, injection, source/context snapshots, cancellation, and inbox lifecycle correlation through the public methods. Type-level coverage uses structural `Agent` fakes and rejects routing fields on `SendOptions` and contexts on `InjectOptions`. The keyless Cordis inspection snapshot pins the model-facing interface without a configurable delivery primitive or abstract-class implementation.
|
||||
Focused agent-loop coverage exercises direct fully resolved acceptance, waking sends, quiet queues, active and idle steering, injection, source/context snapshots, cancellation, and inbox lifecycle correlation through the public methods. Type-level coverage uses structural `Agent` fakes, requires every `ResolvedAgentInput` field, requires empty contexts on its injection variant, and keeps routing fields out of `SendOptions`. The keyless Cordis inspection snapshot pins the structural interface without an abstract-class implementation.
|
||||
|
||||
## Consequences
|
||||
|
||||
Callers choose one verb instead of encoding two routing axes. The concrete loop retains one acceptance path and one ownership boundary, while the structural interface restores simple adapters and fakes. Adding a delivery intent now requires an explicit public name and mapping rather than another matrix combination.
|
||||
Ordinary callers choose one verb instead of encoding two routing axes; advanced callers may submit the exact discriminated route. The concrete loop retains one acceptance path and one ownership boundary, while the structural interface preserves simple adapters and fakes. Adding a common delivery intent still requires an explicit public helper and mapping rather than another optional matrix combination.
|
||||
|
||||
Four short public methods duplicate a small amount of argument resolution inside the concrete adapter. That duplication is deliberate locality: defaults and routing stay beside the only implementation that owns them, and no generator support is needed merely to expose a class-shaped `Agent`.
|
||||
The advanced method adds interface surface and requires structural fakes to implement it. In return, resolved routing has one typed representation, while helper defaults and mappings stay beside the only implementation that owns them.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
公开的 `send(content, { target, wakeup, ... })` 会迫使每个调用方理解循环的路由矩阵、默认值,以及活跃轮次目标与模型激活之间的相互作用。可选路由字段还会让看似高级的调用悄然变成普通投递。该机制在具体驱动器内部很有用,但将其公开只会让插件背负实现知识,却得不到相应收益。
|
||||
可配置的 `send(content, { target?, wakeup?, ... })` 会迫使每个调用方理解循环的路由矩阵、默认值,以及活跃轮次目标与模型激活之间的相互作用。可选路由字段还会让看似高级的调用悄然变成普通投递。大多数调用方只有一种语义意图,而有些适配器已经持有确切的路由信息,不应再被迫将这些信息反向映射为某个辅助方法名称。
|
||||
|
||||
通过抽象 `Agent` 类共享辅助方法的实现,实际上也会让公开 seam 具有名义类型约束。对象字面量适配器和测试必须继承原型方法,尽管该包承诺提供一个可替换的结构化句柄。共享基类只负责转发固定参数,而具体循环仍是唯一的生产适配器。
|
||||
|
||||
## 决策
|
||||
|
||||
`Agent` 是一个结构化接口,提供四种按意图命名的投递方法:
|
||||
`Agent` 是一个结构化接口,提供四种按意图命名的投递辅助方法:
|
||||
|
||||
- `send()` 将一个普通轮次入队并唤醒驱动器。
|
||||
- `queue()` 将一个普通轮次入队,但不唤醒空闲驱动器。
|
||||
@@ -21,15 +21,17 @@ Status: implemented
|
||||
|
||||
`send`、`queue` 和 `steer` 接收 `SendOptions`;`inject` 接收 `InjectOptions`,后者不包含附加上下文,因为注入没有 inbox 项来拥有它们。接口不提供 `followup`:普通 `send` 已经为既有的常见操作命名,而「follow-up」不适用于会话的第一条消息。
|
||||
|
||||
`ReactLoopAgent` 把每次公开调用解析为一个模块私有的 `ResolvedAgentInput`,再将其传给原生私有的 `#acceptInput`。每个内部字段都必须提供:内容、来源、上下文、元数据(可以是 `undefined`)、目标和唤醒标志。注入会把上下文解析为空元组。这个私有名称说明同步边界所保证的事实:接受之后,工作仍可能在稍后出队、被丢弃或持久注入,而不是最终必然送达。
|
||||
`Agent` 还公开 `acceptInput(ResolvedAgentInput)`,供已经持有完整路由的调用方使用。每个字段都必须提供:内容、来源、上下文、元数据(可以是 `undefined`)、目标和唤醒标志。对于目标为下一步且不触发唤醒的注入,可辨识联合类型要求上下文为空元组。`ReactLoopAgent` 统一实现这个方法;四个辅助方法都会先解析各自的默认值,再委托给它。这个名称说明同步边界所保证的事实:接受之后,工作仍可能在稍后出队、被丢弃或持久注入,而不是最终必然送达。
|
||||
|
||||
target/wakeup 矩阵仍是 `dsh-agent-loop` 中的实现机制。它不会导出,不是 protected 成员,也不由基类表示。只有一个具体适配器时,子类投递 seam 只是假想的;调用方和测试使用同一个公开 `Agent` 接口。
|
||||
结构化 `Agent` 接口显式包含面向高级用法的 target/wakeup 矩阵;该矩阵不属于普通辅助方法的选项,也不是基类实现 seam。只有一个具体适配器时,protected 子类 seam 只是假想的;调用方和测试使用同一个公开接口。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留公开的可配置原语。** 强制提供路由参数可以消除默认值错误,但仍会要求每个调用方理解矩阵,也允许出现无效的意图组合,例如为注入附加上下文。具体循环需要这种灵活性;普通插件不需要。
|
||||
**让解析后的原语保持私有。** 这会把公开方法数量降到最低,但会迫使已经持有精确 target/wakeup 路由信息的适配器将其反向映射为辅助方法调用,也会移除表示该解析后状态的可复用类型。
|
||||
|
||||
**把原语重命名为 `sendInternal` 或 `addMessageAdvanced`。** 可见性应由类型和运行时边界表达,而不是在公开名称中加入警告。`addMessageAdvanced` 也不准确,因为接受操作可能唤醒、排队、中途引导、注入,或在之后丢弃工作。
|
||||
**使用可配置的 `send` 作为原语。** 即使强制提供所有路由参数,也会让这个常用方法承载高级用法的复杂性。让 `send` 只表达语义意图,可以保留其带默认值的简单调用形式;单独的可辨识输入类型则会拒绝为注入附加上下文。
|
||||
|
||||
**把原语重命名为 `sendInternal` 或 `addMessageAdvanced`。** 公开方法不应在名称中把自己称为内部方法。`addMessageAdvanced` 也不准确,因为接受操作可能唤醒、排队、中途引导、注入,或在之后丢弃工作;`acceptInput` 描述的则是同步边界所保证的事实。
|
||||
|
||||
**保留 `followup` 作为唤醒轮次的辅助方法。** 现有生产调用方使用 `send`,而 `followup` 没有 TypeScript 调用方,也无法描述第一条普通消息。复用 `send` 可以保留熟悉的意图,同时不保留别名。
|
||||
|
||||
@@ -37,13 +39,13 @@ target/wakeup 矩阵仍是 `dsh-agent-loop` 中的实现机制。它不会导出
|
||||
|
||||
## 验证
|
||||
|
||||
聚焦的 agent-loop 覆盖率测试通过公开方法覆盖唤醒式投递、静默排队、活跃与空闲状态下的 steering(中途引导)、注入、来源与上下文快照、取消,以及 inbox 生命周期关联。类型级覆盖使用结构化 `Agent` 测试替身,并拒绝 `SendOptions` 上的路由字段和 `InjectOptions` 上的上下文。无密钥的 Cordis 检查快照固定了面向模型的接口,其中既没有可配置的投递原语,也没有抽象类实现。
|
||||
聚焦的 agent-loop 覆盖率测试通过公开方法覆盖直接接受完全解析的输入、唤醒式投递、静默排队、活跃与空闲状态下的 steering(中途引导)、注入、来源与上下文快照、取消,以及 inbox 生命周期关联。类型级覆盖使用结构化 `Agent` 测试替身,要求提供 `ResolvedAgentInput` 的每个字段,要求其注入变体的上下文为空,并确保 `SendOptions` 不包含路由字段。无密钥的 Cordis 检查快照固定了不采用抽象类实现的结构化接口。
|
||||
|
||||
## 后果
|
||||
|
||||
调用方选择一个动词即可,无需编码两条路由轴。具体循环保留一条接受路径和一个归属边界,而结构化接口重新支持简单的适配器和测试替身。新增一种投递意图时,需要显式给出公开名称和映射,而不是再增加一种矩阵组合。
|
||||
普通调用方选择一个动词即可,无需编码两条路由轴;高级调用方则可提交经过判别的精确路由。具体循环保留一条接受路径和一个归属边界,而结构化接口保留了对简单适配器和测试替身的支持。新增一种常见投递意图时,仍需要显式提供公开辅助方法及其映射,而不是再增加一种可选的矩阵组合。
|
||||
|
||||
四个简短的公开方法会在具体适配器中重复少量参数解析。这项重复是为了让实现保持局部:默认值和路由都留在拥有它们的唯一实现旁边,无需仅为了暴露类形态的 `Agent` 而增加生成器支持。
|
||||
这个高级方法会扩大接口范围,并要求结构化测试替身实现它。作为回报,解析后的路由只有一种类型化表示,而辅助方法的默认值和映射仍留在拥有它们的唯一实现旁边。
|
||||
|
||||
## 相关
|
||||
|
||||
|
||||
Reference in New Issue
Block a user