Rename Cordis tools for temporary plugins

Clarify process-local lifecycle semantics and refresh generated documentation, demos, and snapshots.
This commit is contained in:
NI0317
2026-07-27 16:57:26 +08:00
parent 5be0118784
commit b4a1304489
73 changed files with 1807 additions and 1002 deletions

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
README.md: 1309fe9b2935d3224f097ceb2e80501c8075a933
README.zh.md: 2e3e7d7206d0d676ae7d9c9b3a2c2f8be26aafe7
# pnpm run verify-translation-pairing --write examples/cordis-agent/README.md
README.md: cabdd063094b520039d1ca84d22e3471198df1b1
README.zh.md: 346c4ef93634e4e917178c98c10e727a04682cda

View File

@@ -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 hands the model three tools over the **live cordis runtime it is running inside** — inspect it, mount new plugins into it, and dispose them again. The `ctx.fs` and `ctx.web` services are mounted (provider-only, no model-facing file/web tools) so the plugins the agent writes have real capabilities to build on; Node built-ins are trapped in the sandbox and redirect to those services. The design (sandbox semantics, mount lifecycle, cross-mount composition, caveats) 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, 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).
## Run it
@@ -16,16 +16,16 @@ pnpm run demo:cordis
The intended demo is staged — verify the listener link first, then let the agent extend itself:
```
> Mount a 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] mounted dyn-1 (plugin "status-logger", state: active)
> 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).
[tool call] bash({"command": "echo hi"})
[cordis:dyn-1] status → … ← the mounted listener firing, live
> Now give yourself a reverse_text tool and use it on "harness".
[tool call] cordis_mount({"code": "return { name: 'reverse-text', inject: ['tools'], apply(ctx) { ctx.tools.register(harness.defineTool({ name: 'reverse_text', … })) } }"})
[tool call] cordis_try({"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
> Unmount both.
[tool call] cordis_unmount({"id": "dyn-1"})
> Stop both temporary Plugins.
[tool call] cordis_stop({"id": "dyn-1"})
```
Ask for `cordis_inspect` with `what: "api"` or `what: "events"` to see the generated service/event reference the agent writes plugin code against, and try two cooperating mounts (`ctx.provide` in one, `inject` in the other) to watch cordis park and revive the consumer.

View File

@@ -16,16 +16,16 @@ pnpm run demo:cordis
预期演示分阶段进行:先验证监听器链接,再让 agent 扩展自身:
```
> Mount a 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] mounted dyn-1 (plugin "status-logger", state: active)
> 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).
[tool call] bash({"command": "echo hi"})
[cordis:dyn-1] status → … ← the mounted listener firing, live
> Now give yourself a reverse_text tool and use it on "harness".
[tool call] cordis_mount({"code": "return { name: 'reverse-text', inject: ['tools'], apply(ctx) { ctx.tools.register(harness.defineTool({ name: 'reverse_text', … })) } }"})
[tool call] cordis_try({"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
> Unmount both.
[tool call] cordis_unmount({"id": "dyn-1"})
> Stop both temporary Plugins.
[tool call] cordis_stop({"id": "dyn-1"})
```
请求 `cordis_inspect` 并使用 `what: "api"` 或 `what: "events"`,即可查看为 agent 生成、供其编写插件时参考的服务/事件资料。还可尝试两个协作挂载(一个中调用 `ctx.provide`,另一个中使用 `inject`),观察 cordis 如何暂停并恢复消费方。

View File

