Merge remote-tracking branch 'origin/stack/agent-profiles-5-web-ui' into stack/agent-profiles-8-authoring

# Conflicts:
#	packages/client/connection/README.i18n.yaml
#	packages/client/connection/README.md
#	packages/client/connection/README.zh.md
This commit is contained in:
Yichen Jiang
2026-08-09 20:41:55 +08:00
1623 changed files with 15506 additions and 5450 deletions

View File

@@ -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/README.md
README.md: 40a888183a9338805d83c86bab9ae16f1c7bc04f
README.zh.md: c0a24d78cb8ff2a2b2ccc2d2a2f4294424be55b0
README.md: 8349371ab565f2e9e735cd959026936c7ec44081
README.zh.md: e19c446584cfac75cb50833fa01586688b9c7c92

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
The session log, system-prompt assembly, tool registry, agent vocabulary, and concrete loop that form the harness's default control spine. These are **product** packages — the stable surface plugins and consumers build against.
The session log, system-prompt assembly, tool registry, agent vocabulary, deployment-default model selection, and concrete loop that form the harness's default control spine. These are **product** packages — the stable surface plugins and consumers build against.
| Package | Role | ctx key |
|---|---|---|
@@ -11,9 +11,10 @@ The session log, system-prompt assembly, tool registry, agent vocabulary, and co
| [`system-prompt/`](system-prompt/README.md) | Prompt and tool-schema assembly registry | `ctx.systemPrompt` |
| [`tools/`](tools/README.md) | Scoped tool registry and execution pipeline | `ctx.tools` |
| [`agent/`](agent/README.md) | Agent interface, registry, and event vocabulary | `ctx.agents` |
| [`agent-default-model/`](agent-default-model/README.md) | Default model selection shared by Agent front doors | `ctx.agentDefaultModel` |
| [`agent-loop/`](agent-loop/README.md) | Default concrete agent driver | `ctx.agentLoop` |
`scope` supplies the shared scoping primitive. `agent` owns the public seam, while `agent-loop` is its default implementation; extension plugins depend on the seam so the driver remains swappable.
`scope` supplies the shared scoping primitive. `agent` owns the public contract, while `agent-loop` is its default implementation; extension plugins depend on the seam so the driver remains swappable. `agent-default-model` owns the deployment selection an Agent front door uses only when a session has no selection of its own.
Runnable compositions belong to [`examples/agent-spine-demo`](../examples/agent-spine-demo/README.md); this group owns only the swappable spine pieces.

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
构成 harness 默认控制主干的会话日志、系统提示词组装、工具注册表、agent智能体词汇和具体循环。这些是**产品**包,即插件和消费方构建所依赖的稳定 surface。
构成 harness 默认控制主干的会话日志、系统提示词组装、工具注册表、agent智能体词汇、部署默认模型选择和具体循环。这些是**产品**包,即插件和消费方构建所依赖的稳定 surface。
| 包 | 职责 | ctx key |
|---|---|---|
@@ -11,9 +11,10 @@
| [`system-prompt/`](system-prompt/README.md) | 提示词和工具 schema 组装注册表 | `ctx.systemPrompt` |
| [`tools/`](tools/README.md) | 作用域工具注册表和执行流水线 | `ctx.tools` |
| [`agent/`](agent/README.md) | Agent 接口、注册表和事件词汇 | `ctx.agents` |
| [`agent-default-model/`](agent-default-model/README.md) | 各 Agent 入口共享的默认模型选择 | `ctx.agentDefaultModel` |
| [`agent-loop/`](agent-loop/README.md) | 默认具体 agent 驱动器 | `ctx.agentLoop` |
`scope` 提供共享作用域原语。`agent` 负责公开 seam`agent-loop` 是其默认实现;扩展插件依赖该 seam从而保持驱动器可替换。
`scope` 提供共享作用域原语。`agent` 负责公开约定`agent-loop` 是其默认实现;扩展插件依赖该 seam从而保持驱动器可替换。`agent-default-model` 负责部署选择Agent 入口仅在会话自身没有选择时使用它。
可运行组合属于 [`examples/agent-spine-demo`](../examples/agent-spine-demo/README.md);该分组只负责可替换的主干组件。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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-default-model/README.md
README.md: 02bcc9be3adee2293a20b3ae87ddaf4d52e70deb
README.zh.md: 807b612bd25e49aa318c13c8c8dc7595a6459080

View File

@@ -0,0 +1,25 @@
# @deepseek-ai/dsh-agent-default-model
English | [中文](README.zh.md)
The deployment default used when a front door creates an Agent that has no session-local model selection. `AgentDefaultModelService` provides `ctx.agentDefaultModel`; direct front doors such as `dsh run` and Host-backed front doors such as ApiProxy read the same service instead of owning parallel provider/model defaults.
The plugin config requires `{ provider, model }`. That composition entry is the base of the `agent-default-model` Settings section; a mounted settings provider layers the user's choice over it and changes are visible on the next `currentSelection()` read. `reasoningEffort` belongs to the Settings section but deliberately not to plugin config: a complete saved selection can clear an effort when the next selected model has none, while a composition value would be inherited again.
- `ctx.agentDefaultModel.currentSelection()` returns a detached `{ provider, model, reasoningEffort? }` selection for a newly created Agent.
- `ctx.agentDefaultModel.saveSelection(selection)` saves the complete user selection. Without a settings provider it is a no-op and the composition entry remains current.
The service does not validate catalog membership. A provider route may serve an unadvertised model, and the consumer that actually opens a model request owns availability diagnostics.
## Model Experience
Indirectly, through the provider/model selection supplied to a front door; request assembly and adapters own the model-visible request.
#### KV Cache effect
Changing the default affects only Agents that subsequently resolve from it. An existing session whose request log already names a selection keeps that selection, so this service does not invalidate its established prefix.
## Known Limitations and Deferred Work
- The service owns one process-wide default; per-session selection remains the front door's responsibility.
- Without a settings provider, `saveSelection()` cannot retain a selection for a later Agent.

View File

@@ -0,0 +1,25 @@
# @deepseek-ai/dsh-agent-default-model
[English](README.md) | 中文
该部署默认值供入口在创建尚无会话级模型选择的 Agent 时使用。`AgentDefaultModelService` 提供 `ctx.agentDefaultModel``dsh run` 这类直接入口与 ApiProxy 这类由 Host 支撑的入口读取同一服务,而不是分别持有平行的提供方/模型默认值。
插件配置必须提供 `{ provider, model }`。该组合配置项构成 Settings 中 `agent-default-model` 分节的基础层;挂载的设置提供方在其上叠加用户选择,更改会在下一次调用 `currentSelection()` 时可见。`reasoningEffort` 属于该 Settings 分节但特意不属于插件配置完整保存的选择必须能在下一个选定模型没有推理reasoning强度时清除旧值而组合配置值会再次被继承。
- `ctx.agentDefaultModel.currentSelection()` 返回一份独立的 `{ provider, model, reasoningEffort? }` 选择,供新创建的 Agent 使用。
- `ctx.agentDefaultModel.saveSelection(selection)` 保存完整的用户选择。未挂载设置提供方时,此调用不执行任何操作,组合配置项仍为当前值。
该服务不校验目录成员关系。提供方路由可以服务未在目录中公布的模型;实际发起模型请求的消费方负责可用性诊断。
## 模型体验
通过提供给入口的提供方/模型选择间接影响;模型可见请求由请求组装与适配器负责。
#### KV Cache 影响
更改默认值只影响之后从该默认值解析选择的 Agent。请求日志已经指明选择的现有会话仍沿用该选择因此本服务不会使其已建立的前缀失效。
## 已知限制与暂缓事项
- 该服务只拥有一项进程级默认值;每个会话的选择仍由入口负责。
- 未挂载设置提供方时,`saveSelection()` 无法保留选择供后续 Agent 使用。

View File

@@ -0,0 +1,44 @@
{
"name": "@deepseek-ai/dsh-agent-default-model",
"description": "Default model selection shared by Agent front doors",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"dependencies": {
"schemastery": "^3.18.0"
},
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-settings": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -0,0 +1,107 @@
/**
* Default model selection for an Agent without a session-specific selection.
*
* @module @deepseek-ai/dsh-agent-default-model
*/
import { Context, Service } from 'cordis'
import z from 'schemastery'
import type { ModelSelection } from '@deepseek-ai/dsh-agent'
import { ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
declare module 'cordis' {
interface Context {
/** Default model selection for Agents created without an explicit model. */
agentDefaultModel: AgentDefaultModelService
}
}
/** Settings namespace carrying the default model selection for future Agents. */
export const AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE = settingsNamespace('agent-default-model')
/** Stored and composed default model selection. */
export interface AgentDefaultModelSettings {
/** Registered provider route. */
provider: string
/** Provider-owned model id. */
model: string
/** Adapter-owned reasoning effort, or provider/default behavior when absent. */
reasoningEffort?: string
}
/** Schema of the default Agent model settings section. */
export const AGENT_DEFAULT_MODEL_SETTINGS_SCHEMA: z<AgentDefaultModelSettings> = z.object({
provider: z.string().required(),
model: z.string().required(),
reasoningEffort: z.string(),
})
/** Composition entry for the default model selection. */
export interface Config {
/** Registered provider route. */
provider: string
/** Provider-owned model id. */
model: string
}
/** Project stored settings onto the Agent-facing selection type. */
function selection(settings: AgentDefaultModelSettings): ModelSelection {
return {
provider: settings.provider,
model: settings.model,
...settings.reasoningEffort === undefined
? {}
: { reasoningEffort: ReasoningEffortId(settings.reasoningEffort) },
}
}
/**
* Owns the default model selection independently of any Host or transport.
* The composition entry remains usable without a settings provider; when one
* is mounted, its user layer is read live.
*/
export class AgentDefaultModelService extends Service {
static Config: z<Config> = z.object({
provider: z.string().required(),
model: z.string().required(),
})
private source: () => AgentDefaultModelSettings
constructor(ctx: Context, config: Config) {
super(ctx, 'agentDefaultModel')
const entry: AgentDefaultModelSettings = { provider: config.provider, model: config.model }
this.source = () => entry
installSettingsSection(ctx, AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE, AGENT_DEFAULT_MODEL_SETTINGS_SCHEMA, entry, {
setSource: (current) => { this.source = current },
// Every consumer reads through currentSelection(), so no registration-level fact
// needs rebuilding when the settings document changes.
onChange: () => {},
})
}
/**
* Read the current default model selection.
* @returns a detached provider, model, and optional reasoning selection.
*/
currentSelection(): ModelSelection {
return selection(this.source())
}
/**
* Save the complete default model selection. A deployment without a settings
* provider keeps its composition entry.
* @param next - resolved selection accepted by a front door.
* @returns fulfillment after the optional settings write settles.
*/
async saveSelection(next: ModelSelection): Promise<void> {
await this.ctx.get('settings')?.replace(AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE, {
provider: next.provider,
model: next.model,
...next.reasoningEffort === undefined ? {} : { reasoningEffort: String(next.reasoningEffort) },
})
}
}
export default AgentDefaultModelService

View File

@@ -0,0 +1,30 @@
/**
* Package-owned invariant companion for the default Agent model selection.
*
* The service owns no independent event relationship: settings registration
* already validates every mutable value before `currentSelection()` can observe it.
* The empty installer keeps that absence explicit in composed invariant sets.
*
* @module @deepseek-ai/dsh-agent-default-model/invariant
*/
import type { Context } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-agent-default-model'
/** Cordis companion plugin name. */
export const name = 'agent-default-model-invariant'
/** Services required before the companion can register. */
export const inject = ['invariants']
/** No runtime invariant: settings validation owns the only mutable-value relationship. */
const install: InvariantInstaller = () => {}
/**
* Register the intentionally empty invariant contribution.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))

View File

@@ -0,0 +1,98 @@
/** Default Agent model settings layered over a real settings provider. */
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import AgentDefaultModelService, { AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE } from '../src/index.ts'
import { Settings } from '@deepseek-ai/dsh-settings'
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'
import { ReasoningEffortId } from '@deepseek-ai/dsh-llm'
/** The smallest real provider: one in-memory document, always writable. */
class MemorySettings extends Settings {
doc: Record<string, unknown> = {}
get writable(): boolean {
return true
}
protected load(): Promise<Record<string, unknown>> {
return Promise.resolve(structuredClone(this.doc))
}
protected persist(ns: SettingsNamespace, section: Record<string, unknown>): Promise<void> {
this.doc = { ...this.doc, [ns]: structuredClone(section) }
return Promise.resolve()
}
}
async function boot(): Promise<{
ctx: Context
settingsFiber: Context['fiber']
defaultModel: AgentDefaultModelService
}> {
const ctx = new Context()
const settingsFiber = ctx.plugin(MemorySettings)
await settingsFiber.await()
await ctx.plugin(AgentDefaultModelService, {
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
})
return { ctx, settingsFiber, defaultModel: ctx.agentDefaultModel }
}
describe('AgentDefaultModelService', () => {
it('resolves the user layer over the composition entry', async () => {
const bench = await boot()
expect(bench.defaultModel.currentSelection()).toEqual({
provider: 'deepseek-official', model: 'deepseek-v4-flash',
})
await bench.defaultModel.saveSelection({
provider: 'acme-gateway', model: 'acme-large', reasoningEffort: ReasoningEffortId('high'),
})
expect(bench.defaultModel.currentSelection()).toEqual({
provider: 'acme-gateway', model: 'acme-large', reasoningEffort: 'high',
})
await bench.ctx.fiber.dispose()
})
it('clears a stored effort when the saved selection has none', async () => {
const bench = await boot()
await bench.defaultModel.saveSelection({
provider: 'acme-gateway', model: 'acme-large', reasoningEffort: ReasoningEffortId('high'),
})
await bench.defaultModel.saveSelection({ provider: 'acme-gateway', model: 'acme-plain' })
expect(bench.defaultModel.currentSelection()).toEqual({ provider: 'acme-gateway', model: 'acme-plain' })
await bench.ctx.fiber.dispose()
})
it('layers a hand-written partial section over the entry', async () => {
const bench = await boot()
await bench.settingsFiber.ctx.settings.replace(AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE, {
model: 'deepseek-reasoner',
})
expect(bench.defaultModel.currentSelection()).toEqual({
provider: 'deepseek-official', model: 'deepseek-reasoner',
})
await bench.ctx.fiber.dispose()
})
it('falls back to the composition entry when the settings provider detaches', async () => {
const bench = await boot()
await bench.defaultModel.saveSelection({ provider: 'acme-gateway', model: 'acme-large' })
expect(bench.defaultModel.currentSelection().provider).toBe('acme-gateway')
await bench.settingsFiber.dispose()
expect(bench.defaultModel.currentSelection()).toEqual({
provider: 'deepseek-official', model: 'deepseek-v4-flash',
})
await bench.ctx.fiber.dispose()
})
it('keeps the composition entry when no settings provider is mounted', async () => {
const ctx = new Context()
await ctx.plugin(AgentDefaultModelService, { provider: 'p', model: 'm' })
await ctx.agentDefaultModel.saveSelection({ provider: 'other', model: 'other' })
expect(ctx.agentDefaultModel.currentSelection()).toEqual({ provider: 'p', model: 'm' })
await ctx.fiber.dispose()
})
})

View File

@@ -0,0 +1,30 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../../vendor/cordis"
},
{
"path": "../../../vendor/schemastery"
},
{
"path": "../../core/agent"
},
{
"path": "../../llm/llm"
},
{
"path": "../../settings/settings"
},
{
"path": "../../support/invariants"
}
]
}

