Merge origin/master into feat/website-docs

Conflict resolution notes:
- package.json/run-gates: both sides' new doc-sync gates kept (master's
  scoped-events/readme gates + this branch's website-api/website-yaml);
  js-yaml devDeps deduped (master added them independently).
- pnpm-workspace/knip: website AND python/sdk-runtime entries kept.
- doc-typecheck/verify-type-equiv: master's condensed headers kept, website
  glob retained in both scan scopes.
- vendor/cordis/src/fiber.ts: master's lifecycle-hardening code taken; this
  branch's richer FiberState JSDoc reapplied on top. vendor/README.md logs
  both local modifications (hardening = 6, JSDoc enrichment = 7).
- pnpm-lock: regenerated from master's side (pnpm install).

Post-merge sync the gates forced (the system working as designed):
- verify-website-yaml caught 4 stale plugin names from master's package
  reorg (dsh-stdio-agent -> dsh-stdio-demo, dsh-acp-agent -> dsh-acp-demo);
  8 references fixed across guide/ and develop/.
- gen-website-api picked up master's 6 new services automatically
  (ctx.approval/permission/sandbox/sessionQuery/skills/tasks -> 6 new pages
  + sidebar); api/index.md hub updated to list them.
- AGENTS.md budget ceiling 1370 -> 1400: the website rows (layout line + two
  command lines) and master's own growth collided with the old ceiling; all
  three website rows are load-bearing (new top-level dir, new CI command).
This commit is contained in:
lintianle
2026-07-16 21:36:43 +08:00
1132 changed files with 71533 additions and 19566 deletions

View File

@@ -30,6 +30,10 @@
"text": "ctx.agents",
"link": "/zh-CN/api/harness/agents"
},
{
"text": "ctx.approval",
"link": "/zh-CN/api/harness/approval"
},
{
"text": "ctx.bash",
"link": "/zh-CN/api/harness/bash"
@@ -50,14 +54,30 @@
"text": "ctx.llm",
"link": "/zh-CN/api/harness/llm"
},
{
"text": "ctx.permission",
"link": "/zh-CN/api/harness/permission"
},
{
"text": "ctx.sandbox",
"link": "/zh-CN/api/harness/sandbox"
},
{
"text": "ctx.sessionPersistence",
"link": "/zh-CN/api/harness/session-persistence"
},
{
"text": "ctx.sessionQuery",
"link": "/zh-CN/api/harness/session-query"
},
{
"text": "ctx.sessions",
"link": "/zh-CN/api/harness/sessions"
},
{
"text": "ctx.skills",
"link": "/zh-CN/api/harness/skills"
},
{
"text": "ctx.subagents",
"link": "/zh-CN/api/harness/subagents"
@@ -66,6 +86,10 @@
"text": "ctx.systemPrompt",
"link": "/zh-CN/api/harness/system-prompt"
},
{
"text": "ctx.tasks",
"link": "/zh-CN/api/harness/tasks"
},
{
"text": "ctx.tools",
"link": "/zh-CN/api/harness/tools"

View File

@@ -19,7 +19,7 @@ Register a cleanup-aware effect on this fiber.
**Returns** a disposer that tears the effect down and settles once done.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L363)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L419)
### ctx.fiber
@@ -36,7 +36,7 @@ The fiber (plugin runtime instance) that owns this context.
Runtime instance of one plugin application.
A fiber tracks dependency state, validated config, lifecycle effects, and cleanup for the plugin context returned by `ctx.plugin()`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L154)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L183)
### fiber.uid
@@ -46,7 +46,7 @@ public uid: number | null
Unique id within the registry; 0 for the root fiber, `null` once disposed.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L156)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L185)
### fiber.ctx
@@ -56,7 +56,7 @@ public readonly ctx: Context
The context this fiber's plugin runs in (extends the parent context).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L158)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L187)
### fiber.config
@@ -66,7 +66,7 @@ public config: any
The validated plugin config (updated by `update()`).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L160)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L189)
### fiber.state
@@ -76,7 +76,7 @@ public state
Current lifecycle state; transitions emit `internal/status`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L162)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L191)
### fiber.dispose
@@ -86,7 +86,7 @@ public readonly dispose: () => Promise<void>
Dispose this fiber: unload the plugin, then settle once cleanup finished.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L164)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L193)
### fiber.store
@@ -96,7 +96,7 @@ public store: Dict<Impl> | undefined
Snapshot of required service implementations while loaded; `undefined` otherwise.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L166)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L195)
### fiber.inertia
@@ -106,7 +106,7 @@ public inertia: Promise<void> | undefined
The in-flight load/unload transition, if one is currently running.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L168)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L197)
### fiber.name
@@ -116,7 +116,7 @@ get name()
The plugin's display name, inherited from the nearest named ancestor, else `'root'`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L284)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L340)
### fiber.assertActive()
@@ -128,7 +128,7 @@ Throw if the fiber has already been disposed.
**Returns** nothing when the fiber is still active.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L299)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L355)
### fiber.effect(execute, label?)
@@ -145,7 +145,7 @@ Register a cleanup-aware effect on this fiber.
**Returns** a disposer that tears the effect down and settles once done.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L363)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L419)
### fiber.getEffects()
@@ -157,7 +157,7 @@ Return metadata for currently registered effects.
**Returns** one `EffectMeta` tree per labeled live effect.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L436)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L572)
### fiber.await()
@@ -169,7 +169,7 @@ Wait for current lifecycle work and rethrow startup errors.
**Returns** this fiber, once it has settled into a stable state.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L560)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L701)
### fiber.restart()
@@ -181,7 +181,7 @@ Dispose and immediately reload this plugin with its current config.
**Returns** a promise resolving once the reload settled.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L574)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L715)
### fiber.update(config, noSave?)
@@ -197,7 +197,7 @@ Runs the `internal/update` waterfall first, so update hooks (and HMR) can veto o
**Returns** nothing; the restart runs behind the `internal/update` waterfall.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L592)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L733)
## Effect
@@ -260,7 +260,7 @@ namespace CordisError {
}
```
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L127)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L156)
## ValidationError

View File

@@ -4,53 +4,52 @@
`AgentLoop` — provided by `@deepseek-ai/dsh-agent-loop`.
The agent-loop plugin (`ctx.agentLoop`): creates ReactLoopAgents, runs their loops, and registers them in `ctx.agents`. Also implements the AgentFactory seam, so plugins create/resume agents through `ctx.agents` (the interface) without depending on this concrete package.
The loop itself is deliberately thin — every behavior beyond "call the model, run the tools, repeat" belongs to plugins listening on the event taxonomy declared in @deepseek-ai/dsh-agent.
Concrete ReactLoopAgent factory and driver service.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L68)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L335)
### ctx.agentLoop.create(id, options?)
### ctx.agentLoop.create(id, options?, meta?)
```ts website-api
create(id: AgentId, options: AgentOptions = {}): ReactLoopAgent
create(id: AgentId, options: AgentOptions = {}, meta: Pick<SessionHeader, 'cwd'> = {}): ReactLoopAgent
```
Config-driven create: an agent on a FRESH, non-colliding session id per run (`${id}-session-<uuid>`, no cwd). Used for `cordis.yml`-configured agents and as the shared core for the programmatic factory createAgent.
Why a per-run id, not a fixed `${id}-session`: once a durable persistence backend is loaded, a fixed id collides on the second run — the backend refuses to re-create an id whose log already exists on disk (the SessionId is the identity). A fresh id means each run is a new session.
TODO(demo): each run starting a brand-new session is fine for demos but is NOT real conversation continuity. A production config-driven agent needs a deliberate resume-or-create policy (resume the prior session if one exists, else start fresh) or an explicit caller-chosen session id — revisit when the UI/ACP path owns session selection.
Create an agent on a fresh per-run session, owned by the accessing fiber. Constructor-driven config calls use the loop fiber itself.
- `id` — the agent id; also seeds the generated session id.
- `options` — loop options (model, limits, …); defaults applied per option.
- `id` — agent registry id.
- `options` — concrete loop options.
- `meta` — optional fresh-session workspace metadata.
**Returns** the running agent, owned by the calling fiber (no handle).
**Returns** the published running agent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L142)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L389)
### ctx.agentLoop.createAgent(options)
### ctx.agentLoop.createAgent(ownerCtx, options)
```ts website-api
createAgent(options: CreateAgentOptions): AgentHandle
async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>
```
Programmatic factory create (AgentFactory): an agent on a caller-supplied `sessionId` (NOT `${id}-session`), with optional session metadata (validated `cwd`, lineage) and an optional `seed` event prefix. The ACP bridge uses this so the client-generated session id becomes the live/persisted session id; the in-process FORK subagent backend passes a `seed` (a balanced completed-turn prefix of the parent's log) so the child starts with the parent's context. Returns an AgentHandle the owner disposes to tear down exactly this agent.
Create an owned agent on a caller-supplied session id.
- `options` — agent id, caller-supplied session id, optional seed/meta, and agent options.
- `ownerCtx` — caller context that structurally owns the transaction.
- `options` — identities, session seed/metadata, loop options, setup, and cancellation.
**Returns** the handle whose dispose tears down exactly this agent.
**Returns** the published handle.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L166)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L412)
### ctx.agentLoop.resume(options)
### ctx.agentLoop.resume(ownerCtx, options)
```ts website-api
async resume(options: ResumeAgentOptions): Promise<AgentHandle>
async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>
```
Resume an agent on a persisted session (AgentFactory). Loads the session log + metadata via `ctx.sessionPersistence`, reconstructs the live session with the loaded events (so `lastTurnNumber`/`deriveMessages` continue), and starts a fresh agent on it. The live session id is the resumed id, NOT `${agentId}-session`.
Requires `ctx.sessionPersistence`; rejects with a clear error if it is not configured. NOT hard-injected (that would make non-persistent demos pend forever) — callers that need resume (ACP) inject `sessionPersistence`, so by the time this runs the service exists.
Resume an owned agent from the configured persistence service.
- `options` — the persisted session id to reload, plus agent id/options.
- `ownerCtx` — caller context that owns load, setup, and the live lifecycle.
- `options` — persisted identity, loop options, setup, and cancellation.
**Returns** the handle for the agent resumed on the reconstructed session.
**Returns** the published handle.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L194)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L443)

View File

@@ -4,9 +4,9 @@
`AgentRegistry` — provided by `@deepseek-ai/dsh-agent`.
Agent registry (`ctx.agents`): tracks live agents so UI, hook, and orchestrator plugins can find them without depending on the concrete loop package. Agent *creation* is provided by whichever plugin implements the AgentFactory (phase 1: `@deepseek-ai/dsh-agent-loop`), registered via setFactory.
Agent registry (`ctx.agents`): tracks live agents so UI, hook, and orchestrator plugins can find them without depending on the concrete loop package. Agent *creation* is provided by whichever plugin implements the AgentFactory (`@deepseek-ai/dsh-agent-loop`), registered via setFactory.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L117)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L133)
### ctx.agents.setFactory(factory)
@@ -14,27 +14,27 @@ Agent registry (`ctx.agents`): tracks live agents so UI, hook, and orchestrator
setFactory(factory: AgentFactory): () => void
```
Register the agent-creation factory (the loop calls this on construction, effect-scoped). Throws if a factory is already registered. Returns the disposer; on dispose the factory slot is cleared.
Register the effect-scoped creation factory, rejecting a duplicate. Service factories are retraced through each create/resume caller for ownership.
- `factory` — the loop-owned factory `create`/`resume` delegate to.
**Returns** the disposer that clears the factory slot.
**Returns** the exact Cordis effect disposer.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L132)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L152)
### ctx.agents.create(options)
```ts website-api
create(options: CreateAgentOptions): AgentHandle
async create(options: CreateAgentOptions): Promise<AgentHandle>
```
Create, start, and register a new agent through the registered factory. Distinct from register (which records an already-constructed agent): this constructs the agent and its session. Throws if no factory is registered. Returns an AgentHandle — the owner disposes it to tear down exactly this agent.
Create and publish an owned agent and session through the active factory. Rejects if no factory is registered or creation, setup, or publication fails.
- `options` — agent id, session id/seed/metadata, and agent options.
**Returns** the handle whose dispose tears down exactly this agent.
**Returns** the handle after setup, rollback-covered publication, and loop start complete.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L150)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L177)
### ctx.agents.resume(options)
@@ -42,13 +42,13 @@ Create, start, and register a new agent through the registered factory. Distinct
async resume(options: ResumeAgentOptions): Promise<AgentHandle>
```
Load a persisted session and resume an agent on it through the registered factory. Rejects if no factory is registered; the factory rejects if session persistence is not configured. Returns an AgentHandle.
Load a persisted session and resume an agent on it through the registered factory. Rejects if no factory is registered; the factory rejects if session persistence is not configured or persistence/setup fails.
- `options` — the persisted session id plus agent id and options.
- `options` — persisted identity, configuration, and optional setup.
**Returns** the handle for the resumed agent.
**Returns** the handle after setup, rollback-covered publication, and loop start complete.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L162)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L193)
### ctx.agents.register(agent)
@@ -56,13 +56,39 @@ Load a persisted session and resume an agent on it through the registered factor
register(agent: Agent): () => void
```
Register a live agent. Throws if an agent with the same id is already registered. Emits `agent/created` on registration and `agent/disposed` when the calling fiber is disposed. Returns the disposer.
Register a live agent in the calling effect scope, with scope-filtered creation and disposal events. Duplicate ids throw.
- `agent` — the already-constructed agent to record in the store.
**Returns** the disposer that removes the agent and emits `agent/disposed`.
**Returns** the exact Cordis effect disposer for nested teardown ordering.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L174)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L207)
### ctx.agents.enter(agent)
```ts website-api
enter(agent: Agent): () => void
```
Insert an unpublished agent for an ordered factory transaction.
- `agent` — the prepared, unpublished agent.
**Returns** an idempotent closure that removes this exact entry and emits the paired disposal edge; detachment during creation dispatch is deferred.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L222)
### ctx.agents.announce(agent)
```ts website-api
announce(agent: Agent): void
```
Announce an agent previously inserted with enter.
- `agent` — the live inserted agent to announce.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L290)
### ctx.agents.get(id)
@@ -76,7 +102,7 @@ Look up a live agent.
**Returns** the agent, or undefined when no live agent has that id.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L216)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L324)
### ctx.agents.list()
@@ -88,4 +114,4 @@ All live agents, in registration order.
**Returns** a fresh array; mutating it does not affect the registry.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L224)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L332)