@@ -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 own runtime and mount/unmount plugins into it.
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.
```mermaid
flowchart LR

View File

@@ -1,9 +1,9 @@
# Self-referential TUI demo: the coding spine plus tools to inspect the live
# service/plugin/tool/mount/API/event state, mount a model-written plugin under
# `cordis-dynamic`, and quiescently unmount it. The app bin loads the gitignored
# service/plugin/tool/temporary/API/event state, try a model-written temporary
# Plugin, and quiescently stop 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; mounted code can reach live capabilities
# access but are not a security boundary; temporary Plugin code reaches live capabilities
# such as `ctx.bash`. Grant this toolset like bash access. See
# ../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md.
@@ -60,7 +60,7 @@
persistenceRoot: './.sessions'
workspaceContext:
maxBytes: 65536
welcome: 'cordis-agent ready. Ask it to inspect its runtime, mount a listener, or invent a tool for itself.'
welcome: 'cordis-agent ready. Ask it to inspect its runtime, try 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,13 +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_mount to add a plugin (an event
listener, a brand-new tool for yourself, or a service other mounts
inject), cordis_unmount to clean one up. In mounted code, NEVER use Node
your Plugin code can use), cordis_try to try 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
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 unmount what you no longer
events unless you intend to intercept, and stop what you no longer
need. Report results briefly.
# The self-referential cordis toolset (loaded after the app so ctx.tools exists).

View File

@@ -8,7 +8,7 @@ const testToolSignal = new AbortController().signal
/**
* With-key smoke for the self-referential cordis tools: a REAL model drives
* cordis_mount/cordis_unmount against the live context the test observes.
* cordis_try/cordis_stop 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,15 +37,15 @@ 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('mounts a status listener whose tagged output actually fires, then unmounts it', async () => {
it('tries a temporary status listener whose tagged output actually fires, then stops 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_mount to mount a plugin that listens to the \'agent/status\' '
+ 'cordis event and logs every change with console.log. Reply "mounted" once done.',
text: 'Use cordis_try 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)
@@ -54,18 +54,18 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('cordis tools: a real model modif
expect(taggedCalls(log).length).toBeGreaterThan(0)
const mid = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('verify-mounted'), name: 'cordis_inspect', arguments: { what: 'dynamic' },
callId: CallId('verify-mounted'), name: 'cordis_inspect', arguments: { what: 'temporary' },
})
expect(resultText(mid)).toContain('dyn-')
agent.followup([{ type: 'text', text: 'Now unmount the plugin you just mounted.' }])
agent.followup([{ type: 'text', text: 'Now stop the temporary Plugin you just tried.' }])
await waitForIdle(ctx, agent)
const after = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('verify-unmounted'), name: 'cordis_inspect', arguments: { what: 'dynamic' },
callId: CallId('verify-unmounted'), name: 'cordis_inspect', arguments: { what: 'temporary' },
})
expect(resultText(after)).toContain('(no dynamic plugins mounted)')
expect(resultText(after)).toContain('No temporary Plugins are running.')
}, 120_000)
it('builds itself a reverse_text tool and actually calls it', async () => {
@@ -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_mount to mount a plugin with '
text: 'Give yourself a new tool: use cordis_try 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_mount')).toBe(true)
expect(calls.some(event => event.data.name === 'cordis_try')).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_mount')
.filter(event => event.data.name === 'cordis_try')
.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 mounts through provide/inject, and unmounting the provider parks the consumer', async () => {
it('composes two temporary Plugins through provide/inject, and stopping 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: 'Mount TWO separate plugins with cordis_mount. First a provider: apply calls '
text: 'Try TWO separate temporary Plugins with cordis_try. 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,16 +144,16 @@ 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 unmount ONLY the provider plugin (the one that provided shouter).' }])
agent.followup([{ type: 'text', text: 'Now stop 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,
// dependent tool unregistered, dynamic table naming the missing service.
// dependent tool unregistered, temporary section naming the missing service.
expect(ctx.get('shouter')).toBeUndefined()
expect(ctx.tools.get('shout_text')).toBeUndefined()
const after = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('verify-parked'), name: 'cordis_inspect', arguments: { what: 'dynamic' },
callId: CallId('verify-parked'), name: 'cordis_inspect', arguments: { what: 'temporary' },
})
expect(resultText(after)).toContain('waiting for: shouter')
}, 120_000)

View File

@@ -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_mount to add a plugin, cordis_unmount '
+ 'to clean one up. Follow the tool descriptions exactly and report results briefly.'
+ '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.'
export async function cordisHarness(): Promise<Context> {
const ctx = new Context()