Merge remote-tracking branch 'origin/master' into xtr/identified-immutable-messages
# Conflicts: # docs/cordis-catalog/events.md # docs/core-data-structures/core.i18n.yaml # docs/event-producer-consumer.md # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # packages/core/agent-loop/README.i18n.yaml # packages/core/agent/README.i18n.yaml
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 packages/core/agent-loop/README.md
|
||||
README.md: 99fdc73e2dcb09ed7d47f021598dea8720cada5d
|
||||
README.zh.md: fe9db9a625ea289e7e9364a182061dee1a0e773c
|
||||
README.md: 6bb8b12af69f54c2a75cd672e4d3802887808c76
|
||||
README.zh.md: f9eb8aa3cdead427a88492e35c00eab80ba12f91
|
||||
|
||||
@@ -42,13 +42,14 @@ interface Config {
|
||||
id: string // required
|
||||
provider?: string
|
||||
model?: string
|
||||
maxTokens?: number // positive per-request output-token cap
|
||||
resumeSessionId?: string // load this persisted session instead of creating one
|
||||
cwd?: string // optional workspace cwd for the fresh session
|
||||
}>
|
||||
}
|
||||
```
|
||||
|
||||
Configured agents start automatically. A model call requires both `provider` and `model`; `agent/request` may supply a missing pair before dispatch. `maxParallelToolCalls` bounds every agent's rolling pool for parallel-safe calls and defaults to `10`. `cwd` applies only to fresh sessions, while `resumeSessionId` retains persisted metadata. Configured agents use the deployment persona, and programmatic setup can shadow it per agent. This plugin supplies the per-agent `provider`, `model`, and `cwd` prompt variables; harness identity and deployment persona belong to `dsh-system-prompt`.
|
||||
Configured agents start automatically. A model call requires both `provider` and `model`; `agent/request` may supply a missing pair before dispatch. An optional positive `maxTokens` seeds each conversation request's output cap and is logged in its request header. `maxParallelToolCalls` bounds every agent's rolling pool for parallel-safe calls and defaults to `10`. `cwd` applies only to fresh sessions, while `resumeSessionId` retains persisted metadata. Configured agents use the deployment persona, and programmatic setup can shadow it per agent. This plugin supplies the per-agent `provider`, `model`, and `cwd` prompt variables; harness identity and deployment persona belong to `dsh-system-prompt`.
|
||||
|
||||
### Internal concrete driver
|
||||
|
||||
|
||||
@@ -42,13 +42,14 @@ interface Config {
|
||||
id: string // required
|
||||
provider?: string
|
||||
model?: string
|
||||
maxTokens?: number // positive per-request output-token cap
|
||||
resumeSessionId?: string // load this persisted session instead of creating one
|
||||
cwd?: string // optional workspace cwd for the fresh session
|
||||
}>
|
||||
}
|
||||
```
|
||||
|
||||
通过配置创建的 agent 会自动启动。模型调用同时需要 `provider` 和 `model`;`agent/request` 可以在分发前补齐缺失的这一对值。`maxParallelToolCalls` 限制每个 agent 针对并行安全调用使用的滚动池,默认值为 `10`。`cwd` 仅应用于全新会话,而 `resumeSessionId` 保留持久化元数据。通过配置创建的 agent 使用部署 persona;编程式 setup 可以按 agent 遮蔽它。该插件提供逐 agent 的 `provider`、`model` 和 `cwd` 提示词变量;harness 身份与部署 persona 属于 `dsh-system-prompt`。
|
||||
通过配置创建的 agent 会自动启动。模型调用同时需要 `provider` 和 `model`;`agent/request` 可以在分发前补齐缺失的这一对值。可选的正整数 `maxTokens` 会为每次对话请求提供初始输出上限,并记录在请求 header 中。`maxParallelToolCalls` 限制每个 agent 针对并行安全调用使用的滚动池,默认值为 `10`。`cwd` 仅应用于全新会话,而 `resumeSessionId` 保留持久化元数据。通过配置创建的 agent 使用部署 persona;编程式 setup 可以按 agent 遮蔽它。该插件提供逐 agent 的 `provider`、`model` 和 `cwd` 提示词变量;harness 身份与部署 persona 属于 `dsh-system-prompt`。
|
||||
|
||||
### 包内部实体驱动器
|
||||
|
||||
|
||||
@@ -581,11 +581,16 @@ export class ReactLoopAgent implements Agent {
|
||||
&& persistedConfig.model === route.model
|
||||
? persistedConfig.reasoningEffort
|
||||
: undefined
|
||||
const maxTokens = this.options.maxTokens
|
||||
const seedConfig = deepFreeze(structuredClone(
|
||||
this.requestHeaderLogged
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- the instance logged the header it now folds
|
||||
? persistedConfig!
|
||||
: { ...route, ...reasoningEffort === undefined ? {} : { reasoningEffort } },
|
||||
: {
|
||||
...route,
|
||||
...reasoningEffort === undefined ? {} : { reasoningEffort },
|
||||
...maxTokens === undefined ? {} : { maxTokens },
|
||||
},
|
||||
))
|
||||
const proposedConfig = await this.loopCtx.waterfall(
|
||||
agentCarrier(this), 'agent/request', this, turn, step, signal,
|
||||
|
||||
@@ -112,6 +112,14 @@ function resolveMaxParallelToolCalls(value: number | undefined): number {
|
||||
return maxParallelToolCalls
|
||||
}
|
||||
|
||||
/** Reject an output-token cap that cannot be represented exactly on the request wire. */
|
||||
function assertAgentOptions(options: AgentOptions): void {
|
||||
if (options.maxTokens !== undefined
|
||||
&& (!Number.isSafeInteger(options.maxTokens) || options.maxTokens <= 0)) {
|
||||
throw new TypeError('agent maxTokens must be a positive safe integer')
|
||||
}
|
||||
}
|
||||
|
||||
/** Prepared-but-unpublished agent resources sharing one memoized teardown. */
|
||||
interface PreparedAgent {
|
||||
agent: ReactLoopAgent
|
||||
@@ -196,6 +204,7 @@ export class AgentLoop extends Service implements AgentFactory {
|
||||
sessionId: z.string().min(1),
|
||||
provider: z.string(),
|
||||
model: z.string(),
|
||||
maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER),
|
||||
cwd: z.string(),
|
||||
resumeSessionId: z.string(),
|
||||
})).default([]),
|
||||
@@ -327,6 +336,7 @@ export class AgentLoop extends Service implements AgentFactory {
|
||||
* fuses caller cancellation with lifecycle teardown for setup awaits.
|
||||
*/
|
||||
private prepare(ownerCtx: Context, id: SessionId, options: AgentOptions, session: Session, callerSignal?: AbortSignal): PreparedAgent {
|
||||
assertAgentOptions(options)
|
||||
ownerCtx.fiber.assertActive()
|
||||
// Every caller reaches prepare() synchronously from a service method
|
||||
// whose Cordis dispatch already requires the live factory fiber, or
|
||||
|
||||
@@ -46,6 +46,19 @@ function send(agent: Agent, text: string) {
|
||||
}
|
||||
|
||||
describe('agent loop', () => {
|
||||
it.each([0, -1, 1.5, Number.NaN, Number.MAX_SAFE_INTEGER + 1])(
|
||||
'rejects invalid AgentOptions.maxTokens %s before publication',
|
||||
async (maxTokens) => {
|
||||
const ctx = await harness(new MockAdapter([]))
|
||||
expect(() => ctx.agentLoop.create(
|
||||
SessionId('invalid-max-tokens'),
|
||||
{ provider: 'mock', model: 'mock', maxTokens },
|
||||
)).toThrow('agent maxTokens must be a positive safe integer')
|
||||
expect(ctx.agents.list()).toEqual([])
|
||||
expect(ctx.sessions.list()).toEqual([])
|
||||
},
|
||||
)
|
||||
|
||||
it('runs a simple turn: queued message → model → idle, with ordered events', async () => {
|
||||
const adapter = new MockAdapter([textResponse('hello there')])
|
||||
const ctx = await harness(adapter)
|
||||
|
||||
@@ -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 packages/core/agent/README.md
|
||||
README.md: 9fe207d940ddc80153131c3d76c992804bb83773
|
||||
README.zh.md: c7975fe5e0368aa46253c47c15f04e12a24a3b7b
|
||||
README.md: 9ca79f28506b133a555bd7d1e984386c715fd9d6
|
||||
README.zh.md: 165f71f1b395bdf0c229e2c4b1a30e89347b6be1
|
||||
|
||||
@@ -14,6 +14,8 @@ Tracks live agents and carries the initiating Agent through asynchronous driver
|
||||
|
||||
The scoped-registration surface: `Agent.ctx` is the agent's scope context (`dsh-scope`, key = the agent) — register tools/sections/variables/listeners through it for that agent alone, all unwound on disposal. `agentEvents(ctx, agent)` is the fused dispatcher for ordinary agent-subject operations (carrier + injected subject in one move); its notification mode invokes every listener and contains both synchronous throws and returned-promise rejections. The registry lifecycle pair reuses one stable routing carrier. `assembleContextFor(agent)` builds the per-agent assembly context (`agent` + `scope` together). `installAgentLlmTarget(agentCtx, target)` snapshots a mutable provider/model/reasoning-effort selection during prompt assembly, applies the route to prompt variables, and applies the complete target to request routing for one step; an absent selected effort clears an inherited effort so the target uses adapter/provider defaults. `CreateAgentOptions.setup(agentCtx)` and `ResumeAgentOptions.setup(agentCtx)` compose a fresh or resumed agent's scoped world while both objects remain unpublished. Setup is trusted, composition-only same-process code: drive the agent only after creation resolves.
|
||||
|
||||
`AgentOptions` supplies the initial provider/model route and an optional positive `maxTokens` output cap. The concrete loop records the cap in the request header and applies it to each conversation-model request; callers that omit it leave provider defaults in control.
|
||||
|
||||
- `ctx.agents.register(agent: Agent): () => void` — record an **already-constructed** agent. Disposed with the calling fiber.
|
||||
- Advanced ordered lifecycle: `enter(agent, owner): () => void` enforces `agent.id === agent.session.id`, performs the authoritative ID collision check, and inserts without announcing; `owner` explicitly records the live creator-agent relation (or `undefined` for a root), independently of durable session lineage. `announce(agent)` emits `agent/created` exactly once. A detach requested synchronously by a creation listener is deferred until that dispatch unwinds, and every detach checks the captured entry object, so a stale capability cannot delete a later same-ID replacement. The async factory uses this split; ordinary plugins use `register()`.
|
||||
- `ctx.agents.get(id: SessionId): Agent | undefined`
|
||||
@@ -58,7 +60,7 @@ Turn and step boundaries and the model token stream are durable `session/event`
|
||||
|
||||
The handle every plugin programs against:
|
||||
|
||||
- `agent.send(message, options)` — the one delivery primitive over the (`target` × `wakeup`) matrix. `message` is an already identified `UserMessage`; callers normally create it with `createUserMessage()` before routing begins. `SendOptions` owns only the `target` and `wakeup` policy. The agent detaches and freezes the complete value without minting or replacing its identity. The message's `agent/inbox/enqueue`/`dequeue`/`discard` events carry it so callers can correlate a queued item with its lifecycle; enqueue and dequeue also carry the resolved `queued | steering` placement so repeated message identities retire from the correct FIFO. `target: 'next-turn'` queues one independent FIFO item that, if admitted, becomes the sole ordinary prompt in its turn. `target: 'next-step'` with `wakeup: true` submits steering, while `target: 'next-step'` with `wakeup: false` injects durable context without running the model. The [one-send-one-turn Agent Note](../../../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md) owns the turn rationale.
|
||||
- `agent.send(message, options)` — the one delivery primitive over the (`target` × `wakeup`) matrix. `message` is an already identified, frozen `UserMessage`; callers normally create it with `createUserMessage()` before routing begins. `SendOptions` owns only the `target` and `wakeup` policy. The agent publishes or queues the complete value as-is without minting or replacing its identity. The message's `agent/inbox/enqueue`/`dequeue`/`discard` events carry it so callers can correlate a queued item with its lifecycle; enqueue and dequeue also carry the resolved `queued | steering` placement so repeated message identities retire from the correct FIFO. `target: 'next-turn'` queues one independent FIFO item that, if admitted, becomes the sole ordinary prompt in its turn. `target: 'next-step'` with `wakeup: true` submits steering, while `target: 'next-step'` with `wakeup: false` injects durable context without running the model. The [one-send-one-turn Agent Note](../../../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md) owns the turn rationale.
|
||||
- `agent.followup(input)` — the `next-turn`/wakeup preset of `send()`: queue an ordinary follow-up turn and wake the driver.
|
||||
- `agent.steer(input)` — the `next-step`/wakeup preset: during prompt admission or an open turn, stage steering for the next safe boundary without dispatching `agent/prompt-submit`; outside that acceptance window, delegate to a woken follow-up. Admission failure leaves staged steering for retry or a later admitted prompt, while cancellation or disposal may discard it.
|
||||
- `agent.inject(input)` — the `next-step`/no-wakeup preset: append model-facing context without running the model; the next request sees a verbatim user-role message whose provenance is carried by the required `input.source`. During prompt admission or an open turn, injection waits in the outbox for the next safe boundary. Outside that acceptance window, it appends immediately without opening a turn; a context-only admission batch takes this fallback if admission closes without a turn, while context staged beside steering remains pending with it. Persistence reacts to `session/event` independently. Injection emits no `agent/inbox/*` event.
|
||||
|
||||
@@ -14,6 +14,8 @@ Agent 接口、注册表、进程本地发起方作用域,以及 `agent/*` 事
|
||||
|
||||
带作用域的注册表层:`Agent.ctx` 是 agent 的作用域上下文(`dsh-scope`,键 = 该 agent)。通过它注册工具/段/变量/监听器,只对该 agent 生效,并在释放时全部撤销。`agentEvents(ctx, agent)` 是普通 agent 主体操作的融合分发器(一次完成载体 + 注入主体);其通知 mode 会调用每个监听器,并同时收容同步抛出和返回 Promise 的拒绝。注册表生命周期对复用一个稳定路由载体。`assembleContextFor(agent)` 构建按 agent 的组装上下文(同时包含 `agent` + `scope`)。`installAgentLlmTarget(agentCtx, target)` 在提示词组装期间快照可变的提供方/模型/推理(reasoning)强度选择,将路由应用到提示词变量,并将完整目标应用到一个步骤的请求路由;如果没有选定推理强度,则会清除继承的推理强度,使该目标使用适配器/提供方默认值。`CreateAgentOptions.setup(agentCtx)` 和 `ResumeAgentOptions.setup(agentCtx)` 在新建或恢复的 agent 尚未发布时,组合其带作用域的世界。Setup 是受信任、仅用于组合的同进程代码:只有创建完成后才能驱动 agent。
|
||||
|
||||
`AgentOptions` 提供初始的提供方/模型路由,以及可选的正整数 `maxTokens` 输出上限。实体循环会把该上限记录到请求 header,并应用到每次对话模型请求;调用方省略时由提供方默认值控制。
|
||||
|
||||
- `ctx.agents.register(agent: Agent): () => void`:记录一个 **已经构造完成** 的 agent。随调用 fiber 释放。
|
||||
- 高级有序生命周期:`enter(agent, owner): () => void` 强制 `agent.id === agent.session.id`,执行权威 ID 冲突检查,并在不通知的情况下插入;`owner` 显式记录实时创建方 agent 关系(根 agent 为 `undefined`),与持久会话谱系无关。`announce(agent)` 恰好发出一次 `agent/created`。创建监听器同步请求的 detach 会延后到该次分发结束;每次 detach 都会检查捕获的条目对象,因此陈旧能力无法删除后续使用同一 ID 的替代项。异步工厂使用这一拆分;普通插件使用 `register()`。
|
||||
- `ctx.agents.get(id: SessionId): Agent | undefined`
|
||||
@@ -58,7 +60,7 @@ Agent *创建* 由实现 `AgentFactory` 的插件(`dsh-agent-loop`)提供,
|
||||
|
||||
每个插件面向的 handle:
|
||||
|
||||
- `agent.send(message, options)`:覆盖(`target` × `wakeup`)矩阵的唯一投递原语。`message` 是已有标识的 `UserMessage`;调用方通常会在开始路由前使用 `createUserMessage()` 创建它。`SendOptions` 只持有 `target` 与 `wakeup` 策略。agent 会将完整值与输入分离并冻结,但不会生成或替换其标识。该消息的 `agent/inbox/enqueue`/`dequeue`/`discard` 事件会携带其 id,调用方可据此把排队项与其生命周期关联;入队与出队事件还会携带解析出的 `queued | steering` 路由归类,使重复出现的消息标识能在正确的 FIFO 中完成结算。`target: 'next-turn'` 排队一条独立 FIFO 项,获准后成为其轮次中唯一的普通提示词。`target: 'next-step'` 且 `wakeup: true` 提交 steering(中途引导),而 `target: 'next-step'` 且 `wakeup: false` 注入持久上下文,不运行模型。轮次原理由 [one-send-one-turn Agent Note](../../../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md)拥有。
|
||||
- `agent.send(message, options)`:覆盖(`target` × `wakeup`)矩阵的唯一投递原语。`message` 是已有标识且已冻结的 `UserMessage`;调用方通常会在开始路由前使用 `createUserMessage()` 创建它。`SendOptions` 只持有 `target` 与 `wakeup` 策略。agent 会原样发布或排队完整值,不会生成或替换其标识。该消息的 `agent/inbox/enqueue`/`dequeue`/`discard` 事件会携带完整消息,调用方可据此把排队项与其生命周期关联;入队与出队事件还会携带解析出的 `queued | steering` 路由归类,使重复出现的消息标识能在正确的 FIFO 中完成结算。`target: 'next-turn'` 排队一条独立 FIFO 项,获准后成为其轮次中唯一的普通提示词。`target: 'next-step'` 且 `wakeup: true` 提交 steering(中途引导),而 `target: 'next-step'` 且 `wakeup: false` 注入持久上下文,不运行模型。轮次原理由 [one-send-one-turn Agent Note](../../../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md)拥有。
|
||||
- `agent.followup(input)`:`send()` 的 `next-turn`/wakeup 预设:排队一个普通后续轮次并唤醒驱动器。
|
||||
- `agent.steer(input)`:`next-step`/wakeup 预设:提示词接纳期间或轮次打开时,为下一个安全边界暂存 steering,且不分发 `agent/prompt-submit`;该接收窗口之外则委托给会唤醒的后续轮次。接纳失败会保留暂存的 steering,以供重试或之后获准的提示词使用,而取消或 dispose 可能丢弃它。
|
||||
- `agent.inject(input)`:`next-step`/不唤醒预设:追加面向模型的上下文而不运行模型;下一次请求会看到一条逐字的 user role 消息,其来源由必填的 `input.source` 携带。提示词接纳期间或轮次打开时,注入会在 outbox 中等待下一个安全边界。该接收窗口之外,它会立即追加而不开启轮次;如果接纳结束却未开启轮次,仅含上下文的接纳批次会采用这一回退,而与 steering 一同暂存的上下文则会随其继续待处理。持久化独立地响应 `session/event`。注入不发出 `agent/inbox/*` 事件。
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface AgentOptions {
|
||||
provider?: string
|
||||
/** Model id interpreted by the selected provider adapter. */
|
||||
model?: string
|
||||
/** Maximum output tokens for each conversation-model request. */
|
||||
maxTokens?: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user