View File

@@ -0,0 +1,23 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# ctx.approval
`ApprovalService` — provided by `@deepseek-ai/dsh-user-approval`.
Approval service that applies session policy before answerers and logs every ask/outcome pair to the requesting session. It exposes deterministic policy changes to the model through prompt and pre-step notices.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/user-approval/src/index.ts#L229)
### ctx.approval.request(req)
```ts website-api
async request(req: ApprovalRequest): Promise<ApprovalOutcome>
```
Ask the composed answerers to decide one readonly same-process request. The service borrows the request, agent, session, and live signal directly. The request requires an open turn because the audit pair must be enclosed by the durable log's commit/replay boundary; an idle ask rejects before appending anything. The answerer phase always produces an outcome: an aborted signal yields `'cancelled'`, a missing or throwing answerer yields `'unavailable'` (fail closed), and a rogue non-vocabulary return value is normalized to `'unavailable'`. A failure that prevents either audit append from committing still rejects because returning an unlogged decision would violate the pair. Session contains post-commit observer failures, so an authoritative append cannot reject the request or suppress its matching audit event.
- `req` — the pending decision (agent, tool identity, reason, signal).
**Returns** the closed outcome; `'allowed-once'` is the only grant.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/user-approval/src/index.ts#L313)

View File

@@ -5,13 +5,23 @@
`BashExecutor` (abstract seam) — provided by `@deepseek-ai/dsh-bash`.
Abstract bash execution service. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.bash` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior).
Semantics every implementation must honor:
- run REJECTS only for infrastructure failures (unusable workdir, missing shell, pre-aborted signal). Nonzero exits, timeout kills, and abort kills RESOLVE with a descriptive BashRunResult — reporting a failed command is the tool layer's job, not an exception.
- start returns immediately; no timeout applies to background tasks (callers stop them via kill or the spec's AbortSignal). Completion must fire the onTaskDone listeners exactly once per task, and must NOT fire after the service is disposed.
- readOutput is incremental: consecutive reads never re-deliver output. Implementations bound their buffers; reads that lost data flag `lossy` and point at full-stream spill files when available.
- Disposal kills every running task and awaits their exit (no orphan processes survive `fiber.dispose()`).
Implementations must honor these semantics:
- run rejects only for infrastructure failures. Nonzero exits, timeout kills, and abort kills resolve with a BashRunResult.
- start returns immediately; no timeout applies to background processes. `done` settles at process close and never rejects; spawn failures settle as `killed` with the error on stderr.
- BashProcess.readOutput is incremental: consecutive reads never repeat output. Lossy reads report truncation and available spill files.
- Disposal kills all running background processes and awaits their exit.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L59)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L46)
### ctx.bash.sandboxMode
```ts website-api
get sandboxMode(): SandboxMode | undefined
```
The sandbox mode this executor applies by default, or `undefined` when it does not sandbox commands.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L56)
### ctx.bash.resolve(request)
@@ -19,13 +29,13 @@ Semantics every implementation must honor:
abstract resolve(request: BashExecRequest): BashExecSpec
```
Resolve a caller's BashExecRequest into a fully-specified BashExecSpec, applying this implementation's config defaults and caps (working directory, default/max timeout). Consumers (tool layer) call this, then pass the result to run/start — keeping defaulting in the implementation that owns the config while the seam type stays explicit (no hidden `?? default` inside run/start).
Apply implementation-owned defaults and caps to a request before execution.
- `request` — the caller's request; omitted fields get this implementation's defaults, capped fields are clamped.
**Returns** the fully-specified spec to hand to `run`/`start`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L84)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L66)
### ctx.bash.run(spec)
@@ -39,100 +49,18 @@ Run a command in the foreground; resolves when it finishes.
**Returns** the outcome; nonzero exits, timeout kills, and abort kills resolve with a descriptive result rather than reject.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L92)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L74)
### ctx.bash.start(spec)
```ts website-api
abstract start(spec: BashExecSpec): BashTask
abstract start(spec: BashExecSpec): BashProcess
```
Start a background task and return its handle immediately.
Start a background process and return its handle immediately.
- `spec` — a resolved spec from `resolve`, never a raw request.
**Returns** the live task handle; completion fires `onTaskDone`.
**Returns** the live process handle (reads, kill, quiescence promise).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L99)
### ctx.bash.get(id)
```ts website-api
abstract get(id: BashTaskId): BashTask | undefined
```
Look up a background task by id.
- `id` — the task id to look up.
**Returns** the tracked task, or undefined for an id this executor never issued.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L106)
### ctx.bash.ownerOf(id)
```ts website-api
abstract ownerOf(id: BashTaskId): OwnerToken | undefined
```
The opaque OWNER token recorded for a background task at start (from the BashExecSpec's `owner`), or `undefined` for an unknown id OR a known-but-ownerless task. The executor stores and returns the token verbatim — it never interprets it; the access POLICY (who may read/kill a task) lives in the consumer (`@deepseek-ai/dsh-tool-bash`), which compares `ownerOf(id)` to the caller's token. Collapsing unknown-id and known-but-unowned into the same `undefined` is fine: the consumer's access gate treats `undefined` as "open", and a genuinely unknown id then fails loudly at the subsequent readOutput/kill ("unknown task"). Storing ownership in the executor (disposed with ITS fiber) — not in the tool plugin — is what makes ownership survive a `tool-bash` HMR reload.
- `id` — the background task id to look up ownership for.
**Returns** the token recorded at start, verbatim; undefined for an unknown id or a known-but-ownerless task.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L124)
### ctx.bash.list()
```ts website-api
abstract list(): BashTask[]
```
All tracked background tasks (insertion order).
**Returns** every task this executor started, running or finished.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L130)
### ctx.bash.readOutput(id)
```ts website-api
abstract readOutput(id: BashTaskId): BashTaskRead
```
Read output produced since the previous read. Throws for unknown ids.
- `id` — the task to read from.
**Returns** the incremental read; consecutive reads never re-deliver output.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L137)
### ctx.bash.kill(id)
```ts website-api
abstract kill(id: BashTaskId): boolean
```
Kill a running background task. Returns false when it had already finished (no-op). Throws for unknown ids.
- `id` — the task to kill.
**Returns** true when this call killed it, false when it had already finished.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L145)
### ctx.bash.onTaskDone(listener)
```ts website-api
onTaskDone(listener: BashTaskListener): () => void
```
Register a background-task completion listener (disposed with the calling fiber). Listeners never fire after this service is disposed.
- `listener` — called exactly once per task completion.
**Returns** the disposer that unregisters the listener.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L153)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L81)

View File

@@ -4,14 +4,9 @@
`CodeRuntime` (abstract seam) — provided by `@deepseek-ai/dsh-code-runtime`.
Abstract code-execution service. Subclass, implement run and the two descriptors, and load the subclass as a plugin — it registers as `ctx.codeRuntime` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior).
Semantics every implementation must honor:
- run resolves with an error FIELD for every program outcome — parse/transform failures, thrown exceptions, budget expiry, abort, substrate death (CodeRunFailure's taxonomy). It REJECTS only for caller misuse of the seam itself (e.g. a run submitted after disposal).
- Binding calls bridge to the caller's CodeBindingFunctions verbatim; arguments and resolutions must be structured-cloneable, and the runtime treats the program as a hostile peer (arbitrary binding names are own properties, malformed traffic is rejected or ignored, never crashes the host).
- Runs are isolated from each other: no state survives from one run to the next through the runtime.
- Disposal reaches quiescence: in-flight runs are terminated AND awaited before the service's own teardown completes (no orphan substrate survives `fiber.dispose()`).
Registers one `ctx.codeRuntime` implementation. Program, budget, abort, and substrate failures resolve in CodeRunResult; only seam misuse rejects. Implementations bridge structured-cloneable bindings while treating programs as hostile peers, isolate runs from one another, and terminate and await in-flight runs during disposal.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/code-runtime/code-runtime/src/index.ts#L59)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/code-runtime/code-runtime/src/index.ts#L30)
### ctx.codeRuntime.language
@@ -21,7 +16,7 @@ abstract readonly language: string
The source language run expects `program` to be written in, as a lowercase identifier. Informational, not gating — a consumer that generates language-specific presentation (typed SDK stubs, usage instructions) switches on it and fails loud on a language it cannot present. Well-known value: `'typescript'`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/code-runtime/code-runtime/src/index.ts#L67)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/code-runtime/code-runtime/src/index.ts#L38)
### ctx.codeRuntime.isolation
@@ -31,7 +26,7 @@ abstract readonly isolation: string
The execution substrate, as a lowercase identifier. Informational, not gating — a descriptor so deployments and diagnostics can tell backends apart, not a security claim. Well-known values: `'worker-thread'`, `'process'`, `'container'`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/code-runtime/code-runtime/src/index.ts#L75)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/code-runtime/code-runtime/src/index.ts#L46)
### ctx.codeRuntime.run(request)
@@ -45,4 +40,4 @@ Execute one program against the request's bindings and capture what it emitted.
**Returns** the run's outcome: completion value (when transferable), the ordered log capture, and the failure (if any).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/code-runtime/code-runtime/src/index.ts#L90)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/code-runtime/code-runtime/src/index.ts#L61)

View File

@@ -4,13 +4,9 @@
`CompactService` (abstract seam) — provided by `@deepseek-ai/dsh-compact`.
Abstract compaction service. Subclass implement the two abstract methods, and load the subclass as a plugin — it registers as `ctx.compact` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior).
Both core methods are abstract: the contract states WHAT compaction does, while the entire strategy — token estimation, retention policy, event sequencing, summarization — is a HOW decision owned by the implementation.
Implementations MUST honor:
- **Surface contract**: a successful compaction shadows the compacted surface nodes with a SINGLE replacement node carrying the summary. Because `SurfaceEventType` is a closed union, that node is a `user/message` with `surfaceOp: { op:'replace', start, end }`; the `compact/*` events are log-only (lock + provenance).
- **Blocking**: no compaction begins while another is in progress for the same session. The recommended mechanism is the log-recorded lock — append `compact/start` before the slow work and `compact/end` after (even on failure) — so the lock is visible to replay and crash recovery.
Abstract compaction service. Implementations own token estimation, retention, and summarization, but a successful run must replace the selected surface span with one summary node and prevent concurrent compaction of the same session. Load one implementation per context as `ctx.compact`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact/src/index.ts#L65)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact/src/index.ts#L36)
### ctx.compact.compactIfNeeded(agent, fullSystemPrompt, sessionPrefix, signal)
@@ -18,21 +14,16 @@ Implementations MUST honor:
abstract compactIfNeeded( agent: CompactAgentContext, fullSystemPrompt: string, sessionPrefix: readonly Message[], signal: AbortSignal, ): Promise<CompactionResult | null>
```
Check token pressure and compact if the conversation is too large.
Estimates the NEXT request's size — the session prefix, the surface-derived history, and the system prompt — and if it exceeds the backend's threshold, compacts an older range via compactRegion, keeping recent context intact. Returns `null` when no compaction is needed.
Scope and guarantees a backend MUST honor:
- **Compaction acts on surface-derived history only**, but the ESTIMATE counts everything the request carries: the loop composes the session prefix before the pre-step seam fires and hands it here, so the gate sees the prefix this instance will actually send (`EpochHeader.messagePrefix` — request-only, never derived history). Non-surface context injected downstream (into the request `messages` by a later listener) is out of this accounting by construction.
- **Head-anchored, best-effort.** Auto-compaction consolidates from the surface HEAD up to a balanced tool-pairing cutoff, so a prior head checkpoint is re-summarized into one fresh checkpoint (the surface holds at most one auto-generated checkpoint, always at the head). It is best-effort over CLOSED steps: when the only compactable content left is an un-splittable open tail step, it declines (`null`) and retries once that step closes.
- **Single-unit overflow is out of scope.** If a single retained unit (one closed step, or a large free node such as a pasted `user/message`) ALONE exceeds the budget, compaction cannot help and the call may go out over-budget. Bounding an individual unit's size is a separate concern — as is a session prefix that alone approaches the window (a configuration error no compactor fixes: compaction cannot shrink the prefix).
Check token pressure and compact if the conversation is too large. Estimate the next request, including its session prefix, derived history, and system prompt. Above threshold, compact a head-anchored range ending at a balanced tool boundary and reconsolidate any prior automatic checkpoint. Return `null` when no compaction is needed or an open tail leaves no safe cutoff. A single oversized retained unit or prefix cannot be repaired here.
- `agent` — agent context owning the session surface and model options.
- `fullSystemPrompt` — assembled system prompt, counted toward the estimate.
- `sessionPrefix` — the instance's composed session prefix, counted toward the estimate.
- `signal` — cancellation signal. A backend summarizing via `ctx.llm.stream()` MUST forward this into the call's `GenerateOptions.signal` so an abort/dispose tears down the in-flight summarization rather than leaving an orphaned model call running past the cancellation.
- `signal` — cancellation signal; model-backed implementations must forward it.
**Returns** the compaction result, or `null` if no compaction was needed.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact/src/index.ts#L111)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact/src/index.ts#L56)
### ctx.compact.compactRegion(session, start, end, agent, signal?)
@@ -40,16 +31,14 @@ Scope and guarantees a backend MUST honor:
abstract compactRegion( session: Session, start: number, end: number, agent: CompactAgentContext, signal?: AbortSignal, ): Promise<CompactionResult>
```
Forcibly compact a range of surface nodes into a single summary node.
`start` and `end` are inclusive seqs of surface nodes to shadow; the backend summarizes their content and appends a replacement surface node. Used by the (future) `/compact` tool and internally by compactIfNeeded.
The region MUST NOT split a step's `assistant/message` tool-calls from their `tool/result`s, leaving the rehydrated transcript with a dangling tool-call or an orphaned tool-result that every provider rejects. A region is safe iff both its edges are balanced cuts on the surface: the cut before `start` and the cut after `end` each have no unanswered tool-call before them. A node that belongs to no step (a pre-step user message, inter-step steering, or an injection context message) is a balanced (free) boundary; an `end` inside an open (unclosed) tail step is invalid — its tool-calls have no results yet. `dsh-session` exports `isToolPairingBalanced` for this check.
Forcibly compact a range of surface nodes into a single summary node. `start` and `end` name an inclusive span by surface position, not numeric seq order; replacements can make visible seqs non-monotonic. Both edges must be balanced so assistant tool calls remain paired with their results. A model- backed implementation forwards cancellation and rejects active, missing, reversed, or unbalanced ranges.
- `session` — the session whose surface is mutated.
- `start` — inclusive seq of the first surface node to compact.
- `end` — inclusive seq of the last surface node to compact.
- `agent` — agent context used by router-aware summarizers.
- `signal` — optional cancellation signal. A backend that summarizes via `ctx.llm.stream()` MUST forward this into the call's `GenerateOptions.signal` so an abort/dispose tears down the in-flight summarization rather than leaving an orphaned model call running past the cancellation.
- `session` — session to mutate.
- `start` — first surface seq, inclusive.
- `end` — last surface seq, inclusive.
- `agent` — summarizer context.
- `signal` — optional cancellation; model-backed implementations must forward it.
**Returns** what the compaction did (the replaced range and its summary node).
**Returns** the replaced range and summary.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact/src/index.ts#L151)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact/src/index.ts#L79)

