Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input
# Conflicts: # docs/architecture.i18n.yaml # docs/architecture.md # docs/architecture.zh.md # docs/config-catalog.md # docs/core-data-structures/core.i18n.yaml # docs/core-data-structures/llm-streaming.i18n.yaml # docs/module-graph.md # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # packages/README.i18n.yaml # packages/client/connection/src/client/fixture.ts # packages/client/connection/src/index.ts # packages/client/runtime/README.i18n.yaml # packages/client/runtime/README.md # packages/client/runtime/README.zh.md # packages/client/runtime/src/client/sessions/conversation.ts # packages/client/ui-conversation/README.i18n.yaml # packages/client/ui-conversation/src/client/apply.ts # packages/client/ui-conversation/src/client/chat/ChatView.tsx # packages/client/ui-conversation/src/client/chat/MessageItem.tsx # packages/client/ui-conversation/src/client/contract/slots.ts # packages/client/ui-trajectory/tests/views.spec.tsx # packages/compact/compact-basic/README.i18n.yaml # packages/cordis/tool-cordis/src/api-catalog.ts # packages/host/apiproxy/src/api-proxy.ts # packages/host/apiproxy/src/api/index.ts # packages/host/apiproxy/src/api/sessions.ts # packages/host/apiproxy/src/index.ts # packages/host/apiproxy/tests/fetch-carrier.spec.ts # packages/llm/llm-deepseek/src/adapter.ts # packages/llm/llm-deepseek/tests/adapter.spec.ts # packages/llm/llm-deepseek/tests/serialize.spec.ts # packages/llm/llm-pi-ai/README.i18n.yaml # packages/llm/llm-pi-ai/src/adapter.ts # packages/llm/llm-pi-ai/src/index.ts # packages/llm/llm-pi-ai/tests/adapter.spec.ts # packages/llm/llm/src/types.ts # packages/ui/tui/README.i18n.yaml # packages/ui/tui/src/index.ts # packages/ui/tui/tests/tui.spec.ts
This commit is contained in:
@@ -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
|
||||
README.md: b3a70b07c57ae1a2e3dd975a64c840d90c5a84ad
|
||||
README.zh.md: 5832310cfe14a299ac5998a28bcbbb500caf86b5
|
||||
# pnpm run verify-translation-pairing --write packages/cordis/README.md
|
||||
README.md: a47b9ba20789bb6b9a36b1af9b3942b90e61b365
|
||||
README.zh.md: cc91e68f0dfa9fb343c332eba2579c2a077beb64
|
||||
|
||||
@@ -6,4 +6,4 @@ Model-facing tools over the live cordis runtime the agent itself runs inside: in
|
||||
|
||||
| Package | Role | ctx key |
|
||||
|---|---|---|
|
||||
| [`tool-cordis/`](tool-cordis/README.md) | The `cordis_inspect` / `cordis_mount` / `cordis_unmount` tools: read the runtime, evaluate model-written plugin code in a `node:vm` sandbox, and manage the dynamic mounts under one group fiber | registers on `ctx.tools` |
|
||||
| [`tool-cordis/`](tool-cordis/README.md) | The `cordis_inspect` / `cordis_mount` / `cordis_unmount` tools: read the current-process runtime and manage in-memory temporary Plugins under one owned group fiber | registers on `ctx.tools` |
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
面向模型、作用于 agent(智能体)自身所在实时 Cordis 运行时的工具:检查已加载插件与服务接口、挂载模型编写的插件,以及再次释放这些插件。设计归档见[工具集 Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)。
|
||||
面向模型、作用于 agent(智能体)所在实时 Cordis 运行时的工具:检查当前 DSH 进程,并挂载或卸载仅存于内存的临时 Plugin。设计归档见[工具集 Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)。
|
||||
|
||||
| 包(package) | 角色 | ctx 键 |
|
||||
|---|---|---|
|
||||
| [`tool-cordis/`](tool-cordis/README.md) | `cordis_inspect`/`cordis_mount`/`cordis_unmount` 工具:读取运行时、在 `node:vm` 沙箱中求值模型编写的插件代码,并在同一个分组 fiber 下管理动态挂载 | 注册到 `ctx.tools` |
|
||||
| [`tool-cordis/`](tool-cordis/README.md) | `cordis_inspect`/`cordis_mount`/`cordis_unmount` 工具:读取当前进程运行时,并在一个自有分组 fiber 下管理临时 Plugin | 注册到 `ctx.tools` |
|
||||
|
||||
@@ -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
|
||||
README.md: 022e25decad650e03aa621cfa2b3f33ccc8d9743
|
||||
README.zh.md: 99a79209a2a895ee8e11e3b7345a305da514ac1e
|
||||
# pnpm run verify-translation-pairing --write packages/cordis/tool-cordis/README.md
|
||||
README.md: 5b58e665dae95aea0d0ad094238fef5d3dc0fb97
|
||||
README.zh.md: 11e5be11dca84247b19888fefa7d70e5d74da174
|
||||
|
||||
@@ -2,17 +2,19 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The self-referential cordis toolset: three model-facing tools over the live runtime the agent runs inside. Design home — sandbox semantics, mount lifecycle, cross-mount composition, the generated API catalog, standing decisions: [the toolset Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md).
|
||||
The self-referential Cordis toolset: three model-facing tools over the live runtime in the current DSH process. Design home — sandbox semantics, temporary-plugin lifecycle and composition, the generated API catalog, standing decisions: [the toolset Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md).
|
||||
|
||||
## What it does
|
||||
|
||||
- `cordis_inspect` — read-only report over the runtime: services, the loaded-plugin list, registered tools, the dynamic-mount table, and the catalog-backed `api` / `events` references. An exact `name` with `what: "api"` or `what: "events"` narrows the report and adds the original source JSDoc.
|
||||
- `cordis_mount` — evaluates model-written JavaScript (the body of an async function) in a `node:vm` sandbox; the code must `return` a cordis plugin, which is mounted under the `cordis-dynamic` group fiber and tracked as `dyn-<n>`.
|
||||
- `cordis_unmount` — disposes one mount by id, returning only after quiescence.
|
||||
- `cordis_inspect` — read-only report over the current process: services, all live plugin fibers, registered tools, the `cordis_mount` temporary-Plugin subset, and the catalog-backed `api` / `events` references. An exact `name` with `what: "api"` or `what: "events"` narrows the report and adds the original source JSDoc.
|
||||
- `cordis_mount` — evaluates model-written JavaScript now and saves it nowhere; the code must return an in-memory temporary Plugin tracked as `dyn-<n>`.
|
||||
- `cordis_unmount` — unmounts one `dyn-<n>` temporary Plugin and returns only after its owned effects reach quiescence. It cannot remove Loader, configured, or installed Plugins.
|
||||
|
||||
Exact model-facing schemas: [the generated tool catalog](../../../docs/tool-catalog.md).
|
||||
|
||||
Canonical successes are the inspection string, mount `{ id, pluginName, state, provides, waitingFor }`, and unmount `{ id, pluginName }`. Native renderers preserve the existing prose, so programs can use `mounted.id` while ordinary function calling still sees `mounted dyn-1 (...)`.
|
||||
Canonical successes are the inspection string, mount `{ id, pluginName, state, provides, waitingFor }`, and unmount `{ id, pluginName }`. Native rendering says whether the temporary Plugin is running or pending and that it remains available until unmounted or DSH restarts; unmount confirms that it was removed.
|
||||
|
||||
Temporary Plugins live only in the shared DSH process memory. They remain active across later turns and may affect other sessions in that process, but disappear after `cordis_unmount`, toolset unload, or DSH restart. They create no Plugin file, install no package, change no `cordis.yml` or personal/project configuration, do not survive restart, and cannot be promoted automatically. To keep an experiment, ask the Agent to implement a normal local, project, or repository Plugin through the regular development workflow.
|
||||
|
||||
## Trust stance
|
||||
|
||||
@@ -22,7 +24,7 @@ The sandbox isolates globals but is not a security boundary. Node globals are ab
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `vmTimeoutMs` | `5000` | Bound on the SYNCHRONOUS portion of mount-code evaluation; an async body escapes it |
|
||||
| `vmTimeoutMs` | `5000` | Bound on the SYNCHRONOUS portion of temporary-Plugin code evaluation; an async body escapes it |
|
||||
|
||||
## The generated API catalog
|
||||
|
||||
@@ -30,7 +32,7 @@ The sandbox isolates globals but is not a security boundary. Node globals are ab
|
||||
|
||||
## Rendering
|
||||
|
||||
All three tools render `generic` cards (`read` / `execute` / `delete`); `cordis_mount` carries the mount code as `rawInput`. Presenters are pure functions of the args; results keep the default text rendering.
|
||||
All three tools render `generic` cards (`read` / `execute` / `delete`); `cordis_mount` carries the temporary-Plugin code as `rawInput`. Presenters are pure functions of the args; results keep the default text rendering.
|
||||
|
||||
## Export shape
|
||||
|
||||
@@ -56,7 +58,7 @@ Prefix-stable while this tool view is unchanged. Scoping or plugin lifecycle cha
|
||||
|
||||
#### What the model sees
|
||||
|
||||
Inspect joins selected sections exactly as `## <section>` then a newline and the data-dependent body, with one blank line between sections. Its broad API/event reports omit JSDoc; `name` with `what: "api"` or `what: "events"` returns one exact target with its original JSDoc. Mount returns `mounted <id> (plugin "<name>", state: <state>)`, optionally inserting ` — waiting for service(s): <names> (activates when provided)` before the closing parenthesis. Unmount returns `unmounted <id> (plugin "<name>")`; an unknown id becomes `Error: no dynamic plugin with id "<id>" (list mounts with cordis_inspect what:"dynamic")`. The submitted mount program remains in the assistant tool-call history.
|
||||
Inspect joins selected sections exactly as `## <section>` then a newline and the data-dependent body, with one blank line between sections; `what: "temporary"` uses the `## Temporary Plugins` heading. Each temporary-Plugin row reports running/pending state, provided and awaited services, and its lifetime until unmounted or DSH restart. The empty state explains that `cordis_mount` Plugins disappear on restart. Broad API/event reports omit JSDoc; `name` with `what: "api"` or `what: "events"` returns one exact target with its original JSDoc. Mount returns `Temporary Plugin <id> is running (...)` or `Temporary Plugin <id> is pending (...)`; unmount returns `Temporary Plugin <id> was unmounted and removed.` The submitted program remains in assistant tool-call history.
|
||||
|
||||
#### Token effect
|
||||
|
||||
@@ -66,19 +68,19 @@ Inspect output and mount code are data-dependent and resent until compaction; li
|
||||
|
||||
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
||||
|
||||
### Later requests after a mount
|
||||
### Later requests after cordis_mount
|
||||
|
||||
#### What the model sees
|
||||
|
||||
A mounted plugin may register tools, prompt contributions, or listeners that change later requests for the scopes it targets; unmount removes those contributions after quiescence.
|
||||
A temporary Plugin may register tools, prompt contributions, or listeners that change later requests for the scopes it targets; `cordis_unmount` removes those contributions after quiescence.
|
||||
|
||||
#### Token effect
|
||||
|
||||
Indirect token impact equals the mounted plugin's contributions and lasts only for the mount lifetime.
|
||||
Indirect token impact equals the temporary Plugin's contributions and lasts only for its process-local lifetime.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
Mounting or unmounting a prompt or tool contribution changes later request prefixes and may invalidate reuse from the first changed contribution; an unchanged mount set remains prefix-stable.
|
||||
Mounting or unmounting a prompt or tool contribution changes later request prefixes and may invalidate reuse from the first changed contribution; an unchanged temporary-Plugin set remains prefix-stable.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
|
||||
@@ -2,17 +2,19 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
自引用 cordis 工具集:三个面向模型的工具,操作 agent 所处的存活运行时。设计归属(沙箱语义、挂载生命周期、跨挂载组合、生成的 API 目录、既定决策)见[工具集 Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)。
|
||||
自引用 Cordis 工具集:三个面向模型的工具,操作当前 DSH 进程中的存活运行时。设计归属(沙箱语义、临时 Plugin 生命周期与组合、生成的 API 目录、既定决策)见[工具集 Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)。
|
||||
|
||||
## 功能
|
||||
|
||||
- `cordis_inspect`:运行时的只读报告,包括服务、已加载插件列表、已注册工具、动态挂载表,以及目录支持的 `api`/`events` 参考。精确的 `name` 配合 `what: "api"` 或 `what: "events"` 可缩窄报告,并附上原始源代码 JSDoc。
|
||||
- `cordis_mount`:在 `node:vm` 沙箱中求值模型编写的 JavaScript(一个 async 函数的主体);代码必须 `return` 一个 cordis 插件,系统将其挂载在 `cordis-dynamic` 分组 fiber 下,并以 `dyn-<n>` 跟踪。
|
||||
- `cordis_unmount`:按 id 释放一项挂载,只在完全停稳后返回。
|
||||
- `cordis_inspect`:当前进程运行时的只读报告,包括服务、全部存活 Plugin fiber、已注册工具、`cordis_mount` 临时 Plugin 子集,以及目录支持的 `api`/`events` 参考。精确的 `name` 配合 `what: "api"` 或 `what: "events"` 可缩窄报告,并附上原始源代码 JSDoc。
|
||||
- `cordis_mount`:立即求值模型编写的 JavaScript 且不保存到任何位置;代码必须返回一个以 `dyn-<n>` 跟踪、仅存于内存的临时 Plugin。
|
||||
- `cordis_unmount`:卸载一个 `dyn-<n>` 临时 Plugin,并只在其自有效果完全停稳后返回;它不能删除 Loader、配置或已安装的 Plugin。
|
||||
|
||||
精确的面向模型 schema 见[生成的工具目录](../../../docs/tool-catalog.md)。
|
||||
|
||||
规范成功值分别为检查字符串、挂载 `{ id, pluginName, state, provides, waitingFor }`,以及卸载 `{ id, pluginName }`。原生 renderer 保留现有文本,因此程序可以使用 `mounted.id`,普通 Function Calling 仍会看到 `mounted dyn-1 (...)`。
|
||||
规范成功值分别为检查字符串、挂载 `{ id, pluginName, state, provides, waitingFor }`,以及卸载 `{ id, pluginName }`。原生 renderer 会说明临时 Plugin 正在运行还是等待中,并说明它可用至被卸载或 DSH 重启;卸载结果确认它已移除。
|
||||
|
||||
临时 Plugin 只存在于共享 DSH 进程内存中。它可跨后续 turn 保持活跃,也可能影响同一进程中的其他 session,但会在 `cordis_unmount`、工具集卸载或 DSH 重启后消失。它不会创建 Plugin 文件、安装 package、修改 `cordis.yml` 或个人/项目配置、跨重启存续,也不能自动转为正式 Plugin。若要保留实验结果,应让 Agent 通过常规开发流程实现普通的本地、项目或仓库 Plugin。
|
||||
|
||||
## 信任立场
|
||||
|
||||
@@ -22,7 +24,7 @@
|
||||
|
||||
| 字段 | 默认值 | 含义 |
|
||||
|---|---|---|
|
||||
| `vmTimeoutMs` | `5000` | 挂载代码求值中同步部分的边界;async 主体可逃出该边界 |
|
||||
| `vmTimeoutMs` | `5000` | 临时 Plugin 代码求值中同步部分的边界;async 主体可逃出该边界 |
|
||||
|
||||
## 生成的 API 目录
|
||||
|
||||
@@ -30,7 +32,7 @@
|
||||
|
||||
## 渲染
|
||||
|
||||
三个工具都渲染 `generic` 卡片(`read`/`execute`/`delete`);`cordis_mount` 以 `rawInput` 携带挂载代码。presenter 是 args 的纯函数;结果保留默认文本渲染。
|
||||
三个工具都渲染 `generic` 卡片(`read`/`execute`/`delete`);`cordis_mount` 以 `rawInput` 携带临时 Plugin 代码。presenter 是 args 的纯函数;结果保留默认文本渲染。
|
||||
|
||||
## 导出形状
|
||||
|
||||
@@ -56,7 +58,7 @@ Namespace 插件:命名导出 `name`/`inject`/`Config`/`apply`,无默
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
检查会精确地用 `## <section>` 加换行及数据相关主体来拼接选中区段,各区段之间留一个空行。宽泛的 API/事件报告省略 JSDoc;`name` 配合 `what: "api"` 或 `what: "events"` 返回一个精确目标及其原始 JSDoc。挂载返回 `mounted <id> (plugin "<name>", state: <state>)`,并可在右括号前插入 ` — waiting for service(s): <names> (activates when provided)`。卸载返回 `unmounted <id> (plugin "<name>")`;未知 id 会变成 `Error: no dynamic plugin with id "<id>" (list mounts with cordis_inspect what:"dynamic")`。提交的挂载程序保留在 assistant 工具调用历史中。
|
||||
检查会精确地用 `## <section>` 加换行及数据相关主体来拼接选中区段,各区段之间留一个空行;`what: "temporary"` 使用 `## Temporary Plugins` 标题。每个临时 Plugin 行都会报告 running/pending 状态、提供与等待的服务,以及持续至卸载或 DSH 重启的生命周期;空状态说明 `cordis_mount` Plugin 会在重启时消失。宽泛的 API/事件报告省略 JSDoc;`name` 配合 `what: "api"` 或 `what: "events"` 返回一个精确目标及其原始 JSDoc。挂载返回 `Temporary Plugin <id> is running (...)` 或 `Temporary Plugin <id> is pending (...)`;卸载返回 `Temporary Plugin <id> was unmounted and removed.`。提交的程序保留在 assistant 工具调用历史中。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
@@ -66,19 +68,19 @@ Namespace 插件:命名导出 `name`/`inject`/`Config`/`apply`,无默
|
||||
|
||||
仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV-cache 配置项失效。
|
||||
|
||||
### 挂载后的后续请求
|
||||
### cordis_mount 后的后续请求
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
已挂载插件可以注册工具、提示词贡献或监听器,改变其目标 scope 的后续请求;卸载会在完全停稳后移除这些贡献。
|
||||
临时 Plugin 可以注册工具、提示词贡献或监听器,改变其目标 scope 的后续请求;`cordis_unmount` 会在完全停稳后移除这些贡献。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
间接 token 影响等于已挂载插件的贡献,且只在挂载生命周期内持续。
|
||||
间接 token 影响等于临时 Plugin 的贡献,且只在其进程内生命周期内持续。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
挂载或卸载提示词/工具贡献会改变后续请求前缀,并可能使从第一个变化的贡献起的复用失效;挂载集合不变时,前缀保持稳定。
|
||||
挂载或卸载提示词/工具贡献会改变后续请求前缀,并可能使从第一个变化的贡献起的复用失效;临时 Plugin 集合不变时,前缀保持稳定。
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
},
|
||||
{
|
||||
signature: 'async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>',
|
||||
jsDoc: '/**\n * Create an owned agent on a caller-supplied session id.\n * @param ownerCtx - caller context that structurally owns the transaction.\n * @param options - identities, session seed/metadata, loop options, setup, and cancellation.\n * @returns the published handle.\n */',
|
||||
jsDoc: '/**\n * Create an owned agent on a caller-supplied session id.\n * @param ownerCtx - caller context that structurally owns the lifecycle.\n * @param options - identities, session seed/metadata, loop options, setup, and cancellation.\n * @returns the published handle.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>',
|
||||
@@ -394,17 +394,29 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
signature: 'listProviders(): LlmProviderInfo[]',
|
||||
jsDoc: '/**\n * Describe provider routes with a registered adapter.\n * @returns detached provider metadata in registration order.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'providerRetryPolicy(provider: string): ResolvedRetryPolicy',
|
||||
jsDoc: '/**\n * Resolve the retry policy captured when one provider route was registered.\n * @param provider - registered provider route to inspect.\n * @returns the provider-owned policy, with normal defaults already resolved.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'async listModels(provider: string): Promise<LlmModelInfo[]>',
|
||||
jsDoc: '/**\n * Discover models advertised by one registered provider. Catalog membership\n * is advisory and never changes routing or request validation.\n * @param provider - registered provider route to inspect.\n * @returns detached model metadata in adapter-preferred order.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'async resolveModelContext( provider: string, model: string, ): Promise<LlmModelContext | undefined>',
|
||||
jsDoc: '/**\n * Resolve context capacity from the adapter that owns one exact route.\n * This query is independent of the advisory model catalog: an unlisted model\n * may return metadata, while `undefined` never rejects later routing.\n * @param provider - registered provider route to inspect.\n * @param model - exact model id passed to the adapter.\n * @returns detached context metadata, or `undefined` when the adapter has none.\n */',
|
||||
signature: 'async resolveModelInfo( provider: string, model: string, signal?: AbortSignal, ): Promise<LlmResolvedModelInfo>',
|
||||
jsDoc: '/**\n * Resolve and validate all metadata from the adapter that owns one exact\n * route. The result is detached from adapter-owned objects; catalog\n * membership remains advisory and does not control request routing.\n * @param provider - registered provider route to inspect.\n * @param model - exact model id passed to the adapter.\n * @param signal - optional cancellation for adapter-owned asynchronous lookup.\n * @returns exact model identity plus available context and reasoning metadata.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<LlmCallConfig>',
|
||||
jsDoc: '/**\n * Validate a conversation call config against its exact model capability and\n * materialize an adapter-configured default. Unsupported explicit efforts\n * reject before provider I/O; no clamping or aliasing is performed. This\n * standalone query does not bind a later dispatch; use {@link prepareCall}\n * when logging and streaming must share one adapter registration.\n * @param config - provider/model route and optional request controls.\n * @param signal - optional cancellation for adapter-owned capability lookup.\n * @returns a detached config only when a default must be materialized.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>',
|
||||
jsDoc: '/**\n * Resolve one call under its current adapter registration. The returned\n * one-shot handle keeps that registration across header logging and dispatch,\n * so HMR cannot combine one adapter\'s capability result with another adapter.\n * @param config - provider/model route and optional request controls.\n * @param signal - optional cancellation for adapter-owned capability lookup.\n * @returns a prepared config and its registration-bound stream entry point.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'stream(options: GenerateOptions): AsyncIterable<StreamChunk>',
|
||||
jsDoc: '/**\n * Stream one model call as raw chunks (token-level deltas). Throws\n * `LlmError` with code `NO_ADAPTER` if no adapter is registered for\n * `options.provider`. Replay state is retained only when the same adapter\n * instance owns its historical provider and the target provider. Final\n * adapter selection, dispatch, and iteration failures retain their original\n * Error identity and are tagged in a call-local scope for narrow agent-loop\n * request recovery; middleware and nested-call failures remain untagged for\n * the outer call.\n * @param options - the full request; `options.provider` selects the adapter.\n * @returns the chunk stream, possibly wrapped by `llm/stream` listeners.\n */',
|
||||
jsDoc: '/**\n * Stream one model call as raw chunks (token-level deltas). Throws\n * `LlmError` with code `NO_ADAPTER` if no adapter is registered for\n * `options.provider`. Replay state is retained only when the same adapter\n * instance owns its historical provider and the target provider. Final\n * adapter selection remains fixed through asynchronous exact-model resolution\n * and dispatch. Selection, dispatch, and iteration failures retain their\n * original Error identity and are tagged in a call-local scope for narrow\n * agent-loop request recovery; middleware and nested-call failures remain\n * untagged for the outer call.\n * @param options - the full request; `options.provider` selects the adapter.\n * @returns the chunk stream, possibly wrapped by `llm/stream` listeners.\n */',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -440,7 +452,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
},
|
||||
{
|
||||
signature: 'set(agent: Agent, active: boolean): void',
|
||||
jsDoc: '/**\n * Select whether plan mode should be active from the next turn boundary.\n * Repeated selection of the current or already-pending state is a no-op.\n *\n * @param agent The agent to switch.\n * @param active Whether plan mode should be active.\n */',
|
||||
jsDoc: '/**\n * Select whether plan mode should be active from the next request boundary.\n * Repeated selection of the current or already-pending state is a no-op.\n *\n * @param agent The agent to switch.\n * @param active Whether plan mode should be active.\n */',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -608,11 +620,11 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
methods: [
|
||||
{
|
||||
signature: 'async listCandidates( agent: Agent, query = \'\', limit = this.config.candidateLimit, signal?: AbortSignal, ): Promise<SessionReferenceCandidate[]>',
|
||||
jsDoc: '/**\n * List reference candidates, ranked by working-directory affinity.\n * @param agent - target agent; self is excluded and its cwd drives ranking.\n * @param query - optional case-insensitive session-id/cwd substring.\n * @param limit - optional positive result cap.\n * @param signal - optional cancellation boundary for host autocomplete teardown.\n * @returns candidates labeled by latest title or, when absent, session id.\n */',
|
||||
jsDoc: '/**\n * List reference candidates, ranked by working-directory affinity.\n * @param agent - target agent; self is excluded and its cwd drives ranking.\n * @param query - optional case-insensitive session-id/cwd/title substring.\n * @param limit - optional positive result cap.\n * @param signal - optional cancellation boundary for host autocomplete teardown.\n * @returns candidates labeled by latest title or, when absent, session id.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise<PreparedReferencedMessage>',
|
||||
jsDoc: '/**\n * Snapshot all references before enqueue and return one aggregated durable context.\n * @param agent - target agent; references to it are rejected.\n * @param content - already host-normalized readable message content.\n * @param references - structured source sessions in mention order.\n * @param signal - optional cancellation boundary for host request teardown.\n * @returns detached content and zero or one prepared contexts.\n */',
|
||||
jsDoc: '/**\n * Snapshot all references before enqueue and return one aggregated durable context.\n * @param agent - target agent; references to it are rejected.\n * @param content - already host-normalized readable message content.\n * @param references - structured source sessions in mention order.\n * @param signal - optional cancellation boundary for host request teardown.\n * @returns detached content and optional referenced-session context.\n */',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -762,6 +774,16 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'subprocess',
|
||||
summary: 'Abstract subprocess service.',
|
||||
methods: [
|
||||
{
|
||||
signature: 'abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle',
|
||||
jsDoc: '/**\n * Start one managed child process from a fully-specified spec; this seam\n * applies no defaults.\n * @param spec - argv, directory, stdio dispositions, grace, cancellation, and environment.\n * @returns the live process handle (streams/readers, signalling, outcome promise).\n */',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'systemPrompt',
|
||||
summary: 'Registry service for the prompt inputs assembled before each model step.',
|
||||
@@ -822,6 +844,24 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'telemetry',
|
||||
summary: 'The backend contract in its loadable form: one implementation per context — the cordis `Service` registration under the `telemetry` key throws on a duplicate, cordis\' standard behavior.',
|
||||
methods: [
|
||||
{
|
||||
signature: 'abstract emit(record: TelemetryRecord): void',
|
||||
jsDoc: '/**\n * See {@link TelemetryBackend.emit} — the seam declaration is the contract\'s one home.\n * @param record - the logical record to report; owned by the backend after the call.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'flush?(): void',
|
||||
jsDoc: '/** See {@link TelemetryBackend.flush}. */',
|
||||
},
|
||||
{
|
||||
signature: 'abstract shutdown(): Promise<void>',
|
||||
jsDoc: '/**\n * See {@link TelemetryBackend.shutdown}.\n * @returns resolves when the backend\'s pipeline has quiesced.\n */',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'tokenMeter',
|
||||
summary: 'Replay owner for one service-wide estimator and isolated per-session folds.',
|
||||
@@ -960,6 +1000,10 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
signature: 'list(): Workspace[]',
|
||||
jsDoc: '/**\n * Synchronous workspace projection in durable registry order. Every\n * entity\'s `sessionIds` getter is already filtered by the startup/live\n * canonical-cwd header index; this method performs no persistence reads.\n * @returns a fresh ordered array of workspace entities.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'delete(id: WorkspaceId): Promise<boolean>',
|
||||
jsDoc: '/**\n * Delete one workspace registration while retaining its directory and every\n * session log. The durable order is updated before the table deletion; a\n * failed table write restores the prior order and keeps the entity\n * published. Unknown ids are an idempotent no-op for domain callers.\n * @param id - Workspace registration to remove.\n * @returns `true` when a record was deleted, `false` when it was unknown.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'async resolveByPath(path: string): Promise<Workspace | undefined>',
|
||||
jsDoc: '/**\n * Resolve by canonical directory path without creating or mutating a\n * workspace. A missing path rejects during `realpath`; an existing unowned\n * directory returns `undefined`.\n * @param path - Existing directory path in any spelling.\n * @returns the workspace owning the canonical path, when one exists.\n */',
|
||||
@@ -981,8 +1025,8 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
name: 'agent/cancel-requested',
|
||||
mode: 'emit',
|
||||
signature: '\'agent/cancel-requested\'(this: Scoped<Agent>, agent: Agent, cause: AgentCancelCause): void',
|
||||
jsDoc: '/**\n * Effective broad cancellation was requested, before queued/steering work\n * is cleared or the active turn is aborted. This observe-only notification\n * cannot veto cancellation; listener failures are contained.\n * @param agent - the agent whose current work is being cancelled.\n * @param cause - resolved typed cancellation cause, including the default.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
summary: 'Effective broad cancellation was requested, before queued/steering work is cleared or the active turn is aborted.',
|
||||
jsDoc: '/**\n * Effective broad cancellation was requested, before queued/outbox work\n * is cleared or the active turn is aborted. This observe-only notification\n * cannot veto cancellation; listener failures are contained.\n * @param agent - the agent whose current work is being cancelled.\n * @param cause - the explicit typed cancellation cause.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
summary: 'Effective broad cancellation was requested, before queued/outbox work is cleared or the active turn is aborted.',
|
||||
},
|
||||
{
|
||||
name: 'agent/created',
|
||||
@@ -995,14 +1039,14 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
name: 'agent/disposed',
|
||||
mode: 'emit',
|
||||
signature: '\'agent/disposed\'(this: Scoped<Agent>, agent: Agent): void',
|
||||
jsDoc: '/**\n * An agent left the registry; AgentLoop emits this after driver quiescence\n * but before session detachment and scoped-registration unwind. Custom\n * registry users own their driver-ordering contract.\n * @param agent - the exact agent removed from the registry.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
summary: 'An agent left the registry; AgentLoop emits this after driver quiescence but before session detachment and scoped-registration unwind.',
|
||||
jsDoc: '/**\n * An agent left the registry; AgentLoop emits this after driver quiescence\n * and scoped-registration unwind, but before session detachment. Custom\n * registry users own their driver-ordering contract.\n * @param agent - the exact agent removed from the registry.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
summary: 'An agent left the registry; AgentLoop emits this after driver quiescence and scoped-registration unwind, but before session detachment.',
|
||||
},
|
||||
{
|
||||
name: 'agent/error',
|
||||
mode: 'emit',
|
||||
signature: '\'agent/error\'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: Error): void',
|
||||
jsDoc: '/**\n * A step or turn errored. The loop reports a failure here (plus the logger)\n * even when the error has no in-turn position for a session `error` event.\n * @param agent - the agent whose turn errored.\n * @param turn - the turn in which the failure surfaced.\n * @param step - the step at which the failure surfaced.\n * @param error - the failure, verbatim.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
signature: '\'agent/error\'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: unknown): void',
|
||||
jsDoc: '/**\n * A step or turn errored. The machine reports a failure here (plus the\n * logger) even when the error has no in-turn position for a durable record.\n * @param agent - the agent whose turn errored.\n * @param turn - the turn in which the failure surfaced.\n * @param step - the step at which the failure surfaced.\n * @param error - the failure, verbatim.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
summary: 'A step or turn errored.',
|
||||
},
|
||||
{
|
||||
@@ -1016,57 +1060,36 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
name: 'agent/inbox/discard',
|
||||
mode: 'emit',
|
||||
signature: '\'agent/inbox/discard\'(this: Scoped<Agent>, agent: Agent, messages: AgentMessage[]): void',
|
||||
jsDoc: '/**\n * Pending inbox items were dropped without delivering them, so every\n * enqueued id receives exactly one terminal `agent/inbox/dequeue` OR\n * `agent/inbox/discard`. Emitters: `cancel()` without `keepInbox` (after\n * `agent/cancel-requested`, before the abort); a terminal `agent/turn-stop`\n * dropping pending steering (in-turn and on the post-turn late-steering\n * drain); and disposal of any still-pending items (before\n * `agent/status(\'disposed\')`). Fires once per drop with every dropped item.\n * @param agent - the agent whose inbox items were dropped.\n * @param messages - the discarded messages in FIFO order (queued then steering); never empty.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
jsDoc: '/**\n * Pending inbox items were dropped without delivering them, so every\n * enqueued id receives exactly one terminal `agent/inbox/dequeue` OR\n * `agent/inbox/discard`. `cancel()` without `keepInbox`, including disposal,\n * emits this after `agent/cancel-requested` when applicable and before\n * aborting the active work. Fires once per drop with every dropped item.\n * @param agent - the agent whose inbox items were dropped.\n * @param messages - the discarded messages in FIFO order (queued then steering); never empty.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
summary: 'Pending inbox items were dropped without delivering them, so every enqueued id receives exactly one terminal `agent/inbox/dequeue` OR `agent/inbox/discard`.',
|
||||
},
|
||||
{
|
||||
name: 'agent/inbox/enqueue',
|
||||
mode: 'emit',
|
||||
signature: '\'agent/inbox/enqueue\'(this: Scoped<Agent>, agent: Agent, message: AgentMessage): void',
|
||||
jsDoc: '/**\n * A detached, frozen item entered the agent\'s inbox (queued or steering\n * FIFO). Source defaults are already applied, so `message` holds the exact\n * accepted values. This is the enqueue-time live signal; the durable record\n * is the eventual `user/message`/`steering/message`. Injection through\n * `agent.inject()` or equivalent `send()` routing bypasses the FIFOs\n * and does not emit this.\n * @param agent - the agent whose inbox received the item.\n * @param message - the accepted message (its returned `id`, content, source, contexts, steering, and wakeup facts).\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
summary: 'A detached, frozen item entered the agent\'s inbox (queued or steering FIFO).',
|
||||
},
|
||||
{
|
||||
name: 'agent/post-step',
|
||||
mode: 'serial',
|
||||
signature: '\'agent/post-step\'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, signal: AbortSignal): Promise<void> | void',
|
||||
jsDoc: '/**\n * Awaited serial checkpoint after the response, real or synthetic tool\n * results, injected context, and steering are durable but before `step/end`.\n * A cancelled tool batch reaches this checkpoint with an aborted signal.\n * @param agent - the agent whose step is settling.\n * @param turn - the open turn number.\n * @param step - the open step number.\n * @param signal - the turn abort signal.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode serial\n */',
|
||||
summary: 'Awaited serial checkpoint after the response, real or synthetic tool results, injected context, and steering are durable but before `step/end`.',
|
||||
},
|
||||
{
|
||||
name: 'agent/pre-step',
|
||||
mode: 'serial',
|
||||
signature: '\'agent/pre-step\'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, signal: AbortSignal): Promise<void> | void',
|
||||
jsDoc: '/**\n * Awaited serial checkpoint before `step/start`; appends land outside the\n * pending step and are included when the loop derives request history.\n * `signal` cancels listener work.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @param agent - the agent opening the step.\n * @param turn - the open turn number.\n * @param step - the pending step number.\n * @param signal - the turn abort signal.\n * @mode serial\n */',
|
||||
summary: 'Awaited serial checkpoint before `step/start`; appends land outside the pending step and are included when the loop derives request history.',
|
||||
signature: '\'agent/inbox/enqueue\'(this: Scoped<Agent>, agent: Agent, message: AgentMessage, placement: InboxPlacement): void',
|
||||
jsDoc: '/**\n * An item entered the queued or steering inbox. `placement` is the\n * acceptance-time routing result; listeners must not reconstruct it from\n * later agent or session state.\n * @param agent - the owning agent.\n * @param message - accepted content, source, and correlation identity.\n * @param placement - resolved queued or steering placement.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
summary: 'An item entered the queued or steering inbox.',
|
||||
},
|
||||
{
|
||||
name: 'agent/prompt-submit',
|
||||
mode: 'waterfall',
|
||||
signature: '\'agent/prompt-submit\'(this: Scoped<Agent>, agent: Agent, content: ContentBlock[], source: MessageSource, signal: AbortSignal, next: () => Promise<PromptDecision>): Promise<PromptDecision>',
|
||||
jsDoc: '/**\n * Allow, rewrite, or block one claimed prompt before it becomes a user\n * message. Call `next()` for the unchanged default. A listener wrapping a\n * downstream `allow` must preserve its `content` and `additionalContexts`\n * unless it intentionally replaces them. The signal controls only this turn;\n * listeners may cooperate with it but must not retain it to control another\n * turn. Steering messages do not dispatch this event; they join an open turn\n * at a steering checkpoint.\n * @param agent - the agent whose turn claimed the message.\n * @param content - the claimed message\'s blocks, as queued.\n * @param source - the message\'s resolved source.\n * @param signal - the current turn\'s explicit abort signal.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode waterfall\n */',
|
||||
summary: 'Allow, rewrite, or block one claimed prompt before it becomes a user message.',
|
||||
jsDoc: '/**\n * Allow, rewrite, or block one claimed prompt before it becomes a user\n * message or opens a turn. Call `next()` for the unchanged default. The\n * signal controls only this admission attempt; listeners may cooperate with\n * it but must not retain it for a later attempt or turn.\n * @param agent - the agent whose turn claimed the message.\n * @param content - the claimed message\'s blocks, as queued.\n * @param source - the message\'s resolved source.\n * @param signal - the current turn\'s explicit abort signal.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode waterfall\n */',
|
||||
summary: 'Allow, rewrite, or block one claimed prompt before it becomes a user message or opens a turn.',
|
||||
},
|
||||
{
|
||||
name: 'agent/request',
|
||||
mode: 'waterfall',
|
||||
signature: '\'agent/request\'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, config: LlmCallConfig, signal: AbortSignal, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>',
|
||||
jsDoc: '/**\n * Replace the frozen call configuration. Model-visible content must use\n * logged channels; this seam cannot mutate messages. Injection here joins\n * the next request because the current step boundary is already fixed.\n * @param agent - the agent making the model call.\n * @param turn - the open turn number.\n * @param step - the step whose request this is.\n * @param config - the config the loop would use (frozen); return a replacement to switch.\n * @param signal - the current turn\'s explicit abort signal; ambient\n * initiator identity does not imply liveness or cancellation authority.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode waterfall\n */',
|
||||
signature: '\'agent/request\'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, signal: AbortSignal, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>',
|
||||
jsDoc: '/**\n * Replace the frozen call configuration. `await next()` yields the config\n * the machine would use (agent options on the first request, the logged\n * header afterwards); return a replacement to switch. Model-visible\n * content must use logged channels; this seam cannot mutate messages.\n * @param agent - the agent making the model call.\n * @param turn - the open turn number.\n * @param step - the step whose request this is.\n * @param signal - the current turn\'s explicit abort signal.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode waterfall\n*/',
|
||||
summary: 'Replace the frozen call configuration.',
|
||||
},
|
||||
{
|
||||
name: 'agent/request-error',
|
||||
mode: 'waterfall',
|
||||
signature: '\'agent/request-error\'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: RequestError, failure: LlmFailure, priorFailures: readonly LlmFailure[], signal: AbortSignal, next: () => Promise<RequestErrorDecision>): Promise<RequestErrorDecision>',
|
||||
jsDoc: '/**\n * Recover a model-request failure after its failed step has closed. `retry`\n * opens a new numbered step; `fail` preserves the original request error.\n * Call `next()` to delegate to the next recovery listener or the default.\n * @param agent - the agent whose request failed.\n * @param turn - the open turn number.\n * @param step - the failed step number.\n * @param error - the original model-request failure.\n * @param failure - serializable facts normalized at the final adapter boundary.\n * @param priorFailures - immutable failures that already authorized another request in this consecutive sequence.\n * @param signal - the turn abort signal.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode waterfall\n */',
|
||||
summary: 'Recover a model-request failure after its failed step has closed.',
|
||||
},
|
||||
{
|
||||
name: 'agent/session-prefix',
|
||||
mode: 'waterfall',
|
||||
signature: '\'agent/session-prefix\'(this: Scoped<Agent>, agent: Agent, prefix: Message[], signal: AbortSignal, next: () => Promise<Message[]>): Promise<Message[]>',
|
||||
jsDoc: '/**\n * Compose request-only messages placed before derived history. The frozen\n * result is computed once per loop instance, logged on its anchoring request\n * header, and reused so the provider prefix remains stable. Interrupted\n * composition is discarded. Composition precedes the first `agent/pre-step`\n * and request boundary, so listener appends join the current request.\n * Changing context belongs in history; contributors should prepend to\n * `await next()` to preserve registration order.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @param agent - the agent whose session prefix is being composed.\n * @param prefix - the frozen seed; return an extended replacement.\n * @param signal - the current turn\'s explicit abort signal.\n * @mode waterfall\n */',
|
||||
summary: 'Compose request-only messages placed before derived history.',
|
||||
signature: '\'agent/request-error\'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: RequestError, failure: LlmFailure, priorFailures: readonly LlmFailure[], retryPolicy: ResolvedRetryPolicy | undefined, signal: AbortSignal, next: () => Promise<RequestErrorAction>): Promise<RequestErrorAction>',
|
||||
jsDoc: '/**\n * Handle a model-request failure after its failed step has closed but\n * before the failed turn closes. A listener returns `{ kind: \'retry\' }`\n * without calling `next()` when it owns the error, or calls `next()` to\n * delegate. The default `undefined` leaves the failure terminal.\n * @param agent - the agent whose request failed.\n * @param turn - the open turn number.\n * @param step - the failed step number.\n * @param error - the original model-request failure.\n * @param failure - serializable facts normalized at the final adapter boundary.\n * @param priorFailures - immutable failures that already authorized another\n * retry turn in this consecutive sequence.\n * @param retryPolicy - immutable policy of the adapter registration that served\n * the failed request, or `undefined` if no final adapter served it.\n * @param signal - the turn abort signal.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode waterfall\n */',
|
||||
summary: 'Handle a model-request failure after its failed step has closed but before the failed turn closes.',
|
||||
},
|
||||
{
|
||||
name: 'agent/session-start',
|
||||
@@ -1075,33 +1098,33 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
jsDoc: '/**\n * The session lifecycle began, once before the first turn. Use\n * `agent.inject()` to seed model-facing context. This is a notification, not\n * a veto; disposal requested by a lifecycle owner is rechecked before the\n * driver starts.\n * @param agent - the agent whose session lifecycle began.\n * @param source - why the session started (fresh startup, resume, …).\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
summary: 'The session lifecycle began, once before the first turn.',
|
||||
},
|
||||
{
|
||||
name: 'agent/settled',
|
||||
mode: 'emit',
|
||||
signature: '\'agent/settled\'(this: Scoped<Agent>, agent: Agent, turn: number, reason: SettleReason): void',
|
||||
jsDoc: '/**\n * One drain chain reached its terminal turn: that turn\'s `turn/end` is\n * already committed. Automatically recovered failed turns do not emit this\n * notification, and neither does a run that aborts or fails before its\n * `turn/start` commits — there is no durable turn to settle against.\n * `reason` says why; model-request recovery is exhausted when an error\n * reaches it.\n * @param agent - the agent whose turn closed.\n * @param turn - the terminal turn number.\n * @param reason - why the terminal turn ended, with live error facts when it failed.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
summary: 'One drain chain reached its terminal turn: that turn\'s `turn/end` is already committed.',
|
||||
},
|
||||
{
|
||||
name: 'agent/status',
|
||||
mode: 'emit',
|
||||
signature: '\'agent/status\'(this: Scoped<Agent>, agent: Agent, status: AgentStatus): void',
|
||||
jsDoc: '/**\n * Agent status changed (`idle` ⇄ `running`, or → `disposed`). A waking\n * delivery does not enter `running` synchronously; drive lifecycle from this event.\n * @param agent - the agent whose status flipped.\n * @param status - the status just entered (the transition\'s destination).\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
summary: 'Agent status changed (`idle` ⇄ `running`, or → `disposed`).',
|
||||
jsDoc: '/**\n * Agent status changed (`idle` ⇄ `running`). `send()` does not enter\n * `running` synchronously; drive lifecycle from this event.\n * @param agent - the agent whose status flipped.\n * @param status - the status just entered (the transition\'s destination).\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode emit\n */',
|
||||
summary: 'Agent status changed (`idle` ⇄ `running`).',
|
||||
},
|
||||
{
|
||||
name: 'agent/step-result',
|
||||
mode: 'waterfall',
|
||||
signature: '\'agent/step-result\'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, message: Message, signal: AbortSignal, next: () => Promise<Message>): Promise<Message>',
|
||||
jsDoc: '/**\n * Waterfall: post-process the assembled assistant {@link Message} before\n * tool dispatch (validation, content rewriting, …).\n * @param agent - the agent that received the step\'s response.\n * @param turn - the open turn number.\n * @param step - the step that produced the message.\n * @param message - the assistant message as assembled from the stream.\n * @param signal - the current turn\'s explicit abort signal.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode waterfall\n */',
|
||||
summary: 'Waterfall: post-process the assembled assistant Message before tool dispatch (validation, content rewriting, …).',
|
||||
},
|
||||
{
|
||||
name: 'agent/turn-continuation',
|
||||
mode: 'waterfall',
|
||||
signature: '\'agent/turn-continuation\'(this: Scoped<Agent>, agent: Agent, turn: number, defaultDecision: ContinuationDecision, signal: AbortSignal, next: () => Promise<ContinuationDecision>): Promise<ContinuationDecision>',
|
||||
jsDoc: '/**\n * Override whether the turn continues. The default continues after tool\n * calls or steering and stops otherwise; a continue reason becomes steering.\n * @param agent - the agent deciding whether to run another step.\n * @param turn - the turn being continued or stopped.\n * @param defaultDecision - what the loop would do absent an override.\n * @param signal - the current turn\'s explicit abort signal.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode waterfall\n */',
|
||||
summary: 'Override whether the turn continues.',
|
||||
},
|
||||
{
|
||||
name: 'agent/turn-stop',
|
||||
name: 'agent/step',
|
||||
mode: 'serial',
|
||||
signature: '\'agent/turn-stop\'(this: Scoped<Agent>, agent: Agent, turn: number, signal: AbortSignal): Promise<ContinuationStop | undefined> | ContinuationStop | undefined',
|
||||
jsDoc: '/**\n * Monotonic terminal-stop checkpoint after continuation and steering are\n * folded; a stop remains authoritative through turn close and flush:\n * steering queued in that window is discarded, while ordinary sends survive.\n * @param agent - the agent whose composed continuation outcome may be stopped.\n * @param turn - the turn at its terminal-stop checkpoint.\n * @param signal - the current turn\'s explicit abort signal.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode serial\n */',
|
||||
summary: 'Monotonic terminal-stop checkpoint after continuation and steering are folded; a stop remains authoritative through turn close and flush: steering queued in that window is discarded, while ordinary sends survive.',
|
||||
signature: '\'agent/step\'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, signal: AbortSignal): Promise<void> | void',
|
||||
jsDoc: '/**\n * Awaited serial checkpoint before EVERY request of a turn is built (the\n * first as well as each post-tools continuation). The single "between\n * steps" extension point: inject context, steer, or edit the session log\n * here — the request\'s history derives from the log right after this settles.\n * @param agent - the agent about to send a request.\n * @param turn - the open turn number.\n * @param step - the step number about to open.\n * @param signal - the turn abort signal.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode serial\n */',
|
||||
summary: 'Awaited serial checkpoint before EVERY request of a turn is built (the first as well as each post-tools continuation).',
|
||||
},
|
||||
{
|
||||
name: 'agent/turn-stopping',
|
||||
mode: 'serial',
|
||||
signature: '\'agent/turn-stopping\'(this: Scoped<Agent>, agent: Agent, turn: number, signal: AbortSignal): Promise<void> | void',
|
||||
jsDoc: '/**\n * The turn is about to close: the model owes no response (no live tool\n * calls, no fresh steering). Awaited before the boundary commits — a\n * listener that objects steers (`agent.steer(...)`) and the machine\n * re-reads its inbox: fresh steering runs another step, none closes the\n * turn. Data decides, so listener order cannot change the outcome. The\n * inverse control (stop a tool loop early) is data too: a tool result\n * carrying `concludesTurn` ends the turn at its step.\n * @param agent - the agent whose turn is at its stop boundary.\n * @param turn - the turn about to close.\n * @param signal - the current turn\'s explicit abort signal.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode serial\n */',
|
||||
summary: 'The turn is about to close: the model owes no response (no live tool calls, no fresh steering).',
|
||||
},
|
||||
{
|
||||
name: 'approval/request',
|
||||
@@ -1257,6 +1280,13 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
jsDoc: '/**\n * Emitted when any prompt provider changes. This registry notification is\n * unfiltered because a global change affects every scope.\n * @mode emit\n */',
|
||||
summary: 'Emitted when any prompt provider changes.',
|
||||
},
|
||||
{
|
||||
name: 'telemetry/record',
|
||||
mode: 'waterfall',
|
||||
signature: '\'telemetry/record\'(record: TelemetryRecord, next: () => TelemetryRecord): TelemetryRecord',
|
||||
jsDoc: '/**\n * Transform one outbound record before it reaches the backend. This\n * waterfall is the seam\'s redaction extension point. It ships NO rules\n * of its own: the\n * innermost `next()` passes the record through unchanged, and with no\n * listener mounted records reach the backend as captured, so exported\n * data is exactly as clean as the rules a deployment mounts. Listeners\n * stack by transforming `next()`\'s return value; returning without\n * `next()` replaces everything beneath. Dispatched synchronously on the\n * capture hot path inside the coordinator\'s containment: a throwing\n * listener withholds that one record (fail-closed) and never reaches the\n * agent loop. Redaction applies to the exported copy only; the canonical\n * session log is never rewritten.\n * @param record - the candidate record, already the coordinator\'s own deep\n * copy; listeners return a (possibly new) record and must not mutate it.\n * @mode waterfall\n */',
|
||||
summary: 'Transform one outbound record before it reaches the backend.',
|
||||
},
|
||||
{
|
||||
name: 'tools/change',
|
||||
mode: 'emit',
|
||||
@@ -1347,7 +1377,7 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
{
|
||||
name: 'Agent',
|
||||
declaration: 'export interface Agent {\n readonly id: SessionId;\n readonly options: AgentOptions;\n readonly session: Session;\n readonly status: AgentStatus;\n readonly ctx: Context;\n followup(content: ContentBlock[], options?: SendOptions): AgentMessageId;\n queue(content: ContentBlock[], options?: SendOptions): AgentMessageId;\n steer(content: ContentBlock[], options?: SendOptions): AgentMessageId;\n inject(content: ContentBlock[], options?: InjectOptions): AgentMessageId;\n send(input: ResolvedAgentInput): AgentMessageId;\n cancel(cause?: AgentCancelCause, options?: CancelOptions): void;\n whenIdle(): Promise<void>;\n}',
|
||||
declaration: 'export interface Agent {\n readonly id: SessionId;\n readonly options: AgentOptions;\n readonly session: Session;\n readonly status: AgentStatus;\n readonly acceptsNextStep: boolean;\n readonly ctx: Context;\n send(input: UserMessageData, options: SendOptions): AgentMessageId;\n cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n whenIdle(): Promise<void>;\n followup(input: UserMessageData): AgentMessageId;\n steer(input: UserMessageData): AgentMessageId;\n inject(input: UserMessageData): AgentMessageId;\n}',
|
||||
},
|
||||
{
|
||||
name: 'AgentCancelCause',
|
||||
@@ -1371,7 +1401,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'AgentStatus',
|
||||
declaration: 'export type AgentStatus = \'idle\' | \'running\' | \'disposed\';',
|
||||
declaration: 'export type AgentStatus = \'idle\' | \'running\';',
|
||||
},
|
||||
{
|
||||
name: 'ApprovalOutcome',
|
||||
@@ -1615,7 +1645,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'EpochHeader',
|
||||
declaration: 'export interface EpochHeader {\n config: LlmCallConfig;\n system?: string;\n tools?: ToolSchema[];\n messagePrefix?: Message[];\n}',
|
||||
declaration: 'export interface EpochHeader {\n config: LlmCallConfig;\n system?: string;\n tools?: ToolSchema[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'FileDiff',
|
||||
@@ -1675,7 +1705,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'GenerateOptions',
|
||||
declaration: 'export interface GenerateOptions {\n provider: string;\n model: string;\n messages: Message[];\n system?: string;\n tools?: ToolSchema[];\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n signal?: AbortSignal;\n sessionId?: Branded<\'SessionId\'>;\n purpose?: \'compaction\' | \'session-title\';\n}',
|
||||
declaration: 'export interface GenerateOptions {\n provider: string;\n model: string;\n reasoningEffort?: ReasoningEffortId;\n messages: Message[];\n system?: string;\n tools?: ToolSchema[];\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n signal?: AbortSignal;\n sessionId?: Branded<\'SessionId\'>;\n purpose?: \'compaction\' | \'session-title\';\n}',
|
||||
},
|
||||
{
|
||||
name: 'GenericCallView',
|
||||
@@ -1713,10 +1743,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'GoalView',
|
||||
declaration: 'export interface GoalView extends GoalSnapshot {\n readonly roundsStarted: number;\n readonly createdAt: number;\n readonly updatedAt: number;\n readonly activation: GoalActivation;\n}',
|
||||
},
|
||||
{
|
||||
name: 'HookContext',
|
||||
declaration: 'export interface HookContext {\n content: ContentBlock[];\n source: MessageSource;\n placement?: \'separate\' | \'prompt-prefix\';\n meta?: JsonValue;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ImageAttachmentRef',
|
||||
declaration: 'export interface ImageAttachmentRef {\n attachmentId: AttachmentId;\n mediaType: ImageMediaType;\n bytes: number;\n width: number;\n height: number;\n name?: string;\n}',
|
||||
@@ -1729,10 +1755,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'ImageMediaType',
|
||||
declaration: 'export type ImageMediaType = \'image/png\' | \'image/jpeg\' | \'image/webp\' | \'image/gif\';',
|
||||
},
|
||||
{
|
||||
name: 'InjectOptions',
|
||||
declaration: 'export interface InjectOptions {\n source?: MessageSource;\n meta?: JsonValue;\n}',
|
||||
},
|
||||
{
|
||||
name: 'InvariantFailure',
|
||||
declaration: 'export type InvariantFailure = (message: string) => never;',
|
||||
@@ -1767,11 +1789,11 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'LlmAdapter',
|
||||
declaration: 'export abstract class LlmAdapter {\n providerInfo(provider: string): LlmProviderInfo;\n listModels(_provider: string): Promise<readonly LlmModelInfo[]>;\n resolveModelContext(_provider: string, _model: string): Promise<LlmModelContext | undefined>;\n abstract stream(options: GenerateOptions): AsyncIterable<StreamChunk>;\n}',
|
||||
declaration: 'export abstract class LlmAdapter {\n providerInfo(provider: string): LlmProviderInfo;\n providerRetryPolicy(_provider: string): ResolvedRetryPolicy | undefined;\n listModels(_provider: string): Promise<readonly LlmModelInfo[]>;\n resolveModel(provider: string, model: string, _signal?: AbortSignal): Promise<LlmResolvedModelInfo>;\n abstract stream(options: GenerateOptions): AsyncIterable<StreamChunk>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'LlmCallConfig',
|
||||
declaration: 'export interface LlmCallConfig {\n provider: string;\n model: string;\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n}',
|
||||
declaration: 'export interface LlmCallConfig {\n provider: string;\n model: string;\n reasoningEffort?: ReasoningEffortId;\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'LlmFailure',
|
||||
@@ -1785,10 +1807,22 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'LlmModelInfo',
|
||||
declaration: 'export interface LlmModelInfo {\n provider: string;\n id: string;\n name: string;\n description?: string;\n inputModalities?: readonly ModelModality[];\n outputModalities?: readonly ModelModality[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'LlmModelReasoningInfo',
|
||||
declaration: 'export interface LlmModelReasoningInfo {\n efforts: readonly LlmReasoningEffortInfo[];\n defaultEffort?: ReasoningEffortId;\n}',
|
||||
},
|
||||
{
|
||||
name: 'LlmProviderInfo',
|
||||
declaration: 'export interface LlmProviderInfo {\n id: string;\n name: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'LlmReasoningEffortInfo',
|
||||
declaration: 'export interface LlmReasoningEffortInfo {\n id: ReasoningEffortId;\n name: string;\n description?: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'LlmResolvedModelInfo',
|
||||
declaration: 'export interface LlmResolvedModelInfo extends LlmModelInfo {\n context?: LlmModelContext;\n reasoning?: LlmModelReasoningInfo;\n}',
|
||||
},
|
||||
{
|
||||
name: 'Message',
|
||||
declaration: 'export interface Message {\n role: \'system\' | \'user\' | \'assistant\';\n content: ContentBlock[];\n provenance?: AssistantProvenance;\n}',
|
||||
@@ -1821,9 +1855,13 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'OutOfBandSessionEventType',
|
||||
declaration: 'export type OutOfBandSessionEventType = Exclude<Extract<SessionEventType, keyof OutOfBandSessionEventMap>, SurfaceEventType>;',
|
||||
},
|
||||
{
|
||||
name: 'PreparedLlmCall',
|
||||
declaration: 'export interface PreparedLlmCall {\n readonly config: LlmCallConfig;\n stream(options: GenerateOptions): AsyncIterable<StreamChunk>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'PreparedReferencedMessage',
|
||||
declaration: 'export interface PreparedReferencedMessage {\n content: ContentBlock[];\n contexts: HookContext[];\n}',
|
||||
declaration: 'export interface PreparedReferencedMessage {\n content: ContentBlock[];\n additionalContext?: UserMessageData;\n}',
|
||||
},
|
||||
{
|
||||
name: 'PresetOption',
|
||||
@@ -1837,18 +1875,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'PromptAssembly',
|
||||
declaration: 'export interface PromptAssembly {\n sections: AssembledSection[];\n tools: ToolSchema[];\n variables: Record<string, string | undefined>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'PromptMessageData',
|
||||
declaration: 'export interface PromptMessageData {\n content: ContentBlock[];\n source: MessageSource;\n envelope?: PromptMessageEnvelope;\n meta?: JsonValue;\n}',
|
||||
},
|
||||
{
|
||||
name: 'PromptMessageEnvelope',
|
||||
declaration: 'export interface PromptMessageEnvelope {\n displayContent: ContentBlock[];\n prefixContexts: PromptPrefixContext[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'PromptPrefixContext',
|
||||
declaration: 'export interface PromptPrefixContext {\n source: MessageSource;\n meta?: JsonValue;\n}',
|
||||
},
|
||||
{
|
||||
name: 'PromptSection',
|
||||
declaration: 'export interface PromptSection {\n readonly name: string;\n readonly order: number;\n readonly text: string | ((context: AssembleContext) => string);\n}',
|
||||
@@ -1941,13 +1967,29 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'ReasoningBlock',
|
||||
declaration: 'export interface ReasoningBlock {\n type: \'reasoning\';\n text: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ReasoningEffortId',
|
||||
declaration: 'export type ReasoningEffortId = Branded<\'ReasoningEffortId\'>;',
|
||||
},
|
||||
{
|
||||
name: 'RequestHeaderReason',
|
||||
declaration: 'export type RequestHeaderReason = \'initial\' | \'resume\' | \'change\';',
|
||||
},
|
||||
{
|
||||
name: 'ResolvedAgentInput',
|
||||
declaration: 'export type ResolvedAgentInput = {\n content: ContentBlock[];\n source: MessageSource;\n meta: JsonValue | undefined;\n} & ({\n target: \'next-turn\';\n wakeup: boolean;\n contexts: HookContext[];\n} | {\n target: \'next-step\';\n wakeup: true;\n contexts: HookContext[];\n} | {\n target: \'next-step\';\n wakeup: false;\n contexts: [\n ];\n});',
|
||||
name: 'ResolvedAlwaysRetryPolicy',
|
||||
declaration: 'export interface ResolvedAlwaysRetryPolicy extends ResolvedRetryBackoff {\n readonly mode: \'always\';\n}',
|
||||
},
|
||||
{
|
||||
name: 'ResolvedNormalRetryPolicy',
|
||||
declaration: 'export interface ResolvedNormalRetryPolicy extends ResolvedRetryBackoff {\n readonly mode: \'normal\';\n readonly maxRetries: number;\n readonly retryableCodes: readonly string[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'ResolvedRetryBackoff',
|
||||
declaration: 'export interface ResolvedRetryBackoff {\n readonly initialDelayMs: number;\n readonly maxDelayMs: number;\n readonly jitterRatio: number;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ResolvedRetryPolicy',
|
||||
declaration: 'export type ResolvedRetryPolicy = ResolvedNormalRetryPolicy | ResolvedAlwaysRetryPolicy;',
|
||||
},
|
||||
{
|
||||
name: 'ResumeAgentOptions',
|
||||
@@ -1987,11 +2029,15 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'SendOptions',
|
||||
declaration: 'export interface SendOptions {\n source?: MessageSource;\n contexts?: HookContext[];\n meta?: JsonValue;\n}',
|
||||
declaration: 'export interface SendOptions {\n target: SendTarget;\n wakeup: boolean;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SendTarget',
|
||||
declaration: 'export type SendTarget = \'next-turn\' | \'next-step\';',
|
||||
},
|
||||
{
|
||||
name: 'Session',
|
||||
declaration: 'export class Session {\n get surface(): SessionSurface;\n readonly header: SessionHeader;\n get id(): SessionId;\n constructor(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader);\n get events(): readonly SessionEvent[];\n get seq(): number;\n append<T extends SessionEventType>(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [\n opts: SurfaceIntent\n ] : [\n ]): SessionEvent<T>;\n requestHeader(): EpochHeader | undefined;\n deriveMessages(): Message[];\n deriveEventMessage(event: SessionEvent): Message | null;\n}',
|
||||
declaration: 'export class Session {\n get surface(): SessionSurface;\n readonly header: SessionHeader;\n get id(): SessionId;\n readonly firstLiveSeq: number;\n constructor(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader);\n get events(): readonly SessionEvent[];\n get seq(): number;\n append<T extends SessionEventType>(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [\n opts: SurfaceIntent\n ] : [\n ]): SessionEvent<T>;\n requestHeader(): EpochHeader | undefined;\n deriveMessages(): Message[];\n deriveEventMessage(event: SessionEvent): Message | null;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionAvailability',
|
||||
@@ -2003,7 +2049,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'SessionEventMap',
|
||||
declaration: 'export interface SessionEventMap {\n \'turn/start\': {\n turn: number;\n trigger: TurnTrigger;\n };\n \'turn/end\': {\n turn: number;\n reason: TurnEndReason;\n };\n \'step/start\': {\n turn: number;\n step: number;\n };\n \'step/end\': {\n turn: number;\n step: number;\n };\n \'user/message\': PromptMessageData;\n \'prompt/blocked\': {\n content: ContentBlock[];\n source: MessageSource;\n reason: string;\n };\n \'assistant/chunk\': {\n turn: number;\n step: number;\n chunk: StreamChunk;\n };\n \'assistant/message\': {\n turn: number;\n step: number;\n content: ContentBlock[];\n provenance: AssistantProvenance;\n usage?: TokenUsage;\n };\n \'tool/call\': {\n turn: number;\n step: number;\n callId: CallId;\n name: string;\n arguments: string;\n };\n \'tool/result\': {\n turn: number;\n step: number;\n callId: CallId;\n content: ContentBlock[];\n isError: boolean;\n error?: {\n name: string;\n code: string;\n };\n meta?: JsonValue;\n };\n \'steering/message\': PromptMessageData & {\n turn: number;\n };\n \'todo/write\': {\n todos: TodoItem[];\n };\n \'request/header\': {\n header: EpochHeader;\n reason: RequestHeaderReason;\n };\n}',
|
||||
declaration: 'export interface SessionEventMap {\n \'turn/start\': {\n turn: number;\n trigger: TurnTrigger;\n };\n \'turn/end\': {\n turn: number;\n reason: TurnEndReason;\n };\n \'step/start\': {\n turn: number;\n step: number;\n };\n \'step/end\': {\n turn: number;\n step: number;\n };\n \'user/message\': UserMessageData;\n \'assistant/chunk\': {\n turn: number;\n step: number;\n chunk: StreamChunk;\n };\n \'assistant/message\': {\n turn: number;\n step: number;\n content: ContentBlock[];\n provenance: AssistantProvenance;\n usage?: TokenUsage;\n };\n \'tool/call\': {\n turn: number;\n step: number;\n callId: CallId;\n name: string;\n arguments: string;\n };\n \'tool/result\': {\n turn: number;\n step: number;\n callId: CallId;\n content: ContentBlock[];\n isError: boolean;\n error?: {\n name: string;\n code: string;\n };\n meta?: JsonValue;\n };\n \'steering/message\': UserMessageData & {\n turn: number;\n };\n \'todo/write\': {\n todos: TodoItem[];\n };\n \'request/header\': {\n header: EpochHeader;\n reason: RequestHeaderReason;\n };\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionEventMetadataFilter',
|
||||
@@ -2219,7 +2265,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'SkillSource',
|
||||
declaration: 'export type SkillSource = \'project-dsh\' | \'project-agents\' | \'runtime\' | \'user-dsh\' | \'user-agents\' | \'custom\' | (string & {});',
|
||||
declaration: 'export type SkillSource = \'project-dsh\' | \'project-agents\' | \'runtime\' | \'user-dsh\' | \'user-agents\' | \'custom\' | \'bundled\' | (string & {});',
|
||||
},
|
||||
{
|
||||
name: 'SkillSummary',
|
||||
@@ -2281,6 +2327,46 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'SubagentStopReasonMap',
|
||||
declaration: 'export interface SubagentStopReasonMap {\n completed: \'completed\';\n aborted: \'aborted\';\n error: \'error\';\n \'max-tokens\': \'max-tokens\';\n refusal: \'refusal\';\n}',
|
||||
},
|
||||
{
|
||||
name: 'SubprocessCollect',
|
||||
declaration: 'export interface SubprocessCollect {\n maxBytes: number;\n spill?: {\n maxBytes: number;\n };\n}',
|
||||
},
|
||||
{
|
||||
name: 'SubprocessCollectedOutputs',
|
||||
declaration: 'export interface SubprocessCollectedOutputs {\n readonly stdout?: SubprocessOutputReader;\n readonly stderr?: SubprocessOutputReader;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SubprocessHandle',
|
||||
declaration: 'export interface SubprocessHandle {\n readonly pid: number;\n readonly stdin: Writable | undefined;\n readonly stdout: Readable | undefined;\n readonly stderr: Readable | undefined;\n readonly collected: SubprocessCollectedOutputs;\n readonly done: Promise<SubprocessOutcome>;\n terminate(): void;\n waitForExit(signal?: AbortSignal): Promise<boolean>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SubprocessOutcome',
|
||||
declaration: 'export interface SubprocessOutcome {\n exitCode: number | null;\n signal: NodeJS.Signals | null;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SubprocessOutputMode',
|
||||
declaration: 'export type SubprocessOutputMode = \'pipe\' | \'inherit\' | SubprocessCollect;',
|
||||
},
|
||||
{
|
||||
name: 'SubprocessOutputRead',
|
||||
declaration: 'export interface SubprocessOutputRead {\n text: string;\n nextOffset: number;\n lossy: boolean;\n spillPath?: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SubprocessOutputReader',
|
||||
declaration: 'export interface SubprocessOutputReader {\n readFrom(fromByte: number): SubprocessOutputRead;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SubprocessSpawnSpec',
|
||||
declaration: 'export interface SubprocessSpawnSpec {\n argv: readonly string[];\n cwd: string;\n stdio: SubprocessStdio;\n graceMs: number;\n signal?: AbortSignal | undefined;\n env?: Record<string, string> | undefined;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SubprocessStdinMode',
|
||||
declaration: 'export type SubprocessStdinMode = \'ignore\' | \'pipe\' | {\n readonly data: string;\n};',
|
||||
},
|
||||
{
|
||||
name: 'SubprocessStdio',
|
||||
declaration: 'export interface SubprocessStdio {\n stdin: SubprocessStdinMode;\n stdout: SubprocessOutputMode;\n stderr: SubprocessOutputMode;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SurfaceEvent',
|
||||
declaration: 'export type SurfaceEvent = SessionEvent<SurfaceEventType> & {\n surfaceOp: SurfaceOp;\n};',
|
||||
@@ -2345,6 +2431,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'TaskStatus',
|
||||
declaration: 'export type TaskStatus = \'running\' | \'stopping\' | \'completed\' | \'killed\' | \'failed\';',
|
||||
},
|
||||
{
|
||||
name: 'TelemetryRecord',
|
||||
declaration: 'export interface TelemetryRecord {\n channel: \'ledger\' | \'ops\';\n time: number;\n severity: TelemetrySeverity;\n attributes: Record<string, string | number>;\n body: unknown;\n}',
|
||||
},
|
||||
{
|
||||
name: 'TelemetrySeverity',
|
||||
declaration: 'export type TelemetrySeverity = \'info\' | \'warn\' | \'error\';',
|
||||
},
|
||||
{
|
||||
name: 'TerminalCallView',
|
||||
declaration: 'export interface TerminalCallView {\n card: \'terminal\';\n title: string;\n description?: string;\n cwd?: string;\n}',
|
||||
@@ -2399,7 +2493,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'ToolExecutionFailure',
|
||||
declaration: 'export interface ToolExecutionFailure {\n readonly isError: true;\n readonly error: ToolFailure;\n readonly value?: never;\n readonly content: ContentBlock[];\n readonly meta?: JsonValue;\n readonly additionalContexts?: HookContext[];\n}',
|
||||
declaration: 'export interface ToolExecutionFailure {\n readonly isError: true;\n readonly error: ToolFailure;\n readonly value?: never;\n readonly content: ContentBlock[];\n readonly meta?: JsonValue;\n readonly additionalContexts?: UserMessageData[];\n readonly concludesTurn?: never;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ToolExecutionInput',
|
||||
@@ -2415,7 +2509,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'ToolExecutionSuccess',
|
||||
declaration: 'export interface ToolExecutionSuccess {\n readonly isError: false;\n readonly value: JsonValue;\n readonly content: ContentBlock[];\n readonly error?: never;\n readonly meta?: JsonValue;\n readonly additionalContexts?: HookContext[];\n}',
|
||||
declaration: 'export interface ToolExecutionSuccess {\n readonly isError: false;\n readonly value: JsonValue;\n readonly content: ContentBlock[];\n readonly error?: never;\n readonly meta?: JsonValue;\n readonly additionalContexts?: UserMessageData[];\n readonly concludesTurn?: true;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ToolExecutionToken',
|
||||
@@ -2455,71 +2549,19 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'ToolRunContext',
|
||||
declaration: 'export interface ToolRunContext extends ToolExecution {\n deferContext(context: HookContext): void;\n}',
|
||||
declaration: 'export interface ToolRunContext extends ToolExecution {\n deferContext(context: UserMessageData): void;\n concludeTurn(): void;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ToolSchema',
|
||||
declaration: 'export interface ToolSchema {\n name: string;\n description: string;\n parameters: Record<string, unknown>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'TuiComponent',
|
||||
declaration: 'export interface TuiComponent {\n render(width: number): string[];\n handleInput?(data: string): void;\n wantsKeyRelease?: boolean;\n invalidate(): void;\n}',
|
||||
},
|
||||
{
|
||||
name: 'TuiFocusable',
|
||||
declaration: 'export interface TuiFocusable {\n focused: boolean;\n}',
|
||||
},
|
||||
{
|
||||
name: 'TuiOverlayAnchor',
|
||||
declaration: 'export type TuiOverlayAnchor = \'center\' | \'top-left\' | \'top-right\' | \'bottom-left\' | \'bottom-right\' | \'top-center\' | \'bottom-center\' | \'left-center\' | \'right-center\';',
|
||||
},
|
||||
{
|
||||
name: 'TuiOverlayCloseReason',
|
||||
declaration: 'export type TuiOverlayCloseReason = \'closed\' | \'aborted\' | \'owner-disposed\' | \'tui-disposed\' | \'error\';',
|
||||
},
|
||||
{
|
||||
name: 'TuiOverlayHost',
|
||||
declaration: 'export interface TuiOverlayHost {\n readonly signal: AbortSignal;\n readonly viewport: TuiViewport;\n readonly theme: TuiTheme;\n display(value: string): string;\n invalidate(): void;\n close(): void;\n}',
|
||||
},
|
||||
{
|
||||
name: 'TuiOverlayMargin',
|
||||
declaration: 'export interface TuiOverlayMargin {\n readonly top?: number;\n readonly right?: number;\n readonly bottom?: number;\n readonly left?: number;\n}',
|
||||
},
|
||||
{
|
||||
name: 'TuiOverlayOptions',
|
||||
declaration: 'export interface TuiOverlayOptions {\n readonly width?: number | `${number}%`;\n readonly minWidth?: number;\n readonly maxHeight?: number | `${number}%`;\n readonly anchor?: TuiOverlayAnchor;\n readonly margin?: number | TuiOverlayMargin;\n}',
|
||||
},
|
||||
{
|
||||
name: 'TuiOverlayOutcome',
|
||||
declaration: 'export type TuiOverlayOutcome = {\n readonly reason: Exclude<TuiOverlayCloseReason, \'error\'>;\n} | {\n readonly reason: \'error\';\n readonly error: unknown;\n};',
|
||||
},
|
||||
{
|
||||
name: 'TuiOverlayRequest',
|
||||
declaration: 'export interface TuiOverlayRequest {\n readonly create: (host: TuiOverlayHost) => TuiComponent & Partial<TuiFocusable>;\n readonly options?: TuiOverlayOptions;\n readonly signal?: AbortSignal;\n}',
|
||||
},
|
||||
{
|
||||
name: 'TuiOverlaySession',
|
||||
declaration: 'export interface TuiOverlaySession {\n readonly state: TuiOverlayState;\n readonly closed: Promise<TuiOverlayOutcome>;\n close(): Promise<TuiOverlayOutcome>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'TuiOverlayState',
|
||||
declaration: 'export type TuiOverlayState = \'queued\' | \'active\' | \'closed\';',
|
||||
},
|
||||
{
|
||||
name: 'TuiTheme',
|
||||
declaration: 'export interface TuiTheme {\n readonly text: (value: string) => string;\n readonly muted: (value: string) => string;\n readonly dim: (value: string) => string;\n readonly accent: (value: string) => string;\n readonly success: (value: string) => string;\n readonly warning: (value: string) => string;\n readonly error: (value: string) => string;\n readonly bold: (value: string) => string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'TuiViewport',
|
||||
declaration: 'export interface TuiViewport {\n readonly columns: number;\n readonly rows: number;\n}',
|
||||
},
|
||||
{
|
||||
name: 'TurnEndReason',
|
||||
declaration: 'export type TurnEndReason = TurnEndReasonMap[keyof TurnEndReasonMap];',
|
||||
},
|
||||
{
|
||||
name: 'TurnEndReasonMap',
|
||||
declaration: 'export interface TurnEndReasonMap {\n completed: {\n kind: \'completed\';\n };\n aborted: {\n kind: \'aborted\';\n };\n error: {\n kind: \'error\';\n step: number;\n } & ({\n failure: LlmFailure;\n message?: never;\n code?: never;\n } | {\n message: string;\n code?: string;\n failure?: never;\n });\n disposed: {\n kind: \'disposed\';\n };\n \'max-tokens\': {\n kind: \'max-tokens\';\n };\n rejected: {\n kind: \'rejected\';\n reason: string;\n };\n interrupted: {\n kind: \'interrupted\';\n };\n}',
|
||||
declaration: 'export interface TurnEndReasonMap {\n completed: {\n kind: \'completed\';\n };\n aborted: {\n kind: \'aborted\';\n };\n error: {\n kind: \'error\';\n step: number;\n } & ({\n failure: LlmFailure;\n message?: never;\n code?: never;\n } | {\n message: string;\n code?: string;\n failure?: never;\n });\n disposed: {\n kind: \'disposed\';\n };\n \'max-tokens\': {\n kind: \'max-tokens\';\n };\n interrupted: {\n kind: \'interrupted\';\n };\n}',
|
||||
},
|
||||
{
|
||||
name: 'TurnTrigger',
|
||||
@@ -2527,12 +2569,16 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'TurnTriggerMap',
|
||||
declaration: 'export interface TurnTriggerMap {\n message: {\n kind: \'message\';\n source: MessageSource;\n };\n injection: {\n kind: \'injection\';\n source: MessageSource;\n };\n}',
|
||||
declaration: 'export interface TurnTriggerMap {\n message: {\n kind: \'message\';\n source: MessageSource;\n };\n retry: {\n kind: \'retry\';\n };\n injection: {\n kind: \'injection\';\n source: MessageSource;\n };\n}',
|
||||
},
|
||||
{
|
||||
name: 'UserInteractionProvider',
|
||||
declaration: 'export interface UserInteractionProvider {\n ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'UserMessageData',
|
||||
declaration: 'export interface UserMessageData {\n content: ContentBlock[];\n source: MessageSource;\n}',
|
||||
},
|
||||
{
|
||||
name: 'WebFetchBody',
|
||||
declaration: 'export type WebFetchBody = {\n readonly kind: \'html\';\n readonly content: string;\n} | {\n readonly kind: \'text\';\n readonly content: string;\n};',
|
||||
|
||||
@@ -684,7 +684,7 @@ function sandboxContext(ctx: Context): Context {
|
||||
if (ctx.get(prop) !== undefined) {
|
||||
throw new Error(
|
||||
`service "${prop}" is not injected. Declare it: inject: ['${prop}', …] on your plugin, `
|
||||
+ 'so cordis parks this mount if the provider is later unmounted.',
|
||||
+ 'so cordis parks this temporary Plugin if the provider is later unmounted.',
|
||||
)
|
||||
}
|
||||
throw new Error(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Self-referential runtime tools: inspect live services/plugins/tools, mount a returned plugin
|
||||
* under an owned dynamic fiber, and unmount it to quiescence. Registrations are fiber effects,
|
||||
* Self-referential runtime tools: inspect live services/plugins/tools, mount a returned temporary
|
||||
* plugin under an owned dynamic fiber, and unmount it to quiescence. Registrations are fiber effects,
|
||||
* so plugin disposal removes the entire dynamic subtree. The VM and context façade prevent
|
||||
* accidental misuse, not hostile code: an allowed service such as `ctx.bash` reaches the real
|
||||
* runtime. Named exports preserve loader injection metadata.
|
||||
@@ -40,8 +40,8 @@ export const Config: z<Config> = z.object({
|
||||
type ResolvedConfig = Required<Config>
|
||||
|
||||
/**
|
||||
* Mount the three cordis tools on `ctx.tools` and create the `cordis-dynamic`
|
||||
* group fiber every dynamic mount hangs under.
|
||||
* Register the three cordis tools and own every temporary plugin under one
|
||||
* `cordis-dynamic` group fiber.
|
||||
* @param ctx - the plugin context (`tools` injected).
|
||||
* @param config - the schemastery-resolved {@link Config}.
|
||||
*/
|
||||
@@ -56,19 +56,21 @@ export function apply(ctx: Context, config: Config): void {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'cordis_inspect',
|
||||
description:
|
||||
'Inspect the live cordis runtime that is running THIS agent. Read-only. '
|
||||
'Inspect the live Cordis runtime in the current DSH process. Read-only. '
|
||||
+ 'Sections: `services` (every provided ctx service and the plugin fiber that owns it), '
|
||||
+ '`plugins` (a flat list of the loaded plugins with their lifecycle states), '
|
||||
+ '`plugins` (all live plugin fibers with their lifecycle states), '
|
||||
+ '`tools` (the model-facing tools currently registered, i.e. what you can call), '
|
||||
+ '`dynamic` (plugins you mounted via cordis_mount: id, name, state, provided services, awaited services), '
|
||||
+ '`temporary` (only temporary Plugins created by cordis_mount: id, name, state, provided services, awaited services, and lifetime), '
|
||||
+ '`api` (method signatures AND argument/return type shapes for every LIVE service — read this before writing plugin code that calls a service), '
|
||||
+ '`events` (every harness event with its dispatch mode and exact signature — pick listener targets here). '
|
||||
+ 'Omit `what` to get all six sections. With `what:"api"` or `what:"events"`, pass an exact `name` '
|
||||
+ 'Temporary Plugins exist only in memory, remain active across later turns, and disappear after cordis_unmount, toolset unload, or DSH restart; they are not restored automatically. '
|
||||
+ 'The `temporary` section is a subset of `plugins`. Omit `what` to get all six sections. '
|
||||
+ 'With `what:"api"` or `what:"events"`, pass an exact `name` '
|
||||
+ 'to narrow to one service/event and include its original source JSDoc.',
|
||||
parameters: {
|
||||
what: {
|
||||
type: 'string',
|
||||
enum: ['services', 'plugins', 'tools', 'dynamic', 'api', 'events'],
|
||||
enum: ['services', 'plugins', 'tools', 'temporary', 'api', 'events'],
|
||||
description: 'Limit the report to one section. Omit for all sections.',
|
||||
},
|
||||
name: {
|
||||
@@ -84,19 +86,19 @@ export function apply(ctx: Context, config: Config): void {
|
||||
if (args.name !== undefined && args.what !== 'api' && args.what !== 'events') {
|
||||
throw new Error('name is valid only with what:"api" or what:"events"')
|
||||
}
|
||||
const sections: [heading: string, body: () => string[]][] = [
|
||||
['services', () => describeServices(ctx)],
|
||||
['plugins', () => describePlugins(ctx)],
|
||||
const sections: [key: string, heading: string, body: () => string[]][] = [
|
||||
['services', 'services', () => describeServices(ctx)],
|
||||
['plugins', 'plugins', () => describePlugins(ctx)],
|
||||
// The calling agent's view: scoped/shadowed tools included, restricted
|
||||
// globals absent — "what you can call", not the global registry.
|
||||
['tools', () => describeTools(ctx, exec.agent)],
|
||||
['dynamic', () => describeDynamic(ctx, mounts)],
|
||||
['api', () => describeApi(ctx, SERVICE_API, INHERITED_CTX_API, TYPE_API, args.name)],
|
||||
['events', () => describeEvents(EVENT_API, args.name)],
|
||||
['tools', 'tools', () => describeTools(ctx, exec.agent)],
|
||||
['temporary', 'Temporary Plugins', () => describeDynamic(ctx, mounts)],
|
||||
['api', 'api', () => describeApi(ctx, SERVICE_API, INHERITED_CTX_API, TYPE_API, args.name)],
|
||||
['events', 'events', () => describeEvents(EVENT_API, args.name)],
|
||||
]
|
||||
const selected = sections.filter(([heading]) => args.what === undefined || args.what === heading)
|
||||
const selected = sections.filter(([key]) => args.what === undefined || args.what === key)
|
||||
const text = selected
|
||||
.map(([heading, body]) => `## ${heading}\n${body().join('\n')}`)
|
||||
.map(([, heading, body]) => `## ${heading}\n${body().join('\n')}`)
|
||||
.join('\n\n')
|
||||
return Promise.resolve(text)
|
||||
},
|
||||
@@ -106,8 +108,13 @@ export function apply(ctx: Context, config: Config): void {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'cordis_mount',
|
||||
description:
|
||||
'Mount a NEW cordis plugin into the live runtime that is running THIS agent '
|
||||
+ '(self-modification). `code` runs as the body of an async JavaScript function '
|
||||
'Mount a temporary Cordis Plugin in the current DSH process. '
|
||||
+ 'This creates an in-memory runtime Plugin, not an installed or configured Plugin. '
|
||||
+ 'It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. '
|
||||
+ 'It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. '
|
||||
+ 'To keep it, ask the Agent to implement a normal local, project, or repository Plugin through the regular development workflow. '
|
||||
+ 'It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. '
|
||||
+ '`code` runs now as the body of an async JavaScript function '
|
||||
+ 'in an isolated sandbox and MUST `return` a plugin. Two forms: '
|
||||
+ 'FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register '
|
||||
+ 'tools, listen to events, and provide services, but reaching ANY service (e.g. '
|
||||
@@ -131,10 +138,10 @@ export function apply(ctx: Context, config: Config): void {
|
||||
+ 'oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: \'object\', properties, required?: […] } wrapper is also accepted with open-by-default objects. A '
|
||||
+ 'tool\'s `execute` MUST return the lossless JSON value declared by `output.schema`; '
|
||||
+ '`output.render(args, value)` separately returns Native/model content blocks. '
|
||||
+ 'Mounts can COMPOSE: one plugin may `ctx.provide(\'name\', value)` a service and '
|
||||
+ 'Temporary Plugins can COMPOSE: one Plugin may `ctx.provide(\'name\', value)` a service and '
|
||||
+ 'another may declare `inject: [\'name\']` to consume it — the consumer stays pending '
|
||||
+ 'until the provider exists and returns to pending when the provider is unmounted. '
|
||||
+ 'Everything registered inside `apply` is cleaned up automatically on unmount. '
|
||||
+ 'Everything registered inside `apply` is cleaned up automatically by cordis_unmount. '
|
||||
+ 'Sandbox globals: `console` (tagged `[cordis:<id>]`, writes through to the harness '
|
||||
+ 'terminal), `harness.defineTool`, `harness.registerTool`, '
|
||||
+ '`btoa`, `atob`, `TextEncoder`, `TextDecoder`. '
|
||||
@@ -143,7 +150,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
+ 'errors; `process` and `Buffer` are undefined. Instead use inject: [\'fs\'] + ctx.fs for '
|
||||
+ 'files, inject: [\'web\'] + ctx.web for HTTP, inject: [\'bash\'] + ctx.bash for processes, '
|
||||
+ 'and inject: [\'timer\'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, '
|
||||
+ 'auto-cleaned on unmount) — cordis_inspect what:"api" shows what THIS runtime provides. '
|
||||
+ 'auto-cleaned when unmounted) — cordis_inspect what:"api" shows what THIS runtime provides. '
|
||||
+ 'Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). '
|
||||
+ 'Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a '
|
||||
+ 'trailing `next` callback which MUST be called — returning without `next()` '
|
||||
@@ -159,7 +166,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
code: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Body of an async JS function; must `return` the plugin to mount.',
|
||||
description: 'JavaScript body returning a temporary Plugin; evaluated now and saved nowhere.',
|
||||
},
|
||||
},
|
||||
output: {
|
||||
@@ -179,12 +186,12 @@ export function apply(ctx: Context, config: Config): void {
|
||||
},
|
||||
},
|
||||
render: (_args, value) => {
|
||||
const note = value.waitingFor.length > 0
|
||||
? ` — waiting for service(s): ${value.waitingFor.join(', ')} (activates when provided)`
|
||||
: ''
|
||||
const status = value.waitingFor.length > 0
|
||||
? `is pending (plugin "${value.pluginName}"; missing services: ${value.waitingFor.join(', ')}`
|
||||
: `is running (plugin "${value.pluginName}"`
|
||||
return [{
|
||||
type: 'text',
|
||||
text: `mounted ${value.id} (plugin "${value.pluginName}", state: ${value.state}${note})`,
|
||||
text: `Temporary Plugin ${value.id} ${status}; available until unmounted or DSH restarts).`,
|
||||
}]
|
||||
},
|
||||
},
|
||||
@@ -195,13 +202,13 @@ export function apply(ctx: Context, config: Config): void {
|
||||
if (!isPlugin(evaluated)) {
|
||||
if (evaluated === undefined) {
|
||||
throw new Error(
|
||||
'mount code returned `undefined` — did you forget `return`?\n'
|
||||
'temporary Plugin code returned `undefined` — did you forget `return`?\n'
|
||||
+ ' ✓ return (ctx) => { … }\n'
|
||||
+ ' ✓ return { name: \'…\', inject: […], apply(ctx) { … } }',
|
||||
)
|
||||
}
|
||||
throw new Error(
|
||||
'mount code must `return` a plugin: a function, or an object with an `apply(ctx)` method',
|
||||
'temporary Plugin code must `return` a Plugin: a function, or an object with an `apply(ctx)` method',
|
||||
)
|
||||
}
|
||||
const fiber = await mountDynamic(group, evaluated)
|
||||
@@ -225,15 +232,13 @@ export function apply(ctx: Context, config: Config): void {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'cordis_unmount',
|
||||
description:
|
||||
'Dispose a plugin previously mounted with cordis_mount, by id. All its '
|
||||
+ 'registrations (event listeners, tools, services) are cleaned up through '
|
||||
+ 'the cordis effect lifecycle. Returns only after disposal has fully '
|
||||
+ 'completed (quiescence, not just a request to stop).',
|
||||
'Unmount a current-process temporary Plugin created by cordis_mount. Waits for its tools, listeners, services, timers, and other owned effects to clean up completely. '
|
||||
+ 'Only dyn-N temporary ids are accepted; this cannot remove Loader, configured, or installed Plugins.',
|
||||
parameters: {
|
||||
id: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The dynamic mount id returned by cordis_mount (e.g. "dyn-1").',
|
||||
description: 'The temporary Plugin id returned by cordis_mount (for example "dyn-1"); valid only in this process and invalid after unmount or restart.',
|
||||
},
|
||||
},
|
||||
output: {
|
||||
@@ -245,12 +250,12 @@ export function apply(ctx: Context, config: Config): void {
|
||||
pluginName: { type: 'string', required: true },
|
||||
},
|
||||
},
|
||||
render: (_args, value) => [{ type: 'text', text: `unmounted ${value.id} (plugin "${value.pluginName}")` }],
|
||||
render: (_args, value) => [{ type: 'text', text: `Temporary Plugin ${value.id} was unmounted and removed.` }],
|
||||
},
|
||||
async execute(args) {
|
||||
const mount = mounts.get(args.id)
|
||||
if (!mount) {
|
||||
throw new Error(`no dynamic plugin with id "${args.id}" (list mounts with cordis_inspect what:"dynamic")`)
|
||||
throw new Error(`no temporary Plugin with id "${args.id}" (list them with cordis_inspect what:"temporary")`)
|
||||
}
|
||||
await mount.fiber.dispose()
|
||||
mounts.delete(args.id)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Read-only renderers over the live runtime for `cordis_inspect`: the service list, the flat
|
||||
* plugin list, the registered tools, the dynamic-mount table (with per-mount provides/waits),
|
||||
* plugin list, the registered tools, the temporary-plugin table (with per-plugin provides/waits),
|
||||
* and the catalog-backed `api` / `events` sections. Exact-name lookups add the
|
||||
* original source JSDoc without inflating the default reports.
|
||||
* @module @deepseek-ai/dsh-tool-cordis/inspect
|
||||
@@ -63,8 +63,8 @@ export function describeServices(ctx: Context): string[] {
|
||||
/**
|
||||
* The `plugins` section: a flat list of every fiber the registry knows, one
|
||||
* line per fiber with its lifecycle state, sorted by plugin name (a plugin
|
||||
* mounted more than once repeats — one line per instance). Dynamic mounts are
|
||||
* listed like any other plugin; their ids live in the `dynamic` section.
|
||||
* mounted more than once repeats — one line per instance). Temporary plugins are
|
||||
* listed like any other plugin; their ids live in the `temporary` section.
|
||||
* @param ctx - the runtime whose registry is enumerated.
|
||||
* @returns one line per loaded plugin fiber.
|
||||
*/
|
||||
@@ -91,7 +91,7 @@ export function describeTools(ctx: Context, scope?: ScopeKey): string[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* The `dynamic` section: one line per mount with id, plugin name, lifecycle
|
||||
* The `temporary` section: one line per temporary plugin with id, plugin name, lifecycle
|
||||
* state, the services its subtree provides, and — for a pending mount — the
|
||||
* services it waits for.
|
||||
* @param ctx - the runtime the mounts live in.
|
||||
@@ -99,13 +99,14 @@ export function describeTools(ctx: Context, scope?: ScopeKey): string[] {
|
||||
* @returns one line per mount, or a single placeholder line when none exist.
|
||||
*/
|
||||
export function describeDynamic(ctx: Context, mounts: ReadonlyMap<string, DynamicMount>): string[] {
|
||||
if (mounts.size === 0) return ['(no dynamic plugins mounted)']
|
||||
if (mounts.size === 0) {
|
||||
return ['No temporary Plugins are running. Temporary Plugins created with cordis_mount disappear when DSH restarts.']
|
||||
}
|
||||
return [...mounts].map(([id, mount]) => {
|
||||
const provides = providedServices(ctx, mount.fiber)
|
||||
const waiting = missingServices(ctx, mount.fiber)
|
||||
const providesNote = provides.length > 0 ? ` — provides: ${provides.join(', ')}` : ''
|
||||
const waitingNote = waiting.length > 0 ? ` — waiting for: ${waiting.join(', ')}` : ''
|
||||
return `- ${id}: ${mount.pluginName} [${STATE_LABELS[mount.fiber.state]}]${providesNote}${waitingNote}`
|
||||
const state = mount.fiber.state === FiberState.ACTIVE ? 'running' : STATE_LABELS[mount.fiber.state]
|
||||
return `- Temporary Plugin ${id}: ${mount.pluginName} [${state}] — provides: ${provides.join(', ') || 'none'}; waiting for: ${waiting.join(', ') || 'none'}; lifetime: until unmounted or DSH restarts`
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ export async function mountDynamic(group: Fiber, plugin: Plugin): Promise<Fiber>
|
||||
// while the old mount still holds the name — teach the replace recipe.
|
||||
if (message.includes('already registered')) {
|
||||
throw new Error(
|
||||
`${message} — to REPLACE something an earlier mount registered, first cordis_unmount that mount's id `
|
||||
+ '(find it with cordis_inspect what:"dynamic"), then mount the new version.',
|
||||
`${message} — to REPLACE something an earlier temporary Plugin registered, first cordis_unmount that Plugin's id `
|
||||
+ '(find it with cordis_inspect what:"temporary"), then mount the new version.',
|
||||
)
|
||||
}
|
||||
throw error instanceof Error ? error : new Error(message)
|
||||
|
||||
@@ -25,7 +25,7 @@ export function presentInspectCall(args: { what?: string; name?: string }): Gene
|
||||
}
|
||||
|
||||
/**
|
||||
* The `cordis_mount` call card: an execute carrying the mount code as raw input.
|
||||
* The `cordis_mount` call card: an execute carrying the temporary-plugin code as raw input.
|
||||
* @param args - the validated call arguments.
|
||||
* @returns the generic call card.
|
||||
*/
|
||||
@@ -33,13 +33,13 @@ export function presentMountCall(args: { code: string }): GenericCallView {
|
||||
return {
|
||||
card: 'generic',
|
||||
kind: 'execute',
|
||||
title: 'Mount plugin into live cordis runtime',
|
||||
title: 'Mount temporary Cordis Plugin',
|
||||
rawInput: { code: args.code },
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `cordis_unmount` call card: a delete, titled with the mount id.
|
||||
* The `cordis_unmount` call card: a delete, titled with the temporary-plugin id.
|
||||
* @param args - the validated call arguments.
|
||||
* @returns the generic call card.
|
||||
*/
|
||||
@@ -47,6 +47,6 @@ export function presentUnmountCall(args: { id: string }): GenericCallView {
|
||||
return {
|
||||
card: 'generic',
|
||||
kind: 'delete',
|
||||
title: `Unmount ${args.id}`,
|
||||
title: `Unmount temporary Cordis Plugin ${args.id}`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ function patchDualRealmInstanceof(sandbox: object): void {
|
||||
|
||||
const TIMER_REDIRECT
|
||||
= 'Node timers are unavailable. Use the cordis timer service instead: declare inject: [\'timer\'] on your plugin '
|
||||
+ 'and call ctx.setTimeout / ctx.setInterval — those are fiber effects, cleaned up automatically on unmount.'
|
||||
+ 'and call ctx.setTimeout / ctx.setInterval — those are fiber effects, cleaned up automatically when unmounted.'
|
||||
|
||||
/**
|
||||
* The callable Node APIs the sandbox deliberately disables, each mapped to the
|
||||
@@ -80,7 +80,7 @@ function nodeApiTraps(): Record<string, () => never> {
|
||||
const traps: Record<string, () => never> = {}
|
||||
for (const [name, redirect] of Object.entries(NODE_API_REDIRECTS)) {
|
||||
traps[name] = () => {
|
||||
throw new Error(`${name} is not available in the mount sandbox — ${redirect}`)
|
||||
throw new Error(`${name} is not available in the temporary Plugin sandbox — ${redirect}`)
|
||||
}
|
||||
}
|
||||
return traps
|
||||
@@ -163,14 +163,14 @@ export async function evaluateMountCode(sandbox: object, code: string, id: strin
|
||||
const offendingLine = context.split('\n')[1] ?? ''
|
||||
if (/\bas\b/.test(offendingLine)) {
|
||||
throw new Error(
|
||||
`mount code failed to parse:\n${context}\n`
|
||||
`temporary Plugin code failed to parse:\n${context}\n`
|
||||
+ 'The sandbox runs plain JavaScript, not TypeScript. Remove type annotations:\n'
|
||||
+ ' ✗ { type: \'text\' as const, text: x }\n'
|
||||
+ ' ✓ { type: \'text\', text: x }',
|
||||
)
|
||||
}
|
||||
throw new Error(
|
||||
`mount code failed to parse:\n${context}\n`
|
||||
`temporary Plugin code failed to parse:\n${context}\n`
|
||||
+ 'Note: `code` runs as the BODY of an async function (line numbers are offset by the 1-line wrapper). '
|
||||
+ 'Check bracket balance — ending the returned plugin object with `});` closes a call that was never opened; '
|
||||
+ 'a plain `return { … }` ends with `}` (an optional `;`), never `)`.',
|
||||
|
||||
@@ -12,11 +12,11 @@ describe('cross-mount provide/inject', () => {
|
||||
it('provider first: the consumer activates immediately and its tool reaches the provided service', async () => {
|
||||
const ctx = await setup()
|
||||
const provider = await call(ctx, 'cordis_mount', { code: PROVIDER_CODE })
|
||||
expect(text(provider)).toContain('state: active')
|
||||
expect(text(provider)).toContain('is running')
|
||||
|
||||
const consumer = await call(ctx, 'cordis_mount', { code: CONSUMER_CODE })
|
||||
expect(consumer.isError).toBe(false)
|
||||
expect(text(consumer)).toContain('state: active')
|
||||
expect(text(consumer)).toContain('is running')
|
||||
|
||||
// The vm-realm service value is callable across mounts, and the result
|
||||
// normalizes into the host realm like any dynamic tool result.
|
||||
@@ -29,9 +29,9 @@ describe('cross-mount provide/inject', () => {
|
||||
const ctx = await setup()
|
||||
const consumer = await call(ctx, 'cordis_mount', { code: CONSUMER_CODE })
|
||||
expect(consumer.isError).toBe(false)
|
||||
expect(text(consumer)).toContain('state: pending')
|
||||
expect(text(consumer)).toContain('waiting for service(s): greeter')
|
||||
expect(text(await call(ctx, 'cordis_inspect', { what: 'dynamic' }))).toContain('waiting for: greeter')
|
||||
expect(text(consumer)).toContain('is pending')
|
||||
expect(text(consumer)).toContain('missing services: greeter')
|
||||
expect(text(await call(ctx, 'cordis_inspect', { what: 'temporary' }))).toContain('waiting for: greeter')
|
||||
expect(ctx.tools.get('greet')).toBeUndefined()
|
||||
|
||||
await call(ctx, 'cordis_mount', { code: PROVIDER_CODE })
|
||||
@@ -48,8 +48,8 @@ describe('cross-mount provide/inject', () => {
|
||||
const unmounted = await call(ctx, 'cordis_unmount', { id: 'dyn-1' })
|
||||
expect(unmounted.isError).toBe(false)
|
||||
expect(ctx.tools.get('greet')).toBeUndefined()
|
||||
const report = text(await call(ctx, 'cordis_inspect', { what: 'dynamic' }))
|
||||
expect(report).toContain('dyn-2: greeter-consumer [pending] — waiting for: greeter')
|
||||
const report = text(await call(ctx, 'cordis_inspect', { what: 'temporary' }))
|
||||
expect(report).toContain('Temporary Plugin dyn-2: greeter-consumer [pending] — provides: none; waiting for: greeter; lifetime: until unmounted or DSH restarts')
|
||||
})
|
||||
|
||||
it('re-providing the service re-runs the consumer through the same guard (active again, tool back)', async () => {
|
||||
@@ -62,7 +62,7 @@ describe('cross-mount provide/inject', () => {
|
||||
await call(ctx, 'cordis_mount', { code: PROVIDER_CODE }) // dyn-3
|
||||
expect(ctx.tools.get('greet')).toBeDefined()
|
||||
expect(text(await call(ctx, 'greet', { name: 'again' }))).toBe('hi again')
|
||||
expect(text(await call(ctx, 'cordis_inspect', { what: 'dynamic' }))).toContain('dyn-2: greeter-consumer [active]')
|
||||
expect(text(await call(ctx, 'cordis_inspect', { what: 'temporary' }))).toContain('Temporary Plugin dyn-2: greeter-consumer [running]')
|
||||
})
|
||||
|
||||
it('a duplicate provide fails loud with the owning fiber named, and the failed mount is disposed', async () => {
|
||||
@@ -71,8 +71,8 @@ describe('cross-mount provide/inject', () => {
|
||||
const duplicate = await call(ctx, 'cordis_mount', { code: PROVIDER_CODE })
|
||||
expect(duplicate.isError).toBe(true)
|
||||
expect(text(duplicate)).toContain('has been registered')
|
||||
const report = text(await call(ctx, 'cordis_inspect', { what: 'dynamic' }))
|
||||
expect(report).toContain('dyn-1: greeter-provider')
|
||||
const report = text(await call(ctx, 'cordis_inspect', { what: 'temporary' }))
|
||||
expect(report).toContain('Temporary Plugin dyn-1: greeter-provider')
|
||||
expect(report).not.toContain('dyn-2')
|
||||
})
|
||||
|
||||
@@ -81,8 +81,8 @@ describe('cross-mount provide/inject', () => {
|
||||
await call(ctx, 'cordis_mount', { code: PROVIDER_CODE })
|
||||
await call(ctx, 'cordis_mount', { code: CONSUMER_CODE })
|
||||
|
||||
const dynamic = text(await call(ctx, 'cordis_inspect', { what: 'dynamic' }))
|
||||
expect(dynamic).toContain('dyn-1: greeter-provider [active] — provides: greeter')
|
||||
const dynamic = text(await call(ctx, 'cordis_inspect', { what: 'temporary' }))
|
||||
expect(dynamic).toContain('Temporary Plugin dyn-1: greeter-provider [running] — provides: greeter; waiting for: none; lifetime: until unmounted or DSH restarts')
|
||||
|
||||
const services = text(await call(ctx, 'cordis_inspect', { what: 'services' }))
|
||||
expect(services).toContain('- greeter (provided by greeter-provider)')
|
||||
@@ -126,7 +126,7 @@ describe('cross-mount provide/inject', () => {
|
||||
`,
|
||||
})
|
||||
expect(consumer.isError).toBe(false)
|
||||
expect(text(consumer)).toContain('state: active')
|
||||
expect(text(consumer)).toContain('is running')
|
||||
expect(text(await call(ctx, 'answer', {}))).toBe('42/42/null')
|
||||
})
|
||||
|
||||
@@ -139,6 +139,6 @@ describe('cross-mount provide/inject', () => {
|
||||
expect(ctx.tools.get('greet')).toBeUndefined()
|
||||
const services = text(await call(ctx, 'cordis_inspect', { what: 'services' }))
|
||||
expect(services).toContain('- greeter (provided by greeter-provider)')
|
||||
expect(text(await call(ctx, 'cordis_inspect', { what: 'dynamic' }))).toContain('dyn-1: greeter-provider [active]')
|
||||
expect(text(await call(ctx, 'cordis_inspect', { what: 'temporary' }))).toContain('Temporary Plugin dyn-1: greeter-provider [running]')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('cordis_inspect', () => {
|
||||
const report = text(result)
|
||||
if (result.isError) throw new Error('expected cordis_inspect success')
|
||||
expect(result.value).toBe(report)
|
||||
for (const heading of ['services', 'plugins', 'tools', 'dynamic', 'api', 'events']) {
|
||||
for (const heading of ['services', 'plugins', 'tools', 'Temporary Plugins', 'api', 'events']) {
|
||||
expect(report).toContain(`## ${heading}`)
|
||||
}
|
||||
// The services section sees the real providers; the plugins list shows
|
||||
@@ -28,7 +28,7 @@ describe('cordis_inspect', () => {
|
||||
expect(report).toContain('- tool-cordis [active]')
|
||||
expect(report).toContain('- cordis-dynamic [active]')
|
||||
expect(report).toContain('- cordis_mount')
|
||||
expect(report).toContain('(no dynamic plugins mounted)')
|
||||
expect(report).toContain('No temporary Plugins are running. Temporary Plugins created with cordis_mount disappear when DSH restarts.')
|
||||
})
|
||||
|
||||
it('limits the report to one section via `what`', async () => {
|
||||
@@ -40,11 +40,12 @@ describe('cordis_inspect', () => {
|
||||
expect(report).not.toContain('## plugins')
|
||||
})
|
||||
|
||||
it('shows a mount in the dynamic section and in the flat plugins list', async () => {
|
||||
it('shows a temporary Plugin in its exact section and in the flat plugins list', async () => {
|
||||
const ctx = await setup()
|
||||
await call(ctx, 'cordis_mount', { code: LISTENER_CODE })
|
||||
const report = text(await call(ctx, 'cordis_inspect', {}))
|
||||
expect(report).toContain('- dyn-1: change-logger [active]')
|
||||
expect(report).toContain('## Temporary Plugins')
|
||||
expect(report).toContain('- Temporary Plugin dyn-1: change-logger [running] — provides: none; waiting for: none; lifetime: until unmounted or DSH restarts')
|
||||
expect(report).toContain('- change-logger [active]')
|
||||
})
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
|
||||
import * as ToolCordis from '../src/index.ts'
|
||||
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
|
||||
import { REVERSE_TOOL_CODE } from './helpers.ts'
|
||||
import { call, REVERSE_TOOL_CODE, setup, text } from './helpers.ts'
|
||||
|
||||
/**
|
||||
* Full-loop integration: a scripted mock model mounts a plugin that registers
|
||||
@@ -47,7 +48,7 @@ describe('cordis tools through the agent loop', () => {
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('it-cordis'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
agent.followup([{ type: 'text', text: 'give yourself reverse_text, use it, clean up' }])
|
||||
agent.followup({ content: [{ type: 'text', text: 'give yourself reverse_text, use it, clean up' }], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const log = agent.session.events
|
||||
@@ -65,4 +66,36 @@ describe('cordis tools through the agent loop', () => {
|
||||
// After the unmount the self-made tool is gone from the registry.
|
||||
expect(ctx.tools.get('reverse_text')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps a temporary Plugin across turns, unmounts it, and does not restore it in a new runtime', async () => {
|
||||
const adapter = new MockAdapter([
|
||||
toolCallResponse('mount-1', 'cordis_mount', { code: 'return { name: \'turn-marker\', apply() {} }' }),
|
||||
toolCallResponse('inspect-1', 'cordis_inspect', { what: 'temporary' }),
|
||||
textResponse('Turn one complete.'),
|
||||
toolCallResponse('inspect-2', 'cordis_inspect', { what: 'temporary' }),
|
||||
toolCallResponse('unmount-1', 'cordis_unmount', { id: 'dyn-1' }),
|
||||
toolCallResponse('inspect-3', 'cordis_inspect', { what: 'temporary' }),
|
||||
textResponse('Turn two complete.'),
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('it-cordis-turn-lifetime'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
agent.followup({ content: [{ type: 'text', text: 'Mount the marker and inspect it.' }], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, agent)
|
||||
agent.followup({ content: [{ type: 'text', text: 'On this later turn, inspect the marker, unmount it, then inspect again.' }], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const resultText = new Map(
|
||||
agent.session.events
|
||||
.filter(event => event.type === 'tool/result')
|
||||
.map(event => [event.data.callId, event.data.content.filter(block => block.type === 'text').map(block => block.text).join('')]),
|
||||
)
|
||||
expect(resultText.get(CallId('inspect-1'))).toContain('Temporary Plugin dyn-1: turn-marker [running]')
|
||||
expect(resultText.get(CallId('inspect-2'))).toContain('Temporary Plugin dyn-1: turn-marker [running]')
|
||||
expect(resultText.get(CallId('unmount-1'))).toBe('Temporary Plugin dyn-1 was unmounted and removed.')
|
||||
expect(resultText.get(CallId('inspect-3'))).toContain('No temporary Plugins are running.')
|
||||
|
||||
const restarted = await setup()
|
||||
expect(text(await call(restarted, 'cordis_inspect', { what: 'temporary' }))).toContain('No temporary Plugins are running.')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('cordis_mount', () => {
|
||||
provides: [],
|
||||
waitingFor: [],
|
||||
})
|
||||
expect(text(result)).toContain('mounted dyn-1 (plugin "change-logger", state: active)')
|
||||
expect(text(result)).toBe('Temporary Plugin dyn-1 is running (plugin "change-logger"; available until unmounted or DSH restarts).')
|
||||
|
||||
// Fire a REAL tools/change by registering a tool; the mounted listener logs.
|
||||
ctx.tools.register(dummyTool('trigger_a'))
|
||||
@@ -665,8 +665,7 @@ describe('cordis_mount', () => {
|
||||
provides: [],
|
||||
waitingFor: ['no-such-service'],
|
||||
})
|
||||
expect(text(result)).toContain('state: pending')
|
||||
expect(text(result)).toContain('waiting for service(s): no-such-service')
|
||||
expect(text(result)).toBe('Temporary Plugin dyn-1 is pending (plugin "waiter"; missing services: no-such-service; available until unmounted or DSH restarts).')
|
||||
// Unmounting a pending mount works like any other.
|
||||
const unmounted = await call(ctx, 'cordis_unmount', { id: 'dyn-1' })
|
||||
expect(unmounted.isError).toBe(false)
|
||||
@@ -677,7 +676,7 @@ describe('cordis_mount', () => {
|
||||
const result = await call(ctx, 'cordis_mount', { code: 'throw new Error(\'boom in sandbox\')' })
|
||||
expect(result.isError).toBe(true)
|
||||
expect(text(result)).toContain('boom in sandbox')
|
||||
expect(text(await call(ctx, 'cordis_inspect', { what: 'dynamic' }))).toContain('(no dynamic plugins mounted)')
|
||||
expect(text(await call(ctx, 'cordis_inspect', { what: 'temporary' }))).toContain('No temporary Plugins are running.')
|
||||
})
|
||||
|
||||
it('passes non-Error and null throws through untouched (no SyntaxError misclassification)', async () => {
|
||||
@@ -693,7 +692,7 @@ describe('cordis_mount', () => {
|
||||
const ctx = await setup()
|
||||
const result = await call(ctx, 'cordis_mount', { code: 'return 42' })
|
||||
expect(result.isError).toBe(true)
|
||||
expect(text(result)).toContain('must `return` a plugin')
|
||||
expect(text(result)).toContain('must `return` a Plugin')
|
||||
})
|
||||
|
||||
it('answers a missing return with the two valid plugin forms', async () => {
|
||||
@@ -710,7 +709,7 @@ describe('cordis_mount', () => {
|
||||
})
|
||||
expect(result.isError).toBe(true)
|
||||
expect(text(result)).toContain('apply exploded')
|
||||
expect(text(await call(ctx, 'cordis_inspect', { what: 'dynamic' }))).toContain('(no dynamic plugins mounted)')
|
||||
expect(text(await call(ctx, 'cordis_inspect', { what: 'temporary' }))).toContain('No temporary Plugins are running.')
|
||||
})
|
||||
|
||||
it('rolls back a plugin that collides with an existing tool name, keeping the original tool intact', async () => {
|
||||
@@ -754,16 +753,16 @@ describe('cordis_mount', () => {
|
||||
})
|
||||
|
||||
it.each([
|
||||
['require(\'fs\')', 'require is not available in the mount sandbox', 'inject: [\'fs\']'],
|
||||
['setTimeout(() => {}, 5)', 'setTimeout is not available in the mount sandbox', 'ctx.setTimeout'],
|
||||
['fetch(\'https://example.com\')', 'fetch is not available in the mount sandbox', 'ctx.web'],
|
||||
['require(\'fs\')', 'require is not available in the temporary Plugin sandbox', 'inject: [\'fs\']'],
|
||||
['setTimeout(() => {}, 5)', 'setTimeout is not available in the temporary Plugin sandbox', 'ctx.setTimeout'],
|
||||
['fetch(\'https://example.com\')', 'fetch is not available in the temporary Plugin sandbox', 'ctx.web'],
|
||||
])('traps the Node API call %s with a redirect to the cordis alternative', async (invocation, trapMessage, redirect) => {
|
||||
const ctx = await setup()
|
||||
const result = await call(ctx, 'cordis_mount', { code: `${invocation}\nreturn (ctx) => {}` })
|
||||
expect(result.isError).toBe(true)
|
||||
expect(text(result)).toContain(trapMessage)
|
||||
expect(text(result)).toContain(redirect)
|
||||
expect(text(await call(ctx, 'cordis_inspect', { what: 'dynamic' }))).toContain('(no dynamic plugins mounted)')
|
||||
expect(text(await call(ctx, 'cordis_inspect', { what: 'temporary' }))).toContain('No temporary Plugins are running.')
|
||||
})
|
||||
|
||||
it('lets a mounted plugin schedule through the cordis timer service (inject: [\'timer\'])', async () => {
|
||||
@@ -781,7 +780,7 @@ describe('cordis_mount', () => {
|
||||
`,
|
||||
})
|
||||
expect(result.isError).toBe(false)
|
||||
expect(text(result)).toContain('state: active')
|
||||
expect(text(result)).toContain('is running')
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
expect(log).toHaveBeenCalledWith('[cordis:dyn-1]', 'tick')
|
||||
})
|
||||
@@ -854,7 +853,7 @@ describe('cordis_mount', () => {
|
||||
const result = await call(ctx, 'cordis_mount', { code: 'while (true) {}' })
|
||||
expect(result.isError).toBe(true)
|
||||
expect(text(result)).toMatch(/timed? ?out/i)
|
||||
expect(text(await call(ctx, 'cordis_inspect', { what: 'dynamic' }))).toContain('(no dynamic plugins mounted)')
|
||||
expect(text(await call(ctx, 'cordis_inspect', { what: 'temporary' }))).toContain('No temporary Plugins are running.')
|
||||
})
|
||||
|
||||
it('makes instanceof inside the sandbox see BOTH realms (patched vm constructors, host untouched)', async () => {
|
||||
|
||||
@@ -22,13 +22,13 @@ describe('presenters', () => {
|
||||
expect(presentMountCall({ code: 'return (ctx) => {}' })).toEqual({
|
||||
card: 'generic',
|
||||
kind: 'execute',
|
||||
title: 'Mount plugin into live cordis runtime',
|
||||
title: 'Mount temporary Cordis Plugin',
|
||||
rawInput: { code: 'return (ctx) => {}' },
|
||||
})
|
||||
})
|
||||
|
||||
it('cordis_unmount renders a generic delete card titled with the id', () => {
|
||||
expect(presentUnmountCall({ id: 'dyn-1' })).toEqual({ card: 'generic', kind: 'delete', title: 'Unmount dyn-1' })
|
||||
expect(presentUnmountCall({ id: 'dyn-1' })).toEqual({ card: 'generic', kind: 'delete', title: 'Unmount temporary Cordis Plugin dyn-1' })
|
||||
})
|
||||
|
||||
it('is wired onto the registered definitions through the defineTool soft-validation path', async () => {
|
||||
@@ -42,7 +42,7 @@ describe('presenters', () => {
|
||||
title: 'Inspect cordis runtime: api: tools',
|
||||
})
|
||||
expect(ctx.tools.get('cordis_mount')!.presentCall!({ code: 'return 1' })).toMatchObject({ kind: 'execute' })
|
||||
expect(ctx.tools.get('cordis_unmount')!.presentCall!({ id: 'dyn-2' })).toMatchObject({ title: 'Unmount dyn-2' })
|
||||
expect(ctx.tools.get('cordis_unmount')!.presentCall!({ id: 'dyn-2' })).toMatchObject({ title: 'Unmount temporary Cordis Plugin dyn-2' })
|
||||
// Soft validation: presenter args that fail the schema render as no card, never a throw.
|
||||
expect(ctx.tools.get('cordis_unmount')!.presentCall!({ id: 42 })).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -186,7 +186,7 @@ describe('sandbox context façade — inject gate on services', () => {
|
||||
`,
|
||||
})
|
||||
expect(result.isError).toBe(false)
|
||||
expect(text(result)).toContain('state: active')
|
||||
expect(text(result)).toContain('is running')
|
||||
})
|
||||
|
||||
it('a cross-mount consumer must declare the provider — the undeclared path is refused, not left as a zombie tool', async () => {
|
||||
|
||||
@@ -31,9 +31,10 @@ describe('tool registration', () => {
|
||||
const ctx = await setup()
|
||||
const names = ctx.tools.schemas().map(schema => schema.name)
|
||||
expect(names).toEqual(expect.arrayContaining(['cordis_inspect', 'cordis_mount', 'cordis_unmount']))
|
||||
expect(names).not.toEqual(expect.arrayContaining(['cordis_try', 'cordis_stop']))
|
||||
const inspect = ctx.tools.schemas().find(schema => schema.name === 'cordis_inspect')!
|
||||
const props = (inspect.parameters as { properties: Record<string, { enum?: string[]; type?: string }> }).properties
|
||||
expect(props.what?.enum).toEqual(['services', 'plugins', 'tools', 'dynamic', 'api', 'events'])
|
||||
expect(props.what?.enum).toEqual(['services', 'plugins', 'tools', 'temporary', 'api', 'events'])
|
||||
expect(props.name?.type).toBe('string')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -28,13 +28,13 @@ describe('cordis_unmount', () => {
|
||||
expect(result.isError).toBe(false)
|
||||
if (result.isError) throw new Error('expected cordis_unmount success')
|
||||
expect(result.value).toEqual({ id: 'dyn-1', pluginName: 'change-logger' })
|
||||
expect(text(result)).toContain('unmounted dyn-1')
|
||||
expect(text(result)).toBe('Temporary Plugin dyn-1 was unmounted and removed.')
|
||||
|
||||
// Immediately after the awaited unmount, the listener must be gone — no
|
||||
// grace period, no eventual consistency.
|
||||
ctx.tools.register(dummyTool('trigger_after'))
|
||||
expect(log).toHaveBeenCalledTimes(1)
|
||||
expect(text(await call(ctx, 'cordis_inspect', { what: 'dynamic' }))).toContain('(no dynamic plugins mounted)')
|
||||
expect(text(await call(ctx, 'cordis_inspect', { what: 'temporary' }))).toContain('No temporary Plugins are running.')
|
||||
})
|
||||
|
||||
it('unregisters a self-made tool on unmount', async () => {
|
||||
@@ -50,7 +50,7 @@ describe('cordis_unmount', () => {
|
||||
const ctx = await setup()
|
||||
const unknown = await call(ctx, 'cordis_unmount', { id: 'dyn-99' })
|
||||
expect(unknown.isError).toBe(true)
|
||||
expect(text(unknown)).toContain('no dynamic plugin with id "dyn-99"')
|
||||
expect(text(unknown)).toContain('no temporary Plugin with id "dyn-99"')
|
||||
|
||||
await call(ctx, 'cordis_mount', { code: LISTENER_CODE })
|
||||
await call(ctx, 'cordis_unmount', { id: 'dyn-1' })
|
||||
|
||||
Reference in New Issue
Block a user