View File

@@ -0,0 +1,25 @@
import { defineConfig } from 'tsdown'
/** Build the package root and invariant companion as independent bundles. */
export default defineConfig([
{
entry: ['lib/types/index.js'],
outDir: 'lib',
format: ['esm'],
platform: 'node',
target: 'es2024',
fixedExtension: false,
dts: false,
clean: false,
},
{
entry: ['lib/types/invariant.js'],
outDir: 'lib',
format: ['esm'],
platform: 'node',
target: 'es2024',
fixedExtension: false,
dts: false,
clean: false,
},
])

View File

@@ -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: 81bc4411977f7335defef5af82d4a67a740b2ed5
README.zh.md: 22f9655d46c4c9764118f05cc919be26a471c00b
README.md: 6092363fae2853d6c5d92aaf8cd01e41e18e0b52
README.zh.md: b65b5334d735a1e0b51fa517ce41c0c953f87cf7

View File

@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
THE concrete agent plugin and loop driver. Its package-internal implementation satisfies the `Agent` interface and drives the session/turn/step lifecycle.
This is the only package in the harness that contains concrete loop logic. Everything else is an abstract service or a plugin against extension seams — new behavior goes into plugins, not here.
This is the only package in the harness that contains concrete loop logic. Everything else is an abstract service or a plugin against extension points — new behavior goes into plugins, not here.
## Service: `AgentLoop` (ctx key: `agentLoop`)
@@ -18,7 +18,7 @@ Each agent and its session share one caller-chosen `SessionId`, assumed globally
- `ctx.agentLoop.create(id: SessionId, options?: AgentOptions, meta?: { cwd?: string }): Agent` — synchronous no-setup create under the exact shared agent/session id, disposed with the calling fiber. Declarative config treats `agents[].id` as a stable label and normally mints `${label}-session-<uuid>` before calling this boundary. An app may instead supply a stable exact `sessionId`: first use creates it, while a remount with persistence already present resumes its materialized history. `resumeSessionId` requires and loads an existing persisted id and is mutually exclusive with `sessionId`. This keeps default fresh restarts collision-free without retaining a second live routing identity.
`AgentLoop` also implements the `AgentFactory` seam and registers itself via `ctx.agents.setFactory(this)`, so plugins create/resume agents through `ctx.agents` (the interface):
`AgentLoop` also implements the `AgentFactory` contract and registers itself via `ctx.agents.setFactory(this)`, so plugins create/resume agents through `ctx.agents`:
- `ctx.agents.create({ sessionId, meta?, seed?, agentOptions?, setup?, signal? }): Promise<AgentHandle>` — programmatic create under the caller-supplied shared id. It awaits the unpublished setup transaction before returning; `meta` carries cwd/lineage/seed-boundary metadata and `seed` reconstructs a forked child prefix after the session boundary validates and snapshots the durable values. `signal` applies only until this promise settles. The resolved [`AgentHandle`](../agent/README.md) owns exact teardown.
- `ctx.agents.resume({ resumeSessionId, agentOptions?, setup?, signal? }): Promise<AgentHandle>` — load a persisted session via `ctx.sessionPersistence` ([session persistence](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)), register the agent under that same id, reconstruct its history, then await setup against a fresh unpublished agent scope before rollback-covered publication. Turn numbering and derived history continue from the loaded log. Requires a session-persistence backend (NOT hard-injected — non-persistent demos still work; `resume` rejects with a clear error when persistence is absent). `signal` is creation-only. Returns an `AgentHandle`.
@@ -63,9 +63,9 @@ Every inbox mutation publishes one normalized `agent/inbox/spliced` event before
The driver owns one agent for its lifetime and runs inside `ctx.agents.withInitiator(agent, ...)`. Package-private orchestration entry points recover the exact Agent, derive `agent.session` once, and let operation-local helpers capture it instead of forwarding the concrete driver or per-operation `Session` through shallow interfaces. A helper keeps an explicit `Session` when that is its actual interface, while creation, persistence load, unpublished setup, services, workers, processes, persistence, and wire protocols retain their explicit identities. The [agent service](../agent/README.md#initiating-agent-scope) owns propagation, teardown, and detached-work rules.
Every provider call that reaches a successful finish appends exactly one `assistant/message` completion anchor, including content-less calls and `max-tokens` finishes. The anchor records the assembled content as-is, retains exact chunk provenance (`[]` for a stream with no chunks), and includes usage when available; empty content stays out of derived message history.
Every provider call that reaches a successful finish appends exactly one `assistant/message` completion anchor, including content-less calls and `max-tokens` finishes. The anchor records the assembled content as-is, lists the exact chunk seqs in `sourceEventSeqs` (`[]` for a stream with no chunks), and includes usage when available; empty content stays out of derived message history.
After `agent/request` returns a provider/model call config, the loop asks `ctx.llm.prepareCall()` to validate adapter-owned fields and materialize configured reasoning-effort and output-token defaults under the active turn signal. The prepared call retains the exact adapter registration across this asynchronous resolution, `request/header` logging, and terminal dispatch, so HMR cannot mix one adapter's capability result with another adapter's request. The header records the effective config and which fields came from the adapter. Before the next waterfall, the loop removes those marked fields from the proposal so the current exact route rematerializes its own defaults; unmarked explicit settings persist across steps and route changes. A route with no registered adapter preserves the proposed config so an `llm/stream` listener can own and short-circuit it; unhandled terminal dispatch still fails with `NO_ADAPTER`. A new loop instance applies the same provenance rule when resuming.
After `agent/request` returns a provider/model call config, the loop asks `ctx.llm.prepareCall()` to validate adapter-owned fields and materialize configured reasoning-effort and output-token defaults under the active turn signal. The prepared call retains the exact adapter registration across this asynchronous resolution, `request/header` logging, and terminal dispatch, so HMR cannot mix one adapter's capability result with another adapter's request. The header records the effective config and which fields came from the adapter. Before the next waterfall, the loop removes those marked fields from the proposal so the current exact route rematerializes its own defaults; unmarked explicit settings persist across steps and route changes. A route with no registered adapter preserves the proposed config so an `llm/stream` listener can own and short-circuit it; unhandled terminal dispatch still fails with `NO_ADAPTER`. A new loop instance follows the same adapter-default marker rule when resuming.
Plugin failure ends the current turn, not the loop. Final adapter selection, dispatch, and iteration failures arrive from `ctx.llm` as terminal error or aborted finishes and enter `agent/request-error`; middleware, result processing, tools, and other extension failures remain thrown and close directly. Recovery receives request coordinates, immutable provider facts, the immutable retry policy captured by the prepared adapter registration, and the turn signal; the policy is absent when middleware owns an unprepared route. A handling listener returns `{ kind: 'retry' }`; an unhandled failure is terminal. AgentLoop owns one cancellation signal for the current admission or turn. An effective `cancel(cause)` clears pending work unless `keepInbox` is set and cooperatively aborts that signal; idle cancellation is a no-op. Waking input that lands after the abort fires but before the activity converges to idle is latched (`wakeRequested`) and replayed at the driver's own convergence boundary, so it runs without a further waking send; a `disposed` cancel never latches, and a wake submitted while already idle always opens its turn boundary (status shows a transient `idle → running → idle` pair even when the message was cleared). Durable `turn/end` records `aborted` for `user` and `parent`, while disposal records `disposed`; undispatched model tool calls receive synthetic `tool/call` and `ABORTED_BEFORE_DISPATCH` result pairs. The cancellation cause changes reporting, not how result context finalized after cancellation is handled. Disposal waits for signal-ignoring work before registry removal. The [explicit-cancellation decision](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md) and the [cancel-convergence wake latch](../../../.agents/notes/implemented/bug-fix/2026-08-07-cancel-convergence-wake-latch.md) own the lifecycle and race contract.
@@ -131,4 +131,4 @@ Append-only; each synthetic result follows the reusable request prefix and does
- **Classification is unary** — calls whose safety depends on comparing siblings or resources must remain exclusive ([rationale](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md)).
- **Config labels are fresh by default** — omitting `sessionId` creates a fresh `${id}-session-<uuid>` on every startup; exact resume-or-create behavior requires an explicit stable `sessionId`, while `resumeSessionId` requires existing persisted history.
- **Config agents have no per-agent persona field or setup hook** — they use the deployment persona; scoped persona/tool composition is available only through the programmatic `ctx.agents.create()` / `resume()` factory options.
- **No built-in turn budget** — tool calls or steering continue the current turn; a policy that bounds runaway turns must cancel from an existing lifecycle seam such as `agent/turn-stopping`.
- **No built-in turn budget** — tool calls or steering continue the current turn; a policy that bounds runaway turns must cancel from an existing lifecycle extension point such as `agent/turn-stopping`.

View File

@@ -4,7 +4,7 @@
唯一的具体 agent智能体插件与循环驱动器。其包内部实现满足 `Agent` 接口,并驱动会话/轮次/步骤生命周期。
这是 harness 中唯一包含具体循环逻辑的包。其他所有内容要么是抽象服务,要么是针对扩展 seam 的插件:新行为应放入插件,而不是这里。
这是 harness 中唯一包含具体循环逻辑的包。其他所有内容要么是抽象服务,要么是针对扩展的插件:新行为应放入插件,而不是这里。
## 服务:`AgentLoop`ctx 键:`agentLoop`
@@ -18,7 +18,7 @@
- `ctx.agentLoop.create(id: SessionId, options?: AgentOptions, meta?: { cwd?: string }): Agent`:在确切共享的 agent会话 id 下同步创建,不运行 setup并随调用 fiber dispose。声明式配置把 `agents[].id` 视为稳定 label通常会先生成 `${label}-session-<uuid>`,再调用此边界。应用也可以提供稳定且确切的 `sessionId`:首次使用时创建;重新挂载且持久化内容已存在时,则恢复已经实体化的历史。`resumeSessionId` 要求并加载现有的持久化 id且与 `sessionId` 互斥。这样,默认的全新重启不会冲突,也无需保留第二个实时路由身份。
`AgentLoop` 还实现 `AgentFactory` seam,并通过 `ctx.agents.setFactory(this)` 注册自身,因此插件会通过接口 `ctx.agents` 创建/恢复 agent
`AgentLoop` 还实现 `AgentFactory` 约定,并通过 `ctx.agents.setFactory(this)` 注册自身,因此插件会通过 `ctx.agents` 创建/恢复 agent
- `ctx.agents.create({ sessionId, meta?, seed?, agentOptions?, setup?, signal? }): Promise<AgentHandle>`:使用调用方提供的共享 id 以编程方式创建。它会等待尚未发布的 setup 事务,然后才返回;`meta` 携带 cwd谱系seed 边界元数据,`seed` 则在会话边界验证并快照持久值后,重建 fork 子级的前缀。`signal` 只在此 Promise 结算前生效。解析得到的 [`AgentHandle`](../agent/README.md) 拥有确切的 teardown。
- `ctx.agents.resume({ resumeSessionId, agentOptions?, setup?, signal? }): Promise<AgentHandle>`:通过 `ctx.sessionPersistence` 加载持久化会话(参见[会话持久化](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)),使用同一 id 注册 agent重建历史然后针对全新且尚未发布的 agent 作用域等待 setup再执行受回滚保护的发布。轮次编号和派生历史从已加载日志继续。此操作要求存在会话持久化后端不会硬注入因此非持久化 demo 仍能工作;缺少持久化时,`resume` 会以明确错误拒绝)。`signal` 仅用于创建。返回 `AgentHandle`
@@ -63,9 +63,9 @@ interface Config {
驱动器在其整个生命周期内拥有一个 agent并在 `ctx.agents.withInitiator(agent, ...)` 内运行。包私有的编排入口点会恢复确切的 Agent一次性派生 `agent.session`,并让操作局部的辅助函数捕获它,而不是通过浅层接口继续传递具体驱动器或每次操作的 `Session`。如果显式 `Session` 正是辅助函数的实际接口,该辅助函数会保留它;创建、持久化加载、未发布 setup、服务、worker、进程、持久化和 wire 协议则继续保留各自的显式身份。[agent 服务](../agent/README.md#initiating-agent-scope)规定传播、teardown 和分离工作规则。
每次提供方调用成功结束时,都会恰好追加一个 `assistant/message` 完成锚点,包括无内容调用和以 `max-tokens` 结束的调用。该锚点原样记录组装后的内容,保留确切的分片溯源(流没有分片时为 `[]`),并在用量可用时包含用量;空内容不会进入派生消息历史。
每次提供方调用成功结束时,都会恰好追加一个 `assistant/message` 完成锚点,包括无内容调用和以 `max-tokens` 结束的调用。该锚点原样记录组装后的内容,`sourceEventSeqs` 中列出确切的分片 seq(流没有分片时为 `[]`),并在用量可用时包含用量;空内容不会进入派生消息历史。
`agent/request` 返回提供方/模型调用配置后,循环会调用 `ctx.llm.prepareCall()`在活跃轮次信号的控制下校验由适配器持有的字段并填入配置的推理reasoning强度和输出 token 默认值。准备完成的调用会在这次异步解析、`request/header` 日志记录和最终分派期间保留同一项确切的适配器注册,因此 HMR热模块替换不会把某个适配器的能力解析结果与另一适配器的请求混用。请求 header 会记录生效配置以及哪些字段来自适配器。下一次 waterfall瀑布式事件循环会从提议中移除这些带标记字段使当前精确路由重新填入自身默认值未带标记的显式设置会跨步骤和路由变化保留。没有已注册适配器的路由会保留原定配置使 `llm/stream` 监听器可以接管并短路该请求;最终分派仍会以 `NO_ADAPTER` 拒绝未得到处理的路由。新循环实例在恢复时会应用同一来源规则。
`agent/request` 返回提供方/模型调用配置后,循环会调用 `ctx.llm.prepareCall()`在活跃轮次信号的控制下校验由适配器持有的字段并填入配置的推理reasoning强度和输出 token 默认值。准备完成的调用会在这次异步解析、`request/header` 日志记录和最终分派期间保留同一项确切的适配器注册,因此 HMR热模块替换不会把某个适配器的能力解析结果与另一适配器的请求混用。请求 header 会记录生效配置以及哪些字段来自适配器。下一次 waterfall瀑布式事件循环会从提议中移除这些带标记字段使当前精确路由重新填入自身默认值未带标记的显式设置会跨步骤和路由变化保留。没有已注册适配器的路由会保留原定配置使 `llm/stream` 监听器可以接管并短路该请求;最终分派仍会以 `NO_ADAPTER` 拒绝未得到处理的路由。新循环实例在恢复时会遵循同一套适配器默认值标记规则。
插件失败会结束当前轮次,而不是结束循环。最终适配器选择、分发与迭代失败会以终止错误或中止结束的形式由 `ctx.llm` 传来,并进入 `agent/request-error`middleware、结果处理、工具及其他扩展失败仍会抛出并直接关闭轮次。恢复逻辑会接收请求坐标、不可变的提供方事实、准备完成的适配器注册所捕获的不可变重试策略以及轮次信号middleware 接管未准备路由时,该策略缺失。处理失败的监听器返回 `{ kind: 'retry' }`未被处理的失败是终态。AgentLoop 为当前接纳或轮次拥有一个取消信号。有效的 `cancel(cause)` 在未设置 `keepInbox` 时清除待处理工作并以协作方式中止该信号空闲取消是空操作。abort 触发后、活动收敛到空闲前到达的唤醒输入会被锁存(`wakeRequested`),并在 driver 自身的收敛边界重放,无需再发一条唤醒 send 即可执行;`disposed` 取消从不锁存,而 agent 已处于空闲时发送的唤醒总是打开自己的 turn 边界(即使消息已被清除,状态也会显示瞬态 `idle → running → idle` 对)。持久 `turn/end``user``parent` 记录 `aborted`dispose 则记录 `disposed`;未分发的模型工具调用会收到合成的 `tool/call``ABORTED_BEFORE_DISPATCH` 结果对。取消原因只改变报告方式不改变对取消后已定案结果上下文的处理。dispose 会等待忽略信号的工作完成,然后才从注册表移除。[显式取消决策](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)与[取消收敛窗口唤醒锁存](../../../.agents/notes/implemented/bug-fix/2026-08-07-cancel-convergence-wake-latch.md)规定生命周期与竞态约定。
@@ -131,4 +131,4 @@ interface Config {
- **分类是一元的**:安全性取决于比较同级调用或资源的调用必须保持独占(参见[设计原理](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md))。
- **配置 label 默认每次新建**:省略 `sessionId` 会在每次启动时创建全新的 `${id}-session-<uuid>`;确切的恢复或创建行为要求显式提供稳定的 `sessionId`,而 `resumeSessionId` 要求已有持久化历史。
- **配置 agent 没有逐 agent persona 字段或 setup 钩子**:它们使用部署 persona只有编程式 `ctx.agents.create()` / `resume()` 工厂选项支持带作用域的 persona工具组合。
- **没有内置轮次预算**:工具调用或 steering 会让当前轮次继续;限制失控轮次的策略必须从既有生命周期 seam(如 `agent/turn-stopping`)执行取消。
- **没有内置轮次预算**:工具调用或 steering 会让当前轮次继续;限制失控轮次的策略必须从既有生命周期扩展点(如 `agent/turn-stopping`)执行取消。

View File

@@ -524,7 +524,7 @@ export class AgentLoop extends Service implements AgentFactory {
assertLive()
// A synchronous announce/session-start listener may have started
// teardown; the machine is already live (delivery works from the
// session-start seam), so only the liveness recheck is owed.
// session-start extension point), so only the liveness recheck is owed.
emitAgentEvent(loopCtx, agent, 'agent/session-start', { source })
assertLive()
return { agent, dispose }

View File

@@ -130,7 +130,7 @@ async function runGroup(
const { session } = ctx.agents.requireInitiator()
const { maxParallelToolCalls } = ctx.agentLoop.config
const slots: (Slot | undefined)[] = group.map(() => undefined)
// Started slots retain their tool/call seq for result provenance.
// Started slots retain their `tool/call` seq so the result can cite it.
const callSeqs: number[] = group.map(() => -1)
let nextToStart = 0
let committed = 0
@@ -258,7 +258,7 @@ function appendSkippedToolCall(session: Session, turn: number, step: number, blo
}, callSeq)
}
/** Append a started call and return its provenance sequence. */
/** Append a started call and return the event seq that its result must cite. */
function appendToolCall(session: Session, turn: number, step: number, block: ToolCallBlock): number {
const event = session.append('tool/call', { turn, step, callId: block.id, name: block.name, arguments: block.arguments })
return event.seq

View File

@@ -485,7 +485,7 @@ describe('Agent.cancel()', () => {
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
// A step/start session-event listener fires AFTER step/start is appended
// (and after the pre-step seam), so cancelling there lands in the SECOND
// (and after the pre-step extension point), so cancelling there lands in the SECOND
// cancel check (the one that must closeStep() to balance the already-open
// step) — distinct from a turn-start cancel, caught before the step opens.
let streamed = false

View File

@@ -59,7 +59,7 @@ function inboxText(message: UserMessage): string {
.join('')
}
describe('assistant replay provenance', () => {
describe('assistant replay provider and model fields', () => {
it('records adapter replay state with the assembled assistant content', async () => {
const response = textResponse('unchanged')
const replayState = { private: 'state' }

View File

@@ -19,7 +19,7 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
/**
* The interception seams introduced by the hooks taxonomy: `agent/pre-step`,
* The interception points introduced by the hooks taxonomy: `agent/pre-step`,
* `agent/session-start`, `agent/turn-stopping`, and the
* `tools/pre-execute` / `tools/post-execute`
* split with `additionalContexts` buffering. These verify the canonical event

View File

@@ -338,7 +338,7 @@ describe('agent loop', () => {
expect(adapter.requests[0]!.system).toBe('You are an AI agent powered by the DeepSeek Harness SDK.\n\nYou run on mock.')
})
it('omits the system field when a system-prompt/assemble veto empties the assembly', async () => {
it('omits the system field when system-prompt/assemble short-circuits with an empty assembly', async () => {
// The documented escape valve: a deployment that must drop the harness
// openers short-circuits the assemble waterfall; the request then carries
// NO system field at all (not an empty string).

View File

@@ -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: 3a3bdf6a4b3bdc5bfef250495e84b7d90b003822
README.zh.md: e421070a6eec2e6e7e1fc7b45f0a5e29040bf712
README.md: 0fb65b94a3b311aa9f0df09d39dd937cba4cc7b4
README.zh.md: 44f67483343a98c280317793ece544bd0b984596

View File

@@ -12,7 +12,7 @@ Tracks live agents and carries the initiating Agent through asynchronous driver
### Public API
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.
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). `installModelSelection(agentCtx, selection)` snapshots a mutable provider/model/reasoning-effort selection during prompt assembly, applies its provider and model to prompt variables, and applies the complete selection to request routing for one step; an absent selected effort clears an inherited effort so adapter/provider defaults apply. `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 resolves any exact-model adapter default, records the effective cap in the request header, and applies it to each conversation-model request; an explicit Agent option wins, while omission leaves the adapter or provider route default in control.
@@ -34,7 +34,7 @@ The scoped-registration surface: `Agent.ctx` is the agent's scope context (`dsh-
The scope carries the `Agent` itself and is process-local. Ambient presence is neither liveness proof nor authorization; explicit Agent fields remain authoritative at service, worker, process, persistence, and wire boundaries. Teardown rejects new boundaries, lets injected dependents and returned-Promise boundaries drain, then disables the underlying `AsyncLocalStorage`; unreturned work remains owned by the subsystem that detached it. If a boundary's inherited async chain starts an owning Cordis fiber's unload, that nested boundary chain is released from the drain so the unload cannot wait on itself; its continuations observe the disposed service after teardown. The [initiator-scope decision](../../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md) owns the detailed boundary and teardown contract.
#### Factory seam (creation)
#### Factory API (creation)
Agent *creation* is provided by the plugin implementing `AgentFactory` (`dsh-agent-loop`), registered via `setFactory`. This keeps creation on the `dsh-agent` interface so consumers (UI, the ACP bridge) program against `ctx.agents` without depending on the concrete loop package. The registry canonicalizes an already traced Service to its concrete target and re-traces each call through the caller's context; this avoids nested Cordis shadows while passing an explicit caller-bound `ownerCtx` to plain factories.
@@ -50,7 +50,7 @@ Agent *creation* is provided by the plugin implementing `AgentFactory` (`dsh-age
The lifecycle edges have two important local caveats. `agent/created` runs after scoped setup and after both session and agent registry entries exist. Setup is trusted composition-only code; the immediately following non-vetoing `agent/session-start` notification is the first supported startup injection point. `agent/disposed` always means the exact agent has left the registry. AgentLoop emits it after its driver is quiescent, while ordered teardown may still be detaching the session and unwinding the scope; custom agents registered directly own any stronger driver-ordering contract themselves.
Most interception points are cooperative waterfalls. `agent/pre-step` receives a payload carrying the subject `agent`, the exclusive claimed `UserMessage[]`, and the proposed `turn`, `step`, and cancellation `signal`; its batch may be empty when tools already require another request. Agent-scoped turn seams carry their explicit `AbortSignal` in the payload; the remaining turn-scoped seams receive it through their request value. Listeners may cooperate with a signal but must not retain it as authority over another turn. `agent/request-error` is the failed-model-request recovery waterfall: it receives request coordinates, normalized failure facts, the serving registration's retry policy when available, and the signal. A listener returns `{ kind: 'retry' }` without calling `next()` when it owns recovery. `agent/turn-stopping` runs before an otherwise completed turn closes. The [explicit-cancellation decision](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md) owns signal lifetime; the [agent-scope runtime-design Agent Note](../../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#three-execution-boundaries-are-deliberately-one-way) owns scoped dispatch and terminal settlement.
Most interception points are cooperative waterfalls. `agent/pre-step` receives a payload carrying the subject `agent`, the exclusive claimed `UserMessage[]`, and the proposed `turn`, `step`, and cancellation `signal`; its batch may be empty when tools already require another request. Agent-scoped turn extension points carry their explicit `AbortSignal` in the payload; the remaining turn-scoped extension points receive it through their request value. Listeners may cooperate with a signal but must not retain it as authority over another turn. `agent/request-error` is the failed-model-request recovery waterfall: it receives request coordinates, normalized failure facts, the serving registration's retry policy when available, and the signal. A listener returns `{ kind: 'retry' }` without calling `next()` when it owns recovery. `agent/turn-stopping` runs before an otherwise completed turn closes. The [explicit-cancellation decision](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md) owns signal lifetime; the [agent-scope runtime-design Agent Note](../../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#three-execution-boundaries-are-deliberately-one-way) owns scoped dispatch and terminal settlement.
`PreStepDecision` is either `{ kind: 'reject' }` or `{ kind: 'enter', messages }`. The enter branch is the complete identified, frozen batch for the proposed step. A listener that wraps downstream entry preserves that batch unless it intentionally replaces it; additions follow the waterfall's natural return order. Claiming already removed the offered messages from the inbox, so rejection does not retain them. Messages inserted after the claim remain pending for a later boundary.
@@ -76,7 +76,7 @@ The handle every plugin programs against:
- Agent creation: `AgentLoop.create()` is the concrete config-path implementation (in `dsh-agent-loop`), while programmatic consumers create/resume owned agents through `ctx.agents.create()` / `ctx.agents.resume()`. Replace the loop by implementing `Agent` and registering via `ctx.agents.register()`.
- Event listeners: all `agent/*` events are declared here — no dependency on the loop package needed.
- Subagent delegation is not an `Agent` method; providers create or drive ordinary handles through the factory seam, so delegation transports stay outside the core agent interface.
- Subagent delegation is not an `Agent` method; providers create or drive ordinary handles through the factory API, so delegation transports stay outside the core agent interface.
## Model Experience
@@ -115,5 +115,5 @@ Prefix-stable while an agent's scoped registrations are unchanged. Setup or relo
- **Inter-agent channels beyond delegation** — shared state, streaming child output, and background/poll semantics remain outside the current synchronous `ctx.subagents` seam.
- **`agent/session-start` cannot gate startup** — it remains a synchronous, veto-less notification; async composition that must finish before publication belongs in the factory's `setup(agentCtx)` transaction instead.
- **`cancel()` clears the inbox by default** — it aborts the in-flight turn plus queued and steering work; `cancel(cause, { keepInbox: true })` aborts only the turn and preserves pending items. There is still no step-only abort that keeps the in-flight turn running ([stop-surface Agent Note](../../../.agents/notes/implemented/simplification/2026-06-20-public-agent-stop-surface.md)).
- **Each additional `UserMessage` carries exactly one `MessageSource`** — contributions from several plugins merged onto one tool call collapse under one source; mixed provenance is unrepresentable.
- **Each additional `UserMessage` carries exactly one `MessageSource`** — contributions from several plugins merged onto one tool call collapse under one source, so the message cannot name several producers.
- **`SessionStartSource` reserves `'clear'`/`'compact'` with no emitter yet** — only `'startup'`/`'resume'` occur until the driving subsystems land (`TODO(compaction)`).

View File

@@ -34,7 +34,7 @@ Agent 接口、注册表、进程本地发起方作用域,以及 `agent/*` 事
该作用域携带 `Agent` 本身并且只在进程内有效。环境中的身份既不是存活证明也不是授权在服务、worker、进程、持久化和 wire 边界,显式 Agent 字段仍是权威来源。Teardown 会拒绝新边界,允许注入的依赖方和返回 Promise 的边界 drain然后禁用底层 `AsyncLocalStorage`;未返回的工作仍归将其分离的子系统所有。如果某个边界继承的异步链开始卸载一个拥有它的 Cordis fiber该嵌套边界链会从 drain 中释放,使卸载不会等待自身;其 continuation 会在 teardown 后观察到已 dispose 的服务。详细边界与 teardown 约定由[发起方作用域决策](../../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md)拥有。
#### 工厂 seam(创建)
#### 工厂 API(创建)
Agent *创建* 由实现 `AgentFactory` 的插件(`dsh-agent-loop`)提供,并通过 `setFactory` 注册。这样,创建功能留在 `dsh-agent` 接口上消费方UI、ACPAgent Client Protocol桥接层可以面向 `ctx.agents` 编程,而不依赖具体循环包。注册表会把已经 traced 的 Service 规范化为具体目标,并通过调用方上下文重新 trace 每次调用;这既避免嵌套 Cordis shadow也会把显式、绑定调用方的 `ownerCtx` 传给普通工厂。
@@ -50,7 +50,7 @@ Agent *创建* 由实现 `AgentFactory` 的插件(`dsh-agent-loop`)提供,
生命周期边有两个重要的本地注意事项。`agent/created` 在作用域 setup 之后、会话与 agent 注册表条目都存在之后运行。Setup 是受信任、仅用于组合的代码;紧随其后且不可 veto 的 `agent/session-start` 通知是第一个受支持的启动注入点。`agent/disposed` 始终表示确切 agent 已离开注册表。AgentLoop 在其驱动器完全停稳后发出该事件,而有序 teardown 此时可能仍在分离会话并撤销作用域;直接注册的自定义 agent 自行拥有任何更强的驱动器顺序约定。
大多数拦截点都是协作式 waterfall瀑布式事件`agent/pre-step` 接收一个 payload携带主体 `agent`、独占的已领取 `UserMessage[]` 以及拟进入的 `turn``step` 与取消 `signal`当工具已经要求继续请求时该批次可以为空。agent 作用域轮次 seam 在 payload 中携带显式 `AbortSignal`;其余轮次作用域 seam 通过其请求值接收它。监听器可以配合信号,但不得将它保留为控制另一轮次的权限。`agent/request-error` 是失败模型请求的恢复 waterfall它接收请求坐标、规范化失败事实、可用时提供服务的注册项重试策略以及信号。拥有恢复权的监听器返回 `{ kind: 'retry' }` 且不调用 `next()``agent/turn-stopping` 在本可完成的轮次关闭前运行。信号生命周期由[显式取消决策](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)拥有;作用域分发与终止结算由 [agent 作用域运行时设计 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#three-execution-boundaries-are-deliberately-one-way)拥有。
大多数拦截点都是协作式 waterfall瀑布式事件`agent/pre-step` 接收一个 payload携带主体 `agent`、独占的已领取 `UserMessage[]` 以及拟进入的 `turn``step` 与取消 `signal`当工具已经要求继续请求时该批次可以为空。agent 作用域轮次扩展点在 payload 中携带显式 `AbortSignal`;其余轮次作用域扩展点通过其请求值接收它。监听器可以配合信号,但不得将它保留为控制另一轮次的权限。`agent/request-error` 是失败模型请求的恢复 waterfall它接收请求坐标、规范化失败事实、可用时提供服务的注册项重试策略以及信号。拥有恢复权的监听器返回 `{ kind: 'retry' }` 且不调用 `next()``agent/turn-stopping` 在本可完成的轮次关闭前运行。信号生命周期由[显式取消决策](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)拥有;作用域分发与终止结算由 [agent 作用域 runtime 设计 Agent Noteagent 决策记录)](../../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#three-execution-boundaries-are-deliberately-one-way)拥有。
`PreStepDecision` 要么是 `{ kind: 'reject' }`,要么是 `{ kind: 'enter', messages }`。enter 分支是拟进入步骤的完整、带标识且冻结的批次。包装下游 enter 的监听器会保留该批次,除非有意替换它;新增消息遵循 waterfall 的自然返回顺序。领取操作已经把候选消息从 inbox 删除,因此 reject 不会保留它们;领取后插入的消息仍等待后续边界。
@@ -76,7 +76,7 @@ inbox 的实时通知刻意采用逐消息的最小载荷:`agent/inbox/inserte
- Agent 创建:`AgentLoop.create()` 是具体配置路径实现(位于 `dsh-agent-loop`),程序化消费方则通过 `ctx.agents.create()`/`ctx.agents.resume()` 创建或恢复有所有权的 agent。替换循环时应实现 `Agent` 并通过 `ctx.agents.register()` 注册。
- 事件监听器:全部 `agent/*` 事件都在此处声明,不需要依赖循环包。
- subagent 委派不是 `Agent` 方法;提供方通过工厂 seam 创建或驱动普通 handle因此委派传输留在核心 agent 接口之外。
- subagent 委派不是 `Agent` 方法;提供方通过工厂 API 创建或驱动普通 handle因此委派传输留在核心 agent 接口之外。
## 模型体验
@@ -115,5 +115,5 @@ inbox 的实时通知刻意采用逐消息的最小载荷:`agent/inbox/inserte
- **委派以外的 agent 间通道**:共享状态、流式子输出和后台/轮询语义仍在当前同步 `ctx.subagents` seam 之外。
- **`agent/session-start` 不能为启动设置门禁**:它仍是同步且不可 veto 的通知;必须在发布前完成的异步组合属于工厂的 `setup(agentCtx)` 事务。
- **`cancel()` 默认清空 inbox**:它会中止正在处理的轮次以及排队和 steering 工作;`cancel(cause, { keepInbox: true })` 只中止轮次并保留待处理项。仍不存在只中止步骤、同时让正在处理的轮次继续运行的操作([关于停止操作接口的 Agent Note](../../../.agents/notes/implemented/simplification/2026-06-20-public-agent-stop-surface.md))。
- **每条附加 `UserMessage` 恰好携带一个 `MessageSource`**:多个插件合并到一次工具调用上的贡献会归入一个来源;无法表示混合来源
- **每条附加 `UserMessage` 恰好携带一个 `MessageSource`**:多个插件合并到一次工具调用上的贡献会归入同一来源,因此该消息无法列出多个生产者
- **`SessionStartSource` 预留 `'clear'`/`'compact'`,但还没有发出方**:在驱动子系统落地前,只会出现 `'startup'`/`'resume'``TODO(compaction)`)。

View File

@@ -68,7 +68,7 @@ export class Inbox {
* @param target - whether this boundary also consumes one queued turn.
* @param turn - turn that will own the claimed batch.
* @returns next-step input followed by the queued turn, when requested.
* @internal - the agent loop's step-boundary operation, not a plugin seam.
* @internal - The agent loop's step-boundary operation, not a plugin extension point.
*/
claim(target: InboxTarget, turn: number): UserMessage[] {
const claimed = this.mutate('next-step', 0, this.nextStep.length, [], false)

View File

@@ -17,7 +17,7 @@ import type { Agent, AgentOptions } from './types.ts'
export * from './types.ts'
export * from './inbox.ts'
export * from './llm-target.ts'
export * from './model-selection.ts'
export { agentCarrier, agentEvents, assembleContextFor, emitAgentEvent } from './dispatch.ts'
export type { AgentEventDispatch, AgentSubjectEvent } from './dispatch.ts'
@@ -313,7 +313,7 @@ export class AgentRegistry extends Service {
* Read the initiating Agent and fail when no initiator boundary is active.
* Use this for private helpers contractually below a driver, or for a
* deployment-owned outbound request whose contract forbids agentless calls.
* Generic or direct-call seams use optional lookup or explicit request fields.
* Generic or direct-call paths use optional lookup or explicit request fields.
* @returns the inherited Agent.
* @throws when no initiator is active or this service instance has been disposed.
*/

View File

@@ -1,13 +1,13 @@
/**
* Agent-scoped LLM target snapshot shared by interactive front doors.
* @module @deepseek-ai/dsh-agent/llm-target
* Agent-scoped model selection shared by interactive front doors.
* @module @deepseek-ai/dsh-agent/model-selection
*/
import type { Context } from 'cordis'
import type { LlmCallConfig, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
/** Complete provider/model route and optional reasoning effort selected for one live agent. */
export interface AgentLlmTarget {
/** Complete provider, model, and optional reasoning effort selected for one live Agent. */
export interface ModelSelection {
/** Registered provider route. */
provider: string
/** Provider-owned model id. */
@@ -16,31 +16,31 @@ export interface AgentLlmTarget {
reasoningEffort?: ReasoningEffortId
}
/** Mutable selection plus the target captured for the current step. */
export interface AgentLlmTargetRef {
/** Target selected for the next step that enters prompt assembly. */
current: AgentLlmTarget | undefined
/** Target captured when the current step entered prompt assembly. */
assembled: AgentLlmTarget | undefined
/** Mutable model selection plus the value captured for the current step. */
export interface ModelSelectionRef {
/** Model selected for the next step that enters prompt assembly. */
current: ModelSelection | undefined
/** Selection captured when the current step entered prompt assembly. */
assembled: ModelSelection | undefined
}
/**
* Couple one mutable target to agent-scoped prompt assembly and request routing.
* Prompt assembly snapshots the selected target before delegating, then applies
* its route to prompt variables and its route/effort to request config so a
* Couple one mutable selection to Agent-scoped prompt assembly and request routing.
* Prompt assembly snapshots the selected model before delegating, then applies
* its provider/model pair and effort to request config so a
* concurrent switch takes effect on a later step instead of splitting the two
* surfaces. An absent selected effort clears any inherited effort so a model
* switch can restore that target's provider/default behavior.
* surfaces. An absent selected effort clears any inherited effort, restoring
* the selected model's provider/default behavior.
*
* @param agentCtx - The target agent's scoped context.
* @param target - Mutable selection owned by the calling front door.
* @param agentCtx - The selected Agent's scoped context.
* @param selection - Mutable selection owned by the calling front door.
* @returns Disposer for both scoped waterfall listeners.
*/
export function installAgentLlmTarget(agentCtx: Context, target: AgentLlmTargetRef): () => void {
export function installModelSelection(agentCtx: Context, selection: ModelSelectionRef): () => void {
const disposeAssembly = agentCtx.on('system-prompt/assemble', async (_assembly, _context, next) => {
const selected = target.current
const selected = selection.current
const assembled = await next()
target.assembled = selected
selection.assembled = selected
if (selected === undefined) return assembled
return {
...assembled,
@@ -55,7 +55,7 @@ export function installAgentLlmTarget(agentCtx: Context, target: AgentLlmTargetR
'agent/request',
async (_payload, next): Promise<LlmCallConfig> => {
const resolved = await next()
const selected = target.assembled
const selected = selection.assembled
if (selected === undefined) return resolved
const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = resolved
return {

View File

@@ -109,7 +109,7 @@ export interface Agent {
* cancel leaves it parked. A wake submitted while already idle always opens
* its turn boundary, even when its message is cleared before the driver
* claims ([cancel-convergence wake latch](../../../../.agents/notes/implemented/bug-fix/2026-08-07-cancel-convergence-wake-latch.md)).
* @param message - identified content and its producer provenance.
* @param message - identified content and the source that supplied it.
* @param target - the preferred next-turn or next-step inbox boundary.
* @param wakeup - whether delivery may wake the driver.
*/
@@ -118,7 +118,7 @@ export interface Agent {
/**
* Queue an ordinary follow-up turn and wake the driver. The item becomes the
* sole ordinary message of its own turn.
* @param message - identified prompt content and its producer provenance.
* @param message - identified prompt content and the source that supplied it.
*/
followup(message: UserMessage): void
@@ -127,7 +127,7 @@ export interface Agent {
* a running driver consumes it at its next step boundary.
* A rejected step leaves steering parked in the inbox until the next
* wake; cancellation or disposal may discard pending steering.
* @param message - identified steering content and its producer provenance.
* @param message - identified steering content and the source that supplied it.
*/
steer(message: UserMessage): void
@@ -137,7 +137,7 @@ export interface Agent {
* idle drivers leave it pending until follow-up or steering
* wakes them. It may miss a request whose pre-step already claimed its
* batch. Cancellation or disposal may discard pending context.
* @param message - identified injected context and its producer provenance.
* @param message - identified injected context and the source that supplied it.
*/
inject(message: UserMessage): void
}
@@ -147,7 +147,7 @@ declare module 'cordis' {
// ---- lifecycle (emit) ----
/**
* A fully configured agent and live session were published. Setup is
* composition-only; `agent/session-start` is the first startup-driving seam.
* composition-only; `agent/session-start` is the first startup-driving extension point.
* Synchronous listener failure vetoes publication, while returned-promise
* rejection is reported. Detach requested during dispatch waits until every
* creation listener has observed the stable entry.
@@ -215,7 +215,7 @@ declare module 'cordis' {
*/
'agent/session-start'(this: Scoped<Agent>, payload: { agent: Agent; source: SessionStartSource }): void
// ---- the machine's extension seams ----
// ---- the machine's extension points ----
/**
* Reject a proposed step or replace the messages that enter it. Calling
* `next()` preserves the current messages.
@@ -232,7 +232,7 @@ declare module 'cordis' {
* Replace the frozen call configuration. `await next()` yields the config
* the machine would use (agent options on the first request, the logged
* header afterwards); return a replacement to switch. Model-visible
* content must use logged channels; this seam cannot mutate messages.
* content must use logged channels; this waterfall cannot mutate messages.
* @param payload.agent - the agent making the model call.
* @param payload.turn - the open turn number.
* @param payload.step - the step whose request this is.

View File

@@ -3,18 +3,18 @@ import { Context } from 'cordis'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import {
agentEvents,
installAgentLlmTarget,
installModelSelection,
type Agent,
type AgentLlmTargetRef,
type ModelSelectionRef,
} from '../src/index.ts'
import { ReasoningEffortId, type LlmCallConfig } from '@deepseek-ai/dsh-llm'
describe('installAgentLlmTarget()', () => {
describe('installModelSelection()', () => {
it('snapshots prompt variables and request routing together, then disposes both listeners', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
const target: AgentLlmTargetRef = { current: undefined, assembled: undefined }
const dispose = installAgentLlmTarget(ctx, target)
const selection: ModelSelectionRef = { current: undefined, assembled: undefined }
const dispose = installModelSelection(ctx, selection)
const agent = {} as Agent
const seed: LlmCallConfig = { provider: 'seed', model: 'seed', temperature: 0.2 }
const signal = new AbortController().signal
@@ -24,13 +24,13 @@ describe('installAgentLlmTarget()', () => {
'agent/request', { turn: 1, step: 0, signal }, () => Promise.resolve(seed),
)).resolves.toBe(seed)
target.current = {
selection.current = {
provider: 'alpha',
model: 'a1',
reasoningEffort: ReasoningEffortId('high'),
}
expect((await ctx.systemPrompt.assemble()).variables).toMatchObject({ provider: 'alpha', model: 'a1' })
target.current = { provider: 'beta', model: 'b1' }
selection.current = { provider: 'beta', model: 'b1' }
await expect(agentEvents(ctx, agent).waterfall(
'agent/request', { turn: 1, step: 0, signal }, () => Promise.resolve(seed),
)).resolves.toEqual({

View File

@@ -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/session/README.md
README.md: 00e007ee6064f485bf80dfda0227ab6e834f6414
README.zh.md: e9d074f52db46cb42e497ad1ef49c2b37a6ec7d8
README.md: db477d94037d3463870fc8e66ea35d5e607fb6fe
README.zh.md: 1ce1e823a7e0fdbcf7b6898764a89c52b74adf6a

View File

@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
Event-sourced session log and in-memory store. A `Session` is the append-only source of truth for an agent's whole interaction history — the LLM message history is *derived* from it. A **surface** layer (an ordered projection of message-producing events) is maintained on top of the raw log for efficient derivation and compaction.
The optional `@deepseek-ai/dsh-session/invariant` companion registers this package's relational trace checks with `ctx.invariants`: monotonic sequence numbers, turn/step enclosure, and same-step tool call/result pairing. It replays existing sessions when loaded or reloaded; storage validation, snapshotting, freezing, provenance, and surface acceptance remain always-on responsibilities of the root session package.
The optional `@deepseek-ai/dsh-session/invariant` companion registers this package's relational trace checks with `ctx.invariants`: monotonic sequence numbers, turn/step enclosure, and same-step tool call/result pairing. It replays existing sessions when loaded or reloaded; storage validation, snapshotting, freezing, cited source-event validation, and surface acceptance remain always-on responsibilities of the root session package.
## Service: `SessionStore` (ctx key: `sessions`)
@@ -26,7 +26,7 @@ Use the split lifecycle only when teardown must be ordered with another resource
- `enter(session)` performs the collision check, publishes without announcing, and returns an entry-bound idempotent detach. Concurrent same-id preparations are allowed, but only one entry succeeds; a stale detach cannot remove its replacement.
- `announce(session)` emits the single creation edge and rejects repeat or reentrant announcements. Detach during that dispatch is deferred and later emits the paired disposal edge; an unannounced entry emits neither lifecycle edge.
`dsh-agent-loop` uses this split so final loop flush precedes session detach; see the [ownership Agent Note](../../../.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md).
`dsh-agent-loop` uses this split so final loop flush precedes session detach; see the [ownership Agent Note](../../../.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.md).
### Live service events
@@ -36,8 +36,8 @@ The store pairs announced creation with disposal, publishes post-commit append n
Plain class (not a Cordis Service). Create live sessions through `ctx.sessions.create()` and detached replay or inspection sessions through `Session.create()`; the detached factory does not publish lifecycle events or bind the session to a fiber.
- `session.append(type, data, opts?)` snapshots and freezes durable data and surface metadata, validates marker shape, provenance, complete replacement coverage, and content-only single-result `tool/result` rewrites, commits synchronously, then notifies observers with independent failure containment. Reentrant attached-session appends reject, and runtime checks cover widened unions and loaded logs.
- `session.deriveMessages()` incrementally projects each new surface entry once and returns a fresh array over the complete identified, frozen messages stored by those entries. Assistant messages preserve provider/model provenance and adapter-private replay state in their model source. A surface rewrite rebuilds the projection; there is no raw-log fallback.
- `session.append(type, data, opts?)` snapshots and freezes durable data and surface metadata, validates marker shape, cited source-event seqs, complete replacement coverage, and content-only single-result `tool/result` rewrites, commits synchronously, then notifies observers with independent failure containment. Reentrant attached-session appends reject, and runtime checks cover widened unions and loaded logs.
- `session.deriveMessages()` incrementally projects each new surface entry once and returns a fresh array over the complete identified, frozen messages stored by those entries. Assistant messages preserve the provider and model that produced them plus adapter-private replay state in their model source. A surface rewrite rebuilds the projection; there is no raw-log fallback.
- `session.deriveEventMessage(event)` is the canonical per-event projection used by reconstruction and request checks.
- `session.surface` exposes the readonly `SessionSurface` view owned by the session's single incremental surface manager; `replaceGeneration` changes on every committed rewrite.
- `session.events` is a cached frozen snapshot invalidated by append; accepted events remain deeply frozen.
@@ -68,7 +68,7 @@ A `user/message` stores the complete `UserMessage` directly, including the ident
### Session event vocabulary (`types.ts`)
The append-only log's event types, enumerated member by member — payloads, surface badges, provenance — in the generated [persistence log event catalog](../../../docs/persistence-catalog.md). Token accounting reads per-step `assistant/chunk { type: 'usage' }` records and treats `assistant/message.usage` as the committed-step fallback when no usage chunk exists; failed model-request attempts have no assistant message. Provider/model/replay provenance rides on `assistant/message`.
The generated [persistence log event catalog](../../../docs/persistence-catalog.md) enumerates each append-only event type with its payload, surface badge, and declaration site. Token accounting reads per-step `assistant/chunk { type: 'usage' }` records and treats `assistant/message.usage` as the committed-step fallback when no usage chunk exists; failed model-request attempts have no assistant message. Each `assistant/message` records the provider, model, and optional replay state.
Merge-extensible via `SessionEventMap` — a plugin declaration-merges its own types (the compaction seam's `compact/*`, bounded recovery's non-surface `llm/retry`, the hook bridges' `hook/*`); merged members appear in the same catalog. A plugin owns the relational invariant for its merged events, including whether a log-only event may appear between turns. A producer that requires durability appends through `Session` and then awaits `ctx.sessions.flush(session)` without fabricating an execution turn.
@@ -78,7 +78,7 @@ An interrupted live turn ends with `{ kind: 'aborted', reason: AgentCancelCause
Every `SessionEvent` carries two optional top-level fields (structural metadata):
- `sourceEventSeqs?: number[]` — seq numbers of provenance sources (e.g., the `assistant/chunk` seqs behind an `assistant/message`, or the shadowed entries behind a compaction replacement entry). On `assistant/message`, a present `[]` records a known empty provider stream, while omission means legacy or otherwise unrecorded provenance; other surface events require a non-empty list when this field is present.
- `sourceEventSeqs?: number[]` — seq numbers of earlier events cited as sources (e.g., the `assistant/chunk` seqs behind an `assistant/message`, or the shadowed entries behind a compaction replacement entry). On `assistant/message`, a present `[]` records a known empty provider stream, while omission means a legacy or foreign event did not record the source stream; other surface events require a non-empty list when this field is present.
- `surfaceOp?: SurfaceOp` — how this event entered the surface. Absent for non-surface events (boundaries, chunks, usage, errors).
### Metadata types (`types.ts`)
@@ -87,7 +87,7 @@ Every `SessionEvent` carries two optional top-level fields (structural metadata)
### Extension points
- Persistence plugins: subscribe to `session/event` (write-behind) and drain on `session/flush` (awaited) and fiber dispose. A durable backend reads the log and reloads it into a live session; the metadata seam (`SessionHeader`, `session.header`) is what such a backend stores beside the log.
- Persistence plugins: subscribe to `session/event` (write-behind) and drain on `session/flush` (awaited) and fiber dispose. A durable backend reads the log and reloads it into a live session; the metadata contract (`SessionHeader`, `session.header`) is what such a backend stores beside the log.
- Replay/fork: `create(id, { seed })` validates and freezes a contiguous current-format log and rebuilds its surface; request headers require provider/model, and assistant messages require provider/model provenance. Persistence owns read compatibility before constructing this current-format seed. `fork(source, boundary?, childSessionId?)` selects a completed-turn prefix and records lineage.
- Compaction: `dsh-compact-basic` appends a `user/message` replacement for summary checkpoints, while `dsh-compact-tool-result-prune` appends a content-only `tool/result` replacement. Tool-pairing boundary policy and its cache belong to the [`dsh-compact` seam](../../compact/compact/README.md), while this package owns ordered surface membership, replacement validation, and `replaceGeneration`.

View File

@@ -4,7 +4,7 @@
事件溯源的会话日志和内存存储。`Session` 是 agent智能体全部交互历史的仅追加真源LLM大语言模型消息历史由它*派生*。原始日志之上维护一个 **surface**产生消息事件的有序投影以便高效派生和压缩compaction
可选配套入口 `@deepseek-ai/dsh-session/invariant` 将此包的关系轨迹检查注册到 `ctx.invariants`:序号单调递增、轮次/步骤闭合,以及同一步骤内的工具调用/结果配对。加载或重新加载时,它会回放现有会话;存储校验、快照、冻结、溯源信息和 surface 准入仍始终由根会话包负责。
可选配套入口 `@deepseek-ai/dsh-session/invariant` 将此包的关系轨迹检查注册到 `ctx.invariants`:序号单调递增、轮次/步骤闭合,以及同一步骤内的工具调用/结果配对。加载或重新加载时,它会回放现有会话;存储校验、快照、冻结、被引用的源事件校验和 surface 准入仍始终由根会话包负责。
## 服务:`SessionStore`ctx 键:`sessions`
@@ -26,7 +26,7 @@
- `enter(session)` 执行冲突检查,在不通知的情况下发布,并返回一个绑定到该条目的幂等脱离函数。允许并发准备相同 id但只有一个条目能够成功进入陈旧的脱离函数无法移除其替代项。
- `announce(session)` 发出唯一一次创建边,并拒绝重复或重入通知。该次分发期间请求的脱离操作会延后,之后再发出成对的释放边;未通知的条目不会发出任何生命周期边。
`dsh-agent-loop` 使用这一拆分,以保证循环的最终刷新先于会话脱离;详见[所有权 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md)。
`dsh-agent-loop` 使用这一拆分,以保证循环的最终刷新先于会话脱离;详见[所有权 Agent Note](../../../.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.md)。
### 实时服务事件
@@ -36,8 +36,8 @@
普通类(不是 Cordis 服务)。活跃会话通过 `ctx.sessions.create()` 创建,脱离态的回放或检查会话通过 `Session.create()` 创建;脱离态工厂不会发布生命周期事件,也不会将会话绑定到 fiber。
- `session.append(type, data, opts?)` 会为持久数据和 surface 元数据制作快照并冻结它们,校验标记形态、溯源信息、替换覆盖完整性,以及仅修改内容的单个 `tool/result` 重写,随后同步提交,再在彼此独立的失败收容下通知观察者。对已附加会话的重入追加会被拒绝,运行时检查也覆盖扩宽后的联合类型和已加载日志。
- `session.deriveMessages()` 对每个新的 surface 条目只做一次增量投影并返回一个新数组其中包含这些条目存储的完整、带标识且冻结的消息。assistant 消息会在其模型来源保留提供方模型溯源信息及适配器私有回放状态。surface 重写会重建投影;不存在原始日志回退。
- `session.append(type, data, opts?)` 会为持久数据和 surface 元数据制作快照并冻结它们,校验标记形态、被引用的源事件 seq、替换覆盖完整性,以及仅修改内容的单个 `tool/result` 重写,随后同步提交,再在彼此独立的失败收容下通知观察者。对已附加会话的重入追加会被拒绝,运行时检查也覆盖扩宽后的联合类型和已加载日志。
- `session.deriveMessages()` 对每个新的 surface 条目只做一次增量投影并返回一个新数组其中包含这些条目存储的完整、带标识且冻结的消息。assistant 消息模型来源保留生成该消息的提供方模型,以及适配器私有回放状态。surface 重写会重建投影;不存在原始日志回退。
- `session.deriveEventMessage(event)` 是重建和请求检查使用的规范逐事件投影。
- `session.surface` 暴露只读 `SessionSurface` 视图,由会话唯一的增量 surface 管理器所有;每次提交重写,`replaceGeneration` 都会变化。
- `session.events` 是按追加失效的缓存冻结快照;已接受事件保持深度冻结。
@@ -68,7 +68,7 @@
### 会话事件词汇(`types.ts`
生成的[持久化日志事件目录](../../../docs/persistence-catalog.md)逐成员列举仅追加日志的事件类型、载荷、surface 标记和溯源信息。Token 记账读取每个步骤的 `assistant/chunk { type: 'usage' }` 记录;如果没有用量分片,则将 `assistant/message.usage` 作为已提交步骤的后备。失败的模型请求尝试没有 assistant 消息。提供方/模型/回放溯源信息随 `assistant/message` 一同保存
生成的[持久化日志事件目录](../../../docs/persistence-catalog.md)逐成员列举仅追加日志的事件类型、载荷、surface 标记与声明位置。Token 记账读取每个步骤的 `assistant/chunk { type: 'usage' }` 记录;如果没有用量分片,则将 `assistant/message.usage` 作为已提交步骤的后备。失败的模型请求尝试没有 assistant 消息。每条 `assistant/message` 都会记录提供方、模型和可选回放状态
`SessionEventMap` 可通过合并扩展:插件使用声明合并添加自身类型(压缩 seam 的 `compact/*`、有界恢复的非 surface `llm/retry`、hook钩子桥接层的 `hook/*`);合并成员会出现在同一目录中。插件拥有其合并事件的关系不变量,包括是否允许纯日志事件出现在轮次之间。需要持久性的生产方通过 `Session` 追加,再等待 `ctx.sessions.flush(session)`,无需虚构一个执行轮次。
@@ -78,7 +78,7 @@
每个 `SessionEvent` 都有两个可选顶层字段(结构元数据):
- `sourceEventSeqs?: number[]`溯源信息的源序号(例如 `assistant/chunk` 的序号,它们是 `assistant/message` 的来源;或压缩替换条目背后被遮蔽条目)。对于 `assistant/message`,存在的 `[]` 记录已知为空的提供方流;省略则表示旧版或其他未记录的溯源信息。其他 surface 事件若有此字段,则要求非空列表。
- `sourceEventSeqs?: number[]`被引用为来源的较早事件 seq(例如 `assistant/message` 引用的 `assistant/chunk` seq或压缩替换条目引用的已遮蔽条目)。对于 `assistant/message`,存在的 `[]` 表示已知提供方流为空;省略则表示旧版或外部事件没有记录源流。其他 surface 事件若有此字段,则要求非空列表。
- `surfaceOp?: SurfaceOp`:事件进入 surface 的方式。非 surface 事件(边界、分片、用量、错误)不含该字段。
### 元数据类型(`types.ts`
@@ -87,7 +87,7 @@
### 扩展点
- 持久化插件:订阅 `session/event`(延后写入),并在 `session/flush`(受等待)及 fiber dispose资源释放时排空。持久后端读取日志并重新加载到实时会话这类后端会把元数据 seam`SessionHeader``session.header`)与日志一同存储。
- 持久化插件:订阅 `session/event`(延后写入),并在 `session/flush`(受等待)及 fiber dispose资源释放时排空。持久后端读取日志并重新加载到实时会话这类后端会把元数据约定`SessionHeader``session.header`)与日志一同存储。
- 回放fork`create(id, { seed })` 校验并冻结连续的当前格式日志,再重建 surface请求头必须包含提供方模型assistant 消息必须包含提供方/模型溯源信息。持久化层在构造该当前格式 seed 前负责读取兼容性处理。`fork(source, boundary?, childSessionId?)` 选择已完成轮次前缀并记录谱系。
- 压缩:`dsh-compact-basic` 为摘要检查点追加一个替换用 `user/message`,而 `dsh-compact-tool-result-prune` 追加仅修改内容的 `tool/result` 替换。工具配对边界策略及其缓存归 [`dsh-compact` seam](../../compact/compact/README.md) 所有;此包拥有有序 surface 成员关系、替换校验与 `replaceGeneration`

View File

@@ -296,7 +296,7 @@ function assertCurrentLlmShape(event: Record<string, unknown>, index: number): v
const allowedAdapterKeys = new Set(['reasoningEffort', 'maxTokens'])
/** Validate adapter-default provenance imported from a durable request header. */
/** Validate adapter-default markers imported from a durable request header. */
function assertAdapterDefaults(
value: unknown,
config: Record<string, unknown>,
@@ -595,8 +595,8 @@ export class Session {
* @param type - The event type (key of {@link SessionEventMap}).
* @param data - The event payload; must be JSON-serializable.
* @param opts - Surface metadata: `surfaceOp` controls how the event enters
* the ordered surface; `sourceEventSeqs` records provenance (the seq
* numbers of events this one derives from). REQUIRED for
* the ordered surface; `sourceEventSeqs` lists the seq numbers of earlier
* events this one derives from. REQUIRED for
* {@link SurfaceEventType} events (every message-producing event must
* declare how it joins the surface, the sole source of derived model
* history) and
@@ -610,7 +610,7 @@ export class Session {
* circular reference, sparse array, or an exotic object such as
* Map/Set/Date/class instance), or when the candidate violates the
* canonical surface contract (marker shape and eligibility, unique
* earlier provenance, positional replacement validity, and complete
* earlier source-event references, positional replacement validity, and complete
* shadowed-node coverage). One recursive pass reads, validates, and
* copies each nested value once, so a stateful getter cannot supply one value
* to validation and another to storage. The event log is the durable source
@@ -919,7 +919,7 @@ export class SessionStore extends Service {
* another create) between them, so a stale prepared session must NOT overwrite
* a live store entry of the same id — its detach disposer would later delete
* the REAL session. The {@link create} convenience and the agent factory call
* the two back-to-back so they never trip this, but the public seam cannot
* the two back-to-back so they never trip this, but the public API cannot
* assume that.
*
* @param session - a {@link prepare}d session not yet in the store.

View File

@@ -125,7 +125,7 @@ function validateEvent(
break
}
case 'tool/result': {
// Session has already validated a provenance-backed content rewrite.
// Session has already validated a content rewrite that cites its replaced event.
// It is durable turn work, not a second execution of the original call.
if (event.surfaceOp !== 'append') {
if (trace.openTurn === null) {

View File

@@ -47,7 +47,7 @@ export function interruptedTurnClosers(events: readonly SessionEvent[]): Session
let openTurn: number | null = null
let openStep: number | null = null
// Reset at each turn boundary so earlier calls cannot leak into tail repair.
// Assistant blocks register calls; later tool/call events add provenance seqs.
// Assistant blocks register calls; later `tool/call` events add their seqs to `sourceEventSeqs`.
const pendingCalls = new Map<CallId, { step: number; callSeq?: number }>()
for (const event of events) {
switch (event.type) {
@@ -76,7 +76,7 @@ export function interruptedTurnClosers(events: readonly SessionEvent[]): Session
}
break
case 'tool/call':
// Add the tool/call seq used as provenance on a synthetic result.
// Cite the `tool/call` seq from the synthetic result.
{
const entry = pendingCalls.get(event.data.callId)
if (entry) {

View File

@@ -207,7 +207,7 @@ function surfaceOpOf(event: SessionEvent): SurfaceOp | undefined {
return op
}
/** Validate provenance against prior log entries and the replacement range. */
/** Validate cited source-event seqs against prior log entries and the replacement range. */
function assertProvenance(
event: SessionEvent,
shadowedSeqs: readonly number[],
@@ -382,7 +382,7 @@ function applySurfacePlan(
* Replay a complete session log through the canonical surface fold.
* @param events - session events in contiguous seq order.
* @returns detached current sequences and replacement history.
* @throws when an event violates surface metadata, provenance, range, or tool-result rewrite rules.
* @throws when an event violates surface metadata, source-event references, range, or tool-result rewrite rules.
*/
export function foldSurface(events: readonly SessionEvent[]): SurfaceFoldResult {
const state = createFoldState()

View File

@@ -353,16 +353,16 @@ export type SurfaceOp =
| { op: 'replace'; start: number; end: number }
/**
* Surface placement and provenance for {@link Session.append}. Required on
* Surface placement and cited source-event seqs for {@link Session.append}. Required on
* message-producing events and forbidden on log-only events.
*/
export interface SurfaceIntent {
surfaceOp: SurfaceOp
/**
* Complete known provenance source set. `assistant/message` may use a
* present empty array for a known empty provider stream; omission means its
* provenance was not recorded. Other surface events require a non-empty set
* when this field is present.
* Complete set of known source-event seqs. `assistant/message` may use a
* present empty array for a known empty provider stream; when the field is
* absent, the event does not record which earlier events produced the message.
* Other surface events require a non-empty set when this field is present.
*/
sourceEventSeqs?: number[]
}
@@ -390,11 +390,12 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
data: SessionEventMap[K]
} & (K extends SurfaceEventType ? {
/**
* Seq numbers of events that are provenance sources of this event
* Seq numbers of earlier events that this event cites as sources
* (e.g. the `assistant/chunk` seqs that built an `assistant/message`,
* or the surface nodes shadowed by a compaction replace node). An
* `assistant/message` may carry a present empty array for a known empty
* provider stream; omission means unrecorded provenance.
* provider stream; when the field is absent, the event does not record which
* earlier events produced the message.
*/
sourceEventSeqs?: number[]
/** How this event entered the surface; absent for non-surface events. */

View File

@@ -95,10 +95,10 @@ describe('gen-persistence-catalog collectLogEvents', () => {
it('extracts a member declaration-merged via the session module', () => {
const events = collectLogEvents(make({
'packages/group/fix/src/types.ts': merge(' /** Merged provenance. */\n \'fix/merged\': { id: string }'),
'packages/group/fix/src/types.ts': merge(' /** Merged event source record. */\n \'fix/merged\': { id: string }'),
}))
expect(events).toHaveLength(1)
expect(events[0]).toMatchObject({ name: 'fix/merged', doc: 'Merged provenance.' })
expect(events[0]).toMatchObject({ name: 'fix/merged', doc: 'Merged event source record.' })
})
it('collapses a newline-separated multi-line payload to a valid one-line fragment', () => {

View File

@@ -417,7 +417,7 @@ describe('Session', () => {
}
})
it('round-trips adapter-default provenance and rejects invalid durable values', () => {
it('round-trips adapter-default markers and rejects invalid durable values', () => {
const valid = {
type: 'request/header',
seq: 0,

View File

@@ -78,8 +78,8 @@ function toolResultEvent(
}
}
describe('foldSurface provenance', () => {
it('accepts absent or valid provenance and complete replacement coverage', () => {
describe('foldSurface source-event references', () => {
it('accepts absent or valid source-event references and complete replacement coverage', () => {
const events = [
provenanceEvent(0, undefined),
provenanceEvent(1, undefined),
@@ -91,7 +91,7 @@ describe('foldSurface provenance', () => {
expect(() => foldSurface(events)).not.toThrow()
})
it('rejects provenance on a non-surface event', () => {
it('rejects source-event references on a non-surface event', () => {
const event = {
type: 'turn/start',
seq: 0,
@@ -102,7 +102,7 @@ describe('foldSurface provenance', () => {
expect(() => foldSurface([event])).toThrow(/cannot carry sourceEventSeqs/)
})
it('accepts explicit empty provenance on an assistant message', () => {
it('accepts an explicit empty source-event list on an assistant message', () => {
const event = {
type: 'assistant/message',
seq: 0,

View File

@@ -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/system-prompt/README.md
README.md: e98c45a8829a945500ef5282d84e1904b31c68bf
README.zh.md: 5827ae27706505bde86a4ee0d86542a7a2ff63d9
README.md: 13b05bfcd19212ade42f22ece455871d022e6260
README.zh.md: 0f9e7a2358134018975db1bc3c6b7206a274b3ec

View File

@@ -19,7 +19,7 @@ System prompt assembly registry. Plugins contribute ordered sections, tool schem
- `ctx.systemPrompt.section(section: PromptSection): () => void` Contribute a section. The layer is the calling context's scope: `agent.ctx` contributes to that agent alone, shadowing a same-named global section there. Duplicate names within one layer and non-finite orders throw. Disposed with the calling fiber.
- `ctx.systemPrompt.tools(provider: (context: AssembleContext) => ToolProviderResult): () => void` Contribute tool schemas, evaluated at each assembly with that assembly's context. `ToolProviderResult` = `{ schemas, knownNames? }`: `schemas` is the post-restriction visible set; `knownNames` is the pre-restriction universe used by `toolOrder`. A provider must not return a schema named `TOOL_ORDER_REST`. Scoped providers are consulted only for their scope's assemblies. Disposed with the calling fiber.
- `ctx.systemPrompt.variable(name: string, provider: (context) => string | undefined): () => void` Contribute a prompt variable, referenced from section text as `{{name}}`. Scoped variables shadow a same-named global for that agent. Duplicate-in-layer or unreferenceable names throw; `undefined` means "no value for this assembly". Disposed with the calling fiber.
- `ctx.systemPrompt.assemble(context?: AssembleContext): Promise<PromptAssembly>` Assemble the prompt for one caller: the global layer merged with `context.scope`'s layer, with tool schemas detached before the transform seam. Runs through the scope-filtered `system-prompt/assemble` waterfall and returns its authoritative result. An optional `context.signal` explicitly controls this assembly request; providers and listeners may cooperate with it but must not retain it for another turn. Rejects when a configured `toolOrder` names a tool outside the providers' `knownNames` universe, or when a provider returns the reserved rest-entry name.
- `ctx.systemPrompt.assemble(context?: AssembleContext): Promise<PromptAssembly>` Assemble the prompt for one caller: the global layer merged with `context.scope`'s layer, with tool schemas detached before the transform waterfall. Runs through the scope-filtered `system-prompt/assemble` waterfall and returns its authoritative result. An optional `context.signal` explicitly controls this assembly request; providers and listeners may cooperate with it but must not retain it for another turn. Rejects when a configured `toolOrder` names a tool outside the providers' `knownNames` universe, or when a provider returns the reserved rest-entry name.
### Live events

View File

@@ -19,7 +19,7 @@
- `ctx.systemPrompt.section(section: PromptSection): () => void`:贡献一个段。层由调用上下文的作用域决定:`agent.ctx` 只为该 agent 贡献,并在该处遮蔽同名全局段。同一层中的重复名称和非有限顺序会抛出。随调用 fiber 一并 dispose资源释放
- `ctx.systemPrompt.tools(provider: (context: AssembleContext) => ToolProviderResult): () => void`:贡献工具 schema每次组装时使用该次组装的上下文求值。`ToolProviderResult` = `{ schemas, knownNames? }``schemas` 是限制后的可见集合;`knownNames` 是限制前由 `toolOrder` 使用的全集。提供方不得返回名为 `TOOL_ORDER_REST` 的 schema。带作用域提供方只在其作用域的组装中查询。随调用 fiber 一并 dispose。
- `ctx.systemPrompt.variable(name: string, provider: (context) => string | undefined): () => void`:贡献提示词变量,在段文本中以 `{{name}}` 引用。带作用域变量会为该 agent 遮蔽同名全局变量。同层重复或无法引用的名称会抛出;`undefined` 表示「本次组装没有值」。随调用 fiber 一并 dispose。
- `ctx.systemPrompt.assemble(context?: AssembleContext): Promise<PromptAssembly>`:为一个调用方组装提示词:将全局层与 `context.scope` 的层合并,并在变换 seam 前分离工具 schema。它经过按作用域筛选的 `system-prompt/assemble` waterfall并返回其权威结果。可选的 `context.signal` 显式控制本次组装请求;提供方与监听器可以配合该信号,但不得将它保留给另一轮次。当已配置的 `toolOrder` 指名提供方 `knownNames` 全集以外的工具,或提供方返回保留的其余项名称时,调用会被拒绝。
- `ctx.systemPrompt.assemble(context?: AssembleContext): Promise<PromptAssembly>`:为一个调用方组装提示词:将全局层与 `context.scope` 的层合并,并在变换 waterfall 前分离工具 schema。它经过按作用域筛选的 `system-prompt/assemble` waterfall并返回其权威结果。可选的 `context.signal` 显式控制本次组装请求;提供方与监听器可以配合该信号,但不得将它保留给另一轮次。当已配置的 `toolOrder` 指名提供方 `knownNames` 全集以外的工具,或提供方返回保留的其余项名称时,调用会被拒绝。
<a id="live-events"></a>

View File

@@ -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/tools/README.md
README.md: 4437880cce55de930d4287e43a6910b5b60fed9f
README.zh.md: 9c084c86e8e8bd8aaf1847db35d152dd653eab4a
README.md: f3d1b4741c7fde64669794d079c36a18e633c0c1
README.zh.md: d3054372095ef0cfdabc6cf80e0faa41a3b12d4c

View File

@@ -55,7 +55,7 @@ The live registry pipeline has three transformable waterfalls, then the definiti
- Tool plugins call `ctx.tools.register()` — schemas flow into the assembly automatically.
- `tools/pre-execute` is the reorderable allow/deny/ask gate; `ctx.tools.guard()` adds monotonic owner policy after it.
- `tools/execute` wraps normalized canonical dispatch for timeout, retry, or metrics. Wrappers may replace only the operational signal; a wrapper-authored success is normalized through the resolved tool's output declaration. Canonical-result provenance belongs to one immutable dispatch token, so a cached result from another call or tool is revalidated under the active declaration.
- `tools/execute` wraps normalized canonical dispatch for timeout, retry, or metrics. Wrappers may replace only the operational signal; a wrapper-authored success is normalized through the resolved tool's output declaration. Each canonical result belongs to one immutable dispatch token, so a cached result from another call or tool is revalidated under the active declaration.
- `tools/post-execute` may replace presentation content, replace the canonical value, block with feedback, or attach ordered contexts. A definition's optional `finalizeContent` then owns its last content-only invariant across normal results and outer pipeline failures; `tools/result` observes the immutable final outcome. Content replacement is not a confidentiality boundary: block or replace the value when programmatic consumers must not receive it.
- Exact signatures and ordering live in the generated region of [tools.md](../../../docs/subsystems/tools.md#cordis-surface) and [pipeline](../../../docs/tool-execution-pipeline.md).
- MCP servers: one plugin per server, discover tools, call `ctx.tools.register()` with the server's schemas.
@@ -187,7 +187,7 @@ Append-only; newly visible content follows the reusable request prefix and does
## Known Limitations and Deferred Work
- **Concurrency policy is not an event seam** — `executionMode()` reads the resolved tool definition directly; plugins can only declare a classifier on definitions they own.
- **Concurrency policy is not an event gate** — `executionMode()` reads the resolved tool definition directly; plugins can only declare a classifier on definitions they own.
- **`tools/pre-execute` deliberately cannot rewrite `exec.arguments`** — logged and rendered args would desync from what ran; the rewrite design is [a proposed Agent Note](../../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md).
- **Caller-defined subagent and workflow structured outputs remain object-rooted** — this is a consumer-level guard; the shared schema vocabulary and tool outputs support every JSON root.
- **`timeoutMs` on a definition is declarative only** — the registry never enforces deadlines; enforcement requires the `@deepseek-ai/dsh-timeout-policy` wrapper.

View File

@@ -55,7 +55,7 @@ tools:
- 工具插件调用 `ctx.tools.register()`schema 会自动流入组装结果。
- `tools/pre-execute` 是可重排的允许/拒绝/询问门禁;`ctx.tools.guard()` 在其后添加单调的拥有方策略。
- `tools/execute` 会环绕包装规范化后的规范分发,以支持超时、重试或指标采集。包装层只能替换操作信号;包装层生成的成功结果会根据已解析工具的输出声明进行规范化。规范结果的来源属于一个不可变分发 token因此来自其他调用或工具的缓存结果会根据当前声明重新验证。
- `tools/execute` 会环绕包装规范化后的规范分发,以支持超时、重试或指标采集。包装层只能替换操作信号;包装层生成的成功结果会根据已解析工具的输出声明进行规范化。每个规范结果属于一个不可变分发 token因此来自其他调用或工具的缓存结果会根据当前声明重新验证。
- `tools/post-execute` 可以替换呈现内容、替换规范值、通过反馈阻止,或附加有序上下文。随后,定义可选的 `finalizeContent` 会在普通结果和外层流水线失败中维护其最终、仅涉及内容的不变式;`tools/result` 观测不可变的最终结果。内容替换不是保密边界:当编程消费方不得接收某个值时,应阻止或替换该值。
- 确切签名与顺序位于 [tools.md](../../../docs/subsystems/tools.md#cordis-surface) 的生成区块和[流水线](../../../docs/tool-execution-pipeline.md)中。
- MCP 服务器:每个服务器使用一个插件;发现工具后,使用服务器的 schema 调用 `ctx.tools.register()`
@@ -187,7 +187,7 @@ The available tools:
## 已知限制与暂缓事项
- **并发策略不是事件 seam**`executionMode()` 直接读取已解析的工具定义;插件只能在自身拥有的定义上声明分类器。
- **并发策略不是事件门禁**`executionMode()` 直接读取已解析的工具定义;插件只能在自身拥有的定义上声明分类器。
- **`tools/pre-execute` 有意不允许改写 `exec.arguments`**:否则日志记录和呈现的参数会与实际运行内容失去同步;改写设计记录在[拟议的 Agent Note](../../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md)中。
- **调用方定义的 subagent 与工作流结构化输出仍要求对象根**:这是消费方层面的守卫;共享 schema 词汇和工具输出支持任意 JSON 根。
- **定义上的 `timeoutMs` 仅为声明**:注册表绝不会强制执行截止时间;要强制执行,必须使用 `@deepseek-ai/dsh-timeout-policy` 包装层。

View File

@@ -373,7 +373,7 @@ export function createRunCodeTool(registry: ToolRegistry, options: RunCodeBridge
let dispatches = 0
// The per-run scheduler, reusing the NATIVE concurrency contract through
// the registry's staged view (the loop scheduler's own seam) — and the
// the registry's staged view (the loop scheduler's own boundary) — and the
// native loop's SEQUENCING: every ordered stage (the dispatch-start
// append, prepare = pre-execute/guards, finalize/finish = post-execute,
// context deferral, the settle append) runs inside ONE driver lane, so

View File

@@ -148,7 +148,7 @@ declare module 'cordis' {
'tools/execute'(this: Scoped<ToolRegistry>, exec: ToolDispatchExecution, next: () => Promise<ToolExecutionResult>): Promise<ToolExecutionResult>
/**
* Accept, replace, enrich, or block a normalized dispatch result. `next()`
* accepts it unchanged; thrown tools still reach this seam as errors. Async
* accepts it unchanged; thrown tools still reach this waterfall as errors. Async
* listeners must observe `exec.signal`; after they settle, caller
* cancellation replaces only a successful accepted outcome with the code
* selected by whether the tool body was invoked.
@@ -418,7 +418,7 @@ export type ScheduledToolDispatch =
/**
* Symbol-keyed scheduler view that keeps pre/post policy ordered while
* overlapping dispatch. Ordinary callers use {@link ToolRegistry.execute};
* this is not a plugin seam.
* this is not a plugin extension point.
* @internal
*/
export interface ToolRegistryScheduler {
@@ -1179,7 +1179,7 @@ export class ToolRegistry extends Service {
* shaping must never fail the dispatch or lose the settle event. Private:
* the ONE consumer is the `run_code` bridge this registry constructs, which
* receives it as a capability parameter (the `requireRuntime` idiom) — the
* waterfall, not this invoker, is the public extension seam.
* waterfall, not this invoker, is the public extension point.
*/
private async shapeDispatchLog(dispatch: CodeDispatchLog): Promise<ContentBlock[]> {
try {

View File

@@ -310,8 +310,8 @@ export interface ReadResultView {
/**
* One citeable source in a completed {@link WebSearchResultView}, the faithful
* projection of one web-search source. The presentation projection of `dsh-web`'s
* `WebSearchSource`: that seam type is the authoritative shape (core cannot depend
* on the web seam, so the two are declared separately and MUST evolve together).
* `WebSearchSource`: that Service Definition type is authoritative (core cannot depend
* on the web Service Definition, so the two are declared separately and MUST evolve together).
* A web tool projects this shape through `output.presentationMeta` because the
* render text cannot losslessly carry it (see the web-result-card Agent Note); its
* `presentResult` reads it back.
@@ -361,7 +361,7 @@ export interface WebSearchResultView {
sources: WebSource[]
/** The provider-generated answer text, when any. */
answer?: string
/** True when the seam cut the source list to honor the result cap. */
/** True when the web service cut the source list to honor the result cap. */
truncated: boolean
}

View File

@@ -500,7 +500,7 @@ function renderType(schema: unknown, className: string, state: RenderState): str
({ schema, className, phase: 'start', listDepth, children: [], childIndex: 0, childTypes: [], entries: [] })
try {
// Validate the WHOLE tree once, then trust it — the same contract the
// sibling ts-types renderer follows at a typed same-process seam. Every
// sibling ts-types renderer follows at a typed same-process boundary. Every
// node past this point is a validated JSON-schema node, so the walk reads
// its fields without re-checking. An unsupported or malformed schema throws
// here (before anything is emitted) and degrades to `Any`, the Python

View File

@@ -19,7 +19,7 @@ const testToolSignal = new AbortController().signal
* misconfiguration rejections, the run_code dispatch bridge (serialization,
* abort, JSON normalization, error mapping, events, quiescence), and HMR
* safety — all against an in-repo fake runtime, exactly the
* interface/implementation/consumer shape the seam promises.
* Service Definition / Service provider / Consumer roles the seam promises.
*/
/** A scriptable in-repo CodeRuntime: each test sets `behavior` to drive the bindings however it needs. */

View File

@@ -1068,7 +1068,7 @@ describe('ToolRegistry', () => {
const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'traced', arguments: { text: 'hi' } })
expect(result).toEqual({ content: [{ type: 'text', text: 'hi' }], isError: false, value: [{ type: 'text', text: 'hi' }] })
// The around seam wraps dispatch; pre gates before it, post runs over its result.
// The around-dispatch extension point wraps dispatch; pre gates before it, post runs over its result.
expect(order).toEqual(['pre', 'execute:before', 'dispatch', 'execute:after', 'post'])
})
@@ -1646,7 +1646,7 @@ describe('ToolRegistry', () => {
const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } })
expect(result.isError).toBe(true)
expect(result.content[0]).toMatchObject({ text: 'Error: nope' })
expect(entered).toBe(false) // a denied call never enters the around-dispatch seam
expect(entered).toBe(false) // A denied call never enters the around-dispatch extension point.
})
it('a thrown tool is normalized to an isError result BEFORE a tools/execute listener sees next()', async () => {