View File

@@ -2,7 +2,7 @@
# Harness events
Every event the harness packages declare on the cordis event bus (35 total), grouped by scope. The **mode** is the dispatch semantics (`emit` fire-and-forget, `parallel` awaited, `serial` first-bail, `waterfall` veto-chain — a waterfall listener MUST call `next()` to delegate).
Every event the harness packages declare on the cordis event bus (39 total), grouped by scope. The **mode** is the dispatch semantics (`emit` fire-and-forget, `parallel` awaited, `serial` first-bail, `waterfall` veto-chain — a waterfall listener MUST call `next()` to delegate).
## agent/*
@@ -11,35 +11,35 @@ Every event the harness packages declare on the cordis event bus (35 total), gro
**Mode:** `emit`
```ts website-api
'agent/created'(agent: Agent): void
'agent/created'(this: Scoped<Agent>, agent: Agent): void
```
An agent was registered in the AgentRegistry and is ready to receive messages.
A fully configured agent and live session were published. Setup is composition-only; `agent/session-start` is the first startup-driving seam. Synchronous listener failure vetoes publication, while returned-promise rejection is reported. Detach requested during dispatch waits until every creation listener has observed the stable entry.
- `agent` — the newly registered agent, already resolvable in the registry.
- `agent` — the newly registered agent with its live session and completed setup. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L265)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L139)
### agent/disposed
**Mode:** `emit`
```ts website-api
'agent/disposed'(agent: Agent): void
'agent/disposed'(this: Scoped<Agent>, agent: Agent): void
```
An agent was disposed and removed from the registry; its fiber and any in-flight turn have been torn down.
An agent left the registry; AgentLoop emits this after driver quiescence but before session detachment and scoped-registration unwind. Custom registry users own their driver-ordering contract.
- `agent` — the agent that was torn down; its handle is now inert.
- `agent` — the exact agent removed from the registry. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L272)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L148)
### agent/error
**Mode:** `emit`
```ts website-api
'agent/error'(agent: Agent, turn: number, step: number, error: Error): void
'agent/error'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: Error): void
```
A step or turn errored. The loop reports a failure here (plus the logger) even when the error has no in-turn position for a session `error` event.
@@ -47,133 +47,130 @@ A step or turn errored. The loop reports a failure here (plus the logger) even w
- `agent` — the agent whose turn errored.
- `turn` — the turn in which the failure surfaced.
- `step` — the step at which the failure surfaced.
- `error` — the failure, verbatim.
- `error` — the failure, verbatim. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L476)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L283)
### agent/pre-step
**Mode:** `serial`
```ts website-api
'agent/pre-step'(agent: Agent, turn: number, step: number, fullSystemPrompt: string, sessionPrefix: readonly Message[], signal: AbortSignal): Promise<void> | void
'agent/pre-step'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, fullSystemPrompt: string, sessionPrefix: readonly Message[], signal: AbortSignal): Promise<void> | void
```
Awaited pre-step surface-mutation checkpoint, fired once per step AFTER `turn/start` (and after the prior step closed) but BEFORE this step's `step/start` — so anything a listener appends lands OUTSIDE the step, between `turn/start`/`step/end` and the upcoming `step/start`. `step` is the number of the step about to start. The loop awaits `ctx.serial('agent/pre-step', …)` after assembling the system prompt, then opens the step and derives the request history ONCE from whatever the surface now holds. This is where compaction belongs: it mutates the session surface in place (shadowing an older range with a summary node) with its log-only `compact/*` records cleanly outside any step, and the single subsequent derive reflects the mutation — so there is no double-derive and no listener can see (or be expected to act on) an assembled `messages` array that does not exist yet.
Serial (awaited in registration order), not a waterfall: a listener mutates the surface as a side effect; there is nothing to transform, but the loop must wait for the mutation to complete before opening the step and deriving. Cordis `serial` bails early if a listener returns a bail value; this event is typed and documented as `void`, so listeners must not return a semantic veto value. `fullSystemPrompt` is the assembled prompt a listener needs to measure pressure (the system prompt counts toward the budget), and `sessionPrefix` is the instance's composed agent/session-prefix product for the same reason — every request carries it in front of the derived history, and it is composed BEFORE this seam fires precisely so a pressure gate counts the prefix the request will actually send (never a stale logged one). `signal` cancels any in-flight work a listener starts (e.g. a summarization model call).
Awaited serial checkpoint for session-surface mutation after prompt assembly and before `step/start`; appends land outside the pending step. The loop derives history once afterward, so compaction records and replacements are included without rewriting an assembled request. The prompt and prefix are the exact pressure inputs for that request, and `signal` cancels listener work. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
- `agent` — the agent about to open the step.
- `turn` — the already-open turn this step belongs to.
- `step` — the number of the step about to start.
- `fullSystemPrompt` — the assembled prompt, for measuring token pressure.
- `sessionPrefix` — the instance's frozen session prefix, for the same measurement.
- `signal` — aborts in-flight listener work when the turn is torn down.
- `agent` — the agent opening the step.
- `turn` — the open turn number.
- `step` — the pending step number.
- `fullSystemPrompt` — the assembled prompt.
- `sessionPrefix` — the frozen request prefix.
- `signal` — the turn abort signal.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L357)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L202)
### agent/prompt-submit
**Mode:** `waterfall`
```ts website-api
'agent/prompt-submit'(agent: Agent, content: ContentBlock[], source: MessageSource, next: () => Promise<PromptDecision>): Promise<PromptDecision>
'agent/prompt-submit'(this: Scoped<Agent>, agent: Agent, content: ContentBlock[], source: MessageSource, next: () => Promise<PromptDecision>): Promise<PromptDecision>
```
Waterfall: decide what happens to ONE drained queued message before it becomes a `user/message` — allow (optionally rewriting the prompt bytes or attaching `additionalContext`) or block it. Fires inside the already-open turn, per drained message. Maps onto Claude Code's `UserPromptSubmit` hook. Call `next()` to delegate to the default (allow unchanged), or return a PromptDecision without calling `next()` to short-circuit.
Allow, rewrite, or block one drained prompt before it becomes a user message. Call `next()` for the unchanged default.
- `agent` — the agent draining its inbox.
- `content` — the drained message's blocks, as queued.
- `source` — the message's resolved source.
- `source` — the message's resolved source. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L370)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L212)
### agent/queued
**Mode:** `emit`
```ts website-api
'agent/queued'(agent: Agent, content: ContentBlock[], info: { source: MessageSource; steering: boolean }): void
'agent/queued'(this: Scoped<Agent>, agent: Agent, content: ContentBlock[], info: { source: MessageSource; steering: boolean }): void
```
A message entered the agent's inbox (queued or steering). `source` is the resolved source (defaults applied), not the caller's raw options.
Detached, frozen content entered the agent's inbox. Source defaults have already been applied, so these are the exact values retained for the log.
- `agent` — the agent whose inbox received the message.
- `content` — the enqueued content blocks, verbatim.
- `info` — the resolved source plus whether it entered as steering.
- `content` — the accepted content blocks retained by the inbox.
- `info` — the accepted source plus whether it entered as steering. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L290)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L167)
### agent/request
**Mode:** `waterfall`
```ts website-api
'agent/request'(agent: Agent, turn: number, step: number, config: LlmCallConfig, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>
'agent/request'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, config: LlmCallConfig, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>
```
Waterfall: shape the step's call configuration — model switching, sampling overrides — by returning a replacement LlmCallConfig (the frozen seed is the config the loop would otherwise use). Config is ALL a listener shapes here: every request is a pure function of the session log (the reconstructability RFC), so model-visible content flows through the log channels — `inject()`, steering, prompt-submit `additionalContext`, prompt sections via `system-prompt/assemble`, or the header-logged session prefix via agent/session-prefix — never through request mutation, and the loop records whatever config the request actually uses as a `request/header*` event before dispatch. The step's messages are already snapshotted when this fires (the `step/start` boundary): an `inject()` from a listener here lands in the log but joins the NEXT request. For surface mutation that must precede the snapshot (compaction), use agent/pre-step. Call `next()` to delegate, or return an LlmCallConfig without it to short-circuit.
Replace the frozen call configuration. Model-visible content must use logged channels; this seam cannot mutate messages. Injection here joins the next request because the current step boundary is already fixed.
- `agent` — the agent making the model call.
- `turn` — the open turn number.
- `step` — the step whose request this is.
- `config` — the config the loop would use (frozen); return a replacement to switch.
- `config` — the config the loop would use (frozen); return a replacement to switch. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L394)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L224)
### agent/session-prefix
**Mode:** `waterfall`
```ts website-api
'agent/session-prefix'(agent: Agent, prefix: Message[], signal: AbortSignal, next: () => Promise<Message[]>): Promise<Message[]>
'agent/session-prefix'(this: Scoped<Agent>, agent: Agent, prefix: Message[], signal: AbortSignal, next: () => Promise<Message[]>): Promise<Message[]>
```
Waterfall: compose the SESSION PREFIX — request-only messages placed in front of the ENTIRE derived history (directly after the provider's system slot) on every request this loop instance sends. Fired ONCE per loop instance, lazily before its first step's agent/pre-step seam — BEFORE the pre-step so a token-pressure gate (compaction) counts the prefix this instance will actually send, never a previous instance's logged one. The composed result is deep-frozen, recorded as `EpochHeader.messagePrefix` on the instance's anchoring `'initial'`/`'resume'` header snapshot, and reused verbatim for every subsequent request — never recomputed mid-session, so the provider prefix cache holds by construction (a process restart or `ctx.agents.resume()` is a new instance: it recomposes, and any drift lands attributably on the `'resume'` snapshot). Composition runs outside the step, before the boundary snapshot: a composing listener's session append joins the CURRENT request's derived history. A composition interrupted by a cancel/dispose landing inside the waterfall is discarded — never cached, logged, or sent — and the next turn recomposes under a live signal, so an abort-aware listener's degraded fallback cannot leak into later requests.
This is the home for session-stable openers the model must always see but that must NOT become durable history — a skills catalog, an AGENTS.md digest, a workspace baseline: `Session.deriveMessages()` never returns the prefix, and the header events are its only durable record, so the request stays reconstructable from the log. Content that CHANGES mid-session belongs in the append-only history channels instead — `agent.inject()`, a `tools/post-execute` decision's `additionalContext`, prompt-submit `additionalContext` — each a durable `context/message` paid once and prefix-cached thereafter.
The seed is a frozen empty list; a contributing listener returns a NEW array — never an in-place push. The canonical contribution is a PREPEND, `[mine, ...await next()]`: the waterfall unwinds innermost-first (the LAST-registered listener's `next()` resolves first), so prepending yields registration order on the wire, and every plugin using it composes deterministically. The append form `[...await next(), mine]` is legal but places a contribution AFTER every later-registered plugin's — reverse registration order when all contributors append. Call `next()` to delegate, or return a list without it to short-circuit.
Compose request-only messages placed before derived history. The frozen result is computed once per loop instance, logged on its anchoring request header, and reused so the provider prefix remains stable. Interrupted composition is discarded. Composition precedes the first `agent/pre-step` and request boundary, so listener appends join the current request and pressure accounting sees the composed prefix. Changing context belongs in history; contributors should prepend to `await next()` to preserve registration order. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
- `agent` — the agent whose session prefix is being composed.
- `prefix` — the frozen empty seed; return an extended replacement to contribute.
- `signal` — aborts in-flight listener work (e.g. a discovery scan) when the step is torn down.
- `prefix` — the frozen seed; return an extended replacement.
- `signal` — aborts composition when the step is torn down.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L441)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L239)
### agent/session-start
**Mode:** `emit`
```ts website-api
'agent/session-start'(agent: Agent, source: SessionStartSource): void
'agent/session-start'(this: Scoped<Agent>, agent: Agent, source: SessionStartSource): void
```
The agent's session lifecycle began, fired once before its first turn. `source` says why (SessionStartSource: fresh startup, a resumed persisted session, …). A pure NOTIFICATION (emit, not waterfall): it carries no veto — a session-start listener that wants to seed context does so via `agent.inject()` (a `context/message` the first request sees), not by returning a decision. Cannot block the session from starting; that gap is deliberate (a bridge logs/injects, it does not gate startup).
The session lifecycle began, once before the first turn. Use `agent.inject()` to seed model-facing context. This is a notification, not a veto; disposal requested by a lifecycle owner is rechecked before the driver starts.
- `agent` — the agent whose session lifecycle began.
- `source` — why the session started (fresh startup, resume, …).
- `source` — why the session started (fresh startup, resume, …). Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L305)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L180)
### agent/status
**Mode:** `emit`
```ts website-api
'agent/status'(agent: Agent, status: AgentStatus): void
'agent/status'(this: Scoped<Agent>, agent: Agent, status: AgentStatus): void
```
Agent status changed (`idle` ⇄ `running`, or → `disposed`). Drive lifecycle off this transition, never off a status you just requested — `send()` does not flip status to `running` before it returns.
Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does not enter `running` synchronously; drive lifecycle from this event.
- `agent` — the agent whose status flipped.
- `status` — the status just entered (the transition's destination).
- `status` — the status just entered (the transition's destination). Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L281)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L157)
### agent/step-result
**Mode:** `waterfall`
```ts website-api
'agent/step-result'(agent: Agent, turn: number, step: number, message: Message, next: () => Promise<Message>): Promise<Message>
'agent/step-result'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, message: Message, next: () => Promise<Message>): Promise<Message>
```
Waterfall: post-process the assembled assistant Message before tool dispatch (validation, content rewriting, …).
@@ -181,25 +178,56 @@ Waterfall: post-process the assembled assistant Message before tool dispatch (va
- `agent` — the agent that received the step's response.
- `turn` — the open turn number.
- `step` — the step that produced the message.
- `message` — the assistant message as assembled from the stream.
- `message` — the assistant message as assembled from the stream. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L451)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L250)
### agent/turn-continuation
**Mode:** `waterfall`
```ts website-api
'agent/turn-continuation'(agent: Agent, turn: number, defaultDecision: ContinuationDecision, next: () => Promise<ContinuationDecision>): Promise<ContinuationDecision>
'agent/turn-continuation'(this: Scoped<Agent>, agent: Agent, turn: number, defaultDecision: ContinuationDecision, next: () => Promise<ContinuationDecision>): Promise<ContinuationDecision>
```
Waterfall: override the turn-continuation decision via a typed ContinuationDecision. The loop's `defaultDecision` is `continue` when the step had tool calls or steering was injected, else `stop`. Listeners force-continue (`/goal`, `/loop` — optionally attaching a `reason` recorded as next-step steering) or force-stop (budget guards). Call `next()` to delegate to the default, or return a decision to override.
Override whether the turn continues. The default continues after tool calls or steering and stops otherwise; a continue reason becomes steering.
- `agent` — the agent deciding whether to run another step.
- `turn` — the turn being continued or stopped.
- `defaultDecision` — what the loop would do absent an override.
- `defaultDecision` — what the loop would do absent an override. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L464)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L260)
### agent/turn-stop
**Mode:** `serial`
```ts website-api
'agent/turn-stop'(this: Scoped<Agent>, agent: Agent, turn: number): ContinuationStop | undefined
```
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.
- `agent` — the agent whose composed continuation outcome may be stopped.
- `turn` — the turn at its terminal-stop checkpoint. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L270)
## approval/*
### approval/request
**Mode:** `waterfall`
```ts website-api
'approval/request'(this: Scoped<ApprovalService>, req: ApprovalRequest, next: () => Promise<ApprovalOutcome>): Promise<ApprovalOutcome>
```
Ask composed answerers for one decision. Return an outcome to claim the request or call `next()`; failure yields the fail-closed default. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
- `req` — the pending decision (agent, tool identity, reason, signal).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/user-approval/src/index.ts#L31)
## fs/*
@@ -211,12 +239,12 @@ Waterfall: override the turn-continuation decision via a typed ContinuationDecis
'fs/edit-intent'(target: FsTarget, actor: object | undefined, next: () => { version: FsVersion } | undefined | Promise<{ version: FsVersion } | undefined>): Promise<{ version: FsVersion } | undefined>
```
Single-slot decision: produce the optional version guard for the next FileSystem.editText. The tool dispatches this as an unbound waterfall and supplies a default thunk returning `undefined` (unconditional edit of the current content — the bare provider; no `stat`). The `@deepseek-ai/dsh-fs-policy` policy listener returns `{ version: vObserved }`, or throws `FS_NOT_OBSERVED` if the actor is unset or has not observed the target. Does NOT call `next()`: one decision, first-wins (see Events.'fs/write-intent').
Single-slot decision for the next FileSystem.editText. Calling `next()` yields an unconditional edit; the first returned guard wins.
- `target` — the resolved target about to be edited.
- `actor` — the opaque tool-execution context the decider keys off.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L123)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L59)
### fs/observed
@@ -226,13 +254,13 @@ Single-slot decision: produce the optional version guard for the next FileSystem
'fs/observed'(target: FsTarget, version: FsVersion, actor: object | undefined): void
```
Record that an actor observed a target at a version, after a successful read/write/edit. Fire-and-forget (plain `emit`). A listener MUST be a synchronous, side-effect-only recorder (`@deepseek-ai/dsh-fs-policy`'s is a `WeakMap.set`): the tool does not guard the emit, so a listener that throws surfaces as the tool's `isError` result, and cordis `emit` does not await listener promises — async or fallible audit/telemetry does not belong here. No listener ⇒ nothing recorded. `actor` is the opaque tool-execution context.
Record a successful observation. Listeners must be synchronous recorders: throws fail the tool call and returned promises are not awaited.
- `target` — the target that was read/written/edited.
- `version` — the version the actor now holds as its observation.
- `actor` — the observing tool-execution context; undefined records nothing useful.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L138)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L68)
### fs/write-intent
@@ -242,12 +270,12 @@ Record that an actor observed a target at a version, after a successful read/wri
'fs/write-intent'(target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise<FsWriteIntent | undefined>): Promise<FsWriteIntent | undefined>
```
Single-slot decision: produce the write intent for the next FileSystem.writeText. The tool dispatches this as an unbound waterfall (no `this`) and supplies a default thunk returning `undefined` (unconditional create-or-overwrite the bare provider). The `@deepseek-ai/dsh-fs-policy` policy listener returns `createIfAbsent` (unobserved actor) or `{ kind: 'replaceIfVersion', version: vObserved }` (observed) and does NOT call `next()` — one decision, not a composable chain. The slot is first-wins: the first non-`next()` decider (registration order, or `prepend`) occupies it; a second decider is a misconfiguration, not layering. `actor` is the opaque tool-execution context, never read here.
Single-slot decision for the next FileSystem.writeText. Calling `next()` yields the bare provider's unconditional write; the first listener that returns an intent owns the decision rather than composing with peers.
- `target` — the resolved target about to be written.
- `actor` — the opaque tool-execution context the decider keys off.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L109)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L51)
## llm/*
@@ -272,43 +300,57 @@ Waterfall around every streaming model call (retry, replay, routing). Bound to t
**Mode:** `emit`
```ts website-api
'session/created'(session: Session): void
'session/created'(this: Scoped<Session>, session: Session): void
```
A session was created in the store.
Creation announcement during session publication. A synchronous throw vetoes and rolls back with a paired disposal; detach requested during dispatch is deferred. A returned-promise rejection is logged but cannot retroactively veto this synchronous boundary. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only sessions entered through that agent's context.
- `session` — the session just entered and announced.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L39)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L47)
### session/disposed
**Mode:** `emit`
```ts website-api
'session/disposed'(this: Scoped<Session>, session: Session): void
```
Emitted once when an announced session leaves the store, including publication rollback, but never for an entry whose creation announcement did not begin. Listener failures are logged and contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the owner scope.
- `session` — the session that is no longer live in the store.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L57)
### session/event
**Mode:** `emit`
```ts website-api
'session/event'(session: Session, event: SessionEvent): void
'session/event'(this: Scoped<Session>, session: Session, event: SessionEvent): void
```
An event was appended to a session log (sync, fire-and-forget). This is the per-append feed a UI or invariant plugin tails.
Post-commit, fire-and-forget append feed. The listener snapshot resolves before the log push, but callbacks run after it; observer failures are logged and contained without making the committed append fail. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only events from sessions entered through that agent's context.
- `session` — the session whose log grew.
- `event` — the appended event, exactly as recorded.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L47)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L69)
### session/flush
**Mode:** `parallel`
```ts website-api
'session/flush'(session: Session): Promise<void> | void
'session/flush'(this: Scoped<Session>, session: Session): Promise<void> | void
```
Awaited durability checkpoint. The agent loop awaits `ctx.parallel('session/flush', session)` at every turn end; persistence plugins (JSONL, SQLite) drain their write-behind buffers here and on fiber dispose. Awaited (parallel), not a waterfall: every listener runs and the loop waits for all of them, but none can veto.
Awaited parallel durability checkpoint: every listener runs and the caller awaits all of them, with no waterfall veto. Dispatch through SessionStore.flush. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the session's owner scope.
- `session` — the session whose buffered events must reach durable storage.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L57)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L79)
## subagent/*
@@ -317,14 +359,14 @@ Awaited durability checkpoint. The agent loop awaits `ctx.parallel('session/flus
**Mode:** `emit`
```ts website-api
'subagent/end'(info: SubagentRunEndInfo): void
'subagent/end'(this: Scoped<SubagentService>, info: SubagentRunEndInfo): void
```
A subagent run settled — emitted when SubagentRun.result resolves (any stop reason). Paired with Events['subagent/start'].
A ready child settled. Scope-filtered dispatch uses the same delegating parent carrier as `subagent/start`, so the lifecycle pair reaches the same scoped audience.
- `info` — the run identity plus stop reason and final output.
- `info` — the run identity and terminal outcome.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L98)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L108)
### subagent/provider-added
@@ -334,11 +376,11 @@ A subagent run settled — emitted when SubagentRun.result resolves (any stop re
'subagent/provider-added'(provider: SubagentProvider): void
```
A provider became resolvable in the SubagentService registry. Consumers that derive state from a named provider (e.g. the model-facing tool wording in `dsh-tool-subagent`) react HERE instead of assuming load order — the cordis Loader starts sibling plugins concurrently, so "listed earlier in cordis.yml" does not mean "registered earlier".
A provider became resolvable in the registry.
- `provider` — the provider that just registered, live in the registry.
- `provider` — the registered provider.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L72)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L82)
### subagent/provider-removed
@@ -348,25 +390,25 @@ A provider became resolvable in the SubagentService registry. Consumers that der
'subagent/provider-removed'(name: string): void
```
A provider left the registry (its plugin's fiber was disposed — an unload or an HMR reload). Consumers holding provider-derived state drop it here; a reload re-fires `subagent/provider-added` with the fresh provider. Delivered with per-listener containment: a throwing subscriber is logged, never starves later subscribers, and never disrupts the provider's teardown.
A provider left the registry. Accepted runs remain holder-owned.
- `name` — the registry name that no longer resolves.
- `name` — the provider name that no longer resolves.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L83)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L88)
### subagent/start
**Mode:** `emit`
```ts website-api
'subagent/start'(info: SubagentRunInfo): void
'subagent/start'(this: Scoped<SubagentService>, info: SubagentRunInfo): void
```
A subagent run started — emitted after the provider is resolved and its capabilities validated, as the child run begins. Paired with Events['subagent/end'].
A provider established a ready child. For in-process providers, `ctx.agents.get(info.id)` resolves during this notification. Scope-filtered dispatch keys the carrier by the delegating parent, so a parent-scoped listener observes only its own delegations. Paired with `subagent/end`.
- `info` — which provider started which child agent.
- `info` — the provider and ready child identity.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L91)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L99)
## system-prompt/*
@@ -375,15 +417,15 @@ A subagent run started — emitted after the provider is resolved and its capabi
**Mode:** `waterfall`
```ts website-api
'system-prompt/assemble'(this: SystemPrompt, assembly: PromptAssembly, context: AssembleContext, next: () => Promise<PromptAssembly>): Promise<PromptAssembly>
'system-prompt/assemble'(this: Scoped<SystemPrompt>, assembly: PromptAssembly, context: AssembleContext, next: () => Promise<PromptAssembly>): Promise<PromptAssembly>
```
Waterfall around prompt assembly — mutate or extend the PromptAssembly (sections + tools + variables) before it is rendered. Bound to the SystemPrompt service; call `next()` to delegate.
Expert waterfall over the assembled sections, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned value is authoritative.
- `assembly` — the assembly built from the registered sections, tool providers, and variable providers; listeners may mutate it or return a replacement.
- `context` — the per-assembly `AssembleContext` the caller passed to `SystemPrompt.assemble` (e.g. which agent the prompt is for), so a listener can filter or extend per agent.
- `assembly` — the mutable assembly built from registered providers.
- `context` — the caller's per-assembly context.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L38)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L27)
### system-prompt/change
@@ -393,9 +435,9 @@ Waterfall around prompt assembly — mutate or extend the PromptAssembly (sectio
'system-prompt/change'(): void
```
A section, tool provider, or variable provider was registered or unregistered (the assembly inputs changed).
Emitted when any prompt provider changes. This registry notification is unfiltered because a global change affects every scope.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L44)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L33)
## tools/*
@@ -407,52 +449,67 @@ A section, tool provider, or variable provider was registered or unregistered (t
'tools/change'(): void
```
A tool was registered or unregistered (the available tool set changed).
A tool was registered or unregistered, or a scoped restriction changed (the available tool set changed — possibly for one scope only). An UNFILTERED registry-subject notification, deliberately not scope-filtered dispatch: a global change concerns every agent's next assembly, so a scoped listener subscribing here sees every change, not just its own scope's.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L132)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L116)
### tools/execute
**Mode:** `waterfall`
```ts website-api
'tools/execute'(this: ToolRegistry, exec: ToolExecution, next: () => Promise<ToolExecutionResult>): Promise<ToolExecutionResult>
'tools/execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, next: () => Promise<ToolExecutionResult>): Promise<ToolExecutionResult>
```
Around-dispatch waterfall wrapping the registry's core tool dispatch, between the `tools/pre-execute` gate and the `tools/post-execute` seam. A listener receives `(exec, next)`: call `next()` to delegate to dispatch (returning its ToolExecutionResult, optionally wrapped), or return a replacement result without calling `next()` to short-circuit dispatch. The base `next()` IS the dispatch-with-normalization thunk — a thrown tool (or unknown tool) is already normalized to an `isError` result by the time a listener's `await next()` returns, so a wrapper never sees a raw throw from the tool body. This is the seam a timeout/retry/metrics plugin wraps: it can mutate `exec` (e.g. replace `exec.signal` with a per-call deadline) BEFORE `next()` and inspect the result AFTER. (Cordis `next()` ignores any passed arguments and re-invokes downstream with the shared payload, so a wrapper mutates `exec` in place rather than passing a new object to `next()`.) Multiple listeners compose by registration order — an outer one wraps the inner ones plus dispatch.
Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns a normalized result; wrappers may change only `exec.signal`, while call identity remains immutable. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls.
- `exec` — the allowed call about to dispatch (name, parsed arguments, caller agent, signal).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L111)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L89)
### tools/post-execute
**Mode:** `waterfall`
```ts website-api
'tools/post-execute'(this: ToolRegistry, exec: ToolExecution, result: ToolExecutionResult, next: () => Promise<PostToolDecision>): Promise<PostToolDecision>
'tools/post-execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, result: Readonly<ToolExecutionResult>, next: () => Promise<PostToolDecision>): Promise<PostToolDecision>
```
Waterfall AFTER a tool runs — where hook plugins inspect the result and accept it (optionally REPLACING the model-facing content, and/or attaching `additionalContext` for the next request) or block it with corrective `feedback` (Claude Code's `PostToolUse`). Listeners receive `(exec, result, next)`: call `next()` to delegate to the default (accept unchanged), or return a PostToolDecision to override. Core tool dispatch runs earlier as the base `next()` of the `tools/execute` waterfall, all inside `execute`'s outer try/catch (and the tool body keeps its own inner try/catch, so a thrown tool still reaches `post-execute` as an `isError` result).
Accept, replace, enrich, or block a normalized dispatch result. `next()` accepts it unchanged; thrown tools still reach this seam as errors. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls.
- `exec` — the call that just ran (name, parsed arguments, caller agent).
- `result` — the dispatch outcome a listener may accept, replace, or block.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L127)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L98)
### tools/pre-execute
**Mode:** `waterfall`
```ts website-api
'tools/pre-execute'(this: ToolRegistry, exec: ToolExecution, next: () => Promise<PreToolDecision>): Promise<PreToolDecision>
'tools/pre-execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, next: () => Promise<PreToolDecision>): Promise<PreToolDecision>
```
Waterfall BEFORE a tool runs — the gate where sandbox, permission, and hook plugins allow or deny a call (Claude Code's `PreToolUse`). Listeners receive `(exec, next)`: call `next()` to delegate to the default (allow), or return a PreToolDecision without calling `next()` to short-circuit. A `deny` skips dispatch and yields an `isError` result; the tool body never runs. Input rewrite is deliberately NOT offered here (see PreToolDecision); `ask` degrades to deny until the permission system lands (`FIXME(permissions)`).
Allow, deny, or ask before dispatch. `next()` delegates to allow; missing approval support turns `ask` into denial. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls.
- `exec` — the pending call (name, parsed arguments, caller agent).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L91)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L80)
### tools/result
**Mode:** `emit`
```ts website-api
'tools/result'(this: Scoped<ToolRegistry>, exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): undefined
```
Observe the frozen, lossless-JSON final outcome. Listener failures are contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): keyed by `exec.agent`.
- `exec` — the execution object that traversed the pipeline.
- `result` — a deep-frozen snapshot of the final returned result.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L106)
## workflow/*
@@ -469,7 +526,7 @@ One `agent()` call settled (clean result, child failure, or run cancellation). P
- `info` — the run's identity snapshot.
- `agent` — the call identity plus its outcome.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L96)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L81)
### workflow/agent-start
@@ -479,12 +536,12 @@ One `agent()` call settled (clean result, child failure, or run cancellation). P
'workflow/agent-start'(info: WorkflowRunInfo, agent: WorkflowAgentInfo): void
```
One `agent()` call started a child run. Paired with Events['workflow/agent-end'] by `agent.seq`.
One `agent()` call established a ready child run. Paired with Events['workflow/agent-end'] by `agent.seq`. A call that never receives a ready run from the provider emits neither event in this pair.
- `info` — the run's identity snapshot.
- `agent` — the call's sequence number, label, phase, and child id.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L85)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L70)
### workflow/end
@@ -499,7 +556,7 @@ A workflow run settled (any stop reason). Fired when WorkflowRun.result resolves
- `info` — the run's identity snapshot.
- `result` — the outcome data (stop reason, error, agent count) — deliberately WITHOUT the result value (see `WorkflowResultInfo`).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L106)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L91)
### workflow/log
@@ -514,7 +571,7 @@ The script emitted a narration line (a `log(message)` call).
- `info` — the run's identity snapshot.
- `message` — the logged message, verbatim.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L77)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L60)
### workflow/phase
@@ -529,7 +586,7 @@ The script entered a phase (a `phase(title)` call) — progress grouping for obs
- `info` — the run's identity snapshot.
- `title` — the phase title, verbatim.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L70)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L53)
### workflow/start
@@ -543,4 +600,4 @@ A workflow run started — the script's meta block validated, the body about to
- `info` — the run's identity snapshot (id + meta).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L62)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L45)

View File

@@ -4,16 +4,9 @@
`FileSystem` (abstract seam) — provided by `@deepseek-ai/dsh-fs`.
Abstract filesystem provider service. Subclass, implement the seven storage primitives, and load the subclass as a plugin — it registers as `ctx.fs` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior).
Semantics every backend must honor:
- resolve returns a stable FsTarget; the same underlying file reached by different input paths must yield the same `targetKey` so stale guards and target lookup agree across paths (e.g. through symlinks).
- stat returns FsInfo metadata (never content) or `undefined` when the target is absent.
- readText/streamText read the whole regular text file (the stream for large files); both own regular-file checks, UTF-8 decoding, binary/NUL rejection, and `FS_NOT_TEXT`.
- listDir returns direct children of a directory in stable name order with resolved child targets and cheap metadata only. It never reads file contents. Missing targets throw `FS_NOT_FOUND`, non-directories throw `FS_NOT_DIRECTORY`, permission failures throw `FS_PERMISSION_DENIED`, and other backend I/O failures throw `FS_IO_ERROR`.
- writeText is atomic temp-file + rename. `expected` is OPTIONAL: omit it for an unconditional create-or-overwrite (the bare-provider default), or supply a FsWriteIntent to guard the write.
- editText verifies `expected.version` BEFORE literal matching (so a stale edit reports `FS_STALE_VERSION`, not `FS_EDIT_NOT_FOUND`/ `FS_AMBIGUOUS_EDIT` against newer content), then applies literal replacement and writes atomically — all inside one mutation critical section. `expected` is OPTIONAL: omit it for an unconditional edit of the current content (a missing target still reports `FS_STALE_VERSION`).
Abstract filesystem provider. Targets must preserve identity across aliases; reads expose regular UTF-8 text or typed errors, listings are stable and content-free, and mutations are atomic. Optional guards add stale protection without changing the unguarded provider contract.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L172)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L78)
### ctx.fs.resolve(path, opts?)
@@ -22,14 +15,13 @@ abstract resolve(path: string, opts?: { cwd?: string }): Promise<FsTarget>
```
Resolve a model/plugin-supplied path into a stable FsTarget. May perform I/O (a remote/sandboxed backend may need a round-trip to map a path to a stable identity), hence async even though the local backend only normalizes + realpaths.
`opts.cwd` is the base directory a RELATIVE `path` resolves against; an absolute `path` ignores it. Omitted ⇒ the backend's own default base (the local backend uses its configured `cwd`). The CALLER supplies this — the seam does not read a session or agent — so a tool can resolve against the caller's per-session workspace (`exec.agent.session.header.cwd`) without the provider depending on `dsh-agent`/`dsh-session`. Mirrors how `dsh-tool-bash` defaults a bash `workdir` to the session cwd.
- `path` — the path to resolve; relative paths resolve against `opts.cwd`.
- `opts` — `cwd` overrides the backend's default base for relative paths.
**Returns** the stable target; the same file yields the same `targetKey`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L194)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L92)
### ctx.fs.stat(target, signal?)
@@ -44,7 +36,7 @@ Return target metadata, or `undefined` when the target does not exist.
**Returns** metadata only, never content; undefined for an absent target.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L202)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L100)
### ctx.fs.readText(target, signal?)
@@ -59,7 +51,7 @@ Read the whole regular text file as a single decoded string.
**Returns** the full decoded UTF-8 content.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L210)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L108)
### ctx.fs.streamText(target, signal?)
@@ -74,7 +66,7 @@ Stream the whole regular text file as decoded text chunks (same text semantics a
**Returns** the chunk iterable, decoded and validated like `readText`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L221)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L119)
### ctx.fs.listDir(target, signal?)
@@ -89,7 +81,7 @@ List direct children of a directory in stable name order. Returns resolved child
**Returns** one entry per direct child, in stable name order.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L230)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L128)
### ctx.fs.writeText(target, content, expected?, signal?)
@@ -97,7 +89,7 @@ List direct children of a directory in stable name order. Returns resolved child
abstract writeText(target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal): Promise<FsWriteOutcome>
```
Create or fully replace a UTF-8 text file atomically. `expected` is the create-vs-replace decision and stale guard when supplied; OMITTING it is an unconditional create-or-overwrite (the bare provider — no version guard, no read-first requirement). Atomic either way.
Atomically create or replace UTF-8 text. `expected` guards intent and staleness; omission allows unconditional overwrite.
- `target` — the resolved target to write.
- `content` — the full new file content.
@@ -106,7 +98,7 @@ Create or fully replace a UTF-8 text file atomically. `expected` is the create-v
**Returns** the outcome, including the version the write produced.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L243)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L139)
### ctx.fs.editText(target, edit, expected?, signal?)
@@ -114,7 +106,7 @@ Create or fully replace a UTF-8 text file atomically. `expected` is the create-v
abstract editText(target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal): Promise<FsEditOutcome>
```
Apply a literal edit to an existing UTF-8 text file. When `expected` is supplied, verifies `expected.version` as the stale guard BEFORE literal matching; OMITTING it edits the current content unconditionally (no version guard). Either way applies the replacement and writes atomically — one mutation critical section — and a missing target reports `FS_STALE_VERSION`.
Atomically edit literal text. When supplied, the version guard is checked before matching so stale content reports `FS_STALE_VERSION`; omission edits the current content without a freshness precondition.
- `target` — the resolved target to edit.
- `edit` — the literal search/replace request.
@@ -123,4 +115,4 @@ Apply a literal edit to an existing UTF-8 text file. When `expected` is supplied
**Returns** the outcome, including the version the edit produced.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L257)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L151)

