mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Implements docs/rfc/.../2026-06-20-extract-example-app-packages.md. Each
example was thick — a hand-rolled start.ts, an infra preamble, nested
base.yml/base-core.yml/acp-tail.yml includes, and a coupled front-door
cluster enforced only by prose. This moves the composition into packages so
each example is a thin leaf cordis.yml: pick the swappable backends, load one
app package.
New packages:
- @deepseek-ai/dsh-agent-core (packages/core/agent-core): one bundle plugin
that loads the providerless/executor-less/UI-less spine (timer + llm +
sessions + system-prompt + tools + agents + invariants + tool-bash +
agent-loop) via ctx.plugin(...) inside apply(), and forwards agent-loop's
`agents` list as its own Config (export const Config = AgentLoop.Config,
default []).
- @deepseek-ai/dsh-stdio-agent (packages/ui/stdio-agent): terminal chat APP —
agent-core + console logger + readline UI + a pre-created `main` agent, with
a bin. The demo:echo/coding front door.
- @deepseek-ai/dsh-acp-agent (packages/ui/acp-agent): ACP server APP —
agent-core + JSONL persistence + the acp bridge, NO stdout logger, with a
bin. The stdout-purity footgun is structurally unreachable from the leaf.
Amendment to the RFC: hmr stays a LEAF cordis.yml entry, not baked into
dsh-stdio-agent. hmr is a Loader-only dev plugin (throws without
--expose-internals; the in-process test tier can't even import its decorator
form), so a package statically importing it could never carry the per-file
coverage gate. Unlike the console logger, a stray hmr is not a stdout-purity
footgun, so leaving it at the leaf costs no safety. With hmr out, all three new
packages carry in-process unit specs at 100%.
Boot glue (Loader tail, .env load, snapshot-mode selection, stdin-dispose
lifecycle) moves into each app's bin; start.ts and base.yml/base-core.yml/
acp-tail.yml are deleted. Each app package gets a keyless real-load-path test
that boots through its bin + the cordis Loader (guarding the unwrapExports
export-shape bug class, postmortem 0001). ACP snapshot replay stays green
against the existing committed goldens (pure boot restructuring). RFC moved
proposed->implemented with the amendment recorded; package/example/architecture
docs and the module graph updated.
71 lines
2.8 KiB
JavaScript
71 lines
2.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* The `dsh-stdio-agent` bin: boot a Cordis app from a leaf `cordis.yml` that
|
|
* loads the {@link @deepseek-ai/dsh-stdio-agent} app plugin (plus a backend LLM
|
|
* adapter and a bash executor). Owns the boot glue the three `examples/*` once
|
|
* duplicated in their `start.ts`: load the gitignored repo-root `.env`, then
|
|
* drive the cordis Loader against the config path (default `./cordis.yml`).
|
|
*
|
|
* Usage: `dsh-stdio-agent [path-to-cordis.yml]`. The `demo:echo` / `demo:coding`
|
|
* scripts invoke it with the example's config.
|
|
*
|
|
* @module @deepseek-ai/dsh-stdio-agent/bin
|
|
*/
|
|
|
|
import { pathToFileURL } from 'node:url'
|
|
import { basename, dirname, resolve } from 'node:path'
|
|
import { Context } from 'cordis'
|
|
import Loader from '@cordisjs/plugin-loader'
|
|
|
|
/**
|
|
* Load `DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL` from a gitignored `.env` in the
|
|
* CURRENT WORKING DIRECTORY (Node native `process.loadEnvFile`). An absent file
|
|
* is fine — the environment may already carry the variables; the leaf
|
|
* `cordis.yml` reads them via the `!!js` tag. A present-but-unreadable/malformed
|
|
* `.env` is a real misconfiguration: surface it on stderr rather than silently
|
|
* running with the wrong environment. The mock-model demo (echo) ships no key
|
|
* and simply has no `.env`.
|
|
*/
|
|
function loadEnv(): void {
|
|
try {
|
|
process.loadEnvFile(resolve(process.cwd(), '.env'))
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException | null)?.code !== 'ENOENT') {
|
|
process.stderr.write(`dsh-stdio-agent: failed to load .env: ${String(error)}\n`)
|
|
}
|
|
// ENOENT (no .env) is fine — rely on the ambient environment.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Boot the Loader against `configPath` (resolved from the CWD). `baseUrl` is
|
|
* pinned to the config's directory and the include is handed only the basename,
|
|
* so the config's relative plugin/include paths resolve exactly as the upstream
|
|
* `cordis` bin does. Returns the root context (the process owns its lifetime).
|
|
*/
|
|
export async function boot(configPath: string): Promise<Context> {
|
|
const absolute = resolve(process.cwd(), configPath)
|
|
const ctx = new Context()
|
|
ctx.baseUrl = pathToFileURL(dirname(absolute)).href + '/'
|
|
await ctx.plugin(Loader)
|
|
await ctx.loader.create({
|
|
name: '@cordisjs/plugin-include',
|
|
config: { path: `./${basename(absolute)}` },
|
|
})
|
|
return ctx
|
|
}
|
|
|
|
/**
|
|
* Entry point: load `.env`, then boot the config named on argv (default
|
|
* `./cordis.yml`). Awaited at the module top level by the published bin
|
|
* (`#!/usr/bin/env node` shebang via the package's `bin` field).
|
|
*/
|
|
export async function main(argv: string[] = process.argv.slice(2)): Promise<void> {
|
|
loadEnv()
|
|
await boot(argv[0] ?? './cordis.yml')
|
|
}
|
|
|
|
/* v8 ignore start -- top-level CLI invocation; the testable core is boot()/main(), driven by the keyless Loader-path smoke */
|
|
await main()
|
|
/* v8 ignore stop */
|