refactor(cli)!: one shared base config with per-surface overlays
`dsh` shipped two config trees that were 43 rows the same: apps/cli/cordis.yml composed web as 74 flat rows, while the TUI booted examples/tui-agent/cordis.yml whose single `@deepseek-ai/dsh-tui-demo` row mounted twelve plugins behind a twenty-key pass-through Config. Neither file was what its location claimed — apps/cli hardcoded the "example" as the product default and the "demo" bundle was the application — and every capability change had to be made twice. - apps/cli/base.cordis.yml holds the 43 shared rows; tui.cordis.yml and web.cordis.yml are patch lists stating only what differs per surface - overlays apply as SIBLING patch lists at one include level, because include patches never cross an include boundary. Precedence: base < surface < (--config | personal ~/.dsh/config.yaml) < launcher flag/profile patches - `--config` now applies an overlay INSTEAD OF the personal one, so a demo or test tree never inherits the user's route; new `--config-replace` boots a file as the entire tree (the old `--config` behaviour). Both survive /resume - vendor/include: index each `insert`ed row as it is added so a later patch can configure or disable it. Upstream built the id index once before the patch loop, leaving every surface-only row — the whole TUI front door — silently unpatchable from user config. Logged as local modification 8 - session identity moves to dsh-agent-loop's CONFIGURED_AGENT_IDENTITIES_KEY; dsh-tui's MAIN_SESSION_ID_KEY is deleted (only the bundle read it) - delete examples/tui-agent, examples/cordis-agent, packages/examples/tui-demo; TUI tests → apps/cli/tests, cordis e2e → packages/cordis/tool-cordis/tests, examples/code-mode survives as an overlay leaf - `dsh web` gains --config, threaded into AppCLIEntry as an extra overlay Three latent defects surfaced and are fixed here: the TUI captured the optional sessionQuery service once at construction and could permanently disable /resume when it won the mount race; the session-store root silently reverted to a project-local ./.sessions; --config-replace was dropped by the resume handoff. Verified by booting each tree through the real Loader (TUI 55 entries, web 75, zero unsettled) rather than reading YAML. All eight terminal snapshots replay byte-identically; 14/14 PTY smoke, 112/112 snapshots, 25/25 doc-sync, hygiene and lint clean.
This commit is contained in:
@@ -1,37 +0,0 @@
|
||||
# cordis-agent
|
||||
|
||||
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, 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
|
||||
|
||||
```sh
|
||||
# repo root .env (gitignored) or exported env:
|
||||
# DEEPSEEK_API_KEY=sk-…
|
||||
# DEEPSEEK_BASE_URL=https://… # optional; defaults to the public API
|
||||
pnpm run demo:cordis # TUI (default)
|
||||
pnpm run demo:cordis web # browser UI at http://127.0.0.1:3081
|
||||
pnpm run demo:cordis acp # ACP server
|
||||
```
|
||||
|
||||
The intended demo is staged — verify the listener link first, then let the agent extend itself:
|
||||
|
||||
```
|
||||
> 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_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
|
||||
> 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 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 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.
|
||||
@@ -1,55 +0,0 @@
|
||||
<!-- Generated by scripts/gen-doc-graphs.ts - do not edit by hand.
|
||||
Run `pnpm run gen-doc-graphs` to regenerate. -->
|
||||
|
||||
# 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 mount or unmount in-memory temporary Plugins.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
cfg["examples/cordis-agent<br/>cordis.yml"]
|
||||
plugin_cordis_hmr["hmr<br/>@cordisjs/plugin-hmr"]
|
||||
cfg --> plugin_cordis_hmr
|
||||
plugin_cordis_llm_deepseek["llm-deepseek<br/>@deepseek-ai/dsh-llm-deepseek"]
|
||||
cfg --> plugin_cordis_llm_deepseek
|
||||
plugin_cordis_subprocess["subprocess<br/>@deepseek-ai/dsh-subprocess-local"]
|
||||
cfg --> plugin_cordis_subprocess
|
||||
plugin_cordis_bash["bash<br/>@deepseek-ai/dsh-bash-local"]
|
||||
cfg --> plugin_cordis_bash
|
||||
plugin_cordis_fs_local["fs-local<br/>@deepseek-ai/dsh-fs-local"]
|
||||
cfg --> plugin_cordis_fs_local
|
||||
plugin_cordis_web["web<br/>@deepseek-ai/dsh-web"]
|
||||
cfg --> plugin_cordis_web
|
||||
plugin_cordis_web_fetch_local["web-fetch-local<br/>@deepseek-ai/dsh-web-fetch-local"]
|
||||
cfg --> plugin_cordis_web_fetch_local
|
||||
plugin_cordis_token_meter["token-meter<br/>@deepseek-ai/dsh-token-meter"]
|
||||
cfg --> plugin_cordis_token_meter
|
||||
plugin_cordis_tui_agent["tui-agent<br/>@deepseek-ai/dsh-tui-demo"]
|
||||
cfg --> plugin_cordis_tui_agent
|
||||
plugin_cordis_tui_agent --> bundle_agent_core["@deepseek-ai/dsh-agent-spine-demo"]
|
||||
plugin_cordis_tui_agent --> bundle_jsonl["@deepseek-ai/dsh-session-persistence-jsonl"]
|
||||
plugin_cordis_tui_agent --> frontdoor_tui["@deepseek-ai/dsh-tui<br/>pre-created main agent"]
|
||||
bundle_agent_core --> spine_llm["ctx.llm"]
|
||||
bundle_agent_core --> spine_sessions["ctx.sessions"]
|
||||
bundle_agent_core --> spine_tools["ctx.tools + tool-bash"]
|
||||
bundle_agent_core --> spine_loop["ctx.agents + ctx.agentLoop"]
|
||||
plugin_cordis_tool_cordis["tool-cordis<br/>@deepseek-ai/dsh-tool-cordis"]
|
||||
cfg --> plugin_cordis_tool_cordis
|
||||
```
|
||||
|
||||
| Plugin id | Package / module |
|
||||
| --- | --- |
|
||||
| `hmr` | `@cordisjs/plugin-hmr` |
|
||||
| `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek` |
|
||||
| `subprocess` | `@deepseek-ai/dsh-subprocess-local` |
|
||||
| `bash` | `@deepseek-ai/dsh-bash-local` |
|
||||
| `fs-local` | `@deepseek-ai/dsh-fs-local` |
|
||||
| `web` | `@deepseek-ai/dsh-web` |
|
||||
| `web-fetch-local` | `@deepseek-ai/dsh-web-fetch-local` |
|
||||
| `token-meter` | `@deepseek-ai/dsh-token-meter` |
|
||||
| `tui-agent` | `@deepseek-ai/dsh-tui-demo` |
|
||||
| `tool-cordis` | `@deepseek-ai/dsh-tool-cordis` |
|
||||
|
||||
Source config: [`examples/cordis-agent/cordis.yml`](cordis.yml).
|
||||
|
||||
Maintenance mode: hybrid: the leaf plugin list is parsed from its `cordis.yml`; app package expansion is curated from package source.
|
||||
@@ -1,87 +0,0 @@
|
||||
# Self-referential TUI demo: the coding spine plus tools to inspect the live
|
||||
# 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
|
||||
# such as `ctx.bash`. Grant this toolset like bash access. See
|
||||
# ../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md.
|
||||
|
||||
# Development-only hot reload; production assemblies omit it.
|
||||
- id: hmr
|
||||
name: '@cordisjs/plugin-hmr'
|
||||
config:
|
||||
root: ['.']
|
||||
|
||||
# The DeepSeek adapter. Shipped default: full thinking at max effort on every
|
||||
# request (wire-only defaults; they never enter the request header).
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
baseURL: !!js process.env.DEEPSEEK_BASE_URL
|
||||
thinking: enabled
|
||||
reasoningEffort: max
|
||||
|
||||
# Local bash executor for agent-spine-demo's tool-bash schema — gives the agent an
|
||||
# ordinary tool whose calls make the mounted listeners observably fire.
|
||||
# Managed child-process groups for the bash executor (spawn/kill/output plumbing).
|
||||
- id: subprocess
|
||||
name: '@deepseek-ai/dsh-subprocess-local'
|
||||
|
||||
- id: bash
|
||||
name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
timeoutMs: 60000
|
||||
|
||||
# Filesystem service for mounted plugins (ctx.fs) — the local provider only.
|
||||
# The model-facing read/write/edit tools stay unmounted on purpose: this demo
|
||||
# is about the agent building its own tools over the services.
|
||||
- id: fs-local
|
||||
name: '@deepseek-ai/dsh-fs-local'
|
||||
config:
|
||||
cwd: !!js process.cwd()
|
||||
|
||||
# Web service for mounted plugins (ctx.web): the seam plus the anonymous local
|
||||
# fetch provider (keyless). No search provider is loaded — ctx.web search
|
||||
# calls fail loud until a deployment adds one.
|
||||
- id: web
|
||||
name: '@deepseek-ai/dsh-web'
|
||||
|
||||
- id: web-fetch-local
|
||||
name: '@deepseek-ai/dsh-web-fetch-local'
|
||||
|
||||
- id: token-meter
|
||||
name: '@deepseek-ai/dsh-token-meter'
|
||||
|
||||
# The app bundle pre-creates the self-referential demo's `main` agent.
|
||||
- id: tui-agent
|
||||
name: '@deepseek-ai/dsh-tui-demo'
|
||||
config:
|
||||
provider: deepseek
|
||||
model: deepseek-v4-pro
|
||||
persistenceRoot: './.sessions'
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
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.
|
||||
|
||||
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 mount an in-memory temporary
|
||||
Plugin (an event listener, a brand-new tool for yourself, or a service
|
||||
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 unmount what you no longer
|
||||
need. Report results briefly.
|
||||
|
||||
# The self-referential cordis toolset (loaded after the app so ctx.tools exists).
|
||||
- id: tool-cordis
|
||||
name: '@deepseek-ai/dsh-tool-cordis'
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"name": "cordis-agent-example",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"description": "Runnable demo: the self-referential harness — an agent that inspects and modifies its own cordis runtime"
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Context } from 'cordis'
|
||||
import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm'
|
||||
import { cordisHarness, waitForIdle } from './harness.ts'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
|
||||
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.
|
||||
* 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
|
||||
* actually LAND in the reflect store. Key-gated (see vitest.e2e.config.ts).
|
||||
*/
|
||||
|
||||
let ctx: Context | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks()
|
||||
// Always dispose the harness, even on failure/retry/timeout: agent-loop
|
||||
// teardown stops the loop, and disposing the tree unwinds every dynamic
|
||||
// mount the model left behind.
|
||||
await ctx?.fiber.dispose()
|
||||
ctx = undefined
|
||||
})
|
||||
|
||||
/** The tagged write-through lines (`[cordis:dyn-n] …`) captured by a console spy. */
|
||||
function taggedCalls(log: { mock: { calls: unknown[][] } }): unknown[][] {
|
||||
return log.mock.calls.filter(call => typeof call[0] === 'string' && /^\[cordis:dyn-\d+\]$/.test(call[0]))
|
||||
}
|
||||
|
||||
/** Model-facing text of one tool result, concatenated. */
|
||||
function resultText(result: { content: { type: string; text?: string }[] }): string {
|
||||
return result.content.filter(block => block.type === 'text').map(block => block.text).join('')
|
||||
}
|
||||
|
||||
describe.skipIf(!process.env.DEEPSEEK_API_KEY)('cordis tools: a real model modifies its own runtime', () => {
|
||||
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(createUserMessage({
|
||||
content: [{
|
||||
type: 'text',
|
||||
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.',
|
||||
}], source: { kind: 'user' } }))
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
// The WORLD check: the turn's own running→idle transition must have driven
|
||||
// the mounted listener through the tagged sandbox console.
|
||||
expect(taggedCalls(log).length).toBeGreaterThan(0)
|
||||
const mid = await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('verify-mounted'), name: 'cordis_inspect', arguments: { what: 'temporary' },
|
||||
})
|
||||
expect(resultText(mid)).toContain('dyn-')
|
||||
|
||||
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'Now unmount the temporary Plugin you just mounted.' }], source: { kind: 'user' } }))
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const after = await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('verify-unmounted'), name: 'cordis_inspect', arguments: { what: 'temporary' },
|
||||
})
|
||||
expect(resultText(after)).toContain('No temporary Plugins are running.')
|
||||
}, 120_000)
|
||||
|
||||
it('builds itself a reverse_text tool and actually calls it', async () => {
|
||||
ctx = await cordisHarness()
|
||||
const agent = ctx.agentLoop.create(SessionId('cordis-e2e-selftool'), { provider: 'deepseek', model: 'deepseek-v4-flash' })
|
||||
|
||||
agent.followup(createUserMessage({
|
||||
content: [{
|
||||
type: 'text',
|
||||
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 '
|
||||
+ 'exact text "harness" and report its exact output.',
|
||||
}], source: { kind: 'user' } }))
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
// World checks: the tool exists in the registry, was invoked as a real tool call, and its
|
||||
// RESULT (the self-made execute actually running) is the reversed string. Model prose is only
|
||||
// self-report and is deliberately not asserted.
|
||||
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)
|
||||
const reverseCalls = calls.filter(event => event.data.name === 'reverse_text')
|
||||
expect(reverseCalls.length).toBeGreaterThan(0)
|
||||
const reverseResults = events
|
||||
.filter(event => event.type === 'tool/result')
|
||||
.filter(event => reverseCalls.some(call => call.data.callId === event.data.message.source.callId))
|
||||
.flatMap(event => event.data.message.content[0].content.filter(block => block.type === 'text').map(block => block.text))
|
||||
// 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')
|
||||
.map(event => event.data.arguments)
|
||||
.join('\n---\n')
|
||||
const trace = events.map((event) => {
|
||||
switch (event.type) {
|
||||
case 'tool/call': return `tool/call:${event.data.name}`
|
||||
case 'tool/result': return `tool/result:${event.data.message.content[0].isError ? 'ERR:' + JSON.stringify(event.data.message.content[0].content).slice(0, 200) : 'ok'}`
|
||||
case 'turn/end': return `turn/end:${JSON.stringify(event.data.reason)}`
|
||||
default: return event.type
|
||||
}
|
||||
}).join('\n')
|
||||
expect(
|
||||
reverseResults.some(text => text.includes('ssenrah')),
|
||||
`no reversed output in reverse_text results.\nresults: ${JSON.stringify(reverseResults)}\nmount code: ${mountCode}\ntrace:\n${trace}`,
|
||||
).toBe(true)
|
||||
}, 120_000)
|
||||
|
||||
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(createUserMessage({
|
||||
content: [{
|
||||
type: 'text',
|
||||
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 '
|
||||
+ 'ctx.shouter.shout(args.text) as a text content block. Then CALL shout_text with "quiet" '
|
||||
+ 'and report the exact output.',
|
||||
}], source: { kind: 'user' } }))
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
// World checks: the service is really in the store, the tool really ran.
|
||||
expect(ctx.get('shouter')).toBeDefined()
|
||||
expect(ctx.tools.get('shout_text')).toBeDefined()
|
||||
const events = [...agent.session.events]
|
||||
const shoutCalls = events
|
||||
.filter(event => event.type === 'tool/call')
|
||||
.filter(event => event.data.name === 'shout_text')
|
||||
expect(shoutCalls.length).toBeGreaterThan(0)
|
||||
const shoutResults = events
|
||||
.filter(event => event.type === 'tool/result')
|
||||
.filter(event => shoutCalls.some(call => call.data.callId === event.data.message.source.callId))
|
||||
.flatMap(event => event.data.message.content[0].content.filter(block => block.type === 'text').map(block => block.text))
|
||||
expect(shoutResults.some(text => text.includes('QUIET'))).toBe(true)
|
||||
|
||||
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'Now unmount ONLY the provider temporary Plugin (the one that provided shouter).' }], source: { kind: 'user' } }))
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
// The consumer must have been parked by cordis itself: service gone,
|
||||
// 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: 'temporary' },
|
||||
})
|
||||
expect(resultText(after)).toContain('waiting for: shouter')
|
||||
}, 120_000)
|
||||
})
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Context } from 'cordis'
|
||||
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 LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
|
||||
|
||||
/**
|
||||
* Shared harness for the cordis-agent e2e suite: the agent spine with the real
|
||||
* DeepSeek adapter and the real `@deepseek-ai/dsh-tool-cordis` plugin, so a
|
||||
* live model can mount plugins into the very context the test observes. Lives
|
||||
* outside the *.e2e.ts pattern so importing it never re-registers another
|
||||
* file's tests.
|
||||
*/
|
||||
|
||||
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 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()
|
||||
await mountAgentLoopTestDependencies(ctx, {
|
||||
systemPrompt: { persona: PERSONA },
|
||||
})
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(LlmDeepSeek)
|
||||
await ctx.plugin(ToolCordis)
|
||||
return ctx
|
||||
}
|
||||
|
||||
export function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const dispose = ctx.on('agent/status', (subject, status) => {
|
||||
if (subject === agent && status === 'idle') {
|
||||
dispose()
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { LOADER_SMOKE_TEST_TIMEOUT_MS } from '@deepseek-ai/dsh-loader-smoke'
|
||||
import { runTuiPtySmoke } from '../../tui-agent/tests/pty-harness.ts'
|
||||
|
||||
const binScript = fileURLToPath(new URL('../../../apps/cli/src/bin.ts', import.meta.url))
|
||||
const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url))
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
|
||||
describe('cordis-agent keyless smoke (real Loader tree in a PTY)', () => {
|
||||
it('boots the full tool-cordis tree and exits cleanly through the TUI', async () => {
|
||||
const output = await runTuiPtySmoke({
|
||||
label: 'cordis-agent',
|
||||
tempDirPrefix: 'cordis-agent-smoke-',
|
||||
binScript,
|
||||
configPath,
|
||||
tsconfigPath,
|
||||
env: { DEEPSEEK_API_KEY: 'keyless-smoke-no-call' },
|
||||
actions: [{ waitFor: 'cordis-agent ready.', send: '/exit\r' }],
|
||||
})
|
||||
expect(output).toContain('cordis-agent ready.')
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
})
|
||||
Reference in New Issue
Block a user