View File

@@ -6,7 +6,7 @@
The abstract `llm` service: an adapter registry plus a streaming model-call surface, interceptable via the `llm/stream` waterfall.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L88)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L75)
### ctx.llm.registerAdapter(models, adapter)
@@ -21,7 +21,7 @@ Register an adapter for the given model names. Throws `LlmError` with code `DUPL
**Returns** the disposer that unregisters all of them.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L103)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L90)
### ctx.llm.models()
@@ -33,7 +33,7 @@ Model names with a registered adapter.
**Returns** the registered names, in registration order.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L124)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L111)
### ctx.llm.stream(options)
@@ -47,4 +47,4 @@ Stream one model call as raw chunks (token-level deltas). Throws `LlmError` with
**Returns** the chunk stream, possibly wrapped by `llm/stream` listeners.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L141)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L128)

View File

@@ -0,0 +1,74 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# ctx.permission
`PermissionService` — provided by `@deepseek-ai/dsh-permission`.
Owns the deployment's permission presets and their write path. Requires a confining `ctx.bash` executor and `ctx.approval`; unmatched knob values are reported as CUSTOM_PRESET, not an error.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/permission/src/index.ts#L94)
### ctx.permission.names
```ts website-api
get names(): readonly string[]
```
The advertised preset names, in the preset table's declaration order.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/permission/src/index.ts#L134)
### ctx.permission.current(events)
```ts website-api
current(events: readonly SessionEvent[]): string
```
Resolve the preset matching the effective knob values. A still-matching last selection wins shared-bundle ties; otherwise the first table match wins, or CUSTOM_PRESET when no entry matches.
- `events` — the session's events in log order.
**Returns** the effective preset name, or `custom` when nothing matches.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/permission/src/index.ts#L145)
### ctx.permission.resolve(name)
```ts website-api
resolve(name: string): PresetSpec
```
Resolve a preset's knob bundle.
- `name` — the preset name to resolve.
**Returns** the configured bundle.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/permission/src/index.ts#L166)
### ctx.permission.optionOf(name)
```ts website-api
optionOf(name: string): PresetOption
```
Build the client option for a table entry or CUSTOM_PRESET. A missing label falls back to the table key.
- `name` — a table key, or `custom`.
**Returns** the option a client renders.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/permission/src/index.ts#L181)
### ctx.permission.set(session, name)
```ts website-api
set(session: Session, name: string): void
```
Record a changed preset, then update each changed knob through its own setter. Selecting the effective preset again appends nothing.
- `session` — the session the switch belongs to.
- `name` — the preset to switch to; unknown names throw.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/permission/src/index.ts#L195)

View File

@@ -0,0 +1,24 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# ctx.sandbox
`SandboxProvider` (abstract seam) — provided by `@deepseek-ai/dsh-sandbox`.
Abstract process-sandbox service. confine must return enforcing argv or fail closed at wrap or runner-execution time; silent unconfined passthrough is forbidden. Functional probes arbitrate multi-runner chains and may be skipped for a sole candidate, whose own refusal remains the fail-closed end.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/sandbox/sandbox/src/index.ts#L111)
### ctx.sandbox.confine(argv, policy)
```ts website-api
abstract confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv
```
Wrap `argv` so it executes confined under `policy` on this host; the caller spawns the returned argv in place of its own.
- `argv` — the exact argv the caller is about to spawn (program plus arguments), NOT a shell string — a shell-shaped consumer passes `['bash', '-c', command]`.
- `policy` — the file-effect policy this execution runs under, carried per call (see `SandboxPolicy`).
**Returns** the argv to spawn instead, plus the enforcement completeness the selected backend achieves for it.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/sandbox/sandbox/src/index.ts#L127)

View File

@@ -4,14 +4,9 @@
`SessionPersistence` (abstract seam) — provided by `@deepseek-ai/dsh-session-persistence`.
Abstract durable session-persistence service. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.sessionPersistence` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior).
Contracts every implementation MUST honor (a DB backend asserts them inside a transaction; a file backend appends at EOF):
- **Append-only; a crashed turn is closed, not truncated.** Committed events — those at or below a flushed `turn/end` — are never rewritten. A crash can leave an unclosed final turn whose events are real (and possibly large); load preserves them and closes the orphaned turn with synthetic boundary events (see load). Only a never-fully-written torn tail fragment is discarded.
- **Contiguous seq.** A persisted log is contiguous: `events[i].seq === i`. load rejects a parse error or a `seq` gap in the COMMITTED region (unloadable); append's first event `seq` MUST equal the backend's stored next-seq (after `load` has balanced any interrupted turn).
- **JSON-serializable data.** `SessionEventMap` is merge-extensible and `event.data` is typed only as `SessionEventMap[K]`, so append REJECTS non-JSON-serializable data with an error naming the offending event type. A backend snapshots (serializes/clones) each event when it buffers, since `session.events` hands out the live mutable object.
- **Durability.** append returns only once the batch is durable (the file backend fsyncs; a DB commits). create MAY defer the physical write until the first append (lazy materialization).
Durable append-only session storage. Implementations preserve contiguous, losslessly JSON-serializable events; append resolves only after durability, and load balances a complete interrupted tail without rewriting committed events.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L102)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L30)
### ctx.sessionPersistence.create(meta)
@@ -23,7 +18,7 @@ Register a new session's metadata. A backend MAY defer the physical write until
- `meta` — the immutable header (id, version, cwd, lineage) to record.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L114)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L42)
### ctx.sessionPersistence.append(id, events)
@@ -36,7 +31,7 @@ Durably persist a batch of events (called from the write-behind drain at the `se
- `id` — the session the batch belongs to.
- `events` — the contiguous batch to persist, in seq order.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L125)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L53)
### ctx.sessionPersistence.load(id)
@@ -44,14 +39,13 @@ Durably persist a batch of events (called from the write-behind drain at the `se
abstract load(id: SessionId): Promise<{ meta: SessionHeader; events: SessionEvent[] }>
```
Reload a session: its SessionHeader plus the event log up to the last durable checkpoint. Returns `meta` AND `events` so the live session is reconstructed with its `cwd`/lineage, not just its log.
The loop only flushes at `turn/end`, so a crash can leave a durable log whose final turn never closed: real, fully-written events sit after the last `turn/end`. Those events are PRESERVED — a single turn can be huge in a long-horizon task, so truncating it would destroy real work — and `load` CLOSES the orphaned turn by durably appending the minimal synthetic boundary events: an error `tool/result` for every `tool-call` the crash left unanswered (so the rehydrated history is a valid provider transcript — a dangling assistant tool-call is otherwise rejected), then a `step/end` if a step was open, then a `turn/end` carrying the `{ kind: 'interrupted' }` reason. The returned `events` therefore end on a balanced `turn/end` and are immediately usable as a session seed. Only a never-fully-written TORN tail fragment (a half-written final record) is discarded. Returned events are contiguous (`events[i].seq === i`); a parse error or a `seq` gap in the COMMITTED region (at or before the last real `turn/end`) makes the session unloadable (reject). Rejects an unknown format `version`. See the session-persistence RFC for the crash-recovery contract.
Load a header and balanced contiguous log. A complete interrupted final turn is preserved and durably closed with missing tool errors plus any open step and turn boundaries; only a torn final record is discarded. Unknown versions and corruption in the committed prefix reject.
- `id` — the persisted session to reload.
**Returns** the header plus the event log, ending on a balanced `turn/end` — immediately usable as a session seed.
**Returns** the header and a log ending on a balanced `turn/end`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L152)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L63)
### ctx.sessionPersistence.list()
@@ -63,4 +57,4 @@ Lightweight listing from metadata, without a full-log parse.
**Returns** one header per materialized session.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L158)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L69)

View File

@@ -0,0 +1,49 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# ctx.sessionQuery
`SessionQueryService` — provided by `@deepseek-ai/dsh-session-query`.
Live-preferred logical-corpus and exact-event read service.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-query/session-query/src/index.ts#L35)
### ctx.sessionQuery.listSessions()
```ts website-api
listSessions(): Promise<SessionRecord[]>
```
List the complete logical corpus using live-preferred records.
**Returns** deterministic newest-first cloned session records.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-query/session-query/src/index.ts#L60)
### ctx.sessionQuery.listEvents(sessionId)
```ts website-api
async listEvents(sessionId: SessionId): Promise<SessionEventRecord[]>
```
List lightweight raw-log event records for one logical session.
- `sessionId` — live-preferred session id to read.
**Returns** event records in ascending seq order.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-query/session-query/src/index.ts#L69)
### ctx.sessionQuery.readEvent(request)
```ts website-api
async readEvent(request: SessionEventReadRequest): Promise<SessionEventWindow>
```
Read one full event plus a bounded raw-log context window.
- `request` — target session/seq and context sizes.
**Returns** cloned target and neighboring events.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-query/session-query/src/index.ts#L79)

View File

@@ -7,7 +7,7 @@
In-memory session store (`ctx.sessions`).
Persistence is intentionally not implemented here — persistence plugins subscribe to `session/event` and flush on `session/flush` / dispose.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L405)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L564)
### ctx.sessions.create(id?, options?)
@@ -16,14 +16,14 @@ create(id?: SessionId, options?: CreateSessionOptions): Session
```
Create a session owned by the calling fiber: disposing that fiber stops event notification and removes the session from the store. `options.seed` populates the session with a copy of those events (replay/fork); `options.meta` attaches creation metadata (validated absolute `cwd`, `parentSession` lineage) as the immutable SessionHeader (the store fills `version`/`id`/`createdAt`).
For an agent whose session must be torn down IN ORDER with its loop (so the loop's final flush is captured before `onAppend` detaches), do NOT use this — fold the session lifecycle into the agent's own effect via prepare + enter + announce (see `dsh-agent-loop`'s `startOwned`).
For an agent whose session must be torn down IN ORDER with its loop (so the loop's final flush is captured before the store attachment ends), do NOT use this — fold the session lifecycle into the agent's own effect via prepare + enter + announce (see `dsh-agent-loop`'s creation transaction).
- `id` — the session id; omitted, the store mints `session-<n>`.
- `options` — seed events and/or creation metadata for the header.
**Returns** the live session, already entered and announced.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L433)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L593)
### ctx.sessions.prepare(id?, options?)
@@ -31,14 +31,14 @@ For an agent whose session must be torn down IN ORDER with its loop (so the loop
prepare(id?: SessionId, options?: CreateSessionOptions): Session
```
Build a session WITHOUT entering it into the store — validate the id/cwd and construct the Session (with its immutable SessionHeader). Pairs with enter + announce: a caller that owns a composite `ctx.effect` (the agent factory) folds the session lifecycle into that ONE effect so a fiber unload tears the session + agent down as a single ORDERED chain rather than as racing sibling effects — which would detach `onAppend` before the loop's closing `session/flush`, dropping the closing events.
Build a session WITHOUT entering it into the store — validate the id/cwd and construct the Session (with its immutable SessionHeader). Pairs with enter + announce: a caller that owns a composite `ctx.effect` (the agent factory) folds the session lifecycle into that ONE effect so a fiber unload tears the session + agent down as a single ORDERED chain rather than as racing sibling effects — which would remove the publication hooks before the loop's closing `session/flush`, dropping the closing events.
- `id` — the session id; omitted, the store mints `session-<n>`.
- `options` — seed events and/or creation metadata for the header.
**Returns** the constructed session, NOT yet in the store.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L461)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L622)
### ctx.sessions.enter(session)
@@ -46,14 +46,14 @@ Build a session WITHOUT entering it into the store — validate the id/cwd and c
enter(session: Session): () => void
```
Enter a prepared session into the store: wire `onAppend` → `session/event` and add it to the store. Returns the DETACH disposer (`onAppend = undefined` + store removal). Does NOT emit `session/created` — the caller yields this disposer inside its effect and THEN calls announce, so a throwing `session/created` listener rolls the attach back instead of leaking it.
Enter a prepared session into the store: install the module-private append publication hooks and add it to the store. Returns the DETACH disposer (hooks + store removal). Does NOT emit `session/created` — the caller yields this disposer inside its effect and THEN calls announce, so a throwing `session/created` listener rolls the attach back instead of leaking it.
Re-checks the id for a duplicate: `prepare` and `enter` are public cross-package primitives and a caller may interleave arbitrary work (or another create) between them, so a stale prepared session must NOT overwrite a live store entry of the same id — its detach disposer would later delete the REAL session. The create convenience and the agent factory call the two back-to-back so they never trip this, but the public seam cannot assume that.
- `session` — a `prepare`d session not yet in the store.
**Returns** the detach disposer (`onAppend = undefined` + store removal).
**Returns** the detach disposer (publication hooks + store removal). When called from a synchronous `session/created` listener, removal and disposal wait until that creation dispatch unwinds.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L499)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L666)
### ctx.sessions.announce(session)
@@ -61,11 +61,25 @@ Re-checks the id for a duplicate: `prepare` and `enter` are public cross-package
announce(session: Session): void
```
Emit `session/created` for an entered session. Separate from enter so the caller can yield the detach disposer first (rollback safety — see enter).
Emit `session/created` exactly once for an entered session (with the carrier enter captured). Separate from enter so the caller can yield the detach disposer first (rollback safety — see enter).
- `session` — the entered session to announce to listeners.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L513)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L721)
### ctx.sessions.flush(session)
```ts website-api
async flush(session: Session): Promise<void>
```
Dispatch the awaited `session/flush` durability checkpoint for `session`, with the carrier captured at enter. THE flush entry point: the store owns the carrier, so callers (the loop's turn-end checkpoint, idle injection, teardown drains) must come through here rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner, one spelling, and the scoped-dispatch invariant can pin it.
- `session` — the session whose buffered events must reach durable storage.
**Returns** resolves when every flush listener has settled; after all settle, rejects with the first registered listener failure if any listener failed.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L773)
### ctx.sessions.get(id)
@@ -79,7 +93,7 @@ Look up a live session.
**Returns** the session, or undefined when no live session has that id.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L522)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L805)
### ctx.sessions.list()
@@ -91,7 +105,7 @@ All live sessions, in creation order.
**Returns** a fresh array; mutating it does not affect the store.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L530)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L813)
### ctx.sessions.fork(source, boundary?, childSessionId?)
@@ -107,4 +121,4 @@ Create a live child session from a turn-enclosed prefix of a live source. `bound
**Returns** The created live child session.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L547)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L830)

