Merge commit '70396085b141370ce32de1be4e225b4384eaf46d' into HEAD

# Conflicts:
#	.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.md
#	.agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.zh.md
#	.agents/notes/implemented/simplification/2026-07-23-acp-automation-only-protocol.i18n.yaml
#	docs/config-catalog.i18n.yaml
#	docs/module-graph.i18n.yaml
#	docs/module-graph.md
#	docs/module-graph.zh.md
#	docs/tool-catalog.i18n.yaml
#	docs/tool-catalog.md
#	docs/tool-catalog.zh.md
#	examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json
#	examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json
#	examples/acp-agent/tests/snapshots/code-mode-turn/tool-schemas.expected.json
#	packages/core/tools/README.i18n.yaml
#	packages/core/tools/README.zh.md
#	packages/core/tools/src/code-mode.ts
#	packages/host/apiproxy/tests/api-proxy-models.spec.ts
#	packages/host/plugin-inventory/tests/inventory.spec.ts
#	packages/mcp/mcp-client/tests/mcp-client.e2e.ts
#	packages/mcp/mcp-client/tests/mcp-client.spec.ts
#	packages/self-modification/tool-cordis/src/api-catalog.ts
#	packages/test-support/acp-snapshot/README.i18n.yaml
#	pnpm-lock.yaml
This commit is contained in:
Tianyi Cui
2026-08-17 11:31:59 +08:00
3845 changed files with 62208 additions and 100402 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/agent-default-model/README.md
README.md: e86be7c37a1f994ca52f018144ef6a2409bd1eea
README.zh.md: 00250c28ef8c03d4b33fe1c1bfca138a022f6638
README.md: 4b9e06c1f68150633afa8aad752845f6d96dda85
README.zh.md: f3b8acb1a64e9a80aece87ac71f49a963d573352

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
The deployment default used when an entry point creates an Agent that has no session-local model selection. `AgentDefaultModelService` provides `ctx.agentDefaultModel`; direct entry points such as `dsh --profile headless` and Host-backed entry points such as ApiProxy read the same service instead of owning parallel provider/model defaults.
The deployment default used when an entry point creates an Agent that has no session-local model selection. `AgentDefaultModelConfig` provides `ctx.agentDefaultModel`; direct entry points such as `dsh --profile headless` and Host-backed entry points 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.

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
该部署默认值供入口在创建尚无会话级模型选择的 Agent 时使用。`AgentDefaultModelService` 提供 `ctx.agentDefaultModel``dsh --profile headless` 这类直接入口与 ApiProxy 这类由 Host 支撑的入口读取同一服务,而不是分别持有平行的提供方/模型默认值。
该部署默认值供入口在创建尚无会话级模型选择的 Agent 时使用。`AgentDefaultModelConfig` 提供 `ctx.agentDefaultModel``dsh --profile headless` 这类直接入口与 ApiProxy 这类由 Host 支撑的入口读取同一服务,而不是分别持有平行的提供方/模型默认值。
插件配置必须提供 `{ provider, model }`。该组合配置项构成 Settings 中 `agent-default-model` 分节的基础层;挂载的设置提供方在其上叠加用户选择,更改会在下一次调用 `currentSelection()` 时可见。`reasoningEffort` 属于该 Settings 分节但特意不属于插件配置完整保存的选择必须能在下一个选定模型没有推理reasoning强度时清除旧值而组合配置值会再次被继承。

View File

