Restore Cordis mount and unmount names
Keep temporary lifecycle descriptions and UI labels explicit.
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write examples/cordis-agent/README.md
|
||||
README.md: 45c7d96f672a6dc7703d750f46e2fee08966adfa
|
||||
README.zh.md: c9991dea8840e1c046f3e0861e65c0d84a81634a
|
||||
README.md: 55970e932bc16d8361932daa9ea55af83ef73d33
|
||||
README.zh.md: c2873b6de96a8b47ad8ea4fb2cf03a7501406300
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The self-referential harness demo: the DeepSeek V4 coding spine on the full-screen TUI plus [`@deepseek-ai/dsh-tool-cordis`](../../packages/cordis/tool-cordis/README.md), which lets the model inspect the current DSH process, try in-memory temporary Plugins, and stop them. Temporary Plugins remain active across turns but disappear on stop, toolset unload, or DSH restart; they create no files or configuration and may affect other sessions in the process. The `ctx.fs` and `ctx.web` services are provider-only capabilities available to those Plugins. The design lives in [the toolset Agent Note](../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md).
|
||||
The self-referential harness demo: the DeepSeek V4 coding spine on the full-screen TUI plus [`@deepseek-ai/dsh-tool-cordis`](../../packages/cordis/tool-cordis/README.md), which lets the model inspect the current DSH process, mount in-memory temporary Plugins, and unmount them. Temporary Plugins remain active across turns but disappear on unmount, toolset unload, or DSH restart; they create no files or configuration and may affect other sessions in the process. The `ctx.fs` and `ctx.web` services are provider-only capabilities available to those Plugins. The design lives in [the toolset Agent Note](../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md).
|
||||
|
||||
## Run it
|
||||
|
||||
@@ -18,20 +18,20 @@ pnpm run demo:cordis acp # ACP server
|
||||
The intended demo is staged — verify the listener link first, then let the agent extend itself:
|
||||
|
||||
```
|
||||
> Try a temporary Plugin that listens to the 'agent/status' event and logs every status change, then run `echo hi` with bash.
|
||||
[tool call] cordis_try({"code": "return { name: 'status-logger', apply(ctx) { ctx.on('agent/status', (agent, status) => console.log('status →', status)) } }"})
|
||||
[tool result] Temporary Plugin dyn-1 is running (plugin "status-logger"; available until stopped or DSH restarts).
|
||||
> Mount a temporary Plugin that listens to the 'agent/status' event and logs every status change, then run `echo hi` with bash.
|
||||
[tool call] cordis_mount({"code": "return { name: 'status-logger', apply(ctx) { ctx.on('agent/status', (agent, status) => console.log('status →', status)) } }"})
|
||||
[tool result] Temporary Plugin dyn-1 is running (plugin "status-logger"; available until unmounted or DSH restarts).
|
||||
[tool call] bash({"command": "echo hi"})
|
||||
[cordis:dyn-1] status → … ← the temporary listener firing, live
|
||||
> Now give yourself a reverse_text tool and use it on "harness".
|
||||
[tool call] cordis_try({"code": "return { name: 'reverse-text', inject: ['tools'], apply(ctx) { ctx.tools.register(harness.defineTool({ name: 'reverse_text', … })) } }"})
|
||||
[tool call] cordis_mount({"code": "return { name: 'reverse-text', inject: ['tools'], apply(ctx) { ctx.tools.register(harness.defineTool({ name: 'reverse_text', … })) } }"})
|
||||
[tool call] reverse_text({"text": "harness"}) ← a tool the agent built for itself, one step earlier
|
||||
> Stop both temporary Plugins.
|
||||
[tool call] cordis_stop({"id": "dyn-1"})
|
||||
> Unmount both temporary Plugins.
|
||||
[tool call] cordis_unmount({"id": "dyn-1"})
|
||||
```
|
||||
|
||||
Ask for `cordis_inspect` with `what: "api"` or `what: "events"` to see the generated service/event reference used to write Plugin code, and try two cooperating temporary Plugins (`ctx.provide` in one, `inject` in the other) to watch Cordis park and revive the consumer.
|
||||
Ask for `cordis_inspect` with `what: "api"` or `what: "events"` to see the generated service/event reference used to write Plugin code, and mount two cooperating temporary Plugins (`ctx.provide` in one, `inject` in the other) to watch Cordis park and revive the consumer.
|
||||
|
||||
## End-to-end tests
|
||||
|
||||
`tests/keyless-smoke.e2e.ts` boots the real `cordis.yml` through the Loader with a dummy key and asserts the banner, package-name resolution, and clean EOF exit. `tests/cordis-tools.e2e.ts` is the with-key smoke: a real model tries a temporary status listener and the test verifies its tagged console line, creates and uses a `reverse_text` tool, and composes two temporary Plugins through provide/inject. [`packages/cordis/tool-cordis`](../../packages/cordis/tool-cordis) carries the unit coverage under the per-file 100% gate.
|
||||
`tests/keyless-smoke.e2e.ts` boots the real `cordis.yml` through the Loader with a dummy key and asserts the banner, package-name resolution, and clean EOF exit. `tests/cordis-tools.e2e.ts` is the with-key smoke: a real model mounts a temporary status listener and the test verifies its tagged console line, creates and uses a `reverse_text` tool, and composes two temporary Plugins through provide/inject. [`packages/cordis/tool-cordis`](../../packages/cordis/tool-cordis) carries the unit coverage under the per-file 100% gate.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
自指 harness 演示:在全屏 TUI 上运行 DeepSeek V4 编码主干,并加载 [`@deepseek-ai/dsh-tool-cordis`](../../packages/cordis/tool-cordis/README.md)。后者让模型检查当前 DSH 进程、尝试仅存于内存的临时 Plugin,并再次停止它们。临时 Plugin 可跨 turn 保持活跃,但会在 stop、工具集卸载或 DSH 重启后消失;它们不创建文件或配置,也可能影响同一进程中的其他 session。`ctx.fs` 和 `ctx.web` 是这些 Plugin 可用的 provider-only 能力。设计详见[工具集 Agent Note](../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)。
|
||||
自指 harness 演示:在全屏 TUI 上运行 DeepSeek V4 编码主干,并加载 [`@deepseek-ai/dsh-tool-cordis`](../../packages/cordis/tool-cordis/README.md)。后者让模型检查当前 DSH 进程、挂载仅存于内存的临时 Plugin,并再次卸载它们。临时 Plugin 可跨 turn 保持活跃,但会在卸载、工具集卸载或 DSH 重启后消失;它们不创建文件或配置,也可能影响同一进程中的其他 session。`ctx.fs` 和 `ctx.web` 是这些 Plugin 可用的 provider-only 能力。设计详见[工具集 Agent Note](../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)。
|
||||
|
||||
## 运行
|
||||
|
||||
@@ -18,20 +18,20 @@ pnpm run demo:cordis acp # ACP server
|
||||
预期演示分阶段进行:先验证监听器链接,再让 agent 扩展自身:
|
||||
|
||||
```
|
||||
> Try a temporary Plugin that listens to the 'agent/status' event and logs every status change, then run `echo hi` with bash.
|
||||
[tool call] cordis_try({"code": "return { name: 'status-logger', apply(ctx) { ctx.on('agent/status', (agent, status) => console.log('status →', status)) } }"})
|
||||
[tool result] Temporary Plugin dyn-1 is running (plugin "status-logger"; available until stopped or DSH restarts).
|
||||
> Mount a temporary Plugin that listens to the 'agent/status' event and logs every status change, then run `echo hi` with bash.
|
||||
[tool call] cordis_mount({"code": "return { name: 'status-logger', apply(ctx) { ctx.on('agent/status', (agent, status) => console.log('status →', status)) } }"})
|
||||
[tool result] Temporary Plugin dyn-1 is running (plugin "status-logger"; available until unmounted or DSH restarts).
|
||||
[tool call] bash({"command": "echo hi"})
|
||||
[cordis:dyn-1] status → … ← the temporary listener firing, live
|
||||
> Now give yourself a reverse_text tool and use it on "harness".
|
||||
[tool call] cordis_try({"code": "return { name: 'reverse-text', inject: ['tools'], apply(ctx) { ctx.tools.register(harness.defineTool({ name: 'reverse_text', … })) } }"})
|
||||
[tool call] cordis_mount({"code": "return { name: 'reverse-text', inject: ['tools'], apply(ctx) { ctx.tools.register(harness.defineTool({ name: 'reverse_text', … })) } }"})
|
||||
[tool call] reverse_text({"text": "harness"}) ← a tool the agent built for itself, one step earlier
|
||||
> Stop both temporary Plugins.
|
||||
[tool call] cordis_stop({"id": "dyn-1"})
|
||||
> Unmount both temporary Plugins.
|
||||
[tool call] cordis_unmount({"id": "dyn-1"})
|
||||
```
|
||||
|
||||
请求 `cordis_inspect` 并使用 `what: "api"` 或 `what: "events"`,即可查看编写 Plugin 代码所用的生成服务/事件资料。还可尝试两个协作临时 Plugin(一个中调用 `ctx.provide`,另一个中使用 `inject`),观察 Cordis 如何暂停并恢复消费方。
|
||||
请求 `cordis_inspect` 并使用 `what: "api"` 或 `what: "events"`,即可查看编写 Plugin 代码所用的生成服务/事件资料。还可挂载两个协作临时 Plugin(一个中调用 `ctx.provide`,另一个中使用 `inject`),观察 Cordis 如何暂停并恢复消费方。
|
||||
|
||||
## 端到端测试
|
||||
|
||||
`tests/keyless-smoke.e2e.ts` 使用虚拟密钥通过 Loader 启动真实 `cordis.yml`,并断言横幅、包名解析和 EOF 后干净退出。`tests/cordis-tools.e2e.ts` 是带密钥的冒烟测试:真实模型尝试一个临时状态 listener,测试验证其带标记的 console 行;然后创建并使用 `reverse_text` 工具,再通过 provide/inject 组合两个临时 Plugin。[`packages/cordis/tool-cordis`](../../packages/cordis/tool-cordis) 在每文件 100% 覆盖率门禁下承载单元覆盖。
|
||||
`tests/keyless-smoke.e2e.ts` 使用虚拟密钥通过 Loader 启动真实 `cordis.yml`,并断言横幅、包名解析和 EOF 后干净退出。`tests/cordis-tools.e2e.ts` 是带密钥的冒烟测试:真实模型挂载一个临时状态 listener,测试验证其带标记的 console 行;然后创建并使用 `reverse_text` 工具,再通过 provide/inject 组合两个临时 Plugin。[`packages/cordis/tool-cordis`](../../packages/cordis/tool-cordis) 在每文件 100% 覆盖率门禁下承载单元覆盖。
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# Cordis Agent App Composition
|
||||
|
||||
The self-referential demo puts @deepseek-ai/dsh-tool-cordis on the coding spine, letting the agent inspect its current-process runtime and try or stop in-memory temporary Plugins.
|
||||
The self-referential demo puts @deepseek-ai/dsh-tool-cordis on the coding spine, letting the agent inspect its current-process runtime and mount or unmount in-memory temporary Plugins.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Self-referential TUI demo: the coding spine plus tools to inspect the live
|
||||
# service/plugin/tool/temporary/API/event state, try a model-written temporary
|
||||
# Plugin, and quiescently stop it. The app bin loads the gitignored
|
||||
# service/plugin/tool/temporary/API/event state, mount a model-written temporary
|
||||
# Plugin, and quiescently unmount it. The app bin loads the gitignored
|
||||
# root `.env` before reading the required DeepSeek key and optional base URL.
|
||||
# Trust stance: the vm and context façade limit accidental global/framework
|
||||
# access but are not a security boundary; temporary Plugin code reaches live capabilities
|
||||
@@ -60,7 +60,7 @@
|
||||
persistenceRoot: './.sessions'
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
welcome: 'cordis-agent ready. Ask it to inspect its runtime, try a temporary listener, or invent a temporary tool for itself.'
|
||||
welcome: 'cordis-agent ready. Ask it to inspect its runtime, mount a temporary listener, or invent a temporary tool for itself.'
|
||||
persona: |
|
||||
You are cordis-agent, a self-referential harness demo powered by the
|
||||
{{model}} model.
|
||||
@@ -68,15 +68,15 @@
|
||||
You run INSIDE a cordis plugin runtime, and your cordis_* tools operate
|
||||
on that live runtime: cordis_inspect to look around (its `api` and
|
||||
`events` sections document the service methods, type shapes, and events
|
||||
your Plugin code can use), cordis_try to try an in-memory temporary
|
||||
your Plugin code can use), cordis_mount to mount an in-memory temporary
|
||||
Plugin (an event listener, a brand-new tool for yourself, or a service
|
||||
another temporary Plugin injects), cordis_stop to clean one up. These
|
||||
Plugins remain across turns but disappear on stop, toolset unload, or
|
||||
another temporary Plugin injects), cordis_unmount to clean one up. These
|
||||
Plugins remain across turns but disappear on unmount, toolset unload, or
|
||||
DSH restart and may affect other sessions in this process. In Plugin code, NEVER use Node
|
||||
built-ins (require/setTimeout/fetch) — use the runtime's cordis services
|
||||
via inject: fs, web, bash, and timer (ctx.setTimeout). Prefer small
|
||||
single-purpose plugins, prefer plain notification events over waterfall
|
||||
events unless you intend to intercept, and stop what you no longer
|
||||
events unless you intend to intercept, and unmount what you no longer
|
||||
need. Report results briefly.
|
||||
|
||||
# The self-referential cordis toolset (loaded after the app so ctx.tools exists).
|
||||
|
||||
@@ -8,7 +8,7 @@ const testToolSignal = new AbortController().signal
|
||||
|
||||
/**
|
||||
* With-key smoke for the self-referential cordis tools: a REAL model drives
|
||||
* cordis_try/cordis_stop against the live context the test observes.
|
||||
* cordis_mount/cordis_unmount against the live context the test observes.
|
||||
* World-verified, not self-reported: the mounted listener must actually WRITE
|
||||
* its tagged console line, the self-made tool must actually EXIST in the
|
||||
* registry and appear as a real `tool/call`, the cross-mount service must
|
||||
@@ -37,14 +37,14 @@ function resultText(result: { content: { type: string; text?: string }[] }): str
|
||||
}
|
||||
|
||||
describe.skipIf(!process.env.DEEPSEEK_API_KEY)('cordis tools: a real model modifies its own runtime', () => {
|
||||
it('tries a temporary status listener whose tagged output actually fires, then stops it', async () => {
|
||||
it('mounts a temporary status listener whose tagged output actually fires, then unmounts it', async () => {
|
||||
ctx = await cordisHarness()
|
||||
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
const agent = ctx.agentLoop.create(SessionId('cordis-e2e-listener'), { provider: 'deepseek', model: 'deepseek-v4-flash' })
|
||||
|
||||
agent.followup([{
|
||||
type: 'text',
|
||||
text: 'Use cordis_try to create a temporary Plugin that listens to the \'agent/status\' '
|
||||
text: 'Use cordis_mount to create a temporary Plugin that listens to the \'agent/status\' '
|
||||
+ 'Cordis event and logs every change with console.log. Reply "running" once done.',
|
||||
}])
|
||||
await waitForIdle(ctx, agent)
|
||||
@@ -58,7 +58,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('cordis tools: a real model modif
|
||||
})
|
||||
expect(resultText(mid)).toContain('dyn-')
|
||||
|
||||
agent.followup([{ type: 'text', text: 'Now stop the temporary Plugin you just tried.' }])
|
||||
agent.followup([{ type: 'text', text: 'Now unmount the temporary Plugin you just mounted.' }])
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const after = await ctx.tools.execute({
|
||||
@@ -74,7 +74,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('cordis tools: a real model modif
|
||||
|
||||
agent.followup([{
|
||||
type: 'text',
|
||||
text: 'Give yourself a new tool: use cordis_try to create a temporary Plugin with '
|
||||
text: 'Give yourself a new tool: use cordis_mount to create a temporary Plugin with '
|
||||
+ 'inject ["tools"] that calls harness.registerTool(ctx, harness.defineTool({...})) '
|
||||
+ 'to register a tool named reverse_text with one required string parameter '
|
||||
+ '"text", returning the text reversed. Then CALL reverse_text with the '
|
||||
@@ -88,7 +88,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('cordis tools: a real model modif
|
||||
expect(ctx.tools.get('reverse_text')).toBeDefined()
|
||||
const events = [...agent.session.events]
|
||||
const calls = events.filter(event => event.type === 'tool/call')
|
||||
expect(calls.some(event => event.data.name === 'cordis_try')).toBe(true)
|
||||
expect(calls.some(event => event.data.name === 'cordis_mount')).toBe(true)
|
||||
const reverseCalls = calls.filter(event => event.data.name === 'reverse_text')
|
||||
expect(reverseCalls.length).toBeGreaterThan(0)
|
||||
const reverseResults = events
|
||||
@@ -98,7 +98,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('cordis tools: a real model modif
|
||||
// On failure, surface what the model actually mounted and what the tool
|
||||
// returned — an e2e failing at a distance is undebuggable without it.
|
||||
const mountCode = calls
|
||||
.filter(event => event.data.name === 'cordis_try')
|
||||
.filter(event => event.data.name === 'cordis_mount')
|
||||
.map(event => event.data.arguments)
|
||||
.join('\n---\n')
|
||||
const trace = events.map((event) => {
|
||||
@@ -115,13 +115,13 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('cordis tools: a real model modif
|
||||
).toBe(true)
|
||||
}, 120_000)
|
||||
|
||||
it('composes two temporary Plugins through provide/inject, and stopping the provider parks the consumer', async () => {
|
||||
it('composes two temporary Plugins through provide/inject, and unmounting the provider parks the consumer', async () => {
|
||||
ctx = await cordisHarness()
|
||||
const agent = ctx.agentLoop.create(SessionId('cordis-e2e-compose'), { provider: 'deepseek', model: 'deepseek-v4-flash' })
|
||||
|
||||
agent.followup([{
|
||||
type: 'text',
|
||||
text: 'Try TWO separate temporary Plugins with cordis_try. First a provider: apply calls '
|
||||
text: 'Mount TWO separate temporary Plugins with cordis_mount. First a provider: apply calls '
|
||||
+ 'ctx.provide(\'shouter\', { shout: (s) => s.toUpperCase() }). Second a consumer with '
|
||||
+ 'inject ["shouter", "tools"] that registers (via harness.registerTool + harness.defineTool) '
|
||||
+ 'a tool named shout_text with one required string parameter "text" whose execute returns '
|
||||
@@ -144,7 +144,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('cordis tools: a real model modif
|
||||
.flatMap(event => event.data.content.filter(block => block.type === 'text').map(block => block.text))
|
||||
expect(shoutResults.some(text => text.includes('QUIET'))).toBe(true)
|
||||
|
||||
agent.followup([{ type: 'text', text: 'Now stop ONLY the provider temporary Plugin (the one that provided shouter).' }])
|
||||
agent.followup([{ type: 'text', text: 'Now unmount ONLY the provider temporary Plugin (the one that provided shouter).' }])
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
// The consumer must have been parked by cordis itself: service gone,
|
||||
|
||||
@@ -15,8 +15,8 @@ import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
|
||||
|
||||
const PERSONA = 'You are cordis-agent, a self-referential harness demo. '
|
||||
+ 'Your cordis_* tools operate on the live cordis runtime you run inside: '
|
||||
+ 'cordis_inspect to look around, cordis_try to try a temporary Plugin, cordis_stop '
|
||||
+ 'to stop one. Follow the tool descriptions exactly and report results briefly.'
|
||||
+ 'cordis_inspect to look around, cordis_mount to mount a temporary Plugin, cordis_unmount '
|
||||
+ 'to unmount one. Follow the tool descriptions exactly and report results briefly.'
|
||||
|
||||
export async function cordisHarness(): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
|
||||
Reference in New Issue
Block a user