View File

@@ -0,0 +1,66 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# ctx.skills
`SkillService` — provided by `@deepseek-ai/dsh-skill`.
Registry of skill providers. It merges provider catalogs with stable first-wins duplicate handling, exposes sorted model-visible summaries, and loads full skill bodies on demand.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/skill/skill/src/index.ts#L141)
### ctx.skills.registerProvider(provider)
```ts website-api
registerProvider(provider: SkillProvider): () => void
```
Register a borrowed same-process provider synchronously during plugin apply. Duplicate and reserved names throw; remote initialization belongs in `list()`. Fiber disposal unregisters the provider and invalidates catalog caches.
- `provider` — the provider to register by `provider.name`.
**Returns** the exact Cordis effect disposer that unregisters this provider; composite effects may yield it directly to preserve teardown ordering.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/skill/skill/src/index.ts#L168)
### ctx.skills.register(skill)
```ts website-api
register(skill: SkillRegistration): () => void
```
Register a borrowed readonly runtime skill. Project entries outrank runtime entries, which outrank user entries. Same-name runtime entries are first-wins; a duplicate logs a warning and receives a no-op disposer so it cannot remove the winner.
- `skill` — the complete skill definition to expose for discovery.
**Returns** the exact Cordis effect disposer, preserving composite teardown order and invalidating caches.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/skill/skill/src/index.ts#L199)
### ctx.skills.list(options?)
```ts website-api
async list(options: SkillLookupOptions = {}): Promise<SkillSummary[]>
```
List model-invocable skill summaries for a workspace. Lookup options and provider candidates are readonly same-process values borrowed throughout discovery.
- `options` — lookup options; `cwd` selects project roots and `signal` cancels discovery.
**Returns** sorted summaries, excluding skills disabled for model invocation.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/skill/skill/src/index.ts#L230)
### ctx.skills.get(name, options?)
```ts website-api
async get(name: string, options: SkillLookupOptions = {}): Promise<SkillDefinition | undefined>
```
Load and validate the winning candidate, passing its opaque discovery locator back to the provider. Cancellation is rechecked after selection, including cache hits, and raced against loading so an uncooperative provider cannot hang the caller.
- `name` — kebab-case skill name.
- `options` — lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work.
**Returns** the full skill, including body content, or `undefined`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/skill/skill/src/index.ts#L246)