@@ -1,9 +1,9 @@
{
"name": "@deepseek-ai/dsh-agent-default-model",
"description": "Default model selection shared by Agent entry points",
"version": "0.0.1-rc.2",
"version": "0.1.0-rc.6",
"publishConfig": {
"access": "restricted"
"access": "public"
},
"repository": {
"type": "git",
@@ -30,7 +30,7 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"dependencies": {
"@deepseek-ai/schemastery": "workspace:^"
},

View File

@@ -13,7 +13,7 @@ import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-sett
declare module '@deepseek-ai/cordis' {
interface Context {
/** Default model selection for Agents created without an explicit model. */
agentDefaultModel: AgentDefaultModelService
agentDefaultModel: AgentDefaultModelConfig
}
}
@@ -61,7 +61,7 @@ function selection(settings: AgentDefaultModelSettings): ModelSelection {
* The composition entry remains usable without a settings provider; when one
* is mounted, its user layer is read live.
*/
export class AgentDefaultModelService extends Service {
export class AgentDefaultModelConfig extends Service {
static Config: z<Config> = z.object({
provider: z.string().required(),
model: z.string().required(),
@@ -104,4 +104,4 @@ export class AgentDefaultModelService extends Service {
}
}
export default AgentDefaultModelService
export default AgentDefaultModelConfig

View File

@@ -2,13 +2,13 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import AgentDefaultModelService, { AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE } from '../src/index.ts'
import { Settings } from '@deepseek-ai/dsh-settings'
import AgentDefaultModelConfig, { AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE } from '../src/index.ts'
import { SettingsProvider } 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 {
class MemorySettings extends SettingsProvider {
doc: Record<string, unknown> = {}
get writable(): boolean {
@@ -28,19 +28,19 @@ class MemorySettings extends Settings {
async function boot(): Promise<{
ctx: Context
settingsFiber: Context['fiber']
defaultModel: AgentDefaultModelService
defaultModel: AgentDefaultModelConfig
}> {
const ctx = new Context()
const settingsFiber = ctx.plugin(MemorySettings)
await settingsFiber.await()
await ctx.plugin(AgentDefaultModelService, {
await ctx.plugin(AgentDefaultModelConfig, {
provider: 'deepseek-official',
model: 'deepseek-v4-flash',
})
return { ctx, settingsFiber, defaultModel: ctx.agentDefaultModel }
}
describe('AgentDefaultModelService', () => {
describe('AgentDefaultModelConfig', () => {
it('resolves the user layer over the composition entry', async () => {
const bench = await boot()
expect(bench.defaultModel.currentSelection()).toEqual({
@@ -90,7 +90,7 @@ describe('AgentDefaultModelService', () => {
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.plugin(AgentDefaultModelConfig, { 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

@@ -24,7 +24,7 @@
"path": "../../settings/settings"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

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: 13f79b9e70bfb658c459240c97acf39025df2ac0
README.zh.md: 3124acc6c458374e3459837111a4424afb604b29
README.md: 683799d1840c857981d4ff30dd3e8e078be03098
README.zh.md: 8009f72a387f5c65ed258eed9ca6d1f14835c41f

View File

@@ -78,7 +78,7 @@ Everything that goes beyond "call the model, run the tools, repeat" belongs to p
- Compaction: pressure on `agent/pre-step`; canonical overflow repair on `agent/request-error`
- Model-request recovery: `dsh-llm-retry` records and waits exact-provider normal or unbounded backoff on `agent/request-error`, emits non-surface `llm/retry` status, then returns a retry action
- Sandbox, permission, plan mode: `tools/pre-execute` for extensible deny/ask, `tools.guard()` for monotonic owner policy, `tools/post-execute` for result decisions, and `tools/result` for final observation
- Sub-agents: implemented outside the loop as `ctx.subagents` providers; in-process providers use `ctx.agents.create()` and owned `AgentHandle` teardown, while generic [`ctx.tasks`](../../tasks/tasks/) plus [`dsh-tool-subagent`](../../subagent/tool-subagent/) own background collection.
- Sub-agents: implemented outside the loop as `ctx.subagents` providers; in-process providers use `ctx.agents.create()` and owned `AgentHandle` teardown, while generic [`ctx.jobs`](../../jobs/jobs/) plus [`dsh-tool-subagent`](../../subagent/tool-subagent/) own background collection.
- Persistence: eager write-behind from `session/event`; `session/flush` is an explicit observation barrier
- UI: `session/event` (assistant token stream, boundaries, tool activity) + `agent/*` control events (`agent/status`, `agent/created`/`agent/disposed`)

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
唯一的具体 agent智能体插件循环驱动器。其包内部实现满足 `Agent` 接口,并驱动会话轮次步骤生命周期。
agent智能体的唯一具体实现插件循环驱动器。其包内部实现满足 `Agent` 接口,并驱动会话轮次步骤生命周期。
这是 harness 中唯一包含具体循环逻辑的包。其他所有内容要么是抽象服务,要么是针对扩展点的插件:新行为应放入插件,而不是这里。
@@ -10,20 +10,20 @@
### 公开 API
创建与恢复属于同一个受回滚保护的事务:构造私有会话、具体 agent 和带作用域的上下文;等待可选 setup进入两个注册表依次宣告 `session/created``agent/created`;发出 `agent/session-start`此后才启动驱动器。Setup 作为受信任的同进程组合代码,接收完整的带作用域 `Context`,并且不得驱动尚未发布的 agent。普通的类型化身份与选项输入遵循只读约定借用方式传入seed 事件会话元数据会跨越持久会话边界,因此系统会验证并快照它们。可选的 `AbortSignal` 只取消加载setup发布并在返回的 handle 可见前分离。
创建与恢复属于同一个受回滚保护的事务:构造私有会话、具体 agent 和带作用域的上下文;等待可选 setup进入两个注册表依次宣告 `session/created``agent/created`;发出 `agent/session-start`此后才启动驱动器。Setup 作为受信任的同进程组合代码,接收完整的带作用域 `Context`,并且不得驱动尚未发布的 agent。普通的类型化身份与选项输入只读约定借用seed 事件会话元数据会跨越持久会话边界,因此系统会对其进行验证并创建快照。可选的 `AbortSignal` 只取消加载setup发布并在返回的 handle 可见前分离。
调用方 fiber 与 AgentLoop 提供方共同拥有 agent。`AgentFactory.createAgent(ownerCtx, options)``resume(ownerCtx, options)` 显式接收调用方所有权,而工厂为 `sessions`/`llm`/`tools`/`systemPrompt` 保留自身的依赖上下文;这样,调用方可以只注入 `agents`,而不会缩减新 agent 的服务接口。调用方卸载、handle dispose资源释放或提供方卸载都会汇合到同一个记忆化的完全停稳边界。提供方关闭会同时等待资源 teardown以及已经观测到停用的公开 create/resume 包装层,因此依赖消失后,任何 continuation 都无法继续发布。
每个 agent 与其会话共享一个由调用方选择的 `SessionId`,并假设它在全局唯一;意外的 UUID 冲突不属于受支持模型。两个使用同一 id 的并发操作都可以进行准备,但最终的 `enter()` 调用会裁决发布,所有失败方都会回滚各自的私有资源。每次 detach 都绑定到确切进入的对象,因此陈旧 disposer 无法移除之后出现的同 id 替代项。在同步创建通知期间请求的 detach 会等待该次分发退栈,从而保留 created/disposed 配对。Teardown 顺序为停止并 drain → 撤销作用域 → detach agent → detach 会话私有作用域清理完成后,该 id 即可复用。普通、不可 veto 的 `agent/*` 通知通过 `agentEvents(ctx, agent)` 发出;逐步骤组装通过 `assembleContextFor(agent)` 完成。
每个 agent 与其会话共享一个由调用方选择的 `SessionId`,并假设它在全局唯一;意外的 UUID 冲突不属于受支持模型。两个使用同一 id 的并发操作都可以进行准备,但最终的 `enter()` 调用会裁决发布,所有失败方都会回滚各自的私有资源。每次 detach 都绑定到确切进入的对象,因此陈旧 disposer 无法移除之后出现的同 id 替代项。在同步创建通知期间请求的 detach 会等待该次分发退栈,从而保留 created/disposed 配对。Teardown 按以下顺序执行:停止并排空 → 撤销作用域 → detach agent → detach 会话私有作用域清理完成后,该 id 即可复用。不具否决能力的普通 `agent/*` 通知通过 `agentEvents(ctx, agent)` 发出;逐步骤组装通过 `assembleContextFor(agent)` 完成。
- `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` 互斥。这样,默认的全新重启不会冲突,也无需保留第二个实时路由身份。
- `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` 约定,并通过 `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.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`
配置驱动的 `ctx.agentLoop.create()` 路径让循环 fiber 拥有其 agent该路径会丢弃 handle。对于以编程方式创建的 agenthandle 持有者是唯一面向消费方的 teardown 能力AgentLoop 提供方卸载是一条独立的结构 teardown 边,而不是向应用代码公开的另一个 handle。
配置驱动的 `ctx.agentLoop.create()` 路径让循环 fiber 拥有其 agent该路径会丢弃 handle。对于以编程方式创建的 agenthandle 持有者是唯一面向消费方的 teardown 能力AgentLoop 提供方卸载是一条独立的结构 teardown 边,而不是向应用代码公开的另一个 handle。
### 注入的服务
@@ -55,9 +55,9 @@ interface Config {
具体 `ReactLoopAgent`、其 inbox 与运行控制均为包内部实现。包根只导出插件/服务/配置约定,包导出映射不提供 `./src/*` 逃逸路径;生命周期拥有方通过 `ctx.agents` 创建 agent而不是点名、构造或启动驱动器内部组件。一个准备完成的会话只能由一个具体驱动器认领所有可观测行为都通过会话事件和 `agent/*` 事件分类体系发生。
统一的 `send()` 原语按(`target` × `wakeup`)路由内容与来源;`followup`/`steer`/`inject` 是它的固定预设别名。`followup()` 追加到 `next-turn` FIFO 并唤醒驱动器,`steer()` 追加到 `next-step` inbox 并唤醒驱动器,`inject()` 则追加到同一个 `next-step` inbox但不唤醒驱动器。在轮次边界驱动器会先打开持久轮次再原子领取待处理的 next-step 输入和一条排队提示词;在步骤之间则只领取 next-step 输入。领取通过纯删除 splice 移除批次,并针对每条消息发出 `agent/inbox/claimed { message, turn }`。随后 `agent/pre-step` 返回拒绝结果,或返回将进入拟议步骤的完整消息。拒绝后,已领取批次保持已删除,并关闭不含步骤的轮次;领取后插入的输入仍等待后续处理,而空闲注入会一直等待,直到 follow-up 或 steering 唤醒驱动器。
统一的 `send()` 原语按(`target` × `wakeup`)路由内容与来源;`followup`/`steer`/`inject` 是它的固定预设别名。`followup()` 追加到 `next-turn` FIFO 并唤醒驱动器,`steer()` 追加到 `next-step` inbox 并唤醒驱动器,`inject()` 则追加到同一个 `next-step` inbox但不唤醒驱动器。在轮次边界驱动器会先打开持久轮次再原子领取待处理的 next-step 输入和一条排队提示词;在步骤之间则只领取 next-step 输入。领取操作通过仅执行删除 splice 移除整批消息,并为每条消息发出一次 `agent/inbox/claimed { message, turn }`。随后 `agent/pre-step` 返回拒绝结果,或返回将进入拟议步骤的完整消息。拒绝后,已领取批次保持已删除,并关闭不含步骤的轮次;领取后插入的输入仍等待后续处理,而空闲注入会一直等待,直到 follow-up 或 steering 唤醒驱动器。
每次 inbox 变更都会先发布一条规范化的 `agent/inbox/spliced` 事件,再修改实时投影。因此,插入、编辑、移除、领取与取消都通过同一组标准 splice 坐标回放。普通删除携带 `outcome: 'canceled'` 并发出 `agent/inbox/discarded { message }`;领取使用不带 outcome 的纯删除,随后由循环发出 `agent/inbox/claimed`。每次插入都会发出 `agent/inbox/inserted { message }``MessageId` 在两个待处理列表之间保持唯一,持久事件的同步观察方可以从 splice 前投影重建被移除的值。
每次 inbox 变更都会在修改实时投影之前,先发布一条规范化的 `agent/inbox/spliced` 事件。因此,插入、编辑、移除、领取与取消都通过同一组标准 splice 坐标回放。普通删除携带 `outcome: 'canceled'` 并发出 `agent/inbox/discarded { message }`;领取使用不带 outcome 的纯删除,随后由循环发出 `agent/inbox/claimed`。每次插入都会发出 `agent/inbox/inserted { message }``MessageId` 在两个待处理列表之间保持唯一,持久事件的同步观察方可以从 splice 前投影重建被移除的值。
### 循环生命周期(`agent.ts`
@@ -65,21 +65,21 @@ interface Config {
每次提供方调用成功结束时,都会恰好追加一个 `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)规定生命周期与竞态约定。
插件失败会结束当前轮次,而不是结束循环。最终适配器选择、分发与迭代失败会以终止错误或中止结束的形式由 `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)规定生命周期与竞态约定。
在步骤内,独占调用形成屏障;并行安全调用使用有界滚动池,并在启动前重新分类。只有分发和调用主体的执行会发生重叠。策略、持久结果和结果上下文仍保持模型顺序。中止会停止新调用drain 已启动的结果,并保留其已定案的结果上下文,不区分取消原因。内部调度器故障会停止新的分发,等待已启动的分发,然后在不虚构工具结果的情况下到达轮次错误边界。
在步骤内,独占调用形成屏障;并行安全调用使用有界滚动池,并在启动前重新分类。只有分发和调用主体的执行会发生重叠。策略、持久结果和结果上下文仍保持模型顺序。中止会阻止启动新的调用,等待已启动调用的结果处理完毕,并保留其完成终结后的结果上下文,不区分取消原因。内部调度器故障会停止新的分发,等待已启动的分发,然后在不虚构工具结果的情况下到达轮次错误边界。
### 插件负责的内容
超出「调用模型、运行工具、重复」的所有内容,都属于监听事件分类体系的插件:
- 钩子与策略:相关的 `agent/*` 检查点,加上受守卫保护的 `tools/pre-execute``tools/execute``tools/post-execute` → 定义拥有的 `finalizeContent``tools/result` 流水线;确切事件签名与 mode 位于 [core.md](../../../docs/subsystems/core.md#cordis-surface) 与 [tools.md](../../../docs/subsystems/tools.md#cordis-surface) 的生成区块
- 压缩compaction`agent/pre-step` 上观测压力;在 `agent/request-error` 上进行规范的溢出修复
- 模型请求恢复:`dsh-llm-retry``agent/request-error` 上记录并等待确切提供方配置的 normal 或无界退避,发出不进入表层的 `llm/retry` 状态,然后返回重试动作
- 模型请求恢复:`dsh-llm-retry``agent/request-error` 上记录并等待针对确切提供方配置的 normal 或无界退避,发出不进入表层的 `llm/retry` 状态,然后返回重试动作
- 沙箱、权限、计划模式:使用 `tools/pre-execute` 提供可扩展的拒绝/询问,使用 `tools.guard()` 提供单调拥有方策略,使用 `tools/post-execute` 处理结果决定,并使用 `tools/result` 进行最终观测
- subagent在循环外部实现为 `ctx.subagents` 提供方;进程内提供方使用 `ctx.agents.create()` 拥有的 `AgentHandle` 行 teardown而通用的 [`ctx.tasks`](../../tasks/tasks/) 与 [`dsh-tool-subagent`](../../subagent/tool-subagent/) 负责后台收集。
- 持久化:`session/event` 立即后写;`session/flush` 是显式观测屏障
- subagent在循环外部实现为 `ctx.subagents` 提供方;进程内提供方使用 `ctx.agents.create()` 创建 agent并通过其拥有的 `AgentHandle` 行 teardown而通用的 [`ctx.jobs`](../../jobs/jobs/) 与 [`dsh-tool-subagent`](../../subagent/tool-subagent/) 负责后台收集。
- 持久化:`session/event` 发生后立即安排延后写`session/flush` 是显式观测屏障
- UI`session/event`assistant token 流、边界、工具活动)+ `agent/*` 控制事件(`agent/status``agent/created`/`agent/disposed`
## 模型体验
@@ -96,7 +96,7 @@ interface Config {
#### KV Cache 影响
只有在同一提供方和模型路由下系统文本、schema 与前历史保持逐字节相同时,才保持仅追加。携带 token 的组装改写或组合变更可能从第一个改变的请求 token 起使复用失效。
只有在同一提供方和模型路由下,系统文本、schema 与前历史保持逐字节一致时,请求 token 序列才保持仅追加。携带 token 的组装改写或组合变更可能从第一个改变的请求 token 起使复用失效。
### 保留的消息历史
@@ -129,6 +129,6 @@ interface Config {
## 已知限制与暂缓事项
- **分类是一元的**:安全性取决于比较同级调用或资源的调用必须保持独占(参见[设计原理](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md))。
- **配置 label 默认每次新建**:省略 `sessionId` 会在每次启动创建新的 `${id}-session-<uuid>`;确切的恢复或创建行为要求显式提供稳定的 `sessionId`,而 `resumeSessionId` 要求已有持久化历史。
- **配置 label 默认对应新会话**:省略 `sessionId` 时,每次启动都会创建新的 `${id}-session-<uuid>`如需确切的恢复或创建行为,必须显式提供稳定的 `sessionId`,而 `resumeSessionId` 要求已有持久化历史。
- **配置 agent 没有逐 agent persona 字段或 setup 钩子**:它们使用部署 persona只有编程式 `ctx.agents.create()` / `resume()` 工厂选项支持带作用域的 persona工具组合。
- **没有内置轮次预算**:工具调用或 steering 会让当前轮次继续;限制失控轮次的策略必须从既有生命周期扩展点(如 `agent/turn-stopping`)执行取消。

View File

@@ -1,9 +1,9 @@
{
"name": "@deepseek-ai/dsh-agent-loop",
"description": "The concrete agent loop plugin for the DeepSeek Harness",
"version": "0.0.1-rc.2",
"version": "0.1.0-rc.6",
"publishConfig": {
"access": "restricted"
"access": "public"
},
"repository": {
"type": "git",
@@ -29,7 +29,7 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",

View File

@@ -139,7 +139,7 @@ export class ReactLoopAgent implements Agent {
if (this.phase.kind !== 'idle') this.phase.abort.abort(cause)
}
runMaintenance<T>(task: (signal: AbortSignal) => Promise<T>): Promise<T> {
runMaintenance<T>(job: (signal: AbortSignal) => Promise<T>): Promise<T> {
if (this.phase.kind !== 'idle') throw new Error(`agent "${this.id}" already has active work`)
const done = Promise.withResolvers<void>()
const maintenance: Phase = {
@@ -152,7 +152,7 @@ export class ReactLoopAgent implements Agent {
this.activityDone = done.promise
return (async () => {
try {
return await task(maintenance.abort.signal)
return await job(maintenance.abort.signal)
} finally {
this.setPhase({ kind: 'idle', lastTurn: maintenance.lastTurn })
if (maintenance.wakeRequested && this.inbox.hasPending) this.wakeDriver()

View File

@@ -62,20 +62,20 @@ class FactoryOwnership {
}
/** Join config startup work that begins before an agent exists. */
trackStartup(task: Promise<void>): void {
this.startupTasks.add(task)
const forget = () => { this.startupTasks.delete(task) }
void task.then(forget, forget)
trackStartup(job: Promise<void>): void {
this.startupTasks.add(job)
const forget = () => { this.startupTasks.delete(job) }
void job.then(forget, forget)
}
/** Join one public create/resume continuation; factory dispose awaits its settlement. */
trackWrapper(task: Promise<unknown>): void {
this.trackStartup(task.then(() => undefined, () => undefined))
trackWrapper(job: Promise<unknown>): void {
this.trackStartup(job.then(() => undefined, () => undefined))
}
/** Resolve `task`, or stop waiting when factory teardown begins. */
async waitWhileActive(task: Promise<void>): Promise<void> {
await Promise.race([task, this.inactive.promise])
async waitWhileActive(job: Promise<void>): Promise<void> {
await Promise.race([job, this.inactive.promise])
}
async dispose(): Promise<void> {

View File

@@ -14,7 +14,7 @@
import type { Context } from '@deepseek-ai/cordis'
import { assertNever, createToolResultMessage, type ToolCallBlock } from '@deepseek-ai/dsh-llm'
import type { Session, UserMessage } from '@deepseek-ai/dsh-session'
import { TOOL_ABORTED_BEFORE_DISPATCH, TOOL_REGISTRY_SCHEDULER, type ToolExecutionInput, type ToolExecutionMode, type ToolExecutionResult, type ToolRunContext } from '@deepseek-ai/dsh-tools'
import { TOOL_ABORTED_BEFORE_DISPATCH, TOOL_RUNTIME_SCHEDULER, type ToolExecutionInput, type ToolExecutionMode, type ToolExecutionResult, type ToolRunContext } from '@deepseek-ai/dsh-tools'
/** One tool call after argument parsing, ready to schedule. */
interface PlannedCall {
@@ -149,8 +149,8 @@ async function runGroup(
if (slot === undefined) break
const call = group[committed]
const result = slot.needsPost
? await ctx.tools[TOOL_REGISTRY_SCHEDULER].finalize(slot.exec, slot.result)
: ctx.tools[TOOL_REGISTRY_SCHEDULER].finish(slot.exec, slot.result)
? await ctx.tools[TOOL_RUNTIME_SCHEDULER].finalize(slot.exec, slot.result)
: ctx.tools[TOOL_RUNTIME_SCHEDULER].finish(slot.exec, slot.result)
// oxlint-disable-next-line typescript/no-non-null-assertion -- bounded index
appendToolResult(session, turn, step, call!.block, result, callSeqs[committed]!)
for (const context of result.additionalContexts ?? []) acceptContext(context)
@@ -166,11 +166,11 @@ async function runGroup(
const call = group[index]!
callSeqs[index] = appendToolCall(session, turn, step, call.block)
started++
const prepared = await ctx.tools[TOOL_REGISTRY_SCHEDULER].prepare(call.exec)
const prepared = await ctx.tools[TOOL_RUNTIME_SCHEDULER].prepare(call.exec)
throwSchedulerFailure()
switch (prepared.kind) {
case 'dispatch': {
const promise = ctx.tools[TOOL_REGISTRY_SCHEDULER].dispatch(prepared.exec).then(
const promise = ctx.tools[TOOL_RUNTIME_SCHEDULER].dispatch(prepared.exec).then(
(outcome) => {
slots[index] = { exec: prepared.exec, result: outcome.result, needsPost: outcome.kind === 'post-result' }
return index

View File

@@ -2,11 +2,11 @@ import { describe, expect, it } from 'vitest'
import { Context, type Fiber } from '@deepseek-ai/cordis'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import LlmService, { createUserMessage, CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { createUserMessage, CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
const testToolSignal = new AbortController().signal
@@ -19,10 +19,10 @@ interface Harness {
async function harness(adapter: LlmAdapter): Promise<Harness> {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
const agentsFiber = await ctx.plugin(AgentRegistry)
const loopFiber = await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
@@ -119,10 +119,10 @@ describe('AgentLoop initiator scope', () => {
it('keeps overlapping driver continuations bound to their exact Agents', async () => {
const ctx = new Context()
const adapter = new OverlapAdapter(ctx)
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
@@ -378,10 +378,10 @@ describe('AgentLoop initiator scope', () => {
it('keeps ALS readable while root disposal drains sibling AgentLoop fibers', async () => {
const ctx = new Context()
const adapter = new ReloadAdapter()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)

View File

@@ -3,18 +3,18 @@ import { describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import LlmService from '@deepseek-ai/dsh-llm'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import { MockAdapter, textResponse } from './mock-adapter.ts'
async function harness(adapter: MockAdapter): Promise<Context> {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)

View File

@@ -9,10 +9,10 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmService from '@deepseek-ai/dsh-llm'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineContentToolFixture, TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { defineContentToolFixture, TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
@@ -23,10 +23,10 @@ function driverDone(agent: Agent): Promise<void> {
async function harness(adapter: MockAdapter) {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
@@ -512,10 +512,10 @@ describe('Agent.cancel()', () => {
it('disposal from a synchronous step/start session-event listener stops before adapter dispatch', async () => {
const adapter = new MockAdapter([textResponse('should not stream')])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)

View File

@@ -4,13 +4,13 @@ import { Context } from '@deepseek-ai/cordis'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import LlmService from '@deepseek-ai/dsh-llm'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, SessionPreparation } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import AgentLoop, { CONFIGURED_AGENT_IDENTITIES_KEY } from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -27,10 +27,10 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
async function makeCoreContext(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
return ctx
}
@@ -89,7 +89,7 @@ describe('config-driven session id', () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-exact-duplicate-'))
dirs.push(root)
const ctx = await makeCoreContext()
await ctx.plugin(SessionPersistenceJsonl, { root })
await ctx.plugin(JsonlSessionPersistence, { root })
const outcome = await ctx.plugin(AgentLoop, {
agents: [
@@ -108,7 +108,7 @@ describe('config-driven session id', () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-exact-reload-'))
dirs.push(root)
const ctx = await makeCoreContext()
await ctx.plugin(SessionPersistenceJsonl, { root })
await ctx.plugin(JsonlSessionPersistence, { root })
ctx.llm.registerAdapter(['mock'], new MockAdapter([textResponse('first'), textResponse('second')]))
const config = { agents: [{ id: 'main', sessionId: SessionId('config-exact-reload'), provider: 'mock', model: 'mock' }] }
@@ -137,7 +137,7 @@ describe('config-driven session id', () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-exact-overlap-'))
dirs.push(root)
const ctx = await makeCoreContext()
await ctx.plugin(SessionPersistenceJsonl, { root })
await ctx.plugin(JsonlSessionPersistence, { root })
ctx.llm.registerAdapter(['mock'], new MockAdapter([textResponse('saved')]))
const sessionId = SessionId('config-exact-overlap')
const config = { agents: [{ id: 'main', sessionId, provider: 'mock', model: 'mock' }] }
@@ -184,7 +184,7 @@ describe('config-driven session id', () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-exact-cancel-'))
dirs.push(root)
const ctx = await makeCoreContext()
await ctx.plugin(SessionPersistenceJsonl, { root })
await ctx.plugin(JsonlSessionPersistence, { root })
const sessionId = SessionId('config-exact-cancel')
const config = { agents: [{ id: 'main', sessionId, model: 'mock' }] }
const firstLoop = await ctx.plugin(AgentLoop, config)
@@ -219,7 +219,7 @@ describe('config-driven session id', () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-exact-failure-'))
dirs.push(root)
const ctx = await makeCoreContext()
await ctx.plugin(SessionPersistenceJsonl, { root })
await ctx.plugin(JsonlSessionPersistence, { root })
const failure = new Error('persistence index failed')
const listenerFailure = new Error('failure observer failed')
const asyncListenerFailure = new Error('async failure observer failed')
@@ -255,7 +255,7 @@ describe('config-driven session id', () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-exact-unrenderable-'))
dirs.push(root)
const ctx = await makeCoreContext()
await ctx.plugin(SessionPersistenceJsonl, { root })
await ctx.plugin(JsonlSessionPersistence, { root })
const unrenderable = {
[Symbol.toPrimitive](): never {
throw new Error('coercion escaped')
@@ -293,7 +293,7 @@ describe('config-driven session id', () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-exact-dispose-'))
dirs.push(root)
const ctx = await makeCoreContext()
await ctx.plugin(SessionPersistenceJsonl, { root })
await ctx.plugin(JsonlSessionPersistence, { root })
const preparing = Promise.withResolvers<SessionPreparation>()
vi.spyOn(ctx.sessionPersistence, 'prepare').mockReturnValue(preparing.promise)
const released = vi.fn()
@@ -325,10 +325,10 @@ describe('config-driven session id', () => {
it('identity-nests the deferred resume fiber under its labeled owner effect', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
const loopFiber = await ctx.plugin(AgentLoop, {
agents: [{ id: SessionId('main'), provider: 'mock', model: 'mock', resumeSessionId: SessionId('deferred') }],
@@ -350,13 +350,13 @@ describe('config-driven session id', () => {
const idPattern = /^cfg-session-[0-9a-f-]{36}$/
// Run 1: a config agent persists a turn under a generated session id.
const ctx1 = new Context()
await ctx1.plugin(LlmService)
await ctx1.plugin(LlmRuntime)
await ctx1.plugin(SessionStore)
await ctx1.plugin(SystemPrompt)
await ctx1.plugin(ToolRegistry)
await ctx1.plugin(ToolRuntime)
await ctx1.plugin(AgentRegistry)
await ctx1.plugin(AgentLoop, { agents: [{ id: SessionId('cfg'), provider: 'mock', model: 'mock' }] })
await ctx1.plugin(SessionPersistenceJsonl, { root })
await ctx1.plugin(JsonlSessionPersistence, { root })
ctx1.llm.registerAdapter(['mock'], new MockAdapter([textResponse('cfg')]))
const a1 = ctx1.agents.list()[0] as Agent
expect(a1.id).toBe(a1.session.id)
@@ -369,13 +369,13 @@ describe('config-driven session id', () => {
// Run 2 over the SAME root: a fresh id means no on-disk collision (a fixed
// ${id}-session would crash here with "already has a persisted log").
const ctx2 = new Context()
await ctx2.plugin(LlmService)
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentLoop, { agents: [{ id: SessionId('cfg'), provider: 'mock', model: 'mock' }] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
await ctx2.plugin(JsonlSessionPersistence, { root })
ctx2.llm.registerAdapter(['mock'], new MockAdapter([textResponse('cfg2')]))
const a2 = ctx2.agents.list()[0] as Agent
expect(a2.id).toBe(a2.session.id)
@@ -393,13 +393,13 @@ describe('config-driven session id', () => {
// Run 1: a programmatically-created agent on a KNOWN session id persists a
// completed turn, so run 2 has a concrete id to resume.
const ctx1 = new Context()
await ctx1.plugin(LlmService)
await ctx1.plugin(LlmRuntime)
await ctx1.plugin(SessionStore)
await ctx1.plugin(SystemPrompt)
await ctx1.plugin(ToolRegistry)
await ctx1.plugin(ToolRuntime)
await ctx1.plugin(AgentRegistry)
await ctx1.plugin(AgentLoop, { agents: [] })
await ctx1.plugin(SessionPersistenceJsonl, { root })
await ctx1.plugin(JsonlSessionPersistence, { root })
ctx1.llm.registerAdapter(['mock'], new MockAdapter([textResponse('first')]))
const a1 = (await ctx1.agents.create({ sessionId: SessionId('sticky-1') })).agent
a1.followup(createUserMessage({ content: [{ type: 'text', text: 'remember me' }], source: { kind: 'user' } }))
@@ -409,13 +409,13 @@ describe('config-driven session id', () => {
// Resume waits for the injected persistence service, so poll until the
// config-created agent appears with its stored history.
const ctx2 = new Context()
await ctx2.plugin(LlmService)
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentLoop, { agents: [{ id: SessionId('main'), provider: 'mock', model: 'mock', resumeSessionId: SessionId('sticky-1') }] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
await ctx2.plugin(JsonlSessionPersistence, { root })
ctx2.llm.registerAdapter(['mock'], new MockAdapter([textResponse('second')]))
// The deferred resume runs after the backend is available.
@@ -434,15 +434,15 @@ describe('config-driven session id', () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-resume-miss-'))
dirs.push(root)
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [{ id: SessionId('main'), provider: 'mock', model: 'mock', resumeSessionId: SessionId('does-not-exist') }] })
const warn = vi.spyOn((ctx.agentLoop as unknown as { ctx: { logger: { warn: (...a: unknown[]) => void } } }).ctx.logger, 'warn')
.mockImplementation(() => undefined)
await ctx.plugin(SessionPersistenceJsonl, { root })
await ctx.plugin(JsonlSessionPersistence, { root })
ctx.llm.registerAdapter(['mock'], new MockAdapter([textResponse('x')]))
// The deferred resume fails (no such session on disk). It must be contained:
@@ -460,7 +460,7 @@ describe('startup reporting after factory teardown', () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-cfg-disposed-report-'))
dirs.push(root)
const ctx = await makeCoreContext()
await ctx.plugin(SessionPersistenceJsonl, { root })
await ctx.plugin(JsonlSessionPersistence, { root })
ctx.llm.registerAdapter(['mock'], new MockAdapter([textResponse('x')]))
// A restore lookup that hangs until after the loop is gone: the eventual

View File

@@ -1,20 +1,20 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmService, { createUserMessage, CallId, LlmError, MessageSource, ProviderRequestId, StreamChunk } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { createUserMessage, CallId, LlmError, MessageSource, ProviderRequestId, StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionEvent, SessionId, TurnEndReason, type UserMessage } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineContentToolFixture, type PostToolDecision } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { defineContentToolFixture, type PostToolDecision } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { ReactLoopAgent } from '../src/agent.ts'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
async function mountInvariants(ctx: Context): Promise<void> {
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(SessionInvariant)
await ctx.plugin(AgentInvariant)
await ctx.plugin(AgentLoopInvariant)
@@ -28,10 +28,10 @@ function driverDone(agent: Agent): Promise<void> {
async function harness(adapter: MockAdapter) {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
@@ -427,7 +427,7 @@ describe('disposal leaves the two-state status contract balanced', () => {
describe('adapter registration, routing, and accepted-input ownership', () => {
it('duplicate adapter registration is rejected', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
const adapter = new MockAdapter([])
ctx.llm.registerAdapter(['m1'], adapter)
expect(() => ctx.llm.registerAdapter(['m1'], new MockAdapter([])))
@@ -523,10 +523,10 @@ describe('turn numbering continues across seeded sessions', () => {
// fork: seed a second context's agent with the first session's log
const second = new MockAdapter([textResponse('turn two')])
const ctx2 = new Context()
await ctx2.plugin(LlmService)
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentLoop, { agents: [] })
ctx2.llm.registerAdapter(['mock'], second)
@@ -674,10 +674,10 @@ describe('turn and step boundary recovery', () => {
// The session invariant companion makes an unbalanced log fail the test.
async function balancedHarness(adapter: MockAdapter) {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await mountInvariants(ctx)
@@ -1106,10 +1106,10 @@ describe('disposal and cancellation during pre-step assembly', () => {
const blocked = new Promise<void>(r => void (releaseAssemble = r))
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await mountInvariants(ctx)
@@ -1156,10 +1156,10 @@ describe('disposal and cancellation during pre-step assembly', () => {
const blocker = new Promise<void>(r => void (releaseAssemble = r))
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await mountInvariants(ctx)
@@ -1206,10 +1206,10 @@ describe('disposal and cancellation during pre-step assembly', () => {
const blocker = new Promise<void>(r => void (releasePreStep = r))
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await mountInvariants(ctx)
@@ -1252,10 +1252,10 @@ describe('disposal and cancellation during pre-step assembly', () => {
const blocker = new Promise<void>(r => void (releasePreStep = r))
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await mountInvariants(ctx)
@@ -1300,10 +1300,10 @@ describe('disposal and cancellation during pre-step assembly', () => {
const blocker = new Promise<void>(r => void (releaseAssemble = r))
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await mountInvariants(ctx)

View File

@@ -1,10 +1,10 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmService, { createUserMessage, CallId, LlmError, StreamChunk, errorChain } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { createUserMessage, CallId, LlmError, StreamChunk, errorChain } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
@@ -16,10 +16,10 @@ function driverDone(agent: Agent): Promise<void> {
async function harness(adapter: MockAdapter) {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)

View File

@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmService, { createUserMessage, CallId } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { createUserMessage, CallId } from '@deepseek-ai/dsh-llm'
import SessionStore, {
SessionId,
type SessionEvent,
@@ -8,7 +8,7 @@ import SessionStore, {
type UserMessage,
} from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineContentToolFixture, type PostToolDecision, type PreToolDecision } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { defineContentToolFixture, type PostToolDecision, type PreToolDecision } from '@deepseek-ai/dsh-tools'
import AgentRegistry, {
type Agent,
type PreStepDecision,
@@ -29,10 +29,10 @@ import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
async function harness(adapter: MockAdapter) {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)

View File

@@ -1,14 +1,14 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
import { createUserMessage, markAgentLoopRequest, type GenerateOptions } from '@deepseek-ai/dsh-llm'
async function setup(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(AgentLoopInvariant)
return ctx
}
@@ -123,7 +123,7 @@ describe('request-reconstruction invariant', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
ctx.on('llm/stream', () => (async function* () {})() as never)
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(AgentLoopInvariant)
const session = ctx.sessions.create(SessionId('prepend-check'))
session.append('turn/start', { turn: 1 })

View File

@@ -1,9 +1,9 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmService, { createUserMessage, CallId, LlmError, StreamChunk } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { createUserMessage, CallId, LlmError, StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
@@ -15,10 +15,10 @@ function driverDone(agent: Agent): Promise<void> {
async function harness(adapter: MockAdapter, persona = '') {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { persona })
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
@@ -252,7 +252,7 @@ describe('agent loop', () => {
await waitForIdle(ctx, agent)
const request = adapter.requests[0]
expect(request!.system).toBe('You are an AI agent powered by the DeepSeek Harness SDK.\n\nYou are a test agent on mock.\n\nUse the noop tool wisely.')
expect(request!.system).toBe('You are an AI agent powered by DeepSeek Harness.\n\nYou are a test agent on mock.\n\nUse the noop tool wisely.')
expect(request!.tools?.map(t => t.name)).toEqual(['noop'])
})
@@ -269,7 +269,7 @@ describe('agent loop', () => {
send(agent, 'hi')
await waitForIdle(ctx, agent)
expect(adapter.requests[0]!.system).toBe('You are an AI agent powered by the DeepSeek Harness SDK.\n\nWorking in /work/space.')
expect(adapter.requests[0]!.system).toBe('You are an AI agent powered by DeepSeek Harness.\n\nWorking in /work/space.')
})
it('contains a strict-variable render failure: the turn errors, the loop keeps serving turns', async () => {
@@ -305,7 +305,7 @@ describe('agent loop', () => {
await waitForIdle(ctx, agent)
expect(adapter.requests).toHaveLength(1)
expect(adapter.requests[0]!.system).toBe('You are an AI agent powered by the DeepSeek Harness SDK.\n\nIn /rescued.')
expect(adapter.requests[0]!.system).toBe('You are an AI agent powered by DeepSeek Harness.\n\nIn /rescued.')
const turnEnds = agent.session.events.filter(e => e.type === 'turn/end')
expect(turnEnds).toHaveLength(2)
expect(turnEnds[1]?.type === 'turn/end' && turnEnds[1].data.reason.kind).toBe('completed')
@@ -335,7 +335,7 @@ describe('agent loop', () => {
expect(adapter.requests).toHaveLength(1)
expect(adapter.requests[0]!.model).toBe('mock')
expect(adapter.requests[0]!.system).toBe('You are an AI agent powered by the DeepSeek Harness SDK.\n\nYou run on mock.')
expect(adapter.requests[0]!.system).toBe('You are an AI agent powered by DeepSeek Harness.\n\nYou run on mock.')
})
it('omits the system field when system-prompt/assemble short-circuits with an empty assembly', async () => {
@@ -671,13 +671,13 @@ describe('agent loop', () => {
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('raw-context'), { provider: 'mock', model: 'mock' })
const text = '<system-reminder>Additional instructions from: pkg/AGENTS.md</system-reminder>'
agent.inject(createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'plugin', plugin: 'workspace-context' } }))
agent.inject(createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'plugin', plugin: 'agent-instructions' } }))
send(agent, 'go')
await waitForIdle(ctx, agent)
const contextEvent = agent.session.events.find(event => event.type === 'user/message' && event.data.source.kind === 'plugin')
expect(contextEvent?.type === 'user/message' && contextEvent.data.source)
.toEqual({ kind: 'plugin', plugin: 'workspace-context' })
.toEqual({ kind: 'plugin', plugin: 'agent-instructions' })
const requestText = JSON.stringify(adapter.requests[0]!.messages)
expect(requestText).toContain('Additional instructions from: pkg/AGENTS.md')
expect(requestText).not.toContain('<context source=')
@@ -1400,10 +1400,10 @@ describe('agent loop', () => {
it('creates agents from config on startup', async () => {
const adapter = new MockAdapter([textResponse('from config')])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, {
agents: [{ id: SessionId('config-agent'), provider: 'mock', model: 'mock' }],
@@ -1424,10 +1424,10 @@ describe('agent loop', () => {
it('attaches config agent cwd to the fresh session header', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, {
agents: [{ id: SessionId('config-agent'), provider: 'mock', model: 'mock', cwd: '/work/project' }],

View File

@@ -11,12 +11,12 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmService from '@deepseek-ai/dsh-llm'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import { createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
@@ -37,10 +37,10 @@ class EchoAdapter extends LlmAdapter {
async function harness() {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], new EchoAdapter())

View File

@@ -1,10 +1,10 @@
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmService from '@deepseek-ai/dsh-llm'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
@@ -39,10 +39,10 @@ afterEach(async () => {
async function loopHarness(): Promise<Context> {
const created = new Context()
await created.plugin(LlmService)
await created.plugin(LlmRuntime)
await created.plugin(SessionStore)
await created.plugin(SystemPrompt, { persona: SYSTEM })
await created.plugin(ToolRegistry)
await created.plugin(ToolRuntime)
await created.plugin(AgentRegistry)
await created.plugin(AgentLoop, { agents: [] })
await created.plugin(LlmDeepSeek)

View File

@@ -2,19 +2,19 @@ import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import LlmService, { createUserMessage, LlmError } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { createUserMessage, LlmError } from '@deepseek-ai/dsh-llm'
import type { LlmFailure, ResolvedRetryPolicy } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import { MockAdapter, textResponse } from './mock-adapter.ts'
async function harness(adapter: MockAdapter): Promise<Context> {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)

View File

@@ -7,11 +7,11 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmService, { createUserMessage, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import LlmRuntime, { createUserMessage, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, LlmModelReasoningInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId, foldRequestHeader } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
@@ -26,10 +26,10 @@ async function harnessRoutes(
persona = 'stable base',
) {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { persona })
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
for (const [provider, adapter] of adapters) ctx.llm.registerAdapter([provider], adapter)
@@ -253,10 +253,10 @@ describe('request stability across the loop', () => {
it('keeps exact-model resolution, request logging, and dispatch on one adapter registration', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { persona: 'stable base' })
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
const started = Promise.withResolvers<undefined>()
@@ -371,10 +371,10 @@ describe('request stability across the loop', () => {
it('lets a short-circuiting llm/stream listener own an unregistered route', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { persona: 'stable base' })
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
let observed: GenerateOptions | undefined

View File

@@ -4,14 +4,14 @@ import { Context } from '@deepseek-ai/cordis'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import LlmService from '@deepseek-ai/dsh-llm'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import SessionStore, { SESSION_FORMAT_VERSION, Session, SessionId, SessionPreparation } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -26,13 +26,13 @@ async function persistentHarness(adapter: MockAdapter): Promise<{ ctx: Context;
async function mountPersistentHarness(root: string, adapter: MockAdapter): Promise<Context> {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SessionPersistenceJsonl, { root })
await ctx.plugin(JsonlSessionPersistence, { root })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx
}
@@ -73,11 +73,11 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
}
/** Fail a lifecycle regression promptly instead of waiting for Vitest's suite timeout. */
async function promptly<T>(task: Promise<T>): Promise<T> {
async function promptly<T>(job: Promise<T>): Promise<T> {
const timeout = Promise.withResolvers<never>()
const timer = setTimeout(() => { timeout.reject(new Error('lifecycle task did not settle promptly')) }, 1000)
try {
return await Promise.race([task, timeout.promise])
return await Promise.race([job, timeout.promise])
} finally {
clearTimeout(timer)
}
@@ -242,13 +242,13 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
// Lifecycle 2: resume it; the header cwd stays undefined (no-cwd branch).
const adapter2 = new MockAdapter([textResponse('b')])
const ctx2 = new Context()
await ctx2.plugin(LlmService)
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentLoop, { agents: [] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
await ctx2.plugin(JsonlSessionPersistence, { root })
ctx2.llm.registerAdapter(['mock'], adapter2)
const a2 = (await ctx2.agents.resume({ resumeSessionId: SessionId('nocwd-sess') })).agent
expect(a2.session.header.cwd).toBeUndefined()
@@ -270,13 +270,13 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
// Lifecycle 2: resuming the persisted session emits session-start 'resume'.
const adapter2 = new MockAdapter([textResponse('b')])
const ctx2 = new Context()
await ctx2.plugin(LlmService)
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentLoop, { agents: [] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
await ctx2.plugin(JsonlSessionPersistence, { root })
ctx2.llm.registerAdapter(['mock'], adapter2)
const sources2: string[] = []
ctx2.on('agent/session-start', ({ source }) => void sources2.push(source))
@@ -520,13 +520,13 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
const sessionId = SessionId('resume-load-factory-unload')
const root = await persistSession(sessionId)
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
const loopFiber = await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SessionPersistenceJsonl, { root })
await ctx.plugin(JsonlSessionPersistence, { root })
ctx.llm.registerAdapter(['mock'], new MockAdapter([textResponse('next')]))
const snapshot = await ctx.sessionPersistence.load(sessionId)
@@ -583,13 +583,13 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
// boundary).
const adapter2 = new MockAdapter([textResponse('b')])
const ctx2 = new Context()
await ctx2.plugin(LlmService)
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentLoop, { agents: [] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
await ctx2.plugin(JsonlSessionPersistence, { root })
ctx2.llm.registerAdapter(['mock'], adapter2)
const a2 = (await ctx2.agents.resume({ resumeSessionId: SessionId('forked-sess') })).agent
expect(a2.session.header.parentSession).toBe('parent-sess')
@@ -607,7 +607,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
const a1 = (await ctx1.agents.create({ sessionId: SessionId('inject-sess'), meta: { cwd: '/w' } })).agent
a1.followup(createUserMessage({ content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }))
await waitForIdle(ctx1, a1)
a1.inject(createUserMessage({ content: [{ type: 'text', text: 'background task 42 finished' }], source: { kind: 'plugin', plugin: 'tool-bash' } }))
a1.inject(createUserMessage({ content: [{ type: 'text', text: 'background job 42 finished' }], source: { kind: 'plugin', plugin: 'tool-bash' } }))
await a1.whenIdle()
await ctx1.sessions.flush(a1.session)
@@ -615,23 +615,23 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
// model-visible when the next turn admits it.
const adapter2 = new MockAdapter([textResponse('next')])
const ctx2 = new Context()
await ctx2.plugin(LlmService)
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentLoop, { agents: [] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
await ctx2.plugin(JsonlSessionPersistence, { root })
ctx2.llm.registerAdapter(['mock'], adapter2)
const loaded = await ctx2.sessionPersistence.load(SessionId('inject-sess'))
expect(loaded.events.some(event => event.type === 'agent/inbox/spliced')).toBe(true)
expect(JSON.stringify(loaded.events)).toContain('background task 42 finished')
expect(JSON.stringify(loaded.events)).toContain('background job 42 finished')
const a2 = (await ctx2.agents.resume({ resumeSessionId: SessionId('inject-sess') })).agent
expect(JSON.stringify(a2.inbox.nextStep)).toContain('background task 42 finished')
expect(JSON.stringify(a2.inbox.nextStep)).toContain('background job 42 finished')
a2.followup(createUserMessage({ content: [{ type: 'text', text: 'continue' }], source: { kind: 'user' } }))
await waitForIdle(ctx2, a2)
const flat = JSON.stringify(a2.session.deriveMessages())
expect(flat).toContain('background task 42 finished')
expect(flat).toContain('background job 42 finished')
await ctx2.fiber.dispose()
await ctx1.fiber.dispose()
})
@@ -651,13 +651,13 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
// Lifecycle 2: a brand-new context over the SAME root; resume the session.
const adapter2 = new MockAdapter([textResponse('second answer')])
const ctx2 = new Context()
await ctx2.plugin(LlmService)
await ctx2.plugin(LlmRuntime)
await ctx2.plugin(SessionStore)
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(ToolRuntime)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentLoop, { agents: [] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
await ctx2.plugin(JsonlSessionPersistence, { root })
ctx2.llm.registerAdapter(['mock'], adapter2)
const a2 = (await ctx2.agents.resume({ resumeSessionId: SessionId('sess-resume') })).agent
@@ -684,10 +684,10 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
// A harness WITHOUT the persistence plugin.
const adapter = new MockAdapter([textResponse('x')])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
@@ -873,12 +873,12 @@ describe('configured-start failure edges', () => {
ctx.sessionPersistence.prepare = () => Promise.reject(new Error('artifact corrupt'))
const configured = new Context()
await configured.plugin(LlmService)
await configured.plugin(LlmRuntime)
await configured.plugin(SessionStore)
await configured.plugin(SystemPrompt)
await configured.plugin(ToolRegistry)
await configured.plugin(ToolRuntime)
await configured.plugin(AgentRegistry)
await configured.plugin(SessionPersistenceJsonl, { root })
await configured.plugin(JsonlSessionPersistence, { root })
configured.llm.registerAdapter(['mock'], new MockAdapter([]))
configured.sessionPersistence.prepare = (id, signal) => ctx.sessionPersistence.prepare(id, signal)
const configFailures: unknown[] = []
@@ -918,12 +918,12 @@ describe('configured-start failure edges', () => {
ctx.on('agent-loop/config-start-failed', ({ error }) => { failures.push(error) })
const configured = new Context()
await configured.plugin(LlmService)
await configured.plugin(LlmRuntime)
await configured.plugin(SessionStore)
await configured.plugin(SystemPrompt)
await configured.plugin(ToolRegistry)
await configured.plugin(ToolRuntime)
await configured.plugin(AgentRegistry)
await configured.plugin(SessionPersistenceJsonl, { root })
await configured.plugin(JsonlSessionPersistence, { root })
configured.llm.registerAdapter(['mock'], new MockAdapter([]))
configured.sessionPersistence.prepare = (id, signal) => ctx.sessionPersistence.prepare(id, signal)
configured.on('agent-loop/config-start-failed', ({ error }) => { failures.push(error) })

View File

@@ -1,10 +1,10 @@
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { describe, expect, it } from 'vitest'
import { Context, symbols, type EffectMeta, type Fiber } from '@deepseek-ai/cordis'
import LlmService from '@deepseek-ai/dsh-llm'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { agentEvents, assembleContextFor } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
@@ -15,10 +15,10 @@ import { MockAdapter, textResponse } from './mock-adapter.ts'
async function harnessWithLoop(adapter: MockAdapter = new MockAdapter([textResponse('ok')])): Promise<{ ctx: Context; loopFiber: Fiber }> {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { persona: 'You are the deployment.' })
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
const loopFiber = await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
@@ -1052,7 +1052,7 @@ describe('agent scope lifecycle', () => {
})
it('drains a run re-entered by cancel\'s own idle transition before removing the scope', async () => {
// Automation shaped like goal-session: the running→idle transition that
// Automation shaped like goal-round-driver: the running→idle transition that
// disposal's cancel produces immediately queues a follow-up prompt. The
// teardown must drain that replacement run to true quiescence instead of
// awaiting only the first captured done and unwinding under a live run.

View File

@@ -3,17 +3,17 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import type { Fiber } from '@deepseek-ai/cordis'
import LlmService from '@deepseek-ai/dsh-llm'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import { Settings } from '@deepseek-ai/dsh-settings'
import { SettingsProvider } from '@deepseek-ai/dsh-settings'
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'
import AgentLoop, { AGENT_LOOP_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-loop'
/** The smallest real provider: one in-memory document, always writable. */
class MemorySettings extends Settings {
class MemorySettings extends SettingsProvider {
doc: Record<string, unknown> = {}
get writable(): boolean {
@@ -32,10 +32,10 @@ class MemorySettings extends Settings {
async function boot(): Promise<{ ctx: Context; settingsFiber: Fiber; loopFiber: Fiber }> {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
const settingsFiber = ctx.plugin(MemorySettings)
await settingsFiber.await()

View File

@@ -8,8 +8,8 @@ import { Context } from '@deepseek-ai/cordis'
import { createUserMessage, CallId, StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import LlmService from '@deepseek-ai/dsh-llm'
import ToolRegistry, { defineContentToolFixture, TOOL_ABORTED_BEFORE_DISPATCH, TOOL_REGISTRY_SCHEDULER, type PostToolDecision, type PreToolDecision } from '@deepseek-ai/dsh-tools'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import ToolRuntime, { defineContentToolFixture, TOOL_ABORTED_BEFORE_DISPATCH, TOOL_RUNTIME_SCHEDULER, type PostToolDecision, type PreToolDecision } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop, { DEFAULT_MAX_PARALLEL_TOOL_CALLS } from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -18,10 +18,10 @@ import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtim
async function harness(adapter: MockAdapter, maxParallelToolCalls?: number) {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { persona: '' })
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, {
agents: [],
@@ -276,10 +276,10 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () =>
it('defaults the cap when direct construction bypasses the config schema', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { persona: '' })
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
const loop = new AgentLoop(ctx, { agents: [] })
@@ -344,10 +344,10 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () =>
textResponse('done'),
])
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { persona: '' })
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [], maxParallelToolCalls: 1 })
ctx.llm.registerAdapter(['mock'], adapter)
@@ -644,7 +644,7 @@ describe('tool-call scheduler: failure quiescence', () => {
ctx.tools.register(gated.tool)
// The registry contains expected failures as results; replace its internal
// view only to inject the invariant violation this boundary must contain.
const scheduler = ctx.tools[TOOL_REGISTRY_SCHEDULER]
const scheduler = ctx.tools[TOOL_RUNTIME_SCHEDULER]
const prepare = scheduler.prepare.bind(scheduler)
const dispatch = scheduler.dispatch.bind(scheduler)
const prepareGate = Promise.withResolvers<undefined>()
@@ -702,10 +702,10 @@ describe('code-mode native-tool denial through the agent loop', () => {
async function codeModeHarness(adapter: MockAdapter) {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { persona: '' })
await ctx.plugin(ToolRegistry, { mode: 'code' })
await ctx.plugin(ToolRuntime, { mode: 'code' })
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- FakeCodeRuntime is an internal test helper with an opaque type shape
await ctx.plugin(FakeCodeRuntime as any)
await ctx.plugin(AgentRegistry)

View File

@@ -9,11 +9,11 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmService from '@deepseek-ai/dsh-llm'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, foldRequestHeader } from '@deepseek-ai/dsh-session'
import SystemPrompt, { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
import type { Config as SystemPromptConfig } from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
@@ -21,10 +21,10 @@ import { MockAdapter, textResponse } from './mock-adapter.ts'
async function harness(adapter: MockAdapter, toolOrder?: SystemPromptConfig['toolOrder']) {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { persona: 'stable base', ...toolOrder !== undefined ? { toolOrder } : {} })
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)

View File

@@ -42,7 +42,7 @@
"path": "../../settings/settings"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -1,6 +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-tool-mode/README.md
README.md: f5f2df21285411dbb3b8c5cac18cc3ab0dc8a22b
README.zh.md: 6246892981bb4dc49cf7a6b99f06b4daf0cb3f3e
# pnpm run verify-translation-pairing --write packages/core/agent-tool-presentation/README.md
README.md: a4747d4d95a732f4eccb81ed44b68c961895d773
README.zh.md: 33b33c63cd61893ea68bd2ab5d7242f8cf7d7c27

View File

@@ -1,4 +1,4 @@
# dsh-agent-tool-mode
# dsh-agent-tool-presentation
English | [中文](README.zh.md)
@@ -12,7 +12,7 @@ What a preset can own is the **presentation** of that registry. `ctx.tools.prese
## What it does
`native` applies immediately. A code mode instead waits for `ctx.codeRuntime`, which is a host-plane service ([`dsh-code-runtime-worker`](../../code-runtime/code-runtime-worker/README.md)): a preset selecting Code Mode against a deployment composing no runtime then holds this row pending, and `dsh-agent-presets` refuses the mount naming this id. The alternative — applying optimistically — moves the failure to the session's first request, where the operator can act on neither the preset nor the composition.
`native` applies immediately. A code mode instead waits for `ctx.codeRuntime`, which is a host-plane service ([`dsh-code-runtime-worker-thread`](../../code-runtime/code-runtime-worker-thread/README.md)): a preset selecting Code Mode against a deployment composing no runtime then holds this row pending, and `dsh-agent-presets` refuses the mount naming this id. The alternative — applying optimistically — moves the failure to the session's first request, where the operator can act on neither the preset nor the composition.
`mode` is required rather than defaulted, because a preset without this row already gets the deployment default; an omitted value would mean the row was composed for nothing.

View File

@@ -1,4 +1,4 @@
# dsh-agent-tool-mode
# dsh-agent-tool-presentation
[English](README.md) | 中文
@@ -12,7 +12,7 @@ preset 能拥有的是这份注册表的**呈现方式**。`ctx.tools.presentAs(
## 它做什么
`native` 立即生效。code 类模式则等待 `ctx.codeRuntime`——这是一个宿主平面服务([`dsh-code-runtime-worker`](../../code-runtime/code-runtime-worker/README.md)):若某个 preset 在未组装运行时的部署上选择 Code Mode本行就停在 pending`dsh-agent-presets` 会指名此 id 拒绝挂载。另一种做法——先乐观应用——会把失败推迟到该会话的第一次请求,那时操作者对 preset 和组装都已无从下手。
`native` 立即生效。code 类模式则等待 `ctx.codeRuntime`——这是一个宿主平面服务([`dsh-code-runtime-worker-thread`](../../code-runtime/code-runtime-worker-thread/README.md)):若某个 preset 在未组装运行时的部署上选择 Code Mode本行就停在 pending`dsh-agent-presets` 会指名此 id 拒绝挂载。另一种做法——先乐观应用——会把失败推迟到该会话的第一次请求,那时操作者对 preset 和组装都已无从下手。
`mode` 是必填而非有默认值:不带这一行的 preset 本来就会拿到部署默认值,省略它等于这一行白组装了。

View File

@@ -1,14 +1,14 @@
{
"name": "@deepseek-ai/dsh-agent-tool-mode",
"name": "@deepseek-ai/dsh-agent-tool-presentation",
"description": "Agent-plane presentation selector: composes one agent's tools as Code Mode, native, or both",
"version": "0.0.1-rc.2",
"version": "0.1.0-rc.6",
"publishConfig": {
"access": "restricted"
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/core/agent-tool-mode"
"directory": "packages/core/agent-tool-presentation"
},
"type": "module",
"main": "lib/index.js",
@@ -30,7 +30,7 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"dependencies": {
"@deepseek-ai/schemastery": "workspace:^"
},

View File

@@ -11,11 +11,11 @@
* process. One row per composition, not one per session.
*
* A code mode needs a TypeScript code runtime, which is a host-plane service
* ([`dsh-code-runtime-worker`](../../code-runtime/code-runtime-worker/README.md)).
* ([`dsh-code-runtime-worker-thread`](../../code-runtime/code-runtime-worker/README.md)).
* This row therefore waits for it rather than assuming it: a preset selecting
* Code Mode against a deployment that composes no runtime fails at mount, named
* in the preset's own activation audit, instead of at the first prompt.
* @module @deepseek-ai/dsh-agent-tool-mode
* @module @deepseek-ai/dsh-agent-tool-presentation
*/
import type { Context } from '@deepseek-ai/cordis'
@@ -25,7 +25,7 @@ import type { ToolPresentationMode } from '@deepseek-ai/dsh-tools'
import type {} from '@deepseek-ai/dsh-tools'
/** Cordis plugin name. */
export const name = 'tool-mode'
export const name = 'tool-presentation'
/**
* Required services. `codeRuntime` is NOT listed: a `native` row must mount in

View File

@@ -1,16 +1,16 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-agent-tool-mode`.
* @module @deepseek-ai/dsh-agent-tool-mode/invariant
* Package-owned invariant companion for `@deepseek-ai/dsh-agent-tool-presentation`.
* @module @deepseek-ai/dsh-agent-tool-presentation/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-agent-tool-mode'
const PACKAGE_NAME = '@deepseek-ai/dsh-agent-tool-presentation'
/** Cordis companion plugin name. */
export const name = 'tool-mode-invariant'
export const name = 'tool-presentation-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']

View File

@@ -12,10 +12,10 @@ import { createScope } from '@deepseek-ai/dsh-scope'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
import ToolRegistry, { RUN_CODE_NAME, defineTool } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { RUN_CODE_NAME, defineTool } from '@deepseek-ai/dsh-tools'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import { apply, Config, inject, name } from '@deepseek-ai/dsh-agent-tool-mode'
import { apply, Config, inject, name } from '@deepseek-ai/dsh-agent-tool-presentation'
/** A runtime that never runs anything: presentation never dispatches. */
class StubRuntime extends CodeRuntime {
@@ -31,7 +31,7 @@ class StubRuntime extends CodeRuntime {
async function host(options: { runtime?: boolean } = {}) {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
await ctx.plugin(ToolRegistry, {})
await ctx.plugin(ToolRuntime, {})
if (options.runtime !== false) await ctx.plugin(StubRuntime)
ctx.tools.register(defineTool({
name: 'echo',
@@ -56,7 +56,7 @@ async function mount(ctx: Context, config: Config, id = 'agent') {
return { agent, fiber, row }
}
describe('the tool-mode row', () => {
describe('the tool-presentation row', () => {
it('declares the services it uses without holding a code runtime hostage', () => {
// A `native` row must mount where no runtime is composed, so the wait is
// conditional inside apply rather than static metadata.

View File

@@ -21,7 +21,7 @@
"path": "../../core/tools"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -1,9 +1,9 @@
{
"name": "@deepseek-ai/dsh-agent",
"description": "Agent interface, registry, initiator scope, and event vocabulary for the DeepSeek Harness",
"version": "0.0.1-rc.2",
"version": "0.1.0-rc.6",
"publishConfig": {
"access": "restricted"
"access": "public"
},
"repository": {
"type": "git",
@@ -35,14 +35,14 @@
"lib/types/**/*.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-scope": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-type-meta": "workspace:^",
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
@@ -51,7 +51,7 @@
"@deepseek-ai/dsh-scope": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-type-meta": "workspace:^",
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
"@deepseek-ai/dsh-typert-registry": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}

View File

@@ -12,7 +12,7 @@ import { isPromise } from 'node:util/types'
import { scopeTarget } from '@deepseek-ai/dsh-scope'
import type { Scoped } from '@deepseek-ai/dsh-scope'
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
import type { TypeRTContext, TypeRTLookup } from '@deepseek-ai/dsh-type-meta'
import type { TypertContext, TypertLookup } from '@deepseek-ai/dsh-typert-protocol'
import type { Agent, AgentOptions } from './runtime-types.ts'
export * from './runtime-types.ts'
@@ -23,13 +23,13 @@ export * from './model-selection.ts'
export { agentCarrier, agentEvents, assembleContextFor, emitAgentEvent } from './dispatch.ts'
export type { AgentEventDispatch, AgentSubjectEvent } from './dispatch.ts'
declare module '@deepseek-ai/dsh-type-meta' {
interface TypeRTLookupMap {
agent: TypeRTLookup<Agent, SessionId>
declare module '@deepseek-ai/dsh-typert-protocol' {
interface TypertLookupMap {
agent: TypertLookup<Agent, SessionId>
}
interface TypeRTContextMap {
agent: TypeRTContext<SessionId>
interface TypertContextMap {
agent: TypertContext<SessionId>
}
}

View File

@@ -143,7 +143,7 @@ describe('Inbox', () => {
})
describe('AgentRegistry', () => {
it('contributes Agent lookup and scoped Context providers while TypeRT is live', async () => {
it('contributes Agent lookup and scoped Context providers while Typert is live', async () => {
const ctx = new Context()
const agentFiber = ctx.plugin(AgentRegistry)
await agentFiber

View File

@@ -3,11 +3,11 @@ import { Context } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
import { scopeTarget } from '@deepseek-ai/dsh-scope'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
async function setup(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(AgentInvariant)
return ctx
}

View File

@@ -27,10 +27,10 @@
"path": "../../core/system-prompt"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
},
{
"path": "../../typert/type-meta"
"path": "../../typert/protocol"
}
]
}

View File

@@ -1,9 +1,9 @@
{
"name": "@deepseek-ai/dsh-scope",
"description": "Scoped-context registration primitive (scope tags, scope-filtered event dispatch) for the DeepSeek Harness",
"version": "0.0.1-rc.2",
"version": "0.1.0-rc.6",
"publishConfig": {
"access": "restricted"
"access": "public"
},
"repository": {
"type": "git",
@@ -30,7 +30,7 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"

View File

@@ -5,11 +5,11 @@ import type { Events } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { scopeTarget } from '@deepseek-ai/dsh-scope'
import * as ScopeInvariant from '@deepseek-ai/dsh-scope/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
async function setup(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(ScopeInvariant)
return ctx
}

View File

@@ -15,7 +15,7 @@
"path": "../../../vendor/cordis"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

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: 57569e9c0dbfa7cb696e3a561a9ff108c2ac981f
README.zh.md: d255e6fa48ef23b89c4d89cd3a0bf260d2858fc6
README.md: 02aae21d71101e626bf2d2ead0bfd5d155723165
README.zh.md: 2da8c5db8fe25d55faafdd6d44ea9090dd232576

View File

@@ -70,7 +70,7 @@ A `user/message` stores the complete `UserMessage` directly, including the ident
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.
Merge-extensible via `SessionEventMap` — a plugin declaration-merges its own types (the compaction seam's `compaction/*`, 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.
Also defines `TurnEndReasonMap`, the merge-extensible `kind`-tagged sum type for turn endings. `turn/start` carries only the turn number; the following entered `user/message` batch records its input, while `llm/retry` records request recovery.
@@ -90,7 +90,7 @@ Every `SessionEvent` carries three optional top-level fields (structural metadat
- 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`.
- Compaction: `dsh-compaction-basic` appends a `user/message` replacement for summary checkpoints, while `dsh-compaction-tool-result-pruner` appends a content-only `tool/result` replacement. Tool-pairing boundary policy and its cache belong to the [`dsh-compaction` seam](../../compaction/compaction/README.md), while this package owns ordered surface membership, replacement validation, and `replaceGeneration`.
## Model Experience

View File

@@ -70,7 +70,7 @@
生成的[持久化日志事件目录](../../../docs/persistence-catalog.md)逐成员列举仅追加日志的事件类型、载荷、surface 标记与声明位置。Token 记账读取每个步骤的 `assistant/chunk { type: 'usage' }` 记录;如果没有用量分片,则将 `assistant/message.usage` 作为已提交步骤的后备。失败的模型请求尝试没有 assistant 消息。每条 `assistant/message` 都会记录提供方、模型和可选回放状态。
`SessionEventMap` 可通过合并扩展:插件使用声明合并添加自身类型(压缩 seam 的 `compact/*`、有界恢复的非 surface `llm/retry`、钩子桥接层的 `hook/*`);合并成员会出现在同一目录中。插件拥有其合并事件的关系不变量,包括是否允许纯日志事件出现在轮次之间。需要持久性的生产方通过 `Session` 追加,再等待 `ctx.sessions.flush(session)`,无需虚构一个执行轮次。
`SessionEventMap` 可通过合并扩展:插件使用声明合并添加自身类型(压缩 seam 的 `compaction/*`、有界恢复的非 surface `llm/retry`、钩子桥接层的 `hook/*`);合并成员会出现在同一目录中。插件拥有其合并事件的关系不变量,包括是否允许纯日志事件出现在轮次之间。需要持久性的生产方通过 `Session` 追加,再等待 `ctx.sessions.flush(session)`,无需虚构一个执行轮次。
此包还定义 `TurnEndReasonMap`,即用于轮次结束、可合并扩展且以 `kind` 为标签的和类型。`turn/start` 只携带轮次编号;随后已进入的 `user/message` 批次记录其输入,`llm/retry` 则记录请求恢复。
@@ -90,7 +90,7 @@
- 持久化插件:订阅 `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`
- 压缩:`dsh-compaction-basic` 为摘要检查点追加一个替换用 `user/message`,而 `dsh-compaction-tool-result-pruner` 追加仅修改内容的 `tool/result` 替换。工具配对边界策略及其缓存归 [`dsh-compaction` seam](../../compaction/compaction/README.md) 所有;此包拥有有序 surface 成员关系、替换校验与 `replaceGeneration`
## 模型体验

View File

@@ -1,9 +1,9 @@
{
"name": "@deepseek-ai/dsh-session",
"description": "Event-sourced session store for the DeepSeek Harness",
"version": "0.0.1-rc.2",
"version": "0.1.0-rc.6",
"publishConfig": {
"access": "restricted"
"access": "public"
},
"repository": {
"type": "git",
@@ -39,13 +39,13 @@
"lib/types/**/*.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-brand": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-scope": "workspace:^",
"@deepseek-ai/dsh-type-meta": "workspace:^",
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
@@ -53,7 +53,7 @@
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-scope": "workspace:^",
"@deepseek-ai/dsh-type-meta": "workspace:^",
"@deepseek-ai/dsh-typert-protocol": "workspace:^",
"@deepseek-ai/dsh-typert-registry": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}

View File

@@ -13,7 +13,7 @@ import { scopeOf, scopeTarget } from '@deepseek-ai/dsh-scope'
import type { Scoped } from '@deepseek-ai/dsh-scope'
import type { Message } from '@deepseek-ai/dsh-llm'
import { SESSION_FORMAT_VERSION, SessionId } from './types.ts'
import type { TypeRTLookup } from '@deepseek-ai/dsh-type-meta'
import type { TypertLookup } from '@deepseek-ai/dsh-typert-protocol'
import type { CreateSessionOptions, EpochHeader, PrepareSessionOptions, RequestContext, SessionEvent, SessionEventMap, SessionEventType, SessionHeader, SurfaceIntent, SurfaceEventType } from './types.ts'
import { snapshotJsonValue } from './json.ts'
import { deriveEventMessage, SurfaceManager } from './surface.ts'
@@ -26,7 +26,7 @@ export type { SessionPreparationOptions } from './preparation.ts'
export type { AssistantMessage, ToolResultMessage, UserMessage } from '@deepseek-ai/dsh-llm'
export { isJsonValue, snapshotJsonValue } from './json.ts'
export type { JsonValue } from './json.ts'
export { interruptedTurnClosers, lastActivityTime, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from './repair.ts'
export { interruptedTurnClosers, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from './repair.ts'
export { decodeStorageRecord, packChunkRuns } from './chunk-rows.ts'
export type { ChunkRow, StorageRecord } from './chunk-rows.ts'
export type { SessionSurface, SurfaceFoldReplacement, SurfaceFoldResult } from './surface.ts'
@@ -86,9 +86,9 @@ declare module '@deepseek-ai/cordis' {
}
}
declare module '@deepseek-ai/dsh-type-meta' {
interface TypeRTLookupMap {
session: TypeRTLookup<Session, SessionId>
declare module '@deepseek-ai/dsh-typert-protocol' {
interface TypertLookupMap {
session: TypertLookup<Session, SessionId>
}
}
@@ -1010,7 +1010,7 @@ export class SessionStore extends Service {
* Dispatch the awaited `session/flush` durability checkpoint for `session`,
* with the carrier captured at {@link enter}. THE flush entry point: the
* store owns the carrier, so callers (the checkpoint policy's per-request
* barrier, goal-session's idle checkpoint, teardown drains, and consumers
* barrier, goal-round-driver's idle checkpoint, teardown drains, and consumers
* that flush themselves before reading storage) must come through here
* rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner,
* one spelling, and the scoped-dispatch invariant can pin it.

View File

@@ -26,10 +26,10 @@ export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet<string> = new Set([
'assistant/message',
'command/done',
'command/run',
'compact/end',
'compact/prune',
'compact/start',
'compact/summary',
'compaction/end',
'compaction/prune',
'compaction/start',
'compaction/summary',
'feedback/record',
'goal/change',
'hook/invoked',

View File

@@ -1,10 +1,7 @@
/**
* Crash-recovery repair for an interrupted session log. It preserves a fully
* written final turn and supplies the missing tool, step, and turn boundaries
* needed to resume with a provider-valid transcript, plus the activity-time
* read that must skip the end-seed boundary — which this module does
* not write (`Session`'s constructor does) but whose synthetic closers can
* inherit that boundary's timestamp, the one real coupling between the two.
* needed to resume with a provider-valid transcript.
* @module @deepseek-ai/dsh-session/repair
*/
@@ -12,22 +9,6 @@ import { MessageId, freezeMessage, type CallId } from '@deepseek-ai/dsh-llm'
import type { ToolResultMessage } from '@deepseek-ai/dsh-llm'
import type { SessionEvent } from './types.ts'
/**
* The `time` of the log's last event representing actual work, skipping the
* `session/end-seed` boundary — picking a session up is not activity, so
* activity ordering must exclude it.
*
* Excluded by type, so a pickup time still leaks when a boundary is the last
* event of an open turn: {@link interruptedTurnClosers} copies it onto the
* synthetic `turn/end`, which this counts as work. Reachable only by seeding an
* unbalanced log directly — `load()` balances first.
* @param events - the log to scan, in seq order.
* @returns the latest non-boundary event's `time`, or undefined when there is none.
*/
export function lastActivityTime(events: readonly SessionEvent[]): number | undefined {
return events.findLast(event => event.type !== 'session/end-seed')?.time
}
/** Recovery code for an assistant tool request that never reached a recorded call start. */
export const TOOL_NOT_STARTED = 'TOOL_NOT_STARTED'

View File

@@ -88,7 +88,7 @@ export function deriveEventMessage(event: SessionEvent): Message | null {
// Ordinary prompts and injected context project in user role: the event's
// model-facing content stays verbatim. Do NOT re-add per-type framing
// (e.g. `<context>`) here: framing is caller-owned — a producer bakes it
// into `content`, as workspace-context does with `<system-reminder>` — or,
// into `content`, as agent-instructions does with `<system-reminder>` — or,
// if reintroduced, must be driven by the event `meta` map and a dedicated
// renderer, keeping this projection a verbatim pass-through. See the
// deferred design note in

View File

@@ -322,8 +322,8 @@ export interface SessionEventMap {
* companion deliberately constrains nothing here, so a plugin appending one
* would silently classify every live bracket before it as seed history.
*
* An owner of a standalone open/close bracket (`compact/start` …
* `compact/end`) reads it because seed history and live work are otherwise
* An owner of a standalone open/close bracket (`compaction/start` …
* `compaction/end`) reads it because seed history and live work are otherwise
* byte-identical: an unmatched opening marker before this event belongs to
* an ended lifecycle, whatever ended it. NOT a liveness signal about other
* writers — a concurrently live session holds its own boundary elsewhere,

View File

@@ -7,7 +7,7 @@ import type { SessionEvent, TurnEndReason } from '@deepseek-ai/dsh-session'
declare module '@deepseek-ai/dsh-session/types' {
interface SessionEventMap {
'test/log-only': { value: string }
/** Stands in for a plugin's open/close bracket (`compact/start`). */
/** Stands in for a plugin's open/close bracket (`compaction/start`). */
'test/bracket-open': { id: string }
}
}

View File

@@ -4,12 +4,12 @@ import { createScope, scopeTarget } from '@deepseek-ai/dsh-scope'
import { createUserMessage, CallId, createMessage, createToolResultMessage, freezeMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, TOOL_NOT_STARTED } from '@deepseek-ai/dsh-session'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import InvariantService, { InvariantError } from '@deepseek-ai/dsh-invariants'
import InvariantRegistry, { InvariantError } from '@deepseek-ai/dsh-invariants'
async function setup(): Promise<{ ctx: Context; fiber: Awaited<ReturnType<Context['plugin']>> }> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
const fiber = await ctx.plugin(SessionInvariant)
return { ctx, fiber }
}
@@ -18,7 +18,7 @@ describe('session-log invariants', () => {
it('keeps registration global when the companion is mounted under a scope', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
let scopedCtx!: Context
await ctx.plugin(Object.assign((inner: Context) => {
scopedCtx = createScope(inner, {}).ctx

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { CallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm'
import { interruptedTurnClosers, lastActivityTime, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from '../src/index.ts'
import { interruptedTurnClosers, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from '../src/index.ts'
import type { SessionEvent, SurfaceEvent } from '../src/index.ts'
/**
@@ -273,44 +273,3 @@ describe('interruptedTurnClosers', () => {
expect(closers.map(e => e.type)).toEqual(['step/end', 'turn/end'])
})
})
describe('lastActivityTime', () => {
const endSeedAt = (seq: number, time: number): SessionEvent =>
({ type: 'session/end-seed', seq, time, data: {} })
it('has no answer for an empty log', () => {
expect(lastActivityTime([])).toBeUndefined()
})
it('reports the log tail when no boundary is present', () => {
const events: SessionEvent[] = [
userTurnStart(1, 0),
{ type: 'turn/end', seq: 1, time: 500, data: { turn: 1, reason: { kind: 'completed' } } },
]
expect(lastActivityTime(events)).toBe(500)
})
it('skips a trailing boundary in favour of the last real work', () => {
const events: SessionEvent[] = [
userTurnStart(1, 0),
{ type: 'turn/end', seq: 1, time: 500, data: { turn: 1, reason: { kind: 'completed' } } },
endSeedAt(2, 9_000),
]
// Resumed long after the work, but never worked in again.
expect(lastActivityTime(events)).toBe(500)
})
it('reports work appended after end-seed', () => {
const events: SessionEvent[] = [
userTurnStart(1, 0),
endSeedAt(1, 9_000),
{ type: 'turn/end', seq: 2, time: 9_500, data: { turn: 1, reason: { kind: 'completed' } } },
]
expect(lastActivityTime(events)).toBe(9_500)
})
it('has no answer for a log of nothing but boundaries', () => {
// Unreachable via the constructor, but the projection is a pure function.
expect(lastActivityTime([endSeedAt(0, 1), endSeedAt(1, 2)])).toBeUndefined()
})
})

View File

@@ -104,13 +104,13 @@ describe('Session', () => {
const session = Session.create(SessionId('s2-raw'))
const message = createUserMessage({
content: [{ type: 'text', text: '<system-reminder>Additional instructions from: pkg/AGENTS.md</system-reminder>' }],
source: { kind: 'plugin', plugin: 'workspace-context' },
source: { kind: 'plugin', plugin: 'agent-instructions' },
})
session.append('user/message', message, { surfaceOp: 'append' })
expect(session.deriveMessages()).toEqual([message])
const event = session.events[0]
expect(event?.type === 'user/message' && event.data.source).toEqual({ kind: 'plugin', plugin: 'workspace-context' })
expect(event?.type === 'user/message' && event.data.source).toEqual({ kind: 'plugin', plugin: 'agent-instructions' })
})
it('replays identically from a seeded event log', () => {

View File

@@ -3,7 +3,7 @@ import { Context } from '@deepseek-ai/cordis'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import TypertRegistry from '@deepseek-ai/dsh-typert-registry'
describe('Session TypeRT provider', () => {
describe('Session Typert provider', () => {
it('contributes live Session lookup in either service load order', async () => {
const ctx = new Context()
const sessionFiber = ctx.plugin(SessionStore)

View File

@@ -24,10 +24,10 @@
"path": "../../core/scope"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
},
{
"path": "../../typert/type-meta"
"path": "../../typert/protocol"
}
]
}

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: cedda783d549633f5be9765a9a074e968d99500d
README.zh.md: ab3dbffa099549cb1d5cc2713d903038e917e695
README.md: d750a507e628e7609af542227e4528d4d4934ce8
README.zh.md: fcf5ebfad1e467b060b2efaa6c6f2816d75945d1

View File

@@ -8,7 +8,8 @@ System prompt assembly registry. Plugins contribute ordered sections, tool schem
| Key | Default | Meaning |
|---|---|---|
| `includeHarnessIdentity` | `true` | Include the fixed `You are an AI agent powered by the DeepSeek Harness SDK.` order-100 opener. Set false only when a compatibility deployment owns the complete system prompt. |
| `includeHarnessIdentity` | `true` | Include the fixed `You are an AI agent powered by DeepSeek Harness.` order-100 opener. Set false only when a compatibility deployment owns the complete system prompt. |
| `includeRuntimeContext` | `true` | Include ordered dynamic contexts in assembly. When false, context providers are not evaluated and contexts added by `system-prompt/assemble` listeners are discarded after the waterfall; other services and their enforcement remain active. |
| `persona` | `''` | The global deployment-persona default: the ONE config-authored prompt fragment, rendered as the order-0 `deployment:persona` section unless an agent-scoped contribution shadows it. A template — complete `{{…}}` groups are interpreted strictly against the registered variables (the shipped loop registers `{{model}}`/`{{cwd}}`), with no escape syntax for literal braces yet. Empty ⇒ the section is dropped at render. |
| `toolOrder` | — | Explicit model-facing tool order, as a list of `ToolSchema.name`s with one `'<unlisted-tools>'` rest entry (`TOOL_ORDER_REST`): listed tools take their listed position, unlisted tools land at the rest entry in lexicographic name order. Absent ⇒ plain lexicographic name order. Applied to the collected tools BEFORE the `system-prompt/assemble` waterfall — like the sections' `order` sort, it canonicalizes what the registry contributed (registration order is a plugin-load artifact), and a waterfall listener that mutates the list owns the determinism of what it emits. Misconfiguration fails loud: a list without exactly one rest entry, or with duplicates, throws at load; a listed name with no registered tool rejects every `assemble()`; a tool provider returning the reserved rest-entry name also rejects. Under the shipped loop the turn fails before any model request. Why a central list and not per-plugin weights: [Explicit model-facing tool order](../../../.agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md). |
@@ -17,13 +18,15 @@ System prompt assembly registry. Plugins contribute ordered sections, tool schem
### Public API
- `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. A `complete: true` section becomes the exact complete prompt after the assembly waterfall; more than one effective complete section rejects assembly. Duplicate names within one layer and non-finite orders throw. Disposed with the calling fiber.
- `ctx.systemPrompt.context(context: PromptContext): () => void` Contribute ordered dynamic context for the calling scope. Providers are evaluated for each eligible assembly and become a sourced runtime-context snapshot in model history under the shipped loop.
- `ctx.systemPrompt.suppressRuntimeContext(): () => void` Suppress every dynamic-context contribution for the calling scope. Multiple registrations compose independently; disposing the returned effect restores context when no suppressor remains.
- `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 waterfall. Runs through the scope-filtered `system-prompt/assemble` waterfall, then restores an effective complete section as the sole prompt section. 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 for multiple complete sections, 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, then restores an effective complete section as the sole prompt section and enforces any active runtime-context suppressor. 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 for multiple complete sections, when a configured `toolOrder` names a tool outside the providers' `knownNames` universe, or when a provider returns the reserved rest-entry name.
### Live events
`system-prompt/assemble` is authoritative for ordinary sections; a complete section is the final prompt constraint applied after the waterfall. Listeners that replace entries must preserve any active Code Mode or structured-output protocol. Use [`ToolRegistry.restrict()`](../tools/README.md) when filtering must stay aligned across presentation, lookup, and execution. Registry-change notifications are unfiltered. The generated region of [system-prompt.md](../../../docs/subsystems/system-prompt.md#cordis-surface) owns signatures and dispatch contracts.
`system-prompt/assemble` is authoritative for ordinary sections; a complete section is the final prompt constraint applied after the waterfall. Listeners that replace entries must preserve any active Code Mode or structured-output protocol. Use [`ToolRuntime.restrict()`](../tools/README.md) when filtering must stay aligned across presentation, lookup, and execution. Registry-change notifications are unfiltered. The generated region of [system-prompt.md](../../../docs/subsystems/system-prompt.md#cordis-surface) owns signatures and dispatch contracts.
### Key types
@@ -38,7 +41,7 @@ Merge-extensible: plugins can declare extra fields on `PromptAssembly` and `Asse
- Section providers: tool packages own their cross-call guidance (`tool:bash`, `tool:read`, …); this plugin owns `harness:identity` and `deployment:persona`.
- Variable providers: the agent loop registers `model` and `cwd`; any plugin can register the facts it owns (a future `date`, git state, …).
- Tool schema providers: `ToolRegistry` registers itself as a tool provider automatically.
- Tool schema providers: `ToolRuntime` registers itself as a tool provider automatically.
- The [`system-prompt/assemble` waterfall](#live-events): cooperatively mutate or replace the assembly per caller before any complete-section constraint is enforced.
Design rationale: [the prompt-variables Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md).
@@ -49,12 +52,12 @@ Design rationale: [the prompt-variables Agent Note](../../../.agents/notes/imple
#### What the model sees
By default every assembly starts with the harness identity below, then the configured persona and ordered plugin sections after strict variable interpolation. `includeHarnessIdentity: false` omits only that fixed opener. Empty sections disappear; scoped sections and variables can shadow globals for one agent. The `system-prompt/assemble` waterfall determines the delivered prompt and tool schemas unless one effective section declares itself complete; that exact section then becomes the whole system prompt while the waterfall's contexts, tools, and variables remain.
By default every assembly starts with the harness identity below, then the configured persona and ordered plugin sections after strict variable interpolation. `includeHarnessIdentity: false` omits only that fixed opener. Empty sections disappear; scoped sections and variables can shadow globals for one agent. The `system-prompt/assemble` waterfall determines the delivered prompt and tool schemas unless one effective section declares itself complete; that exact section then becomes the whole system prompt while the waterfall's contexts, tools, and variables remain. Ordered dynamic contexts are separate from system-prompt sections and become sourced user-role snapshots only when present. `includeRuntimeContext: false` or a scoped suppressor removes all such contexts, including listener additions, without disabling the services that own the underlying policy or state.
##### Harness identity
```markdown
You are an AI agent powered by the DeepSeek Harness SDK.
You are an AI agent powered by DeepSeek Harness.
```
#### Token effect

View File

@@ -2,45 +2,48 @@
[English](README.md) | 中文
系统提示词组装注册表。插件贡献有序段、工具 schema 和具名变量。循环在每个步骤组装一次,并将结果渲染为完整的模型提示词。此插件拥有静态 harness 身份和全局部署 personaagent智能体作用域的 persona 会遮蔽全局默认值。
系统提示词组装注册表。插件可以贡献有序段、工具 schema 和具名变量。循环在每个步骤组装一次,并将结果渲染为完整的模型提示词。此插件拥有静态 harness 身份和全局部署 personaagent智能体作用域的 persona 会遮蔽全局默认值。
## 配置
| 键 | 默认值 | 含义 |
|---|---|---|
| `includeHarnessIdentity` | `true` | 是否包含顺序为 100 的固定开场白 `You are an AI agent powered by the DeepSeek Harness SDK.`。仅当兼容部署拥有完整系统提示词时设为 false。 |
| `includeHarnessIdentity` | `true` | 是否包含顺序为 100 的固定开场白 `You are an AI agent powered by DeepSeek Harness.`。仅当兼容部署拥有完整系统提示词时设为 false。 |
| `includeRuntimeContext` | `true` | 是否在组装中包含有序动态上下文。设为 false 时不会求值上下文提供方,并会在 waterfall 后丢弃 `system-prompt/assemble` 监听器添加的上下文;其他服务及其强制机制仍然生效。 |
| `persona` | `''` | 全局部署 persona 默认值:唯一由配置提供的提示词片段,渲染为顺序为 0 的 `deployment:persona` 段,除非 agent 作用域的贡献将其遮蔽。它是模板,完整的 `{{…}}` 组会严格按已注册变量解释(随附循环注册 `{{model}}`/`{{cwd}}`),目前没有表达字面量花括号的转义语法。为空 ⇒ 渲染时删除该段。 |
| `toolOrder` | 无 | 显式面向模型工具顺序:一个 `ToolSchema.name` 列表,包含一个 `'<unlisted-tools>'` 其余项(`TOOL_ORDER_REST`已列工具占据列出的位置;未列工具按名称字典序落在其余项位置。缺席 ⇒ 直接按名称字典序排列。在 `system-prompt/assemble` waterfall瀑布式事件之前应用于已收集工具与段的 `order` 排序一样,它会规范化注册表贡献的内容注册顺序是插件加载产物),而修改列表的 waterfall 监听器拥有其输出的确定性。配置错误会明确失败:列表没有恰好一个其余项或存在重复项,会在加载时抛出;已列名称没有对应已注册工具,会使每次 `assemble()` 被拒绝;工具提供方返回保留的其余项名称也会被拒绝。在随附循环下,轮次会在任何模型请求前失败。为何采用中心列表而非每插件权重,见[显式面向模型工具顺序](../../../.agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md)。 |
| `toolOrder` | 无 | 显式指定面向模型工具顺序。该列表由 `ToolSchema.name` 组成,并且必须恰好包含一个 `'<unlisted-tools>'` 其余项标记`TOOL_ORDER_REST`已列工具按列表位置排列,未列工具按名称字典序插入该标记所在的位置。缺席 ⇒ 直接按名称字典序排列。该顺序会`system-prompt/assemble` waterfall瀑布式事件之前应用于已收集工具与段的 `order` 排序一样,它会规范化注册表贡献的内容注册顺序是插件加载时序的产物。修改列表的 waterfall 监听器其输出的确定性负责。配置错误会明确失败:列表没有恰好一个其余项或存在重复项,会在加载时抛出;已列名称没有对应已注册工具,会使每次 `assemble()` 被拒绝;工具提供方返回保留的其余项名称也会被拒绝。在随附循环下,轮次会在任何模型请求前失败。为何采用中心列表而非每插件权重,见[显式面向模型工具顺序](../../../.agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md)。 |
## 服务:`SystemPrompt`ctx 键:`systemPrompt`
### 公开 API
- `ctx.systemPrompt.section(section: PromptSection): () => void`:贡献一个段。层由调用上下文的作用域决定:`agent.ctx` 只为该 agent 贡献,并在该处遮蔽同名全局段。一个 `complete: true` 段会在组装 waterfall 之后成为精确的完整提示词;有效 complete 段超过一个时,组装会被拒绝。同一层中的重复名称和非有限顺序会抛出。随调用 fiber 一并 dispose资源释放
- `ctx.systemPrompt.context(context: PromptContext): () => void`:为调用作用域贡献有序动态上下文。每次符合条件的组装都会求值提供方,并在随附循环下成为模型历史中带来源的 runtime-context 快照。
- `ctx.systemPrompt.suppressRuntimeContext(): () => void`抑制调用作用域的所有动态上下文贡献。多个注册会独立组合只有当不再存在抑制器时dispose 返回的 effect 才会恢复上下文。
- `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` 的层合并,并在变换 waterfall 前分离工具 schema。它经过按作用域筛选的 `system-prompt/assemble` waterfall之后将一个有效的 complete 段恢复为唯一的提示词段。可选的 `context.signal` 显式控制本次组装请求;提供方与监听器可以配合该信号,但不得将它保留给另一轮次。存在多个 complete 段、已配置的 `toolOrder` 指名提供方 `knownNames` 全集以外的工具,或提供方返回保留的其余项名称时,调用会被拒绝。
- `ctx.systemPrompt.assemble(context?: AssembleContext): Promise<PromptAssembly>`:为一个调用方组装提示词:将全局层与 `context.scope` 的层合并,并在变换 waterfall 前分离工具 schema。它经过按作用域筛选的 `system-prompt/assemble` waterfall之后将一个有效的 complete 段恢复为唯一的提示词段,并实施任何活动的 runtime-context 抑制器。可选的 `context.signal` 显式控制本次组装请求;提供方与监听器可以配合该信号,但不得将它保留给另一轮次。存在多个 complete 段、已配置的 `toolOrder` 指名提供方 `knownNames` 全集以外的工具,或提供方返回保留的其余项名称时,调用会被拒绝。
<a id="live-events"></a>
### 实时事件
`system-prompt/assemble` 对普通段落具有权威性complete 段在 waterfall 之后应用的最终提示词约束。替换条目的监听器必须保留任何已启用的 Code Mode 或结构化输出协议。筛选需要在呈现、查找与执行之间保持一致时,应使用 [`ToolRegistry.restrict()`](../tools/README.md)。注册表变更通知不经过筛选。[system-prompt.md](../../../docs/subsystems/system-prompt.md#cordis-surface) 的生成区块拥有签名分发约定。
普通段以 `system-prompt/assemble` 的返回结果为准complete 段则会在 waterfall 之后作为最终提示词约束生效。替换条目的监听器必须保留任何已启用的 Code Mode 或结构化输出协议。筛选需要在呈现、查找与执行之间保持一致时,应使用 [`ToolRuntime.restrict()`](../tools/README.md)。注册表变更通知不经过筛选。[system-prompt.md](../../../docs/subsystems/system-prompt.md#cordis-surface) 的生成区块拥有事件签名分发约定。
### 关键类型
- `AssembleContext`:说明一次 `assemble()` 调用的用途。它可通过合并扩展;此处声明 `scope?: ScopeKey`(层选择器)与 `signal?: AbortSignal`(显式请求控制能力),而 `dsh-agent` 声明 `agent?: Agent`(类型化 DX 字段;绝不能在没有 `scope` 时设置,应使用 `assembleContextFor(agent, signal)`)。提供方必须容忍字段缺席,因为裸 `assemble()` 携带的是无作用域、无信号的空上下文。`signal` 是请求值,不是环境 Agent 执行 frame 的一部分。
- `PromptSection``{ name, order, text, complete? }`。各段按 `order` 升序拼接。顺序区间:`-100` 是 harness 身份,`0` 是部署 persona工具引导使用 `100199`。协作式组装完成后,一个有效的 `complete` 段会抑制其他所有段
- `PromptAssembly``{ sections: AssembledSection[], tools: ToolSchema[], variables: Record<string, string | undefined> }`。段文本到达时已解析,但尚未插值;`variables` 包含对上下文解析后的每个已注册变量。工具 schema 按设计属于组装结果:「模型获知自己能做什么」是一个连贯整体,尽管适配器把 schema 作为独立 wire 字段传输。
- `renderPrompt(assembly)`:插值每个段中的 `{{variable}}` 引用,删除空段,并用空行连接。严格规则:未知引用(使用 `Object.hasOwn` 查找,因此 `{{constructor}}` 等原型名称未知)、已注册但无值的引用、格式错误的完整 `{{…}}` 组,或一个起始 `{{` 没有打开完整组、但后面仍有 `}}``{{{model}}}`),都会抛出;明确失败胜过交付格式错误的提示词。孤立的 `{{` 如果后面任何位置都没有 `}}`,会按字面量通过;替换值绝不再次扫描。
- `PromptSection``{ name, order, text, complete? }`。各段按 `order` 升序拼接。顺序区间:`-100` 是 harness 身份,`0` 是部署 persona工具引导使用 `100199`。协作式组装完成后,一个有效的 `complete` 段会抑制其他所有段。
- `PromptAssembly``{ sections: AssembledSection[], tools: ToolSchema[], variables: Record<string, string | undefined> }`段文本到达时已求值,但尚未插值;`variables` 保存所有已注册变量在当前上下文中求得的值。工具 schema 按设计属于组装结果:「模型获知自己能做什么」是一个连贯整体,尽管适配器把 schema 作为独立 wire 字段传输。
- `renderPrompt(assembly)`:插值每个段中的 `{{variable}}` 引用,删除空段,并用空行连接。严格规则:未知引用(使用 `Object.hasOwn` 查找,因此 `{{constructor}}` 等原型名称未知)、已注册但无值的引用、格式错误的完整 `{{…}}` 组,或出现 `{{` 没有形成完整组、而后文仍有 `}}``{{{model}}}`),都会抛出异常;明确失败胜过交付格式错误的提示词。孤立的 `{{` 如果后面任何位置都没有 `}}`,会按字面量通过;替换值绝不再次扫描。
可通过合并扩展:插件可以借助声明合并,为 `PromptAssembly``AssembleContext` 声明额外字段。
### 扩展点
- 段提供方:工具包拥有跨调用导(`tool:bash``tool:read` 等);此插件拥有 `harness:identity``deployment:persona`
- 段提供方:工具包拥有自身的跨调用导(`tool:bash``tool:read` 等);此插件拥有 `harness:identity``deployment:persona`
- 变量提供方agent loop智能体循环注册 `model``cwd`;任何插件都可以注册自己拥有的事实(未来的 `date`、git 状态等)。
- 工具 schema 提供方:`ToolRegistry` 自动将自身注册为工具提供方。
- 工具 schema 提供方:`ToolRuntime` 自动将自身注册为工具提供方。
- [`system-prompt/assemble` waterfall](#live-events):按调用方协作式修改或替换组装结果,之后再实施 complete 段约束。
设计原理:[提示词变量 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)。
@@ -51,12 +54,12 @@
#### 模型看到的内容
默认情况下,每次组装都从下方 harness 身份开始,然后在严格变量插值后追加已配置 persona 与有序插件段。`includeHarnessIdentity: false` 仅省略这个固定开场白。空段会消失;带作用域的段和变量可以为一个 agent 遮蔽全局项。`system-prompt/assemble` waterfall 决定交付的提示词与工具 schema除非一个有效段声明自身为 complete此时该确切段会成为完整的系统提示词,而 waterfall 得到的上下文、工具和变量保持不变。
默认情况下,每次组装都从下方 harness 身份开始,然后在严格变量插值后追加已配置 persona 与有序插件段。`includeHarnessIdentity: false` 仅省略这个固定开场白。空段会消失;带作用域的段和变量可以为一个 agent 遮蔽全局项。`system-prompt/assemble` waterfall 决定交付的提示词与工具 schema除非一个有效段声明自身为 complete此时该确切段会成为完整的系统提示词而 waterfall 得到的上下文、工具和变量保持不变。有序动态上下文与系统提示词段分离,只在存在时才会成为带来源的 user 角色快照。`includeRuntimeContext: false` 或带作用域的抑制器会移除所有这类上下文,包括监听器添加的内容,但不会禁用拥有底层策略或状态的服务。
##### harness 身份
```markdown
You are an AI agent powered by the DeepSeek Harness SDK.
You are an AI agent powered by DeepSeek Harness.
```
#### Token 影响

View File

@@ -1,9 +1,9 @@
{
"name": "@deepseek-ai/dsh-system-prompt",
"description": "System prompt assembly registry for the DeepSeek Harness",
"version": "0.0.1-rc.2",
"version": "0.1.0-rc.6",
"publishConfig": {
"access": "restricted"
"access": "public"
},
"repository": {
"type": "git",
@@ -30,7 +30,7 @@
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",

View File

@@ -186,6 +186,8 @@ function compareToolNames(a: ToolSchema, b: ToolSchema): number {
export interface Config {
/** Include the fixed DeepSeek Harness identity before the deployment persona (default true). */
includeHarnessIdentity?: boolean
/** Include dynamic runtime-context snapshots in model history (default true). */
includeRuntimeContext?: boolean
/**
* Deployment-wide order-0 persona template. A scoped section named
* `deployment:persona` shadows it; `{{variable}}` references are strict.
@@ -302,6 +304,7 @@ type VariableProvider = (context: AssembleContext) => string | undefined
class PromptLayer implements ScopeLayer {
readonly sections: NamedEntries<PromptSection>
readonly contexts: NamedEntries<PromptContext>
readonly runtimeContextSuppressors = new AnonymousEntries<true>()
readonly toolProviders = new AnonymousEntries<ToolProvider>()
readonly variables: NamedEntries<VariableProvider>
@@ -325,6 +328,7 @@ class PromptLayer implements ScopeLayer {
isEmpty(): boolean {
return this.sections.isEmpty()
&& this.contexts.isEmpty()
&& this.runtimeContextSuppressors.isEmpty()
&& this.toolProviders.isEmpty()
&& this.variables.isEmpty()
}
@@ -334,6 +338,7 @@ class PromptLayer implements ScopeLayer {
export class SystemPrompt extends Service {
static Config: z<Config> = z.object({
includeHarnessIdentity: z.boolean().default(true),
includeRuntimeContext: z.boolean().default(true),
persona: z.string().default(''),
// Preserve omission because an explicit empty order lacks the rest marker.
toolOrder: z.array(z.string()).default(undefined as unknown as string[]),
@@ -353,7 +358,7 @@ export class SystemPrompt extends Service {
this.section({
name: 'harness:identity',
order: -100,
text: 'You are an AI agent powered by the DeepSeek Harness SDK.',
text: 'You are an AI agent powered by DeepSeek Harness.',
})
}
this.section({
@@ -362,6 +367,7 @@ export class SystemPrompt extends Service {
// The fallback narrows the optional input type; the schema already defaults it.
text: config.persona ?? '',
})
if (!(config.includeRuntimeContext ?? true)) this.suppressRuntimeContext()
}
/**
@@ -400,6 +406,20 @@ export class SystemPrompt extends Service {
)
}
/**
* Suppress every dynamic runtime-context contribution in the calling
* context's scope without changing the services that own or enforce those
* facts. Multiple suppressors remain independently disposable.
* @returns the exact Cordis effect disposer.
*/
suppressRuntimeContext(): () => void {
return this.layers.effect(
this.ctx,
layer => layer.runtimeContextSuppressors.append(true),
{ label: 'systemPrompt.suppressRuntimeContext()' },
)
}
/**
* Register a tool-schema provider in the calling context's scope. Global and
* matching scoped providers both contribute; returning the reserved
@@ -446,13 +466,16 @@ export class SystemPrompt extends Service {
// Keep configuration failures on the declared asynchronous error path.
async assemble(context: AssembleContext = {}): Promise<PromptAssembly> {
const scope = context.scope
const scopeLayers = this.layers.chainLayers(scope)
const runtimeContextSuppressed = !this.layers.global.runtimeContextSuppressors.isEmpty()
|| scopeLayers.some(layer => !layer.runtimeContextSuppressors.isEmpty())
// Scoped variables shadow globals.
const variables: Record<string, string | undefined> = {}
for (const [name, provider] of this.layers.global.variables.entries()) {
variables[name] = provider(context)
}
// Scope-chain variables, farthest first, so the nearest scope wins a name.
for (const layer of this.layers.chainLayers(scope)) {
for (const layer of scopeLayers) {
for (const [name, provider] of layer.variables.entries()) {
variables[name] = provider(context)
}
@@ -463,7 +486,7 @@ export class SystemPrompt extends Service {
// Validate order against pre-restriction names while collecting visible schemas.
const providers = [
...this.layers.global.toolProviders.values(),
...this.layers.chainLayers(scope).flatMap(layer => [...layer.toolProviders.values()]),
...scopeLayers.flatMap(layer => [...layer.toolProviders.values()]),
]
const collected: ToolSchema[] = []
const knownNames = new Set<string>()
@@ -495,12 +518,14 @@ export class SystemPrompt extends Service {
})
const assembly: PromptAssembly = {
sections,
contexts: [...contextByName.values()]
.sort((a, b) => a.order - b.order)
.map(entry => ({
name: entry.name,
text: typeof entry.text === 'function' ? entry.text(context) : entry.text,
})),
contexts: runtimeContextSuppressed
? []
: [...contextByName.values()]
.sort((a, b) => a.order - b.order)
.map(entry => ({
name: entry.name,
text: typeof entry.text === 'function' ? entry.text(context) : entry.text,
})),
tools: orderTools(collected, this.toolOrder, knownNames),
variables,
}
@@ -508,8 +533,12 @@ export class SystemPrompt extends Service {
scopeTarget(this, scope), 'system-prompt/assemble', assembly, context,
() => Promise.resolve(assembly),
)
if (completeSection === undefined) return transformed
return { ...transformed, sections: [completeSection] }
if (completeSection === undefined && !runtimeContextSuppressed) return transformed
return {
...transformed,
sections: completeSection === undefined ? transformed.sections : [completeSection],
contexts: runtimeContextSuppressed ? [] : transformed.contexts,
}
}
}

View File

@@ -2,11 +2,11 @@ import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import type { PromptAssembly } from '@deepseek-ai/dsh-system-prompt'
import * as SystemPromptInvariant from '@deepseek-ai/dsh-system-prompt/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
async function setup(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(SystemPromptInvariant)
return ctx
}

View File

@@ -142,6 +142,23 @@ describe('scoped cache-safe context', () => {
expect(renderContextSnapshot(await ctx.systemPrompt.assemble({ scope: scopeKeyOf(scope) })))
.toContain('global policy')
})
it('suppresses all context for one scope and restores it when disposed', async () => {
const ctx = await mount()
const scope = await mintScope(ctx, 'suppressed-context')
const key = scopeKeyOf(scope)
ctx.systemPrompt.context({ name: 'policy', order: 1, text: 'global policy' })
const dispose = scope.ctx.systemPrompt.suppressRuntimeContext()
const suppressed = await ctx.systemPrompt.assemble({ scope: key })
expect(suppressed.contexts).toEqual([])
const global = await ctx.systemPrompt.assemble()
expect(renderContextSnapshot(global)).toContain('global policy')
dispose()
expect(renderContextSnapshot(await ctx.systemPrompt.assemble({ scope: key })))
.toContain('global policy')
})
})
describe('scoped tool providers and toolOrder × restriction', () => {

View File

@@ -9,7 +9,7 @@ import SystemPrompt, { AssembleContext, PromptAssembly, renderContextSnapshot, r
* their own sections; the built-ins' behavior is pinned by its own describe.
*/
const BUILT_IN = ['harness:identity', 'deployment:persona']
const IDENTITY = 'You are an AI agent powered by the DeepSeek Harness SDK.'
const IDENTITY = 'You are an AI agent powered by DeepSeek Harness.'
function contributed(assembly: PromptAssembly): PromptAssembly['sections'] {
return assembly.sections.filter(section => !BUILT_IN.includes(section.name))
}
@@ -18,14 +18,14 @@ describe('SystemPrompt', () => {
describe('built-in sections', () => {
it('registers the harness identity and the configured deployment persona', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, { persona: 'You are DeepSeek Harness SDK.' })
await ctx.plugin(SystemPrompt, { persona: 'You are DeepSeek Harness.' })
const assembly = await ctx.systemPrompt.assemble()
expect(assembly.sections.map(s => s.name)).toEqual([
'harness:identity',
'deployment:persona',
])
expect(renderPrompt(assembly)).toBe(`${IDENTITY}\n\nYou are DeepSeek Harness SDK.`)
expect(renderPrompt(assembly)).toBe(`${IDENTITY}\n\nYou are DeepSeek Harness.`)
// The names are reserved by the plugin — one owner per section.
expect(() => ctx.systemPrompt.section({ name: 'deployment:persona', order: 0, text: 'imposter' }))
.toThrow('prompt section "deployment:persona" is already registered')
@@ -49,6 +49,25 @@ describe('SystemPrompt', () => {
expect(renderPrompt(assembly)).toBe('You are a helpful software engineer assistant.')
})
it('can suppress runtime context without evaluating providers or accepting waterfall additions', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, { includeRuntimeContext: false })
let providerCalls = 0
ctx.systemPrompt.context({
name: 'policy',
order: 0,
text: () => `policy ${++providerCalls}`,
})
ctx.on('system-prompt/assemble', async (assembly, _context, next) => {
assembly.contexts.push({ name: 'late', text: 'late context' })
return next()
})
const assembly = await ctx.systemPrompt.assemble()
expect(assembly.contexts).toEqual([])
expect(providerCalls).toBe(0)
})
it('tolerates a schema-bypassing direct construction (persona omitted)', async () => {
// ctx.plugin validates + defaults the config first; a direct construction
// skips the schema, so the ctor's `?? ''` narrowing is what fires.
@@ -60,7 +79,7 @@ describe('SystemPrompt', () => {
it('assembles sections in order with context-resolved text and collected tools', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, { persona: 'You are DeepSeek Harness SDK.' })
await ctx.plugin(SystemPrompt, { persona: 'You are DeepSeek Harness.' })
ctx.systemPrompt.section({ name: 'cwd', order: 20, text: () => 'cwd: /tmp' })
ctx.systemPrompt.section({ name: 'rules', order: 10, text: 'Be precise.' })
@@ -70,14 +89,14 @@ describe('SystemPrompt', () => {
const assembly = await ctx.systemPrompt.assemble()
expect(assembly.sections.map(s => s.name)).toEqual(['harness:identity', 'deployment:persona', 'rules', 'cwd'])
expect(assembly.sections.map(s => s.text)).toEqual([IDENTITY, 'You are DeepSeek Harness SDK.', 'Be precise.', 'cwd: /tmp'])
expect(assembly.sections.map(s => s.text)).toEqual([IDENTITY, 'You are DeepSeek Harness.', 'Be precise.', 'cwd: /tmp'])
expect(assembly.contexts).toEqual([
{ name: 'earlier', text: 'context 1' },
{ name: 'later', text: 'context 2' },
])
expect(assembly.tools).toEqual([{ name: 'echo', description: 'echo back', parameters: {} }])
expect(assembly.variables).toEqual({})
expect(renderPrompt(assembly)).toBe(`${IDENTITY}\n\nYou are DeepSeek Harness SDK.\n\nBe precise.\n\ncwd: /tmp`)
expect(renderPrompt(assembly)).toBe(`${IDENTITY}\n\nYou are DeepSeek Harness.\n\nBe precise.\n\ncwd: /tmp`)
expect(renderContextSnapshot(assembly)).toBe('Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\ncontext 1\n\ncontext 2')
})

View File

@@ -24,7 +24,7 @@
"path": "../../core/scope"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

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: 88fe6660f169f69a70e5630f853104a7c83a5b3c
README.zh.md: 12bc5e81dfdcab3b83685d8a85fc3db445b959e5
README.md: dd20b1b8fdc4ff57ff9850683d6070fdb219e25a
README.zh.md: 3c687ed0aa1347e3ce66da3b7fa2b48f69267b83

View File

@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
Tool registry and execution pipeline. Tool plugins register their schemas and executors; the agent loop executes each call through `tools/pre-execute` (the extensible allow/deny gate) → monotonic registered guards → `tools/execute` (an around-dispatch wrapper for timeout/retry/metrics plugins) → `tools/post-execute` (inspect/replace the result, attach context) → the definition-owned `finalizeContent` boundary → the observe-only `tools/result` notification. The registry also owns HOW its tools are presented to the model — its `mode` config selects native function calling, [Code Mode](#code-mode), or both, and one agent shadows that default for itself with `presentAs`.
## Service: `ToolRegistry` (ctx key: `tools`)
## Service: `ToolRuntime` (ctx key: `tools`)
### Config
@@ -13,7 +13,7 @@ tools:
mode: native # native (default) | code | both
```
`native` contributes visible tools as function definitions. `code` contributes the reserved `run_code` transport, the generated `tools:sdk` section, and the `tools:code-only` rule stating that only `run_code` may be called directly — which the executor then enforces, resolving a model-direct call naming any other tool to `UNKNOWN_TOOL` before policy runs; `both` contributes both forms and states no such rule, because its native calls do execute. This is the default for agents that declare none of their own — an agent preset selects its own with [`dsh-agent-tool-mode`](../agent-tool-mode/README.md). The reserved transport cannot be registered, shadowed, restricted, or removed, and its name is reserved whatever the configured mode, because any agent may select a code mode. Non-native modes require a `ctx.codeRuntime` whose `language` has a registered SDK renderer — TypeScript ships via [`dsh-code-runtime-worker`](../../code-runtime/code-runtime-worker/README.md); a Python renderer is built in and drives any runtime that reports `language: 'python'` (a first-party `dsh-code-runtime-python` backend is delivered separately). A runtime language with no renderer fails prompt assembly loudly, and a `systemPrompt.toolOrder` entry for a tool the mode does not contribute rejects prompt assembly. A `system-prompt/assemble` listener may replace the registry's contributions; its returned assembly is authoritative, so that listener owns preserving a usable Code Mode protocol.
`native` contributes visible tools as function definitions. `code` contributes the reserved `run_code` transport, the generated `tools:sdk` section, and the `tools:code-only` rule stating that only `run_code` may be called directly — which the executor then enforces, resolving a model-direct call naming any other tool to `UNKNOWN_TOOL` before policy runs; `both` contributes both forms and states no such rule, because its native calls do execute. This is the default for agents that declare none of their own — an agent preset selects its own with [`dsh-agent-tool-presentation`](../agent-tool-presentation/README.md). The reserved transport cannot be registered, shadowed, restricted, or removed, and its name is reserved whatever the configured mode, because any agent may select a code mode. Non-native modes require a `ctx.codeRuntime` whose `language` has a registered SDK renderer — TypeScript ships via [`dsh-code-runtime-worker-thread`](../../code-runtime/code-runtime-worker-thread/README.md); a Python renderer is built in and drives any runtime that reports `language: 'python'` (a first-party `dsh-code-runtime-python` backend is delivered separately). A runtime language with no renderer fails prompt assembly loudly, and a `systemPrompt.toolOrder` entry for a tool the mode does not contribute rejects prompt assembly. A `system-prompt/assemble` listener may replace the registry's contributions; its returned assembly is authoritative, so that listener owns preserving a usable Code Mode protocol.
### Public API
@@ -148,14 +148,14 @@ Prefix-stable while visible definitions and their order are unchanged. Registrat
#### What the model sees
Code Mode exposes the generated [`run_code` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tools), the SDK instructions below, and the generated exact SDK block for the loaded runtime's language (the TypeScript `declare const tools` block, or the Python `tools` declaration). `both` exposes normal schemas and this Code Mode API. Under `code` the prompt also carries the `tools:code-only` rule, ordered ahead of the per-tool guidance band so the model reads which tools it may call before it reads what each one is for; `both` renders it empty. The instructions and SDK block match the loaded runtime's language; the TypeScript version (via [`dsh-code-runtime-worker`](../../code-runtime/code-runtime-worker/README.md)) is shown below, and the Python version (for any runtime reporting `language: 'python'`) has the same operations and types in Python syntax (`await tools.name(args)`, subscript access for exotic names, `print(...)` and top-level `return`).
Code Mode exposes the generated [`run_code` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tools), the SDK instructions below, and the generated exact SDK block for the loaded runtime's language (the TypeScript `declare const tools` block, or the Python `tools` declaration). `both` exposes normal schemas and this Code Mode API. Under `code` the prompt also carries the `tools:code-only` rule, ordered ahead of the per-tool guidance band so the model reads which tools it may call before it reads what each one is for; `both` renders it empty. The instructions and SDK block match the loaded runtime's language; the TypeScript version (via [`dsh-code-runtime-worker-thread`](../../code-runtime/code-runtime-worker-thread/README.md)) is shown below, and the Python version (for any runtime reporting `language: 'python'`) has the same operations and types in Python syntax (`await tools.name(args)`, subscript access for exotic names, `print(...)` and top-level `return`).
##### Code Mode SDK instructions
```markdown
## Writing code for run_code
Pass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program:
`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program:
- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools["my-tool"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.
- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.
@@ -192,7 +192,7 @@ Append-only; newly visible content follows the reusable request prefix and does
- **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.
- **`timeoutMs` on a definition is declarative only** — the registry never enforces deadlines; enforcement requires the `@deepseek-ai/dsh-tool-call-timeout-policy` wrapper.
- **Code Mode's SDK language follows the one loaded runtime, and a presentation is per agent rather than per tool** — `mode: code`/`both` rejects prompt assembly unless `ctx.codeRuntime.language` has a registered SDK renderer (TypeScript or Python); scoped restrictions/shadows and `presentAs` choose each agent's visible bindings and their form, but within one agent no tool can be native-only while another is code-only.
- **Code Mode intermediate values are execution-local and unbounded by bytes** — the canonical typed values cannot be reconstructed from session replay and may exhaust process or worker memory; only the outer `run_code` output has the worker's configurable hard cap. The durable log copy of each sub-call IS bounded: the `tools/code-dispatch-log` waterfall lets the spill policy replace an oversized `tool/code-dispatch` content with a preview + locator ([rationale](../../../.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md)).
- **`run_code` state is fresh per run** — a persistent REPL-style kernel is rejected for the MVP (cross-call state would be invisible to the log); see [the Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md).

View File

@@ -2,9 +2,9 @@
[English](README.md) | 中文
工具注册表与执行流水线。工具插件注册各自的 schema 和执行器agent loop智能体循环依次让每次调用经过 `tools/pre-execute`(可扩展的允许/拒绝门禁)→ 已注册的单调守卫 → `tools/execute`(供超时/重试/指标插件使用的环绕分发包装层)→ `tools/post-execute`(检查/替换结果、附加上下文)→ 定义自身`finalizeContent` 终结步骤 → 仅观测的 `tools/result` 通知。注册表还负责决定如何向模型呈现工具:`mode` 配置可以选择原生 Function Calling函数调用、[Code Mode](#code-mode),或同时选择两者;单个 agent 可用 `presentAs` 为自己遮蔽该默认值。
工具注册表与执行流水线。工具插件注册各自的 schema 和执行器agent loop智能体循环依次让每次调用经过 `tools/pre-execute`(可扩展的允许/拒绝门禁)→ 已注册的单调守卫 → `tools/execute`(供超时/重试/指标插件使用的环绕分发包装层)→ `tools/post-execute`(检查/替换结果、附加上下文)→ 由工具定义持有`finalizeContent` 边界 → 仅观测的 `tools/result` 通知。注册表还决定以何种方式向模型呈现工具:`mode` 配置可以选择原生 Function Calling函数调用、[Code Mode](#code-mode),或同时选择两者;单个 agent 可用 `presentAs` 为自己遮蔽该默认值。
## 服务:`ToolRegistry`ctx 键:`tools`
## 服务:`ToolRuntime`ctx 键:`tools`
### 配置
@@ -13,17 +13,17 @@ tools:
mode: native # native (default) | code | both
```
`native` 以函数定义的形式贡献可见工具。`code` 贡献保留的 `run_code` 传输、生成的 `tools:sdk` 段,以及声明「只有 `run_code` 可被直接调用」的 `tools:code-only` 规则——执行器随后强制该规则模型直其他任何工具名都会在策略运行前解析为 `UNKNOWN_TOOL``both` 同时贡献两种形式,且不声明该规则,因为的原生调用确实执行。这是「未作声明的 agent」的默认值——agent preset [`dsh-agent-tool-mode`](../agent-tool-mode/README.md) 为自己选择。不能注册、遮蔽、限制或移除该保留传输,且无论配置何种模式,该名称都是保留的,因为任何 agent 都可能选择 code 模式。非原生模式要求所加载 `ctx.codeRuntime``language` 有已注册的 SDK 渲染器——TypeScript 经 [`dsh-code-runtime-worker`](../../code-runtime/code-runtime-worker/README.md) 交付Python 渲染器内置,驱动任何报告 `language: 'python'` 的运行时(第一方 `dsh-code-runtime-python` 后端另行交付)。没有渲染器的运行时语言会导致提示词组装明确失败;如果 `systemPrompt.toolOrder` 条目指向当前模式未贡献的工具,系统会拒绝组装提示词。`system-prompt/assemble` 监听器可以替换注册表贡献;它返回的组装结果具有权威性,因此该监听器负责保留可用的 Code Mode 协议。
`native` 以函数定义的形式贡献可见工具。`code` 会提供保留的 `run_code` 传输、生成的 `tools:sdk` 段,以及声明「只有 `run_code` 可被直接调用」的 `tools:code-only` 规则执行器随后强制执行该规则模型直接调用其他任何工具时,会在策略运行前将该调用解析为 `UNKNOWN_TOOL``both` 同时提供两种形式,且不声明该规则,因为其中的原生调用确实可以执行。没有单独声明呈现模式的 agent 默认采用此配置;agent preset 可通过 [`dsh-agent-tool-presentation`](../agent-tool-presentation/README.md) 自行选择呈现模式。不能注册、遮蔽、限制或移除该保留传输,且无论配置何种模式,该名称都是保留的,因为任何 agent 都可能选择 code 模式。非原生模式要求所加载 `ctx.codeRuntime``language` 有已注册的 SDK 渲染器——TypeScript 经 [`dsh-code-runtime-worker-thread`](../../code-runtime/code-runtime-worker-thread/README.md) 交付Python 渲染器内置,驱动任何报告 `language: 'python'` 的运行时(第一方 `dsh-code-runtime-python` 后端另行交付)。没有渲染器的运行时语言会导致提示词组装明确失败;如果 `systemPrompt.toolOrder` 条目指向当前模式未贡献的工具,系统会拒绝组装提示词。`system-prompt/assemble` 监听器可以替换注册表贡献;它返回的组装结果具有权威性,因此该监听器负责保留可用的 Code Mode 协议。
### 公开 API
- `ctx.tools.register(definition: ToolDefinition): () => void`:注册一个受信任、带类型的同进程定义,其中必须包含规范的 `output` 声明。所在层由调用上下文的作用域决定普通插件上下文会全局注册agent 的 `agent.ctx` 只为该 agent 注册,并在此处遮蔽同名全局工具。同一层内名称重复会抛出;非原生模式还会拒绝保留的 `run_code` 传输名称。缺失或不受支持的输出声明,以及非正数或非有限的 `timeoutMs`,都会使注册失败。可选的同步 `finalizeContent` 回调会在调用开始时创建快照;在所有流水线结果(包括实体化其他结果字段时发现的错误)规范化之后,它只能替换最终面向模型的内容。随调用 fiber dispose资源释放
- `ctx.tools.presentAs(mode: ToolPresentationMode): () => void`:为本 agent 选择面向模型的呈现方式,仅对该 agent 遮蔽 `mode` 配置;从普通上下文调用会抛出(进程级呈现方式是那个配置字段),同一 scope 内第二次声明也会抛出。code 类模式还会为该 agent 注册它自己的 `tools:sdk` 段。清单本身不变——`schemas(agent)` 报告的仍是该 agent 的能力,仅组装结果中的工具会被折叠。随调用方 fiber dispose。
- `ctx.tools.register(definition: ToolDefinition): () => void`:注册一个受信任、带类型的同进程定义,其中必须包含规范的 `output` 声明。所在层由调用上下文的作用域决定普通插件上下文会全局注册agent 的 `agent.ctx` 只为该 agent 注册,并在此处遮蔽同名全局工具。同一层内名称重复会抛出;非原生模式还会拒绝保留的 `run_code` 传输名称。缺失或不受支持的输出声明,以及非正数或非有限的 `timeoutMs`,都会使注册失败。可选的同步 `finalizeContent` 回调会在调用开始时纳入快照;在所有流水线结果(包括实体化其他结果字段时发现的错误)规范化之后,它只能替换最终面向模型的内容。该注册会随调用 fiber 一同 dispose资源释放
- `ctx.tools.presentAs(mode: ToolPresentationMode): () => void`:为本 agent 选择面向模型的呈现方式,仅对该 agent 遮蔽 `mode` 配置;从普通上下文调用会抛出(进程级呈现方式是那个配置字段),同一 scope 内第二次声明也会抛出。code 类模式还会为该 agent 注册它自己的 `tools:sdk` 段。工具目录保持不变:`schemas(agent)` 仍会报告该 agent 的能力;只有组装结果中的工具列表会按所选呈现方式收束。随调用方 fiber dispose。
- `ctx.tools.restrict(filter)`:对全局工具应用 agent 作用域的允许/拒绝掩码;从普通上下文调用会抛出。筛选器在注册时创建快照;多个掩码取交集,随后再合并作用域本地工具。拒绝掩码会接纳后来出现且未点名的全局工具,而允许掩码会排除后来出现的名称。未知、本地或保留名称以及空筛选器都会被拒绝。这是实时可见性组合,不是权限边界;参见[作用域安全非目标](../../../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-non-goals)。
- `ctx.tools.get(name: string, scope?: ScopeKey): ToolDefinition | undefined`按某个作用域见的结果解析(应用遮蔽;被限制掉的全局工具视为不存在。呈现器会传入发起调用的 agent使卡片与实际执行内容一致。
- `ctx.tools.get(name: string, scope?: ScopeKey): ToolDefinition | undefined`返回指定作用域见的解析结果,其中已应用名称遮蔽;被作用域限制排除的全局工具会被视为不存在。呈现器会传入发起调用的 agent使卡片与实际执行内容一致。
- `ctx.tools.schemas(scope?: ScopeKey): ToolSchema[]`:返回该作用域可见的所有 schema不含 `execute` 函数)。已交付工具的 schema 收录在 [docs/tool-catalog.md](../../../docs/tool-catalog.md) 中;该目录通过启动每个工具插件并采集此方法的结果生成(参见[工具 schema 目录 Agent Note](../../../.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md))。
- `ctx.tools.guard(guard: ToolGuard): () => void`:在 `tools/pre-execute` 之后注册单调同步执行守卫:返回理由会拒绝调用,返回 `undefined` 则保持原决定。普通上下文守卫全局生效;`agent.ctx` 守卫只对该 agent 生效。后续 waterfall瀑布式事件监听器无法将守卫的拒绝重新变为允许。随调用 fiber dispose。
- `ctx.tools.execute(exec)`:以无损方式快照并冻结参数,分配不透明 token运行完整的策略分发结果流水线然后在最终观测前独立快照权威结果。无效参数会进入同一结果路径但不会到达策略或工具主体。环绕包装层只能替换 `signal`;注册表会在调用主体前立即重新融合调用方的原始信号
- `ctx.tools.execute(exec)`:以无损方式快照并冻结参数,分配不透明 token运行完整的策略分发结果流水线然后在最终观测前独立快照权威结果。无效参数会进入同一结果路径但不会到达策略或工具主体。环绕包装层只能替换 `signal`;注册表会在进入工具主体之前,立即将调用方的原始信号重新合并到当前信号中
- `ctx.tools.executionMode(exec)`:返回 `parallel` 的唯一条件是可见定义的 `isConcurrencySafe(exec.arguments)` 分类器恰好返回 `true`;未知、隐藏、未声明、无效或抛出异常的分类结果均为独占。
### 注入的服务
@@ -32,11 +32,11 @@ tools:
### 取消
取消采用协作方式,并等待完全停稳。每次类型化调用都提供由调用方拥有的 `AbortSignal`;工具主体通过必填的只读 `exec.signal` 接收它,只有 `tools/execute` 包装层可以临时替换这个必填信号。注册表会在替换期间保留调用方取消,并且绝不会在已启动的同进程 Promise 尚未结算时提前返回。工具主体调用前发生的取消为 `ABORTED_BEFORE_DISPATCH`调用主体后的取消只能成功结果替换为 `ABORTED`。拒绝、包装层失败、工具失败、后置策略失败或由超时机制产生的 `TOOL_TIMEOUT` 仍保留更具体的结果。入口处已中止的调用会实体化并冻结参数,随后跳过所有策略和分发阶段,只发布一个结果。每个异步工具都必须观测或转发该信号,并且只能在自身拥有的工作停止后结算。[工具取消 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.md) 规定完整约定和强制终止边界。
取消采用协作方式,并等待完全停稳。每次类型化调用都提供由调用方拥有的 `AbortSignal`;工具主体通过必填的只读 `exec.signal` 接收它,只有 `tools/execute` 包装层可以临时替换这个必填信号。注册表会在替换期间保留调用方取消,并且绝不会在已启动的同进程 Promise 尚未结算时提前返回。工具主体调用前发生的取消为 `ABORTED_BEFORE_DISPATCH`工具主体被调用后发生的取消只能成功结果替换为 `ABORTED`。拒绝、包装层失败、工具失败、后置策略失败或由超时机制产生的 `TOOL_TIMEOUT` 仍保留更具体的结果。入口处已中止的调用会实体化并冻结参数,随后跳过所有策略和分发阶段,只发布一个结果。每个异步工具都必须观测或转发该信号,并且只能在其负责的工作停止后结算。[工具取消 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.md) 规定完整约定和强制终止边界。
### 实时事件
实时注册表流水线先经过 3 个可变换的 waterfall再经过由定义有的内容终结器,最后发布仅供观测的 `tools/result` 事件;注册表变更有意作为不过滤的共享状态通知。确切签名、分发 mode、作用域筛选和失败隔离约定位于 [tools.md](../../../docs/subsystems/tools.md#cordis-surface) 的生成区块,完整顺序则在生成的[工具执行流水线](../../../docs/tool-execution-pipeline.md)中可视化。`tools/result` 是实时事件;名称相近的 `tool/result` 是 agent loop 随后追加的持久会话事件。
实时注册表流水线先经过 3 道可转换的 waterfall再经过由工具定义有的内容终结器,最后发布仅供观测的 `tools/result` 事件;注册表变更通知有意不作过滤,并作为共享状态通知发布。确切签名、分发 mode、作用域筛选和失败隔离约定位于 [tools.md](../../../docs/subsystems/tools.md#cordis-surface) 的生成区块,完整顺序则在生成的[工具执行流水线](../../../docs/tool-execution-pipeline.md)中可视化。`tools/result` 是实时事件;名称相近的 `tool/result` 是 agent loop 随后追加的持久会话事件。
### 关键类型
@@ -102,16 +102,16 @@ ctx.tools.register(defineTool({
### 强制执行的原始 JSON Schema 子集
`JsonSchemaNode` 是工具输出、Code Mode 生成、subagent 和工作流共享的原始对应类型。它允许任意 JSON 根、一个仅含 annotation 的无约束 JSON 节点,以及恰好匹配一个分支的 `oneOf`annotation 必须保持为无损 JSON。`assertSupportedJsonSchema()` 拒绝不受支持的构造,而 `validateJsonSchemaValue()` 返回带路径的违规信息。subagent 和工作流通过 `assertObjectJsonSchema()``ObjectJsonSchema` 保留调用方定义的对象根要求,而不是依赖共享词汇的限制。
`JsonSchemaNode` 是工具输出、Code Mode 生成、subagent 和工作流共享的原始 JSON Schema 对应类型。它允许任意 JSON 根、仅含注解且不施加约束 JSON 节点,以及恰好匹配一个分支的 `oneOf`注解必须保持为无损 JSON。`assertSupportedJsonSchema()` 拒绝不受支持的构造,而 `validateJsonSchemaValue()` 返回带路径的违规信息。subagent 和工作流通过 `assertObjectJsonSchema()``ObjectJsonSchema` 保留调用方定义的对象根要求,而不是依赖共享词汇的限制。
### 工具拥有的 UI 呈现
### 工具定义的 UI 呈现
工具可以选择拥有纯 `presentCall()``presentResult()` 呈现意图,使 UI 无需特殊处理工具名称
工具可以选择通过纯函数 `presentCall()``presentResult()` 定义呈现意图,使 UI 无需针对工具名称编写特殊逻辑
- 调用视图为 `{ card: 'generic', title, kind?, rawInput?, content?, locations? }``{ card: 'terminal', title, description?, cwd? }``{ card: 'diff', title, diffs, locations? }`
- 结果视图为 `{ card: 'generic', title?, content? }``{ card: 'terminal', title?, output?, exitCode?, signal? }``{ card: 'diff', title?, diffs }``{ card: 'search', shape, title?, truncated, total, … }`(已完成的发现型搜索——`shape: 'matches'`grep为按文件分组的匹配`shape: 'paths'`glob为扁平路径列表`truncated`/`total` 使 UI 永不把被截断的结果当作完整结果呈现;该视图不携带结果文本,且搜索没有 `card: 'search'` 的调用时对应视图)、`{ card: 'read', title?, path, offset, lines, totalLines, lang?, content? }`(已完成的文件读取→带行号、可选语法高亮的代码视图;`offset` 是窗口请求的 1-based 起始行,即使 `lines` 为空也保留;`lines``{ number, text }[]`,保留每一行的文件行号,`content`无读取能力的 UI 回退时使用的去信封文本)或 `{ card: 'web', kind: 'search' | 'fetch', title?, … }`(已完成的 web 检索;`kind` 各分支携带结构化的搜索来源或抓取摘要,不具备 `web` 能力的 UI 回退到原始结果内容)。
- 结果视图为 `{ card: 'generic', title?, content? }``{ card: 'terminal', title?, output?, exitCode?, signal? }``{ card: 'diff', title?, diffs }``{ card: 'search', shape, title?, truncated, total, … }`(已完成的发现型搜索——`shape: 'matches'`grep为按文件分组的匹配`shape: 'paths'`glob为扁平路径列表`truncated`/`total` 使 UI 永不把被截断的结果当作完整结果呈现;该视图不携带结果文本,且搜索没有 `card: 'search'` 的调用时对应视图)、`{ card: 'read', title?, path, offset, lines, totalLines, lang?, content? }`(已完成的文件读取→带行号、可选语法高亮的代码视图;`offset` 是窗口请求的 1-based 起始行,即使 `lines` 为空也保留;`lines``{ number, text }[]`,保留每一行的文件行号,`content`去除读取结果外层封装后的正文,供不支持读取视图的 UI 回退显示)或 `{ card: 'web', kind: 'search' | 'fetch', title?, … }`(已完成的 web 检索;`kind` 各分支携带结构化的搜索来源或抓取摘要,不具备 `web` 能力的 UI 回退到原始结果内容)。
返回 `undefined` 会选择通用回退。呈现器只依赖其参数和持久结果,因为 UI 会在实时流式输出和日志回放期间调用它们。`output.presentationMeta(args, value)` 为直接接口调用派生 JSON 元数据;该元数据随 `tool/result` 持久化并传回 `presentResult`,而规范值本身仍只存在于执行局部,绝不会回放。嵌套 Code 分发不会计算元数据。`defineTool` 会软验证较旧的日志参数并回退,而不会使回放崩溃。`dsh-tool-bash``dsh-tool-fs` 是参考实现;[规范输出 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md) 规定值/呈现拆分,[呈现意图 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md) 规定卡片词汇。
返回 `undefined` 会选择通用回退。呈现器只依赖其参数和持久结果,因为 UI 会在实时流式输出和日志回放期间调用它们。`output.presentationMeta(args, value)` 为直接的顶层调用派生 JSON 元数据;该元数据随 `tool/result` 持久化并传回 `presentResult`,而规范值本身仍只存在于执行局部,绝不会回放。嵌套 Code 分发不会计算元数据。`defineTool` 会软验证较旧的日志参数并回退,而不会使回放崩溃。`dsh-tool-bash``dsh-tool-fs` 是参考实现;[规范输出 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md) 规定值/呈现拆分,[呈现意图 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md) 规定卡片词汇。
### Code Mode
@@ -119,10 +119,10 @@ ctx.tools.register(defineTool({
`code`(而非 `both`)下,该传输同时也是模型唯一可用的入口:模型直呼其他任何可见工具名,都会在创建执行时、早于 `tools/pre-execute`、审批 `ask` 和 guards 解析为 `UNKNOWN_TOOL`,因此没有任何一方会观察或批准一个注定失败的调用。拒绝信息会给出正确路径(`only \`run_code\` is callable directly — call \`<name>\` from inside a \`run_code\` program instead`),因为同一份提示词刚刚声明过那个工具,只说 `unknown tool` 会被读成部署损坏。SDK 子分发携带外层执行的 `parent` token不受此限制因此程序保留 SDK 声明的全部绑定。参见[执行器塌缩 note](../../../.agents/notes/implemented/bug-fix/2026-08-07-code-mode-executor-collapse.md)、[Code Mode 基础](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)、[类型化返回约定](../../../.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.md)和[代码运行时 seam](../../code-runtime/README.md)。可以运行 `pnpm run demo:code-mode` 试用。
- **SDK 段**`tools:sdk`,顺序 150一个惰性提示词段,每次组装都会重新生成与所加载运行时语言相符的 SDK 文本。TypeScript 形态发出 `JsonValue`、精确的 `ToolArgsMap` / `ToolOutputMap`、`ToolName`、`ToolCallError` 声明、面向调用作用域可见最终能力的映射 `tools` 命名空间(特殊名称使用带引号的键),以及固定用法说明Python 形态(`ctx.codeRuntime.language === 'python'`)发出等价的具名 `TypedDict` 与一个带相同用法说明的 `tools` 对象。其输出具有确定性:工具按字典序排列;工具集合不变时,文本逐字节相同(有利于前缀 cache。两个代码生成器都已导出且绝不会在提示词组装期间抛出`jsonSchemaToTs` 处理统一 schema 的每种构造并将不受支持的原始构造降级为 `unknown``jsonSchemaToPy` 同理,降级为 `Any`(当某字段名不是合法的 `TypedDict` 属性时,或在 SDK 渲染之外被调用时——`TypedDict` 声明所需的命名上下文由该渲染提供——整个对象降级为 `dict[str, Any]`)。
- **分发桥接层**`run_code` 的 execute每个绑定调用都会在分发前快照为无损 JSON`undefined`、`BigInt`、循环、稀疏数组、`-0` 和特殊对象会使该次调用被拒绝),经由每次运行独有、复用原生并发约定的池调度——调用严格按提交顺序启动,连续的 `isConcurrencySafe` 调用最多可重叠经校验的 `maxParallelSubCalls` 配置个(默认 10设为 `1` 即恢复串行分发),被分类为独占的调用先排空池、单独运行并阻挡其后的调用——以外层执行的不透明 token 作为 `parent`,并经过完整的 pre-execute → guards → execute → post-execute → result 流水线。成功会返回策略处理后的最终规范值;失败以一条消息到达 worker并成为 `ToolCallError(toolName, message)`。每个已启动的子调用在进入流水线时记录一条 `tool/code-dispatch-start` 事件(确定性 id `<parent>:code:<n>`,按提交顺序编号),并以一条携带完整模型可见 `content`/`isError` 结果的 `tool/code-dispatch` 事件完结(采用 `tool/result` 词汇,因此 UI 会沿原生路径呈现子调用——这对事件的 `time` 字段承载每个子调用的计时);因 run 结算而被放弃的排队调用两者都不记录。`deriveMessages()` 既不公开这两个事件也不持久化规范值。token 关联让以提交语义的观察器能够把内部成功延迟到最终 `run_code` 结果,而无需公开实时外层执行;普通工具副作用不会回滚。每个子调用的 `additionalContexts` 条目以及每份含图片的成功最终内容序列都会按分发顺序通过外层 `ToolRunContext` 延迟;循环只在父级 `run_code` 结果之后追加这些上下文,从而保持相邻关系和来源归属,即使程序后来失败也不例外。
- **SDK 段**`tools:sdk`,顺序 150一个在组装时求值的提示词段,每次组装都会重新生成与所加载运行时语言相符的 SDK 文本。TypeScript 形态会生成 `JsonValue`、精确的 `ToolArgsMap` / `ToolOutputMap`、`ToolName`、`ToolCallError` 声明,以及映射调用作用域最终可见工具的 `tools` 命名空间(特殊名称使用带引号的键),并附带固定的使用说明Python 形态(`ctx.codeRuntime.language === 'python'`)发出等价的具名 `TypedDict` 与一个带相同用法说明的 `tools` 对象。其输出具有确定性:工具按字典序排列;工具集合不变时,文本逐字节相同(有利于前缀 cache。两个代码生成器都已导出且绝不会在提示词组装期间抛出`jsonSchemaToTs` 处理统一 schema 的每种构造并将不受支持的原始构造降级为 `unknown``jsonSchemaToPy` 同理,降级为 `Any`(当某字段名不是合法的 `TypedDict` 属性时,或在 SDK 渲染之外被调用时——`TypedDict` 声明所需的命名上下文由该渲染提供——整个对象降级为 `dict[str, Any]`)。
- **分发桥接层**`run_code` 的 execute每个绑定调用都会在分发前快照为无损 JSON`undefined`、`BigInt`、循环、稀疏数组、`-0` 和特殊对象会使该次调用被拒绝),经由每次运行独有、复用原生并发约定的池调度——调用严格按提交顺序启动,连续的 `isConcurrencySafe` 调用最多可重叠经校验的 `maxParallelSubCalls` 配置个(默认 10设为 `1` 即恢复串行分发),被分类为独占的调用先排空池、单独运行并阻挡其后的调用——以外层执行的不透明 token 作为 `parent`,并经过完整的 pre-execute → guards → execute → post-execute → result 流水线。成功会返回策略处理后的最终规范值;失败以一条消息到达 worker并成为 `ToolCallError(toolName, message)`。每个已启动的子调用在进入流水线时记录一条 `tool/code-dispatch-start` 事件(确定性 id `<parent>:code:<n>`,按提交顺序编号),并以一条携带完整模型可见 `content`/`isError` 结果的 `tool/code-dispatch` 事件完结(采用 `tool/result` 词汇,因此 UI 会沿原生路径呈现子调用——这对事件的 `time` 字段承载每个子调用的计时);因 run 结算而被放弃的排队调用两者都不记录。`deriveMessages()` 既不公开这两个事件也不持久化规范值。token 关联使按提交语义工作的观察器可以延后提交内部调用的成功结果,直到最终 `run_code` 结果确定,而无需暴露进行中的外层执行;普通工具副作用不会回滚。每个子调用的 `additionalContexts` 条目以及每份成功且含图片的最终内容序列都会按分发顺序通过外层 `ToolRunContext` 延迟;循环只在父级 `run_code` 结果之后追加这些上下文,从而保持相邻关系和来源归属,即使程序后来失败也不例外。
- **结算纪律**:桥接层拥有一个运行作用域的中止机制;该中止会跟随传入的外层信号,并在运行因任何原因结算时触发,因此预算耗尽会中止正在运行的子工具,而不会将其遗留。桥接层随后会在返回之前排空队列,使每个 `tool/code-dispatch` 都落在仍打开的轮次内。失败的运行会抛出 `CodeRunFailedError``code: 'CODE_RUN_FAILED'`message = 失败类型 + 已捕获日志),流水线会将其转换为模型可据以自我修正的结构化 `isError`。
- **结果大小**:中间绑定值会完整传入 worker 进程,且没有逐绑定字节上限。`run_code` 返回规范的 `{ logs: string[], result?: JsonValue }`;字符串原样呈现,其他所有存在的 JSON 根都通过栈安全的美化 JSON 遍历呈现,总缩进最多为 10 个字符(更深的子树保持紧凑),`null` 保持显式,而缺少 `result` 表示程序返回 `undefined`。worker 可配置的 `maxOutputBytes`(默认 64 MiB只应用于组合序列化后的外层日志数组、完成值或失败消息载荷固定的结果 envelope 语法和呈现空白不计入该上限。无效和超限的完成会明确失败,只有外层结果可以使用普通 spill。
- **结果大小**:中间绑定值会完整传入 worker 进程,且没有逐绑定字节上限。`run_code` 返回规范的 `{ logs: string[], result?: JsonValue }`;字符串原样呈现,其他所有存在的 JSON 根都通过栈安全的美化 JSON 遍历呈现,总缩进最多为 10 个字符(更深的子树保持紧凑),`null` 保持显式,而缺少 `result` 表示程序返回 `undefined`。worker 可配置的 `maxOutputBytes`(默认 64 MiB只应用于组合序列化后的外层日志数组、完成值或失败消息载荷固定的结果封装语法和呈现空白不计入该上限。无效和超限的完成会明确失败,只有这个外层结果可以按常规 spill 机制处理
### 并行执行
@@ -148,14 +148,14 @@ agent loop 将连续的 `parallel` 调用归入有界滚动池,并把每个 `e
#### 模型看到的内容
Code Mode 会公开生成的 [`run_code` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tools)、下方 SDK 说明,以及按所加载运行时语言生成的精确 SDK 块TypeScript 的 `declare const tools` 块,或 Python 的 `tools` 声明)。`both` 会同时公开普通 schema 与此 Code Mode API。在 `code` 下,提示词还会带上 `tools:code-only` 规则,其顺序排在逐工具指导段之前,让模型先读到「可以调用哪些工具」再读「每个工具做什么」;`both` 下它渲染为空。说明与 SDK 块随所加载运行时的语言切换;下方展示 TypeScript 版本(经 [`dsh-code-runtime-worker`](../../code-runtime/code-runtime-worker/README.md)Python 版本(用于任何报告 `language: 'python'` 的运行时)以 Python 语法提供相同操作和类型(`await tools.name(args)`、特殊名称用下标访问、`print(...)` 与顶层 `return`)。
Code Mode 会公开生成的 [`run_code` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tools)、下方 SDK 说明,以及按所加载运行时语言生成的精确 SDK 块TypeScript 的 `declare const tools` 块,或 Python 的 `tools` 声明)。`both` 会同时公开普通 schema 与此 Code Mode API。在 `code` 下,提示词还会带上 `tools:code-only` 规则,其顺序排在逐工具指导段之前,让模型先读到「可以调用哪些工具」再读「每个工具做什么」;`both` 下它渲染为空。说明与 SDK 块随所加载运行时的语言切换;下方展示 TypeScript 版本(经 [`dsh-code-runtime-worker-thread`](../../code-runtime/code-runtime-worker-thread/README.md)Python 版本(用于任何报告 `language: 'python'` 的运行时)以 Python 语法提供相同操作和类型(`await tools.name(args)`、特殊名称用下标访问、`print(...)` 与顶层 `return`)。
##### Code Mode SDK 说明
```markdown
## Writing code for run_code
Pass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program:
`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program:
- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools["my-tool"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.
- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.
@@ -177,7 +177,7 @@ The available tools:
#### 模型看到的内容
循环会保留模型发出的参数和注册表的最终内容。任何抛出或被拒绝的调用都会恰好变为 `Error: <message>`。Code Mode 会渲染外层程序打印的行和返回值;两者都为空时返回 `(run_code completed with no output)`;失败时返回 `Error: code run failed (<kind>): <message>`,并根据是否存在已捕获内容,在其后附加 `Captured output:` 与捕获的行。内部分发事件只保留在日志中,而成功且含图片的子结果会在外层结果之后作为带来源归属的上下文追加后置执行监听器也可以在同一边界追加其他带来源归属的上下文。
循环会保留模型发出的参数和注册表的最终内容。任何抛出异常或遭到拒绝的调用都会转换为确切的 `Error: <message>`。Code Mode 只返回外层程序打印的行和呈现后的返回值;两者都为空时返回 `(run_code completed with no output)`;失败时返回 `Error: code run failed (<kind>): <message>`,并根据是否存在已捕获内容,在其后附加 `Captured output:` 与捕获的行。内部分发事件只保留在日志中成功且含图片的子结果会在外层结果之后作为带来源归属的上下文追加后置执行监听器也可以在同一边界追加其他带来源归属的上下文。
#### Token 影响
@@ -192,7 +192,7 @@ The available tools:
- **并发策略不是事件门禁**`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` 包装层。
- **Code Mode 的 SDK 语言跟随已加载的那个运行时,且呈现方式按 agent 而非按工具**`mode: code`/`both` 会拒绝组装提示词,除非 `ctx.codeRuntime.language` 有已注册的 SDK 渲染器TypeScript 或 Python作用域限制遮蔽与 `presentAs` 会选择每个 agent 的可见绑定及其形态,但在同一个 agent 内不能让一个工具仅使用 Native而另一个仅使用 Code。
- **定义的 `timeoutMs` 仅声明之用**:注册表绝不会强制执行截止时间;要强制执行,必须使用 `@deepseek-ai/dsh-tool-call-timeout-policy` 包装层。
- **Code Mode 的 SDK 语言由当前加载的运行时决定,且呈现方式按 agent 而非按工具**`mode: code`/`both` 会拒绝组装提示词,除非 `ctx.codeRuntime.language` 有已注册的 SDK 渲染器TypeScript 或 Python作用域限制遮蔽与 `presentAs` 会选择每个 agent 的可见绑定及其形态,但在同一个 agent 内不能让一个工具仅使用 Native而另一个仅使用 Code。
- **Code Mode 中间值只存在于执行局部,且没有字节上限**:这些规范的类型化值无法从会话回放重建,并可能耗尽进程或 worker 内存;只有外层 `run_code` 输出受 worker 可配置的硬上限约束。每个子调用的持久日志副本则确实有上限:`tools/code-dispatch-log` waterfall 允许 spill 策略把过大的 `tool/code-dispatch` 内容替换为预览加定位符([原理](../../../.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md))。
- **每次运行都会获得全新的 `run_code` 状态**MVP 不采用持久 REPL 风格内核(跨调用状态不会出现在日志中);参见 [Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)。

View File

@@ -1,9 +1,9 @@
{
"name": "@deepseek-ai/dsh-tools",
"description": "Tool registry and execution pipeline for the DeepSeek Harness",
"version": "0.0.1-rc.2",
"version": "0.1.0-rc.6",
"publishConfig": {
"access": "restricted"
"access": "public"
},
"repository": {
"type": "git",
@@ -39,7 +39,7 @@
"lib/types/**/*.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-code-runtime": "workspace:^",

View File

@@ -12,8 +12,8 @@ import type { CodeBindingFunction, CodeRunResult, CodeRuntime } from '@deepseek-
import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
import type { JsonValue } from '@deepseek-ai/dsh-session'
import { defineTool, parameterSchemaSpecToJsonSchema } from './schema.ts'
import { TOOL_REGISTRY_SCHEDULER } from './index.ts'
import type { CodeDispatchLog, ToolDefinition, ToolExecutionResult, ToolRegistry, ToolRunContext } from './index.ts'
import { TOOL_RUNTIME_SCHEDULER } from './index.ts'
import type { CodeDispatchLog, ToolDefinition, ToolExecutionResult, ToolRuntime, ToolRunContext } from './index.ts'
import type {} from './types.ts'
/** The model-facing name of the Code Mode tool. */
@@ -45,10 +45,12 @@ interface RunCodeFlavor {
*/
const TYPESCRIPT_FLAVOR: RunCodeFlavor = {
description:
'Execute a TypeScript program against the available tools. Write the BODY of an '
+ 'async function (erasable syntax only; top-level `await` and `return` work) and '
+ 'call tools as `await tools.name(args)` per the declarations in the system prompt. '
+ 'Only what you print or return is program output; image-bearing subtool results are attached after the run.',
'Execute a TypeScript program against the available tools. Takes two required '
+ 'arguments: `code`, the BODY of an async function (erasable syntax only; top-level '
+ '`await` and `return` work), and `description`, a short summary of what the program '
+ 'does. Call tools as `await tools.name(args)` per the declarations in the system '
+ 'prompt. Only what you print or return is program output — curate it. Image-bearing '
+ 'subtool results are attached after the run.',
codeDescription: 'The program: the body of an async TypeScript function.',
}
@@ -59,11 +61,12 @@ const TYPESCRIPT_FLAVOR: RunCodeFlavor = {
*/
const PYTHON_FLAVOR: RunCodeFlavor = {
description:
'Execute a Python program against the available tools. Write the BODY of an '
+ 'async function (top-level `await` and `return` work) and call tools as '
'Execute a Python program against the available tools. Takes two required '
+ 'arguments: `code`, the BODY of an async function (top-level `await` and `return` '
+ 'work), and `description`, a short summary of what the program does. Call tools as '
+ '`await tools.name(args)` per the declarations in the system prompt. Use '
+ '`print(...)` and/or `return <value>` for program output; image-bearing '
+ 'subtool results attach after the run.',
+ '`print(...)` and/or `return <value>` for program output — curate it. Image-bearing '
+ 'subtool results are attached after the run.',
codeDescription: 'The program: the body of an async Python function.',
}
@@ -290,7 +293,7 @@ export interface RunCodeBridgeOptions {
* @param options - the registry-private capabilities described above.
* @returns the registry-ready definition.
*/
export function createRunCodeTool(registry: ToolRegistry, options: RunCodeBridgeOptions): ToolDefinition {
export function createRunCodeTool(registry: ToolRuntime, options: RunCodeBridgeOptions): ToolDefinition {
const { requireRuntime, peekRuntime, maxParallel, shapeDispatchLog } = options
const definition = defineTool({
name: RUN_CODE_NAME,
@@ -477,7 +480,7 @@ export function createRunCodeTool(registry: ToolRegistry, options: RunCodeBridge
signal: runController.signal,
}
type DispatchOutcome = { isError: true; message: string } | { isError: false; value: JsonValue }
const scheduler = registry[TOOL_REGISTRY_SCHEDULER]
const scheduler = registry[TOOL_RUNTIME_SCHEDULER]
const outcome = await new Promise<DispatchOutcome>((resolve, reject) => {
// Set by the dispatch stage (or start() for a pre-settled result): what commit() finalizes in submission order.
let parked:

View File

@@ -136,7 +136,7 @@ export type {
declare module '@deepseek-ai/cordis' {
interface Context {
tools: ToolRegistry
tools: ToolRuntime
}
interface Events {
@@ -149,7 +149,7 @@ declare module '@deepseek-ai/cordis' {
* @param exec - the pending call (name, parsed arguments, caller agent).
* @mode waterfall
*/
'tools/pre-execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, next: () => Promise<PreToolDecision>): Promise<PreToolDecision>
'tools/pre-execute'(this: Scoped<ToolRuntime>, exec: ToolExecution, next: () => Promise<PreToolDecision>): Promise<PreToolDecision>
/**
* Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns
* a normalized result; wrappers may change only `exec.signal`, while call
@@ -160,7 +160,7 @@ declare module '@deepseek-ai/cordis' {
* @param exec - the allowed call about to dispatch (name, parsed arguments, caller agent, signal).
* @mode waterfall
*/
'tools/execute'(this: Scoped<ToolRegistry>, exec: ToolDispatchExecution, next: () => Promise<ToolExecutionResult>): Promise<ToolExecutionResult>
'tools/execute'(this: Scoped<ToolRuntime>, 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 waterfall as errors. Async
@@ -172,7 +172,7 @@ declare module '@deepseek-ai/cordis' {
* @param result - the dispatch outcome a listener may accept, replace, or block.
* @mode waterfall
*/
'tools/post-execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, result: Readonly<ToolExecutionResult>, next: () => Promise<PostToolDecision>): Promise<PostToolDecision>
'tools/post-execute'(this: Scoped<ToolRuntime>, exec: ToolExecution, result: Readonly<ToolExecutionResult>, next: () => Promise<PostToolDecision>): Promise<PostToolDecision>
/**
* Allow a listener to replace content in the DURABLE LOG COPY of one
* `run_code` sub-dispatch outcome before the bridge appends its
@@ -186,7 +186,7 @@ declare module '@deepseek-ai/cordis' {
* @param dispatch - the parent execution, sub-call identity, and the settled content to log.
* @mode waterfall
*/
'tools/code-dispatch-log'(this: Scoped<ToolRegistry>, dispatch: CodeDispatchLog, next: () => Promise<ContentBlock[]>): Promise<ContentBlock[]>
'tools/code-dispatch-log'(this: Scoped<ToolRuntime>, dispatch: CodeDispatchLog, next: () => Promise<ContentBlock[]>): Promise<ContentBlock[]>
/**
* Observe the frozen, lossless-JSON final outcome. Listener failures are contained.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): keyed by `exec.agent`.
@@ -194,7 +194,7 @@ declare module '@deepseek-ai/cordis' {
* @param result - a deep-frozen snapshot of the final returned result.
* @mode emit
*/
'tools/result'(this: Scoped<ToolRegistry>, exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): undefined
'tools/result'(this: Scoped<ToolRuntime>, exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): undefined
/**
* A tool was registered or unregistered, or a scoped restriction changed
* (the available tool set changed — possibly for one scope only). An
@@ -247,7 +247,7 @@ export interface ToolDefinition extends ToolSchema {
finalizeContent?(exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): ContentBlock[] | undefined
/**
* Cooperative tool-call timeout budget in milliseconds. Omit for no deadline.
* Enforced by `@deepseek-ai/dsh-timeout-policy` (a `tools/execute` wrapper); it
* Enforced by `@deepseek-ai/dsh-tool-call-timeout-policy` (a `tools/execute` wrapper); it
* is NEVER sent to the model — `schemas()` whitelists only name/description/
* parameters. Declaring it asserts this tool forwards `exec.signal` to a
* cooperative implementation that can reach quiescence when the signal aborts.
@@ -307,7 +307,7 @@ declare const toolExecutionTokenBrand: unique symbol
export type ToolExecutionToken = symbol & { readonly [toolExecutionTokenBrand]: true }
/**
* Caller-supplied description of one tool call. {@link ToolRegistry.execute}
* Caller-supplied description of one tool call. {@link ToolRuntime.execute}
* adds the registry-owned token to form a pipeline {@link ToolExecution};
* callers do not choose that token.
*/
@@ -330,7 +330,7 @@ export interface ToolExecutionInput {
* The token also marks the call as a transport sub-dispatch rather than a
* model-direct call: under `mode: 'code'`, only calls WITH a parent may
* execute a native tool name — a model-direct call (no parent) is denied as
* `UNKNOWN_TOOL` before the policy pipeline. See {@link ToolRegistry.execute}.
* `UNKNOWN_TOOL` before the policy pipeline. See {@link ToolRuntime.execute}.
*/
readonly parent?: ToolExecutionToken
/** Required caller-owned cancellation for this invocation. */
@@ -435,7 +435,7 @@ export type ScheduledToolPreparation =
/**
* Scheduler-only dispatch result. A `post-result` still receives post-execute;
* a `final-result` already matches {@link ToolRegistry.execute} failure semantics.
* a `final-result` already matches {@link ToolRuntime.execute} failure semantics.
* @internal
*/
export type ScheduledToolDispatch =
@@ -444,11 +444,11 @@ export type ScheduledToolDispatch =
/**
* Symbol-keyed scheduler view that keeps pre/post policy ordered while
* overlapping dispatch. Ordinary callers use {@link ToolRegistry.execute};
* overlapping dispatch. Ordinary callers use {@link ToolRuntime.execute};
* this is not a plugin extension point.
* @internal
*/
export interface ToolRegistryScheduler {
export interface ToolRuntimeScheduler {
/** Materialize input, run the ordered pre-execute/guard gate, and decide what stage follows. */
prepare(exec: ToolExecutionInput): Promise<ScheduledToolPreparation>
/** Run only the around-dispatch/body stage. */
@@ -463,7 +463,7 @@ export interface ToolRegistryScheduler {
* Scheduler entry point omitted from the generated named service API.
* @internal
*/
export const TOOL_REGISTRY_SCHEDULER: unique symbol = Symbol('@deepseek-ai/dsh-tools.scheduler')
export const TOOL_RUNTIME_SCHEDULER: unique symbol = Symbol('@deepseek-ai/dsh-tools.scheduler')
/** Canonical error code for cancellation after a tool body was invoked. */
export const TOOL_ABORTED = 'ABORTED'
@@ -784,7 +784,7 @@ function resolveMaxParallelSubCalls(value: number | undefined): number {
* Tool registry and execution pipeline. Scoped registrations shadow globals;
* one visibility resolver feeds presentation, lookup, and dispatch.
*/
export class ToolRegistry extends Service {
export class ToolRuntime extends Service {
static inject = ['systemPrompt']
static Config: z<Config> = z.object({
@@ -793,7 +793,7 @@ export class ToolRegistry extends Service {
})
/** Internal staged view consumed by `dsh-agent-loop`'s parallel scheduler. */
readonly [TOOL_REGISTRY_SCHEDULER]: ToolRegistryScheduler = {
readonly [TOOL_RUNTIME_SCHEDULER]: ToolRuntimeScheduler = {
prepare: exec => this.prepareScheduledExecution(exec),
dispatch: exec => this.dispatchScheduledExecution(exec),
finalize: (exec, result) => this.finalizeScheduledExecution(exec, result),
@@ -948,7 +948,7 @@ export class ToolRegistry extends Service {
if (scopeOf(ctx) === undefined) {
throw new Error('tools.presentAs() requires a scoped context (agent.ctx): a context-global presentation is the `mode` config field on the tools row')
}
const dispose = ctx.effect(function* (this: ToolRegistry) {
const dispose = ctx.effect(function* (this: ToolRuntime) {
yield this.layers.effect(
ctx,
(layer) => {
@@ -1019,7 +1019,7 @@ export class ToolRegistry extends Service {
private requireCodeRuntime(mode: ToolPresentationMode): CodeRuntime {
const runtime = this.ctx.get('codeRuntime')
if (!runtime) {
throw new Error(`dsh-tools: mode "${mode}" requires a code runtime — load a ctx.codeRuntime implementation (e.g. @deepseek-ai/dsh-code-runtime-worker) or set tools mode to "native"`)
throw new Error(`dsh-tools: mode "${mode}" requires a code runtime — load a ctx.codeRuntime implementation (e.g. @deepseek-ai/dsh-code-runtime-worker-thread) or set tools mode to "native"`)
}
if (!Object.hasOwn(SDK_RENDERERS, runtime.language)) {
const known = Object.keys(SDK_RENDERERS).map(name => JSON.stringify(name)).join(', ')
@@ -1314,7 +1314,7 @@ export class ToolRegistry extends Service {
*
* Resolved through {@link modeFor}, NOT `defaultMode`: an agent given `code`
* by an agent preset under a native deployment is the composition
* `dsh-agent-tool-mode` exists for, and reading the deployment default would
* `dsh-agent-tool-presentation` exists for, and reading the deployment default would
* leave exactly that agent uncollapsed — announcing one surface while
* executing another, which is the bypass this collapse closes.
* @param name - the tool name as registered.
@@ -1943,4 +1943,4 @@ function toolAbortedBeforeDispatchResult(prior?: ToolExecutionResult): ToolExecu
}
}
export default ToolRegistry
export default ToolRuntime

View File

@@ -61,7 +61,7 @@ export interface GenericCallView {
kind?: ToolCallKind
/**
* The salient input to show in a detail/expanded view (e.g. a background
* task id). Omit to show nothing; a string renders as-is, an object as pretty
* job id). Omit to show nothing; a string renders as-is, an object as pretty
* JSON. NOT the full raw args object unless that is genuinely what a reader wants.
*/
rawInput?: unknown

View File

@@ -733,7 +733,7 @@ export function jsonSchemaToPy(schema: unknown): string {
/** The fixed model-facing usage contract rendered above the declarations. */
const SDK_INSTRUCTIONS = `## Writing code for run_code
Pass \`run_code\` the body of an async Python function (top-level \`await\` and \`return\` both work). At run time exactly two of the names declared below are bound: \`tools\` and \`ToolCallError\`. Everything else is a STATIC STUB describing argument and return types — in particular the \`TypedDict\` classes do NOT exist at run time, so build arguments as plain \`dict\`/\`list\` JSON values: \`await tools.name({"field": 1})\`, never \`FooArgs(field=1)\`, which raises \`NameError\`. Inside the program:
\`run_code\` takes two required arguments: \`code\` the body of an async Python function (top-level \`await\` and \`return\` both work) — and \`description\`, a short summary of what the program does. At run time exactly two of the names declared below are bound: \`tools\` and \`ToolCallError\`. Everything else is a STATIC STUB describing argument and return types — in particular the \`TypedDict\` classes do NOT exist at run time, so build arguments as plain \`dict\`/\`list\` JSON values: \`await tools.name({"field": 1})\`, never \`FooArgs(field=1)\`, which raises \`NameError\`. Inside the program:
- Call tools as \`await tools.name(args)\` — subscript access for exotic, reserved, or underscore-leading names: \`await tools["my-tool"](args)\`. Every call resolves to the tool's typed canonical JSON value (each method's return type below). Tool arguments must be lossless JSON.
- A FAILED tool call raises \`ToolCallError\`, whose \`toolName\` identifies the failed tool and whose message is human-readable — wrap in \`try/except\` to handle and continue.

View File

@@ -249,7 +249,7 @@ export function jsonSchemaToTs(schema: unknown, indent = 0): string {
/** The fixed model-facing usage contract rendered above the declarations (see the Code Mode Agent Note's "What the model sees"). */
const SDK_INSTRUCTIONS = `## Writing code for run_code
Pass \`run_code\` the body of an async TypeScript function (erasable syntax only — no \`enum\` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program:
\`run_code\` takes two required arguments: \`code\` the body of an async TypeScript function (erasable syntax only — no \`enum\` or namespaces; type annotations are advisory, the code runs type-stripped) — and \`description\`, a short summary of what the program does. Inside the program:
- Call tools as \`await tools.name(args)\` — quoted access for exotic names: \`tools["my-tool"](args)\`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.
- A FAILED tool call rejects with \`ToolCallError\`, whose \`toolName\` identifies the failed tool and whose \`message\` is human-readable — \`try/catch\` it to handle and continue.

View File

@@ -6,7 +6,7 @@ import type { Scope } from '@deepseek-ai/dsh-scope'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
import ToolRegistry, { CodeRunFailedError, RUN_CODE_NAME, TOOL_ABORTED_BEFORE_DISPATCH, defineContentToolFixture, defineTool } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { CodeRunFailedError, RUN_CODE_NAME, TOOL_ABORTED_BEFORE_DISPATCH, defineContentToolFixture, defineTool } from '@deepseek-ai/dsh-tools'
import type { Config, JsonSchemaNode, PostToolDecision, ToolExecutionResult } from '@deepseek-ai/dsh-tools'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
@@ -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
* Service Definition / Service provider / Consumer roles 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. */
@@ -50,7 +50,7 @@ interface SetupOptions {
async function setup(options: SetupOptions = {}) {
const ctx = new Context()
await ctx.plugin(SystemPrompt, { ...options.toolOrder ? { toolOrder: options.toolOrder } : {} })
await ctx.plugin(ToolRegistry, { mode: options.mode ?? 'code', ...options.maxParallelSubCalls !== undefined ? { maxParallelSubCalls: options.maxParallelSubCalls } : {} })
await ctx.plugin(ToolRuntime, { mode: options.mode ?? 'code', ...options.maxParallelSubCalls !== undefined ? { maxParallelSubCalls: options.maxParallelSubCalls } : {} })
let runtime: FakeRuntime | undefined
if (options.runtime !== false) {
await ctx.plugin(FakeRuntime, options.runtime ?? {})
@@ -399,6 +399,10 @@ describe('mode-aware wire contribution', () => {
const runCodeSchema = assembly.tools.find(tool => tool.name === RUN_CODE_NAME)
expect(runCodeSchema?.description).toContain('Execute a TypeScript program')
expect(runCodeSchema?.description).toContain('BODY of an')
// Both required arguments are named here, not only in the parameter
// schema: prose that describes the call as "pass the program" is what
// leads a model to emit `{code}` alone and fail INVALID_ARGS.
expect(runCodeSchema?.description).toContain('`description`')
const codeParam = (runCodeSchema?.parameters as { properties: { code: { description: string } } }).properties.code
expect(codeParam.description).toBe('The program: the body of an async TypeScript function.')
})
@@ -410,6 +414,7 @@ describe('mode-aware wire contribution', () => {
const runCodeSchema = assembly.tools.find(tool => tool.name === RUN_CODE_NAME)
expect(runCodeSchema?.description).toContain('Execute a Python program')
expect(runCodeSchema?.description).toContain('`return <value>`')
expect(runCodeSchema?.description).toContain('`description`')
expect(runCodeSchema?.description).not.toContain('TypeScript')
const codeParam = (runCodeSchema?.parameters as { properties: { code: { description: string } } }).properties.code
expect(codeParam.description).toBe('The program: the body of an async Python function.')
@@ -456,7 +461,7 @@ describe('mode-aware wire contribution', () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
await ctx.plugin(FakeRuntime, {})
const fiber = await ctx.plugin(ToolRegistry, { mode: 'code' })
const fiber = await ctx.plugin(ToolRuntime, { mode: 'code' })
expect(ctx.tools.get(RUN_CODE_NAME)).toBeDefined()
await fiber.dispose()
const assembly = await ctx.systemPrompt.assemble()
@@ -1283,7 +1288,7 @@ describe('the run_code dispatch bridge', () => {
it('executing run_code under a missing runtime is a structured isError, not a crash', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
await ctx.plugin(ToolRegistry, { mode: 'code' })
await ctx.plugin(ToolRuntime, { mode: 'code' })
const result = await runCode(ctx, 'program')
expect(result.isError).toBe(true)
expect((result.content[0] as { text: string }).text).toContain('requires a code runtime')
@@ -1635,21 +1640,21 @@ describe('the run_code dispatch bridge', () => {
it('direct construction rejects a non-positive parallel sub-call cap at load', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
expect(() => new ToolRegistry(ctx, { mode: 'code', maxParallelSubCalls: 0 }))
expect(() => new ToolRuntime(ctx, { mode: 'code', maxParallelSubCalls: 0 }))
.toThrow('maxParallelSubCalls must be a positive integer')
})
it('direct construction in code mode defaults the parallel sub-call cap', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
const registry = new ToolRegistry(ctx, { mode: 'code' })
const registry = new ToolRuntime(ctx, { mode: 'code' })
expect(registry.get(RUN_CODE_NAME)).toBeDefined()
})
it('defaults to native mode under direct construction with no config', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
const registry = new ToolRegistry(ctx)
const registry = new ToolRuntime(ctx)
expect(registry.get(RUN_CODE_NAME)).toBeUndefined()
const assembly = await ctx.systemPrompt.assemble()
expect(assembly.sections.some(section => section.name === 'tools:sdk')).toBe(false)
@@ -1657,7 +1662,7 @@ describe('the run_code dispatch bridge', () => {
it('denies a model-direct native-tool call under code mode as UNKNOWN_TOOL', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
const registry = new ToolRegistry(ctx, { mode: 'code' })
const registry = new ToolRuntime(ctx, { mode: 'code' })
registerEcho(ctx, 'write')
const result = await registry.execute({
signal: testToolSignal,
@@ -1677,7 +1682,7 @@ describe('the run_code dispatch bridge', () => {
it('routes a pre-aborted collapsed call through ABORTED_BEFORE_DISPATCH', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
const registry = new ToolRegistry(ctx, { mode: 'code' })
const registry = new ToolRuntime(ctx, { mode: 'code' })
registerEcho(ctx, 'write')
const aborted = new AbortController()
aborted.abort()
@@ -1748,7 +1753,7 @@ describe('per-agent presentation', () => {
// `native` here, so a collapse predicate reading it instead of this
// scope's effective mode would announce [run_code] and still execute the
// native call — the bypass, reopened for exactly the preset composition
// `dsh-agent-tool-mode` produces.
// `dsh-agent-tool-presentation` produces.
expect(ctx.tools.executionMode({
signal: testToolSignal,
callId: CallId('preset-coded-schedule'),

View File

@@ -4,7 +4,7 @@ import { describe, expect, expectTypeOf, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, {
import ToolRuntime, {
defineContentToolFixture,
type ToolDefinition,
type ToolExecutionInput,
@@ -16,7 +16,7 @@ const testToolSignal = new AbortController().signal
async function setup() {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
return ctx
}
@@ -24,7 +24,7 @@ function exec(name: string, args: unknown): ToolExecutionInput {
return { signal: testToolSignal, callId: CallId('c1'), name, arguments: args }
}
describe('ToolRegistry.executionMode', () => {
describe('ToolRuntime.executionMode', () => {
it('returns parallel only for an explicit true classifier', async () => {
const ctx = await setup()
ctx.tools.register(defineContentToolFixture({

View File

@@ -5,9 +5,11 @@
import { describe, expect, it } from 'vitest'
import {
assertManifestComplete,
assertToolsHarvested,
collectToolCatalog,
render,
type ToolCatalog,
type ToolPackage,
} from '../../../../scripts/gen-tool-catalog.ts'
/** JSON Schema shape enough to reach the values AST extraction can't. */
@@ -23,7 +25,7 @@ describe('gen-tool-catalog collectToolCatalog', () => {
it('boots every shipped tool package and harvests its model-facing schemas', async () => {
const catalog = await collectToolCatalog()
const names = catalog.flatMap(entry => entry.schemas.map(s => s.name)).sort()
expect(names).toEqual(['ask_user_question', 'bash', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'interrupt_agent', 'list_agents', 'lsp', 'pwsh', 'ralph', 'read', 'read_image', 'report', 'run_code', 'schedule_create', 'schedule_delete', 'schedule_list', 'send_message', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'str_replace_editor', 'subagent', 'task_kill', 'task_list', 'task_output', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write'])
expect(names).toEqual(['ask_user_question', 'bash', 'bash', 'cordis_define', 'cordis_inspect_list', 'cordis_inspect_query', 'cordis_inspect_self', 'cordis_run', 'cordis_stop', 'cordis_undefine', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'lsp', 'pwsh', 'ralph', 'read', 'read_image', 'report', 'run_code', 'schedule_create', 'schedule_delete', 'schedule_list', 'send_message', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'str_replace_editor', 'subagent', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write'])
// Every tool carries a JSON-Schema `parameters` object (what the model sees).
for (const entry of catalog) {
for (const schema of entry.schemas) {
@@ -46,7 +48,7 @@ describe('gen-tool-catalog collectToolCatalog', () => {
it('attributes each harvested tool with its registering plugin source', async () => {
const catalog = await collectToolCatalog()
const bash = catalog.find(entry => entry.pkg === '@deepseek-ai/dsh-tool-bash')
expect(bash?.sources.bash).toBe('packages/bash/tool-bash/src/index.ts')
expect(bash?.sources.bash).toBe('packages/shell/tool-bash/src/index.ts')
const control = catalog.find(entry => entry.pkg === '@deepseek-ai/dsh-tool-subagent-control')
expect(control?.sources).toEqual({
interrupt_agent: 'packages/subagent/tool-subagent-control/src/index.ts',
@@ -91,6 +93,29 @@ describe('gen-tool-catalog assertManifestComplete', () => {
})
})
describe('gen-tool-catalog assertToolsHarvested', () => {
const entry: ToolPackage = {
pkg: '@deepseek-ai/dsh-tool-demo',
dir: 'tool-demo',
source: 'packages/demo/tool-demo/src/index.ts',
requires: ['ctx.tools', 'ctx.somethingUnmounted'],
writes: ['tool/result'],
mount: () => Promise.resolve(),
}
it('accepts a boot that registered at least one tool', () => {
expect(() => { assertToolsHarvested(entry, 1) }).not.toThrow()
})
it('throws, naming the package and its requirements, when a boot registers nothing', () => {
// The failure this guards is silent by construction: the package is in the
// manifest, its plugin merely stays PENDING on an unmounted service, and the
// catalog would ship without its tools while every gate stays green.
expect(() => { assertToolsHarvested(entry, 0) }).toThrow(/@deepseek-ai\/dsh-tool-demo booted without registering a single tool/)
expect(() => { assertToolsHarvested(entry, 0) }).toThrow(/ctx.somethingUnmounted/)
})
})
describe('gen-tool-catalog render', () => {
it('emits a package heading, a tool heading, and a json schema fence', () => {
const catalog: ToolCatalog = [

View File

@@ -5,14 +5,14 @@ import { CallId } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
import type { ToolExecution, ToolExecutionResult, ToolExecutionToken } from '@deepseek-ai/dsh-tools'
import * as ToolsInvariant from '@deepseek-ai/dsh-tools/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
const testToolSignal = new AbortController().signal
async function setup(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(ToolsInvariant)
return ctx
}
@@ -219,7 +219,7 @@ describe('tool-pipeline invariants', () => {
content: [{ type: 'text', text: 'ok' }],
})
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await expect(ctx.plugin(ToolsInvariant).then(() => undefined)).resolves.toBeUndefined()
})
@@ -233,7 +233,7 @@ describe('tool-pipeline invariants', () => {
name: 'echo',
arguments: {},
})
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await expect(ctx.plugin(ToolsInvariant).then(() => undefined)).rejects.toThrow(/outside any open turn/)
})
})

View File

@@ -166,6 +166,15 @@ describe('renderToolsSdkPy', () => {
expect(text).toContain('tools: Tools')
})
it('names both required call arguments, not just the program', () => {
// The schema requires `code` AND `description`; instructions that mention
// only the program let a model emit `{code}` alone and fail INVALID_ARGS.
const text = renderToolsSdkPy([bash])
expect(text).toContain('`code`')
expect(text).toContain('`description`')
expect(text).toContain('two required arguments')
})
it('renders required as plain fields and optional as NotRequired, with per-field description comments', () => {
const tool: ToolSdkSchema = {
name: 'search',

View File

@@ -4,7 +4,7 @@ import type { Events } from '@deepseek-ai/cordis'
import { bindScopeParent, createScope } from '@deepseek-ai/dsh-scope'
import type { Scope } from '@deepseek-ai/dsh-scope'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import type { PreToolDecision, ToolDefinition, ToolExecution, ToolExecutionInput, ToolExecutionToken } from '@deepseek-ai/dsh-tools'
import type { Agent } from '@deepseek-ai/dsh-agent'
@@ -17,7 +17,7 @@ const testToolSignal = new AbortController().signal
async function mount(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SystemPrompt, {})
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
return ctx
}

View File

@@ -4,7 +4,7 @@ import { createUserMessage, CallId, HarnessError, type ContentBlock } from '@de
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import type { Agent } from '@deepseek-ai/dsh-agent'
import ApprovalService, { type ApprovalOutcome, type ApprovalRequest } from '@deepseek-ai/dsh-user-approval'
import ToolRegistry, {
import ToolRuntime, {
defineContentToolFixture, defineTool, JsonSchemaError, parameterSchemaSpecToJsonSchema, validateArgs, ToolArgsError, ToolNotFoundError,
TOOL_ABORTED, TOOL_ABORTED_BEFORE_DISPATCH,
type InferArgs, type JsonValue, type ParameterSchemaSpec, type PreToolDecision, type PostToolDecision,
@@ -16,7 +16,7 @@ const testToolSignal = new AbortController().signal
async function setup() {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
return ctx
}
@@ -33,7 +33,7 @@ const echoTool = defineTool({
},
})
describe('ToolRegistry', () => {
describe('ToolRuntime', () => {
it('registers tools, exposes schemas, and feeds the system-prompt assembly', async () => {
const ctx = await setup()
ctx.tools.register(echoTool)
@@ -2449,7 +2449,7 @@ describe('schema DSL optional and nested contracts', () => {
})
})
describe('ToolRegistry.get', () => {
describe('ToolRuntime.get', () => {
it('get() returns the registered tool definition', async () => {
const ctx = await setup()
ctx.tools.register(echoTool)

View File

@@ -148,6 +148,15 @@ describe('renderToolsSdk', () => {
expect(text).toContain('lossless JSON')
})
it('names both required call arguments, not just the program', () => {
// The schema requires `code` AND `description`; instructions that mention
// only the program let a model emit `{code}` alone and fail INVALID_ARGS.
const text = renderToolsSdk([bash])
expect(text).toContain('`code`')
expect(text).toContain('`description`')
expect(text).toContain('two required arguments')
})
it('is deterministic: same tool set, byte-identical text regardless of input order', () => {
expect(renderToolsSdk([bash, exotic])).toBe(renderToolsSdk([exotic, bash]))
// Equal names sort stably (the comparator's equal arm).

View File

@@ -36,7 +36,7 @@
"path": "../../interaction/user-approval"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}