View File

@@ -4,9 +4,9 @@
`SubagentService` — provided by `@deepseek-ai/dsh-subagent`.
The `subagents` service: a registry of named SubagentProviders and a capability-checked start surface.
Named provider registry and capability-checked start surface.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L144)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L141)
### ctx.subagents.registerProvider(provider)
@@ -14,13 +14,13 @@ The `subagents` service: a registry of named SubagentProviders and a capability-
registerProvider(provider: SubagentProvider): () => void
```
Register a provider under its `provider.name`. Throws SubagentError (`DUPLICATE_PROVIDER`) if the name is already taken. Effect-scoped: disposed with the calling fiber (HMR-safe). Emits `subagent/provider-added` after the registration and `subagent/provider-removed` on unregistration, so consumers can mirror provider lifecycle instead of assuming load order.
Register a provider under its name. Registration is effect-scoped and HMR safe; removing a provider blocks new starts but does not revoke runs that were already returned to their holders.
- `provider` — the provider; its `name` is the registry key.
- `provider` — the trusted provider implementation.
**Returns** the disposer that unregisters the provider.
**Returns** the exact Cordis effect disposer.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L160)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L155)
### ctx.subagents.getProvider(name)
@@ -28,13 +28,13 @@ Register a provider under its `provider.name`. Throws SubagentError (`DUPLICATE_
getProvider(name: string): SubagentProvider | undefined
```
Look up a registered provider by name (`undefined` if absent).
Look up a provider by name.
- `name` — the provider name as registered.
- `name` — the provider name.
**Returns** the provider, or undefined when the name is unknown.
**Returns** the provider, or undefined when absent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L188)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L178)
### ctx.subagents.list()
@@ -42,23 +42,23 @@ Look up a registered provider by name (`undefined` if absent).
list(): string[]
```
The names of all registered providers (insertion order).
List registered provider names in insertion order.
**Returns** the registered provider names.
**Returns** the registered names.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L196)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L186)
### ctx.subagents.start(name, request)
```ts website-api
start(name: string, request: SubagentStartRequest): SubagentRun
async start(name: string, request: SubagentStartRequest): Promise<SubagentRun>
```
Start a subagent run on the named provider. Resolves the provider (throws `NO_PROVIDER` if absent), validates every requested START-TIME capability against SubagentProvider.capabilities (throws `UNSUPPORTED_CAPABILITY` for the first unmet one — fail loud, before any child is created), then delegates to SubagentProvider.start and emits `subagent/start` / `subagent/end` around the run.
Establish a ready child on the named provider. Capability and semantic checks run before delegation. Provider ownership lasts until its promise fulfills; a rejection therefore has no run for the caller to dispose and emits no run lifecycle events.
- `name` — the provider to run on.
- `request` — the child's prompt, capabilities, and options.
- `name` — the provider to use.
- `request` — child prompt, parent, signal, and optional capabilities.
**Returns** the live run (its `result` resolves when the child settles).
**Returns** the ready holder-owned run.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L211)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L199)

View File

@@ -4,9 +4,9 @@
`SystemPrompt` — provided by `@deepseek-ai/dsh-system-prompt`.
Registry service (`ctx.systemPrompt`): plugins contribute ordered text sections, tool-schema providers, and named prompt variables; the agent loop calls `assemble(context)` once per step. Registers the harness-owned `harness:identity` and `deployment:persona` sections itself (see Config.persona).
Registry service for the prompt inputs assembled before each model step.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L291)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L209)
### ctx.systemPrompt.section(section)
@@ -14,27 +14,27 @@ Registry service (`ctx.systemPrompt`): plugins contribute ordered text sections,
section(section: PromptSection): () => void
```
Contribute a text section to the system prompt. Order is determined by `section.order` (ascending). Throws if a section with the same name is already registered (a duplicate would silently double prompt text — e.g. a double-loaded tool plugin). The section is removed when the calling fiber is disposed. Emits `system-prompt/change` on register/unregister.
Register an ordered prompt section in the calling context's scope. A scoped section shadows a global section with the same name; duplicates within one layer and non-finite orders throw. Registration and disposal emit `system-prompt/change`.
- `section` — the section to contribute (name, order, text or provider).
- `section` — the section to register.
**Returns** the disposer that removes the section.
**Returns** the exact Cordis effect disposer.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L340)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L250)
### ctx.systemPrompt.tools(provider)
```ts website-api
tools(provider: () => ToolSchema[]): () => void
tools(provider: (context: AssembleContext) => ToolProviderResult): () => void
```
Contribute a tool-schema provider that is evaluated at each assembly call (so it can reflect the live registry state). The provider is removed when the calling fiber is disposed. A provider must not return a schema named TOOL_ORDER_REST; that name is reserved for Config.toolOrder's rest entry and rejects the assembly. Emits `system-prompt/change`.
Register a tool-schema provider in the calling context's scope. Global and matching scoped providers both contribute; returning the reserved TOOL_ORDER_REST name makes assembly fail.
- `provider` — evaluated at every `assemble` for fresh schemas.
- `provider` — evaluated for each assembly with its context.
**Returns** the disposer that removes the provider.
**Returns** the exact Cordis effect disposer.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L373)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L291)
### ctx.systemPrompt.variable(name, provider)
@@ -42,14 +42,14 @@ Contribute a tool-schema provider that is evaluated at each assembly call (so it
variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void
```
Contribute a named prompt variable, referenced from section text as `{{name}}`. The provider is evaluated at each assembly with that assembly's AssembleContext; returning `undefined` means "no value for this assembly" (a section referencing it then fails to render — a deployment must not claim facts it does not have). Throws on a name that does not match `[a-z][a-z0-9_]*` (it could never be referenced) or is already registered. Removed when the calling fiber is disposed; emits `system-prompt/change` on register/unregister.
Register a prompt variable in the calling context's scope. Scoped values shadow globals; invalid or duplicate names throw. A provider may return `undefined`, but rendering a section that references that value then fails.
- `name` — the reference name (matches `[a-z][a-z0-9_]*`).
- `provider` — evaluated at every `assemble` for the value.
- `name` — the `[a-z][a-z0-9_]*` reference name.
- `provider` — evaluated for each assembly.
**Returns** the disposer that removes the variable.
**Returns** the exact Cordis effect disposer.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L403)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L325)
### ctx.systemPrompt.assemble(context?)
@@ -57,10 +57,10 @@ Contribute a named prompt variable, referenced from section text as `{{name}}`.
async assemble(context: AssembleContext = {}): Promise<PromptAssembly>
```
Assemble the current prompt for one caller: section texts are resolved against `context` and sorted by order, tools collected from all providers and put in the canonical model-facing order (Config.toolOrder, or lexicographic name order when unconfigured — provider registration order is a plugin-load artifact and never reaches the assembly; a configured order naming a tool no provider contributed rejects the assembly), and every registered variable resolved against `context` into `assembly.variables`. Tool schemas are deep-cloned because adapters and request waterfalls may mutate schema objects. Runs through the `system-prompt/assemble` waterfall, giving listeners the opportunity to mutate or replace the assembly before it reaches the model — like the sections' `order` sort, tool canonicalization happens on the initial assembly, and a listener owns the determinism of whatever it emits. Await the result before reading the assembly values — waterfall listeners may be async. Interpolation happens later, in renderPrompt.
Assemble global and scoped providers, detach tool parameters, apply canonical ordering, then run the assembly waterfall. Scoped sections and variables shadow globals; the returned waterfall value is authoritative.
- `context` — what this assembly is for (defaults to an empty context; see `AssembleContext`).
- `context` — the optional scope and plugin-defined assembly fields.
**Returns** the assembly after the waterfall has run.
**Returns** the authoritative post-waterfall assembly.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L447)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L365)

View File

@@ -0,0 +1,128 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# ctx.tasks
`TaskService` — provided by `@deepseek-ai/dsh-tasks`.
The `tasks` service: the runtime-global background task registry. See the module doc for the ownership, isolation, and lifecycle contracts.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L76)
### ctx.tasks.start(spec)
```ts website-api
start(spec: TaskStart): TaskId
```
Preflight access, validation, and owner cleanup before starting and atomically registering work. A throwing starter leaves nothing registered; after it returns, registration cannot fail. Settlement records the outcome, notifies listeners, and releases waiters.
- `spec` — task identity, owner, and synchronous starter.
**Returns** the registry-issued `<kind>-N` id.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L101)
### ctx.tasks.list(caller?)
```ts website-api
list(caller?: Agent): TaskSnapshot[]
```
List caller-owned and unowned tasks in registration order without exposing another session's labels.
- `caller` — reading agent; a non-agent caller sees only unowned tasks.
**Returns** fresh snapshots.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L153)
### ctx.tasks.get(id, caller?)
```ts website-api
get(id: TaskId, caller?: Agent): TaskSnapshot
```
Return a non-consuming snapshot without changing its read cursor or notice state. Throws for an unknown or foreign task.
- `id` — task to look up.
- `caller` — reading agent checked against the owner.
**Returns** a fresh snapshot.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L167)
### ctx.tasks.read(id, caller?)
```ts website-api
read(id: TaskId, caller?: Agent): TaskRead
```
Read the next stream delta, or the idempotent final output after settlement. A terminal read marks the task reported. Throws for an unknown or foreign task.
- `id` — task to read.
- `caller` — reading agent checked against the owner.
**Returns** output text and the post-read snapshot.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L181)
### ctx.tasks.kill(id, caller?, reason?)
```ts website-api
kill(id: TaskId, caller?: Agent, reason?: string): 'requested' | 'already-finished'
```
Request cancellation, then mark the task stopping and reported. A producer throw propagates without changing task state. Throws for an unknown or foreign task.
- `id` — task to cancel.
- `caller` — killing agent checked against the owner.
- `reason` — logged reason forwarded to the producer.
**Returns** `requested` for live work, otherwise `already-finished`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L200)
### ctx.tasks.wait(id, timeoutMs, caller?, signal?)
```ts website-api
async wait(id: TaskId, timeoutMs: number, caller?: Agent, signal?: AbortSignal): Promise<TaskSnapshot>
```
Wait for settlement or timeout without cancelling the task. Caller abort rejects only while the task is live; after settlement it returns the terminal snapshot so a notice suppressed for this waiter is still delivered. Timed-out and aborted waits detach their resolvers. Throws for invalid, unknown, or foreign input.
- `id` — task to wait for.
- `timeoutMs` — positive finite wait bound in milliseconds.
- `caller` — waiting agent checked against the owner.
- `signal` — optional cancellation of the wait itself.
**Returns** snapshot at settlement or timeout.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L226)
### ctx.tasks.onTaskDone(listener)
```ts website-api
onTaskDone(listener: TaskDoneListener): () => void
```
Register an effect-scoped completion listener. Each listener is contained; returned promises are observed but not awaited. No listener runs after service disposal.
- `listener` — receives each terminal snapshot and its exact owner.
**Returns** disposer that unregisters the listener.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L283)
### ctx.tasks.attachSurface(name)
```ts website-api
attachSurface(name: string): () => void
```
Attach an effect-scoped surface that can read and stop tasks. start refuses work while none is attached.
- `name` — diagnostic label; duplicate names remain independent.
**Returns** disposer that detaches this surface.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L297)

View File

@@ -4,9 +4,9 @@
`ToolRegistry` — provided by `@deepseek-ai/dsh-tools`.
Tool registry (`ctx.tools`): tool plugins register definitions; the agent loop executes calls through the `tools/pre-execute``tools/execute``tools/post-execute` pipeline. The registry contributes its schemas into the system-prompt assembly — WHICH schemas is governed by its `mode` config (see Config.mode); under a non-native mode it also registers the `run_code` tool and the `tools:sdk` prompt section itself.
Tool registry and execution pipeline. Scoped registrations shadow globals; one visibility resolver feeds presentation, lookup, and dispatch.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L345)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L363)
### ctx.tools.register(definition)
@@ -14,50 +14,81 @@ Tool registry (`ctx.tools`): tool plugins register definitions; the agent loop e
register(definition: ToolDefinition): () => void
```
Register a tool. Throws if a tool with the same name is already registered. The tool's schema (minus the `execute` function) is automatically contributed to the system-prompt assembly. Disposed with the calling fiber. Emits `tools/change` on register/unregister.
Register globally or in the calling agent scope. Scoped tools shadow globals; duplicates within one layer and the reserved `run_code` name fail.
- `definition` — the tool's schema plus its execute (and optional presentation) functions.
- `definition` — the tool schema, execution, and optional presentation functions.
**Returns** the disposer that unregisters the tool.
**Returns** the exact disposer that unregisters the tool.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L420)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L453)
### ctx.tools.get(name)
### ctx.tools.restrict(filter)
```ts website-api
get(name: string): ToolDefinition | undefined
restrict(filter: ToolRestriction): () => void
```
Look up a registered tool.
Restrict global tools for the calling agent scope. Empty filters, unknown names, scope-local names, and reserved transport names fail. Restrictions intersect; scoped registrations remain visible.
- `filter` — global-surface mask: `allow` (keep only) and/or `deny` (remove).
**Returns** the exact disposer that lifts this restriction.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L493)
### ctx.tools.guard(guard)
```ts website-api
guard(guard: ToolGuard): () => void
```
Register a monotonic guard after the extensible `tools/pre-execute` waterfall. A plain-context guard applies globally; one registered through `agent.ctx` applies only to that agent. Any matching guard may deny by returning a reason, while no guard can force-allow a call another guard denied. The exact effect disposer is returned for ordered ownership and HMR cleanup.
- `guard` — synchronous check; a returned string denies the execution.
**Returns** the exact disposer that unregisters the guard.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L544)
### ctx.tools.get(name, scope?)
```ts website-api
get(name: string, scope?: ScopeKey): ToolDefinition | undefined
```
Look up a tool as one scope sees it (scoped shadows global; a restricted-away global reads as absent). Presenters pass the calling agent so the rendered card matches the definition that actually executed.
- `name` — the tool name as registered.
- `scope` — the viewing scope (the agent); omitted = the global view.
**Returns** the definition, or undefined when no tool has that name.
**Returns** the definition the scope resolves, or undefined when none is visible.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L447)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L646)
### ctx.tools.schemas()
### ctx.tools.schemas(scope?)
```ts website-api
schemas(): ToolSchema[]
schemas(scope?: ScopeKey): ToolSchema[]
```
Return all registered tool schemas — exactly the model-facing fields (`name`, `description`, `parameters`), as sent to the model via the system-prompt assembly. Constructed EXPLICITLY rather than by stripping known non-schema members: a `ToolDefinition` also carries `execute` and the optional `presentCall`/`presentResult` UI callbacks, and those (especially the functions) must never leak into a model request. An allowlist can't drift when a new non-schema member is added to the definition; a denylist (rest-destructure) would silently leak it.
Project visible definitions onto the allowlisted model-facing schema fields, excluding execution and presentation callbacks.
**Returns** one deep-cloned schema per registered tool, in registration order.
- `scope` — the viewing scope (the agent); omitted = the global view.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L462)
**Returns** one deep-cloned schema per visible tool.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L656)
### ctx.tools.execute(exec)
```ts website-api
async execute(exec: ToolExecution): Promise<ToolExecutionResult>
async execute(exec: ToolExecutionInput): Promise<ToolExecutionResult>
```
Execute one tool call through the `tools/pre-execute` → `tools/execute` (around dispatch) → `tools/post-execute` pipeline. `pre-execute` is the gate (allow/deny), `tools/execute` wraps core dispatch (a timeout/retry/metrics seam), and `post-execute` is the inspect/transform seam; core dispatch sits as the base `next()` of the `tools/execute` waterfall. The whole thing is wrapped in one outer try/catch so a throwing listener (in any waterfall) becomes an `isError` result instead of failing the turn; the tool body ALSO keeps its own inner try/catch, so a thrown tool becomes an `isError` result that `tools/execute` and `post-execute` listeners can still inspect. If the tool is not registered, the result is an `isError` carrying a `UNKNOWN_TOOL` structured error. A thrown HarnessError surfaces its `{ name, code }` on the result.
Execute through pre-policy, guards, around-dispatch, post-policy, and final notification. Tool and listener failures resolve as materialized error results; an invisible tool reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen snapshot final observers receive.
- `exec` — the call to run (name, parsed arguments, caller agent, signal).
- `exec` — the typed same-process call input. The registry assigns its correlation token before policy begins.
**Returns** the final result after every waterfall; failures resolve as `isError` results, never rejections.
**Returns** the materialized final result.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L487)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L679)

View File

@@ -6,14 +6,14 @@
The web access service. Registered as `ctx.web` (one instance per context).
Selection semantics (resolved at execution time, never order-dependent):
- A configured id that is registered and `status().available` → that provider.
- A configured id that is registered and `available()` → that provider.
- A configured id not registered → `WEB_PROVIDER_CONFIGURED_MISSING`.
- A configured id registered but unavailable → `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`.
- No id configured, exactly one registered usable provider → that provider.
- No id configured, multiple usable providers → `WEB_PROVIDER_AMBIGUOUS`.
- No id configured, no usable provider → `WEB_PROVIDER_UNAVAILABLE`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/web/web/src/index.ts#L87)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/web/web/src/index.ts#L74)
### ctx.web.registerSearchProvider(provider)
@@ -27,7 +27,7 @@ Register a search provider. Throws WebError `WEB_DUPLICATE_PROVIDER` if its id i
**Returns** the disposer that unregisters the provider.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/web/web/src/index.ts#L116)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/web/web/src/index.ts#L103)
### ctx.web.registerFetchProvider(provider)
@@ -41,34 +41,34 @@ Register a fetch provider. Throws WebError `WEB_DUPLICATE_PROVIDER` if its id is
**Returns** the disposer that unregisters the provider.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/web/web/src/index.ts#L127)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/web/web/src/index.ts#L114)
### ctx.web.search(request, exec?)
### ctx.web.search(request, signal?)
```ts website-api
async search(request: WebSearchRequest, exec?: WebExecContext): Promise<WebSearchResult>
async search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>
```
Run one search through the selected provider. Resolves the provider at call time with the selection rules above; throws WebError when the capability cannot run. The seam enforces `request.maxResults` on the result: if the provider over-returns, `sources[]` is truncated and `truncated` set.
- `request` — the query plus result-shaping options.
- `exec` — the tool-execution context, forwarded to the provider.
- `signal` — optional cancellation signal forwarded to the provider.
**Returns** the provider's results, capped to `request.maxResults`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/web/web/src/index.ts#L153)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/web/web/src/index.ts#L140)
### ctx.web.fetch(request, exec?)
### ctx.web.fetch(request, signal?)
```ts website-api
async fetch(request: WebFetchRequest, exec?: WebExecContext): Promise<WebFetchResult>
async fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult>
```
Retrieve one URL through the selected provider. Resolves the provider at call time with the selection rules above; throws WebError when the capability cannot run. A non-2xx response is a result, not a throw.
- `request` — the URL plus retrieval options.
- `exec` — the tool-execution context, forwarded to the provider.
- `signal` — optional cancellation signal forwarded to the provider.
**Returns** the retrieval outcome; non-2xx responses resolve descriptively.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/web/web/src/index.ts#L170)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/web/web/src/index.ts#L157)

View File

@@ -4,14 +4,9 @@
`WorkflowService` (abstract seam) — provided by `@deepseek-ai/dsh-workflow`.
Abstract workflow execution service. Subclass, implement start, and load the subclass as a plugin — it registers as `ctx.workflows` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior).
Semantics every implementation must honor:
- start throws synchronously for a request that cannot begin (an unparseable script, an invalid meta block). Once it returns a WorkflowRun, `result` NEVER rejects — every failure resolves with `stopReason: 'error'` (or `'cancelled'`) — and once the run is cancelled, `result` SETTLES within the implementation's bounded grace even if the script itself never settles (a consumer awaiting `result` must never be wedged past a cancellation).
- The `workflow/*` events fire through emitWorkflowEvent (data snapshots, per-listener containment); `workflow/end` fires exactly once per started run, after `result` is settled or as it settles.
- `dispose()` reaches quiescence within a bounded grace: it cancels, waits for the script to settle AND its started children to finish disposing, and abandons whatever is left rather than hanging its caller (the engine documents what abandonment leaves behind).
- Runs are HOLDER-OWNED: the engine hands control (`cancel`/`dispose`) to the `start()` caller and does not track its live runs — disposing the engine's own fiber mid-run deliberately leaves those runs to their holders' teardown, so an engine reload cannot yank a run out from under the consumer awaiting it.
Workflow execution seam. Invalid requests throw before publication; a live run is holder-owned, its result never rejects, cancellation and disposal are bounded, and disposal waits for child cleanup within that bound. Lifecycle listener failures are contained, and `workflow/end` fires exactly once as the result settles.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L210)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L159)
### ctx.workflows.start(request)
@@ -25,4 +20,4 @@ Parse and execute a workflow script.
**Returns** the live run; its `result` resolves when the script settles.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L221)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L170)

View File

@@ -18,15 +18,21 @@ Cordis 微内核提供的基础能力,所有插件开发都建立在这些 API
- [ctx.agentLoop](./harness/agent-loop) — ReAct 循环的创建与恢复
- [ctx.agents](./harness/agents) — Agent 注册表与工厂
- [ctx.approval](./harness/approval) — 用户审批
- [ctx.bash](./harness/bash) — Bash 执行接口(抽象缝)
- [ctx.codeRuntime](./harness/code-runtime) — 代码执行接口(抽象缝)
- [ctx.compact](./harness/compact) — 上下文压缩接口(抽象缝)
- [ctx.fs](./harness/fs) — 文件系统接口(抽象缝)
- [ctx.llm](./harness/llm) — LLM 服务与适配器注册
- [ctx.permission](./harness/permission) — 权限策略
- [ctx.sandbox](./harness/sandbox) — 沙箱执行接口(抽象缝)
- [ctx.sessionPersistence](./harness/session-persistence) — 会话持久化接口(抽象缝)
- [ctx.sessionQuery](./harness/session-query) — 会话检索
- [ctx.sessions](./harness/sessions) — 会话存储
- [ctx.skills](./harness/skills) — 技能加载
- [ctx.subagents](./harness/subagents) — 子代理委派
- [ctx.systemPrompt](./harness/system-prompt) — 系统提示词组装
- [ctx.tasks](./harness/tasks) — 后台任务
- [ctx.tools](./harness/tools) — Tool 注册表
- [ctx.userInteraction](./harness/user-interaction) — 用户交互接口
- [ctx.web](./harness/web) — Web 搜索与抓取

View File

@@ -146,7 +146,7 @@ ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter)
- my-model-v2
- id: stdio-agent
name: '@deepseek-ai/dsh-stdio-agent'
name: '@deepseek-ai/dsh-stdio-demo'
config:
model: my-model-v1 # 引用上面注册的模型名
```

View File

@@ -31,7 +31,7 @@ Harness 使用 `cordis.yml` 描述一个 Agent 加载哪些插件、以什么参
# 应用主体:把 session 管理、tool 调度、agent loop 等组装成一个可交互的终端 Agent
# 只需告诉它用哪个模型 (`model`)、什么人设 (`persona`)
- id: stdio-agent
name: '@deepseek-ai/dsh-stdio-agent'
name: '@deepseek-ai/dsh-stdio-demo'
config:
model: mock-echo
persona: 'You are echo-agent, a demo agent.'
@@ -74,7 +74,7 @@ Harness 使用 `cordis.yml` 描述一个 Agent 加载哪些插件、以什么参
# `persona` 是系统提示词,{{model}} 会被替换为实际模型名
# `resumeSessionId` 设了就恢复旧对话,没设就每次新建
- id: stdio-agent
name: '@deepseek-ai/dsh-stdio-agent'
name: '@deepseek-ai/dsh-stdio-demo'
config:
model: deepseek-v4-flash
resumeSessionId: !!js process.env.RESUME_SESSION_ID
@@ -157,7 +157,7 @@ Harness 使用 `cordis.yml` 描述一个 Agent 加载哪些插件、以什么参
name: '@deepseek-ai/dsh-tool-fs'
```
和 echo-agent 对比:同一个 `dsh-stdio-agent` 应用主体,只是把 mock 换成了真实 API加上了更多工具插件。
和 echo-agent 对比:同一个 `dsh-stdio-demo` 应用主体,只是把 mock 换成了真实 API加上了更多工具插件。
## 语法详解
@@ -222,7 +222,7 @@ config:
### stdio-agent标准应用主体
**包名:** `@deepseek-ai/dsh-stdio-agent`
**包名:** `@deepseek-ai/dsh-stdio-demo`
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
@@ -354,7 +354,7 @@ hmr 仅用于开发环境。它需要 `node --expose-internals` 启动参数,`
1. **hmr** — 热替换(仅开发时需要)
2. **LLM 适配器** — 模型后端
3. **执行器** — bash、fs 等能力提供者
4. **应用主体**`dsh-stdio-agent``dsh-acp-agent`
4. **应用主体**`dsh-stdio-demo``dsh-acp-demo`
5. **附加插件** — compact、subagent、todo 等
应用主体内部已经捆绑了核心能力session、tools、agent-loop不需要手动加载。

View File

@@ -13,7 +13,7 @@ Harness 将一个 AI Agent智能体 所需要的所有能力——LLM 调
apiKey: !!js process.env.DEEPSEEK_API_KEY
# 选择应用模板
- name: '@deepseek-ai/dsh-stdio-agent'
- name: '@deepseek-ai/dsh-stdio-demo'
config:
model: deepseek-v4-flash
```

View File

@@ -90,7 +90,7 @@ agent REPL ready. Give it a coding task.
## 回头看
echo-agent 和 coding-agent 用的是同一个应用框架(`@deepseek-ai/dsh-stdio-agent`),区别只在 `cordis.yml`——换了哪些插件、填了什么配置。你以后定制自己的 Agent 也是同样的方式。
echo-agent 和 coding-agent 用的是同一个应用框架(`@deepseek-ai/dsh-stdio-demo`),区别只在 `cordis.yml`——换了哪些插件、填了什么配置。你以后定制自己的 Agent 也是同样的方式。
## 下一步