website: generate the API reference from source (cordis + all 15 harness services)

scripts/gen-website-api.ts renders website/zh-CN/api/{cordis,harness}/* and the
api-sidebar.json fragment the VitePress config imports, so pages and navigation
can never drift from the code: signatures, @param/@returns prose, dispatch
modes, and GitHub source links are extracted, never transcribed, and the
generator hard-errors on any rendered member missing docs. verify-website-api
(doc-sync + run-gates) is the freshness gate.

Replaces the hand-written zh api pages (7 pages covering 7 of 15 services,
with phantom APIs: Context.current/Context.events, agent/post-step, tool/call,
compact/*, llm/pre-request none of which exist) with generated English
references: 5 cordis pages, 15 per-service pages, and a 35-event catalog
grouped by scope. The hand-written hub api/index.md stays and now indexes the
full surface; zh for these pages arrives with the unified translation flow.
This commit is contained in:
lintianle
2026-07-16 18:13:34 +08:00
parent da26138592
commit efba9fab0a
31 changed files with 2983 additions and 931 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ packages/ Harness packages at packages/<group>/<pkg>/, all named @deepseek-ai
examples/ Runnable demos: thin cordis.yml leaves over the app packages (see examples/AGENTS.md)
docs/ architecture, generated catalogs, RFCs, postmortems, cookbook (see docs/AGENTS.md)
scripts/ repo gates and generators
website/ VitePress docs site (zh-CN)
website/ VitePress docs site (zh-CN); api/ pages generated from source
```
Per-package map: the group READMEs, indexed from [packages/README.md](packages/README.md).
+3 -1
View File
@@ -61,11 +61,13 @@
"verify-persistence-catalog": "tsx scripts/gen-persistence-catalog.ts --check",
"gen-module-graph": "tsx scripts/gen-module-graph.ts",
"verify-module-graph": "tsx scripts/gen-module-graph.ts --check",
"gen-website-api": "tsx scripts/gen-website-api.ts",
"verify-website-api": "tsx scripts/gen-website-api.ts --check",
"verify-website-yaml": "tsx scripts/verify-website-yaml.ts",
"website:dev": "pnpm --filter @deepseek-ai/website run dev",
"website:build": "pnpm --filter @deepseek-ai/website run build",
"constraints": "tsx scripts/check-workspace-constraints.ts",
"doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-cordis-api && pnpm run verify-export-jsdoc && pnpm run verify-tool-catalog && pnpm run verify-config-catalog && pnpm run verify-persistence-catalog && pnpm run verify-doc-graphs && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-mermaid && pnpm run verify-rfc-classification && pnpm run verify-rfc-format && pnpm run verify-type-equiv && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets && pnpm run verify-website-yaml",
"doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-cordis-api && pnpm run verify-export-jsdoc && pnpm run verify-tool-catalog && pnpm run verify-config-catalog && pnpm run verify-persistence-catalog && pnpm run verify-doc-graphs && pnpm run verify-website-api && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-mermaid && pnpm run verify-rfc-classification && pnpm run verify-rfc-format && pnpm run verify-type-equiv && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets && pnpm run verify-website-yaml",
"hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-node-next-types",
"demo:echo": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/echo-agent/cordis.yml",
"demo:repl": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/coding-agent/cordis.yml",
+675
View File
@@ -0,0 +1,675 @@
/**
* Generate (and verify) the website API reference under `website/zh-CN/api/`.
*
* The website's API section is FULLY GENERATED from source — never hand-edit
* it. The hand-written hub `api/index.md` sits OUTSIDE the generated subdirs
* (`api/cordis/`, `api/harness/`), so the orphan sweep never touches it. Two tiers:
*
* - `api/cordis/*` — the vendored cordis framework surface (Context, Events,
* Fiber, Registry, Service), driven by the CORDIS_PAGES manifest below.
* Members come from the real class declarations and the `declare module
* './context.ts'` interface merges (the typed `ctx.*` surface a plugin
* author actually sees).
* - `api/harness/*` — one page per `ctx.<key>` harness service (walked from
* every `declare module 'cordis'` Context merge under `packages/<group>/<pkg>/src`),
* plus `events.md` listing every harness event grouped by scope.
*
* Prose comes from the JSDoc; the generator HARD-ERRORS (aggregated) when a
* rendered member lacks a summary, a parameter lacks `@param`, or a non-void
* annotated return lacks `@returns` — so a vendor sync or a new service method
* cannot land undocumented without CI going red. Pages are English (the
* planned zh translation flow arrives separately; see docs/i18n/README.md).
*
* Signature fences use the ` ```ts website-api ` info string: doc-typecheck
* only processes its known info strings, so these bare (non-compilable)
* signature fragments are skipped there, while VitePress still highlights the
* `ts` token. The sidebar fragment `website/.vitepress/config/api-sidebar.json`
* is generated alongside so navigation can never drift from the page set.
*
* `tsx scripts/gen-website-api.ts` → write pages + sidebar
* `tsx scripts/gen-website-api.ts --check` → exit 1 if committed copies are
* stale (doc-sync / CI gate)
*/
import { globSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import ts from 'typescript'
import { checkParams, checkReturns, parseJsDoc, parseTags, pointer, rawJsDoc, reportViolations, type Mode } from './jsdoc.ts'
const root = resolve(import.meta.dirname, '..')
/** Output roots: generated pages and the generated sidebar fragment. */
const PAGES_DIR = 'website/zh-CN/api'
const SIDEBAR_OUT = 'website/.vitepress/config/api-sidebar.json'
/** GitHub blob base for source links on the public site (repo-relative paths
* do not resolve on the built site, unlike the in-repo catalogs). */
const GITHUB = 'https://github.com/deepseek-harness/deepseek-harness/blob/master'
/** Signature-fence info string (skipped by doc-typecheck, highlighted as ts). */
const FENCE = 'ts website-api'
/** One rendered member: a method/property plus its parsed JSDoc. */
interface MemberDoc {
/** Display name, e.g. `on` or `agent/pre-step`. */
name: string
/** Heading suffix with parameter names, e.g. `(name, listener, options?)`;
* empty for properties. */
heading: string
/** All overload signature lines (bodies stripped). */
signatures: string[]
/** Description prose, one paragraph per line. */
doc: string
/** Parameter name → `@param` text, in declaration order. */
params: { name: string; text: string }[]
/** `@returns` text, or null for void/undocumented. */
returns: string | null
/** Repo-relative `file:line` of the (first) declaration. */
source: string
}
/** A cordis-page section: which declarations it renders. */
type Section =
| { kind: 'class'; file: string; symbol: string; prefix?: string }
| { kind: 'context-merge'; file: string }
| { kind: 'decl'; file: string; symbol: string }
/** One generated cordis page. */
interface CordisPage {
out: string
title: string
intro: string
sections: Section[]
}
/**
* The cordis tier manifest. Deliberately explicit (not a blind walk): the
* vendor `Context` mixes true plugin-author surface with internals, and page
* grouping is an editorial choice — but every member listed here is still
* EXTRACTED, never transcribed, so signatures and docs cannot drift.
*/
const CORDIS_PAGES: CordisPage[] = [
{
out: 'cordis/context.md',
title: 'Context',
intro: 'The context is the core cordis object: every service, event, and lifecycle API is reached through `ctx`. Event methods (`ctx.on`, `ctx.emit`, …) are documented on [Events](./events.md); `ctx.effect` and `ctx.fiber` on [Fiber](./fiber.md); `ctx.plugin` and `ctx.inject` on [Registry](./registry.md).',
sections: [
{ kind: 'class', file: 'vendor/cordis/src/context.ts', symbol: 'Context', prefix: 'ctx.' },
{ kind: 'context-merge', file: 'vendor/cordis/src/reflect.ts' },
],
},
{
out: 'cordis/events.md',
title: 'Events',
intro: 'The event system mixed into every context. Harness-defined events are cataloged on [Harness events](../harness/events.md).',
sections: [
{ kind: 'context-merge', file: 'vendor/cordis/src/events.ts' },
{ kind: 'decl', file: 'vendor/cordis/src/events.ts', symbol: 'EventOptions' },
{ kind: 'decl', file: 'vendor/cordis/src/events.ts', symbol: 'DispatchMode' },
],
},
{
out: 'cordis/fiber.md',
title: 'Fiber',
intro: 'A fiber is one loaded plugin instance: its lifecycle state, validated config, and registered effects. `ctx.fiber` is the current fiber; `ctx.effect()` delegates to it.',
sections: [
{ kind: 'context-merge', file: 'vendor/cordis/src/fiber.ts' },
{ kind: 'class', file: 'vendor/cordis/src/fiber.ts', symbol: 'Fiber' },
{ kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'Effect' },
{ kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'Disposable' },
{ kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'EffectMeta' },
{ kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'CordisError' },
{ kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'ValidationError' },
],
},
{
out: 'cordis/registry.md',
title: 'Registry',
intro: 'Plugin loading and dependency injection.',
sections: [
{ kind: 'context-merge', file: 'vendor/cordis/src/registry.ts' },
{ kind: 'decl', file: 'vendor/cordis/src/registry.ts', symbol: 'Plugin' },
{ kind: 'decl', file: 'vendor/cordis/src/registry.ts', symbol: 'Inject' },
],
},
{
out: 'cordis/service.md',
title: 'Service',
intro: 'Base class for context services: subclass it and load the subclass as a plugin to register `ctx.<name>`.',
sections: [
{ kind: 'class', file: 'vendor/cordis/src/service.ts', symbol: 'Service' },
],
},
]
// ---------------------------------------------------------------------------
// Extraction
// ---------------------------------------------------------------------------
const sfCache = new Map<string, { sf: ts.SourceFile; text: string }>()
/** Parse (and cache) one repo-relative source file. */
function load(rel: string): { sf: ts.SourceFile; text: string } {
const cached = sfCache.get(rel)
if (cached) return cached
const text = readFileSync(resolve(root, rel), 'utf8')
const sf = ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true)
const entry = { sf, text }
sfCache.set(rel, entry)
return entry
}
/** The body of a `declare module './context.ts'` / `declare module 'cordis'`
* block, or null. */
function moduleBody(sf: ts.SourceFile): ts.ModuleBlock | null {
for (const stmt of sf.statements) {
if (!ts.isModuleDeclaration(stmt) || !ts.isStringLiteral(stmt.name)) continue
if (stmt.name.text !== './context.ts' && stmt.name.text !== 'cordis') continue
if (stmt.body && ts.isModuleBlock(stmt.body)) return stmt.body
}
return null
}
/** Signature text of a member: full text minus body/initializer, whitespace
* collapsed, trailing semicolon stripped. */
function signatureOf(member: ts.Node, sf: ts.SourceFile): string {
const full = member.getText(sf)
const tail = (member as { body?: ts.Node; initializer?: ts.Node }).body
?? (member as { initializer?: ts.Node }).initializer
const sig = tail ? full.slice(0, full.length - tail.getText(sf).length).replace(/[=\s]+$/, '') : full
return sig.replace(/\s*;?\s*$/, '').replace(/\s+/g, ' ').trim()
}
/** `(a, b?, ...rest)` heading suffix from a parameter list, `this` dropped. */
function headingParams(parameters: readonly ts.ParameterDeclaration[], sf: ts.SourceFile): string {
const names = parameters
.filter(p => !(ts.isIdentifier(p.name) && p.name.text === 'this'))
.map((p) => {
const dots = p.dotDotDotToken ? '...' : ''
const opt = p.questionToken || p.initializer ? '?' : ''
return `${dots}${p.name.getText(sf)}${opt}`
})
return `(${names.join(', ')})`
}
/** Whether a class member is renderable public API (non-static half). */
function isPublicInstance(member: ts.ClassElement): boolean {
const mods = ts.getCombinedModifierFlags(member)
if (mods & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected | ts.ModifierFlags.Static)) return false
if (!member.name) return false
if (ts.isComputedPropertyName(member.name) || ts.isPrivateIdentifier(member.name)) return false
return !member.name.getText().startsWith('_')
}
/** Whether a class member is renderable public STATIC API. */
function isPublicStatic(member: ts.ClassElement): boolean {
const mods = ts.getCombinedModifierFlags(member)
if (mods & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected)) return false
if (!(mods & ts.ModifierFlags.Static)) return false
if (!member.name || ts.isComputedPropertyName(member.name) || ts.isPrivateIdentifier(member.name)) return false
return !member.name.getText().startsWith('_')
}
/** Build a MemberDoc from a declaration group (overloads share one entry),
* collecting completeness violations for everything rendered. */
function memberDoc(
where: string,
name: string,
group: (ts.MethodDeclaration | ts.MethodSignature | ts.PropertyDeclaration | ts.PropertySignature | ts.GetAccessorDeclaration)[],
rel: string,
violations: string[],
): MemberDoc {
const { sf, text } = load(rel)
const first = group[0]
if (!first) throw new Error(`gen-website-api: empty member group for ${name}`)
// Doc from the first overload that carries JSDoc prose.
const rawDocs = group.map(m => rawJsDoc(text, m))
const docIndex = rawDocs.findIndex(r => parseJsDoc(r).doc !== '')
const raw = docIndex === -1 ? '' : (rawDocs[docIndex] ?? '')
const doc = parseJsDoc(raw).doc
if (!doc) violations.push(`${where} has no JSDoc prose.`)
const { params: tags, returns } = parseTags(raw)
const params: { name: string; text: string }[] = []
let returnsText: string | null = null
const funcLike = group.filter((m): m is ts.MethodDeclaration | ts.MethodSignature => ts.isMethodDeclaration(m) || ts.isMethodSignature(m))
const docCarrier = funcLike[docIndex === -1 ? 0 : docIndex]
if (docCarrier) {
checkParams(where, 'website-api', docCarrier.parameters, tags, sf,
p => ts.isIdentifier(p.name) && p.name.text === 'this', violations)
if (docCarrier.type) {
checkReturns(where, docCarrier.type, returns, sf, violations)
} else if (!returns && ts.isMethodDeclaration(docCarrier)) {
// Comment-only vendor policy: we cannot add a return type annotation to
// pinned upstream source, so an unannotated rendered method must carry
// an explicit @returns describing the result instead.
violations.push(`${where} has no return type annotation; document the result with @returns.`)
}
for (const p of docCarrier.parameters) {
if (ts.isIdentifier(p.name) && p.name.text === 'this') continue
const pname = p.name.getText(sf)
const tag = tags.get(pname)
if (tag) params.push({ name: pname, text: tag })
}
returnsText = returns
}
const headingSource = docCarrier ?? funcLike[0]
return {
name,
heading: headingSource ? headingParams(headingSource.parameters, sf) : '',
signatures: (ts.isMethodDeclaration(first) && funcLike.length > 1
? funcLike.filter(m => ts.isMethodDeclaration(m) && !m.body)
: group).map(m => signatureOf(m, sf)),
doc,
params,
returns: returnsText,
source: pointer(rel, sf, first),
}
}
/** Members of the `interface Context` merge in `rel`, overloads grouped. */
function contextMergeMembers(rel: string, violations: string[]): MemberDoc[] {
const { sf } = load(rel)
const body = moduleBody(sf)
if (!body) throw new Error(`gen-website-api: ${rel} has no context module merge`)
const groups = new Map<string, (ts.MethodSignature | ts.PropertySignature)[]>()
for (const stmt of body.statements) {
if (!ts.isInterfaceDeclaration(stmt) || stmt.name.text !== 'Context') continue
for (const member of stmt.members) {
if (!ts.isMethodSignature(member) && !ts.isPropertySignature(member)) continue
if (ts.isComputedPropertyName(member.name)) continue
const name = member.name.getText(sf)
const group = groups.get(name) ?? []
group.push(member)
groups.set(name, group)
}
}
return [...groups.entries()].map(([name, group]) =>
memberDoc(`ctx.${name} (${rel})`, name, group, rel, violations))
}
/** Instance + static members of one class, as two rendered lists. */
function classMembers(rel: string, className: string, violations: string[]): {
doc: string
instance: MemberDoc[]
statics: MemberDoc[]
source: string
} {
const { sf, text } = load(rel)
const cls = sf.statements.find(
(s): s is ts.ClassDeclaration => ts.isClassDeclaration(s) && s.name?.text === className,
)
if (!cls) throw new Error(`gen-website-api: class ${className} not found in ${rel}`)
const clsDoc = parseJsDoc(rawJsDoc(text, cls)).doc
if (!clsDoc) violations.push(`class ${className} (${pointer(rel, sf, cls)}) has no JSDoc.`)
const instance = new Map<string, (ts.MethodDeclaration | ts.PropertyDeclaration | ts.GetAccessorDeclaration)[]>()
const statics = new Map<string, (ts.MethodDeclaration | ts.PropertyDeclaration)[]>()
for (const member of cls.members) {
const renderable = ts.isMethodDeclaration(member) || ts.isPropertyDeclaration(member) || ts.isGetAccessorDeclaration(member)
if (!renderable) continue
const name = member.name.getText(sf)
if (isPublicInstance(member)) {
const group = instance.get(name) ?? []
group.push(member)
instance.set(name, group)
} else if (isPublicStatic(member) && !ts.isGetAccessorDeclaration(member)) {
const group = statics.get(name) ?? []
group.push(member)
statics.set(name, group)
}
}
type Renderable = ts.MethodDeclaration | ts.PropertyDeclaration | ts.GetAccessorDeclaration
const toDocs = (groups: Map<string, Renderable[]>, prefix: string): MemberDoc[] =>
[...groups.entries()].map(([name, group]) =>
memberDoc(`${prefix}${name} (${rel})`, name, group, rel, violations))
return {
doc: clsDoc,
instance: toDocs(instance, `${className}#`),
statics: toDocs(statics, `${className}.`),
source: pointer(rel, sf, cls),
}
}
/** Splice every function-like BODY out of a declaration's text, leaving the
* signature (`) {` → `)`). A reference paste shows shapes, not implementation;
* property initializers (e.g. an `as const` code table) are data and stay. */
function stripBodies(node: ts.Node, sf: ts.SourceFile): string {
const cuts: { start: number; end: number }[] = []
const visit = (n: ts.Node): void => {
const funcLike = ts.isMethodDeclaration(n) || ts.isConstructorDeclaration(n)
|| ts.isFunctionDeclaration(n) || ts.isGetAccessorDeclaration(n) || ts.isSetAccessorDeclaration(n)
if (funcLike && n.body) {
// Cut from just after the parameter close (or return-type end) through
// the body, so `foo(a: string) { … }` renders as `foo(a: string)`.
const sigEnd = (n.type ?? n.parameters[n.parameters.length - 1] ?? n).getEnd()
// Find the `)` (and optional `: Type`) boundary: body start is exact.
cuts.push({ start: sigEnd, end: n.body.getEnd() })
return // nothing renderable inside the body
}
n.forEachChild(visit)
}
visit(node)
const base = node.getStart(sf)
let out = node.getText(sf)
for (const cut of cuts.sort((a, b) => b.start - a.start)) {
const head = out.slice(0, cut.start - base)
// Keep everything of the signature up to the closing paren / return type,
// drop ` { … }`. The head may end mid-signature (last param), so retain
// the source between sigEnd and the body's `{` MINUS trailing space.
const between = out.slice(cut.start - base, cut.end - base)
const bodyBrace = between.indexOf('{')
out = head + between.slice(0, bodyBrace).trimEnd() + out.slice(cut.end - base)
}
return out
}
/** Verbatim declaration paste: every top-level statement named `symbol`
* (class + merged namespace both), with leading JSDoc prose extracted and
* function bodies stripped (a reference shows shapes, not implementation). */
function declPaste(rel: string, symbol: string): { doc: string; code: string; source: string } {
const { sf, text } = load(rel)
const matches = sf.statements.filter((s) => {
const named = ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s)
|| ts.isClassDeclaration(s) || ts.isEnumDeclaration(s) || ts.isModuleDeclaration(s)
return named && s.name?.getText(sf) === symbol
})
if (matches.length === 0) throw new Error(`gen-website-api: declaration ${symbol} not found in ${rel}`)
const first = matches[0]
if (!first) throw new Error(`gen-website-api: declaration ${symbol} not found in ${rel}`)
const doc = parseJsDoc(rawJsDoc(text, first)).doc
const code = matches.map(s => stripBodies(s, sf).replace(/^export\s+(default\s+)?/, '')).join('\n\n')
return { doc, code, source: pointer(rel, sf, first) }
}
/** One harness service with member-level detail. */
interface HarnessService {
key: string
type: string
abstract: boolean
doc: string
members: MemberDoc[]
source: string
/** Owning npm package name (from the package.json beside the entry). */
pkg: string
}
/** Walk every harness `declare module 'cordis'` Context merge → services. */
function collectHarnessServices(violations: string[]): HarnessService[] {
const services: HarnessService[] = []
for (const rel of globSync('packages/*/*/src/index.ts', { cwd: root }).sort()) {
const { sf, text } = load(rel)
if (!text.includes('interface Context')) continue
const body = moduleBody(sf)
if (!body) continue
const keyToType = new Map<string, string>()
for (const stmt of body.statements) {
if (!ts.isInterfaceDeclaration(stmt) || stmt.name.text !== 'Context') continue
for (const member of stmt.members) {
if (!ts.isPropertySignature(member) || !member.type) continue
keyToType.set(member.name.getText(sf), member.type.getText(sf))
}
}
const pkgJson = rel.replace(/src\/index\.ts$/, 'package.json')
const pkg = (JSON.parse(readFileSync(resolve(root, pkgJson), 'utf8')) as { name: string }).name
for (const [key, type] of keyToType) {
const cls = sf.statements.find(
(s): s is ts.ClassDeclaration => ts.isClassDeclaration(s) && s.name?.text === type,
)
if (!cls) continue // a Pick-mixin member, not a class here
const abstract = cls.modifiers?.some(m => m.kind === ts.SyntaxKind.AbstractKeyword) ?? false
const clsDoc = parseJsDoc(rawJsDoc(text, cls)).doc
if (!clsDoc) violations.push(`service ctx.${key} (${pointer(rel, sf, cls)}): class ${type} has no JSDoc.`)
const groups = new Map<string, ts.MethodDeclaration[]>()
for (const member of cls.members) {
if (!ts.isMethodDeclaration(member)) continue
if (!isPublicInstance(member)) continue
const name = member.name.getText(sf)
const group = groups.get(name) ?? []
group.push(member)
groups.set(name, group)
}
const members = [...groups.entries()].map(([name, group]) =>
memberDoc(`ctx.${key}.${name} (${rel})`, name, group, rel, violations))
services.push({ key, type, abstract, doc: clsDoc, members, source: pointer(rel, sf, cls), pkg })
}
}
return services.sort((a, b) => a.key.localeCompare(b.key))
}
/** One harness event with member-level detail. */
interface HarnessEvent {
name: string
scope: string
mode: Mode | null
signature: string
doc: string
params: { name: string; text: string }[]
source: string
}
/** Walk every harness `interface Events` merge → events. */
function collectHarnessEvents(violations: string[]): HarnessEvent[] {
const events: HarnessEvent[] = []
for (const rel of globSync('packages/*/*/src/*.ts', { cwd: root }).sort()) {
const { sf, text } = load(rel)
if (!text.includes('interface Events')) continue
const body = moduleBody(sf)
if (!body) continue
for (const stmt of body.statements) {
if (!ts.isInterfaceDeclaration(stmt) || stmt.name.text !== 'Events') continue
for (const member of stmt.members) {
if (!ts.isMethodSignature(member)) continue
const name = ts.isStringLiteral(member.name) ? member.name.text : member.name.getText(sf)
const raw = rawJsDoc(text, member)
const { doc, mode } = parseJsDoc(raw)
if (!mode) violations.push(`event '${name}' (${pointer(rel, sf, member)}) is missing @mode.`)
if (!doc) violations.push(`event '${name}' (${pointer(rel, sf, member)}) has no JSDoc prose.`)
const { params: tags } = parseTags(raw)
const last = member.parameters.at(-1)
const hasNext = !!last && last.name.getText(sf) === 'next'
checkParams(`event '${name}' (${pointer(rel, sf, member)})`, 'website-api', member.parameters, tags, sf,
p => (ts.isIdentifier(p.name) && p.name.text === 'this') || (hasNext && p === last), violations)
const params: { name: string; text: string }[] = []
for (const p of member.parameters) {
const pname = p.name.getText(sf)
const tag = tags.get(pname)
if (tag) params.push({ name: pname, text: tag })
}
events.push({ name, scope: name.split('/')[0] ?? name, mode, signature: signatureOf(member, sf), doc, params, source: pointer(rel, sf, member) })
}
}
}
return events.sort((a, b) => a.name.localeCompare(b.name))
}
// ---------------------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------------------
const BANNER = '<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->'
/** GitHub source link for a `file:line` pointer. */
function sourceLink(source: string): string {
const [file, line] = source.split(':')
return `[Source](${GITHUB}/${file}#L${line})`
}
/** Render prose paragraphs (one per line of `doc`). */
function prose(doc: string): string[] {
return doc.split('\n').filter(l => l.trim() !== '')
}
/** Render one member section at heading depth 3. */
function renderMember(prefix: string, m: MemberDoc): string[] {
const lines: string[] = []
const call = m.heading === '' ? '' : m.heading
lines.push(`### ${prefix}${m.name}${call}`, '')
lines.push('```' + FENCE)
for (const sig of m.signatures) lines.push(sig)
lines.push('```', '')
lines.push(...prose(m.doc), '')
if (m.params.length > 0) {
for (const p of m.params) lines.push(`- \`${p.name}\`${p.text}`)
lines.push('')
}
if (m.returns) lines.push(`**Returns** ${m.returns}`, '')
lines.push(sourceLink(m.source), '')
return lines
}
/** Render one cordis-tier page from its manifest entry. */
function renderCordisPage(page: CordisPage, violations: string[]): string {
const lines: string[] = [BANNER, '', `# ${page.title}`, '', page.intro, '']
for (const section of page.sections) {
if (section.kind === 'context-merge') {
for (const m of contextMergeMembers(section.file, violations)) {
lines.push(...renderMember('ctx.', m))
}
} else if (section.kind === 'class') {
const cls = classMembers(section.file, section.symbol, violations)
lines.push(...prose(cls.doc), '', sourceLink(cls.source), '')
const instancePrefix = section.prefix ?? `${section.symbol.toLowerCase()}.`
for (const m of cls.instance) lines.push(...renderMember(instancePrefix, m))
if (cls.statics.length > 0) {
lines.push('## Static members', '')
for (const m of cls.statics) lines.push(...renderMember(`${section.symbol}.`, m))
}
} else {
const decl = declPaste(section.file, section.symbol)
lines.push(`## ${section.symbol}`, '')
if (decl.doc) lines.push(...prose(decl.doc), '')
lines.push('```' + FENCE, decl.code, '```', '', sourceLink(decl.source), '')
}
}
return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`
}
/** kebab-case a ctx key: `agentLoop` → `agent-loop`. */
function kebab(key: string): string {
return key.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`)
}
/** Render one harness service page. */
function renderServicePage(svc: HarnessService): string {
const seam = svc.abstract ? ' (abstract seam)' : ''
const lines: string[] = [
BANNER, '',
`# ctx.${svc.key}`, '',
`\`${svc.type}\`${seam} — provided by \`${svc.pkg}\`.`, '',
...prose(svc.doc), '',
sourceLink(svc.source), '',
]
for (const m of svc.members) lines.push(...renderMember(`ctx.${svc.key}.`, m))
return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`
}
/** Render the harness events page, grouped by scope. */
function renderEventsPage(events: HarnessEvent[]): string {
const lines: string[] = [
BANNER, '',
'# Harness events', '',
`Every event the harness packages declare on the cordis event bus (${events.length} 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).`, '',
]
const scopes = [...new Set(events.map(e => e.scope))].sort()
for (const scope of scopes) {
lines.push(`## ${scope}/*`, '')
for (const e of events.filter(ev => ev.scope === scope)) {
lines.push(`### ${e.name}`, '')
lines.push(`**Mode:** \`${e.mode ?? 'unknown'}\``, '')
lines.push('```' + FENCE, e.signature, '```', '')
lines.push(...prose(e.doc), '')
if (e.params.length > 0) {
for (const p of e.params) lines.push(`- \`${p.name}\`${p.text}`)
lines.push('')
}
lines.push(sourceLink(e.source), '')
}
}
return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`
}
// ---------------------------------------------------------------------------
// Assembly + CLI
// ---------------------------------------------------------------------------
/** Build every generated file as `relPath → content`. */
export function generate(): Map<string, string> {
const violations: string[] = []
const files = new Map<string, string>()
for (const page of CORDIS_PAGES) {
files.set(`${PAGES_DIR}/${page.out}`, renderCordisPage(page, violations))
}
const services = collectHarnessServices(violations)
for (const svc of services) {
files.set(`${PAGES_DIR}/harness/${kebab(svc.key)}.md`, renderServicePage(svc))
}
const events = collectHarnessEvents(violations)
files.set(`${PAGES_DIR}/harness/events.md`, renderEventsPage(events))
reportViolations('gen-website-api', violations)
const sidebar = {
cordis: CORDIS_PAGES.map(p => ({
text: p.title,
link: `/zh-CN/api/${p.out.replace(/\.md$/, '')}`,
})),
harness: [
...services.map(s => ({ text: `ctx.${s.key}`, link: `/zh-CN/api/harness/${kebab(s.key)}` })),
{ text: 'Events', link: '/zh-CN/api/harness/events' },
],
}
files.set(SIDEBAR_OUT, `${JSON.stringify(sidebar, null, 2)}\n`)
return files
}
/** CLI entry: default writes, `--check` fails on stale/orphan files. Guarded
* behind an entry-point check so tests can import `generate()`. */
function main(): void {
const check = process.argv.includes('--check')
const files = generate()
// Orphan detection: a generated-dir page that generate() no longer emits
// (e.g. a service was renamed) must be deleted, not left to rot.
const expected = new Set([...files.keys()])
// Orphans live in the generated subdirs only; the hand-written api/index.md
// is one level up and never matches this glob.
const onDisk = globSync(`${PAGES_DIR}/{cordis,harness}/*.md`, { cwd: root }).sort()
const orphans = onDisk.filter(rel => !expected.has(rel))
if (check) {
const stale: string[] = []
for (const [rel, content] of files) {
let current: string | null = null
try {
current = readFileSync(resolve(root, rel), 'utf8')
} catch {
// Missing file: reported as stale below; readFileSync is the probe.
}
if (current !== content) stale.push(rel)
}
if (stale.length > 0 || orphans.length > 0) {
console.error('gen-website-api: website API reference is stale. Run `pnpm run gen-website-api` and commit the result.')
for (const rel of stale) console.error(` stale: ${rel}`)
for (const rel of orphans) console.error(` orphan (delete): ${rel}`)
process.exit(1)
}
console.log(`gen-website-api: ${files.size} generated file(s) fresh.`)
return
}
for (const [rel, content] of files) {
const abs = resolve(root, rel)
mkdirSync(dirname(abs), { recursive: true })
writeFileSync(abs, content)
}
for (const rel of orphans) {
console.log(`gen-website-api: orphan page ${rel} — delete it (no longer generated).`)
}
console.log(`gen-website-api: wrote ${files.size} file(s).`)
}
// Run only when invoked as a script, not when imported by a test.
if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) {
main()
}
+1
View File
@@ -264,6 +264,7 @@ function docSyncLeafGates(): Gate[] {
pnpmScript('config-catalog', 'verify-config-catalog', { label: 'config catalog' }),
pnpmScript('persistence-catalog', 'verify-persistence-catalog', { label: 'persistence catalog' }),
pnpmScript('doc-graphs', 'verify-doc-graphs', { label: 'doc graphs' }),
pnpmScript('website-api', 'verify-website-api', { label: 'website api' }),
pnpmScript('markdown-wrap', 'verify-md-wrap', { label: 'markdown wrap' }),
pnpmScript('markdown-links', 'verify-md-links', { label: 'markdown links' }),
pnpmScript('doc-refs', 'verify-doc-refs', { label: 'doc refs' }),
@@ -0,0 +1,90 @@
{
"cordis": [
{
"text": "Context",
"link": "/zh-CN/api/cordis/context"
},
{
"text": "Events",
"link": "/zh-CN/api/cordis/events"
},
{
"text": "Fiber",
"link": "/zh-CN/api/cordis/fiber"
},
{
"text": "Registry",
"link": "/zh-CN/api/cordis/registry"
},
{
"text": "Service",
"link": "/zh-CN/api/cordis/service"
}
],
"harness": [
{
"text": "ctx.agentLoop",
"link": "/zh-CN/api/harness/agent-loop"
},
{
"text": "ctx.agents",
"link": "/zh-CN/api/harness/agents"
},
{
"text": "ctx.bash",
"link": "/zh-CN/api/harness/bash"
},
{
"text": "ctx.codeRuntime",
"link": "/zh-CN/api/harness/code-runtime"
},
{
"text": "ctx.compact",
"link": "/zh-CN/api/harness/compact"
},
{
"text": "ctx.fs",
"link": "/zh-CN/api/harness/fs"
},
{
"text": "ctx.llm",
"link": "/zh-CN/api/harness/llm"
},
{
"text": "ctx.sessionPersistence",
"link": "/zh-CN/api/harness/session-persistence"
},
{
"text": "ctx.sessions",
"link": "/zh-CN/api/harness/sessions"
},
{
"text": "ctx.subagents",
"link": "/zh-CN/api/harness/subagents"
},
{
"text": "ctx.systemPrompt",
"link": "/zh-CN/api/harness/system-prompt"
},
{
"text": "ctx.tools",
"link": "/zh-CN/api/harness/tools"
},
{
"text": "ctx.userInteraction",
"link": "/zh-CN/api/harness/user-interaction"
},
{
"text": "ctx.web",
"link": "/zh-CN/api/harness/web"
},
{
"text": "ctx.workflows",
"link": "/zh-CN/api/harness/workflows"
},
{
"text": "Events",
"link": "/zh-CN/api/harness/events"
}
]
}
+6 -14
View File
@@ -1,4 +1,5 @@
import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress'
import apiSidebarData from './api-sidebar.json'
const guideSidebar: DefaultTheme.SidebarItem[] = [
{
@@ -37,29 +38,20 @@ const developSidebar: DefaultTheme.SidebarItem[] = [
},
]
// The API section sidebar is GENERATED (scripts/gen-website-api.ts writes
// api-sidebar.json alongside the pages), so navigation can never drift from
// the generated page set. Only the hand-written hub link lives here.
const apiSidebar: DefaultTheme.SidebarItem[] = [
{
text: '框架 API',
items: [
{ text: '总览', link: '/zh-CN/api/' },
{ text: 'Context', link: '/zh-CN/api/cordis/context' },
{ text: 'Events', link: '/zh-CN/api/cordis/events' },
{ text: 'Fiber', link: '/zh-CN/api/cordis/fiber' },
{ text: 'Registry', link: '/zh-CN/api/cordis/registry' },
{ text: 'Service', link: '/zh-CN/api/cordis/service' },
...apiSidebarData.cordis,
],
},
{
text: 'Harness API',
items: [
{ text: 'Tools (dsh-tools)', link: '/zh-CN/api/harness/tools' },
{ text: 'LLM (dsh-llm)', link: '/zh-CN/api/harness/llm' },
{ text: 'Session (dsh-session)', link: '/zh-CN/api/harness/session' },
{ text: 'Agent (dsh-agent)', link: '/zh-CN/api/harness/agent' },
{ text: 'Bash (dsh-bash)', link: '/zh-CN/api/harness/bash' },
{ text: 'Filesystem (dsh-fs)', link: '/zh-CN/api/harness/fs' },
{ text: 'Subagent (dsh-subagent)', link: '/zh-CN/api/harness/subagent' },
],
items: apiSidebarData.harness,
},
]
+163 -56
View File
@@ -1,85 +1,192 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# Context
上下文对象是 Cordis 的核心。所有服务、方法、属性都通过 `ctx` 访问。
The context is the core cordis object: every service, event, and lifecycle API is reached through `ctx`. Event methods (`ctx.on`, `ctx.emit`, …) are documented on [Events](./events.md); `ctx.effect` and `ctx.fiber` on [Fiber](./fiber.md); `ctx.plugin` and `ctx.inject` on [Registry](./registry.md).
## 服务与混入
Root and child dependency containers for Cordis plugins.
A context is a proxy: normal property reads go through the service resolver, while `extend()`, `isolate()`, and `intercept()` create scoped child contexts without mutating their parent.
Context 基于组合式 API 设计,大部分属性和方法挂载在服务上。以下是核心 API:
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L42)
- [`ctx.on`](./events#ctx-on) — 注册事件监听器
- [`ctx.emit`](./events#ctx-emit) — 触发事件
- [`ctx.bail`](./events#ctx-bail) — 短路事件
- [`ctx.serial`](./events#ctx-serial) — 顺序异步事件
- [`ctx.waterfall`](./events#ctx-waterfall) — 管道事件
- [`ctx.effect`](./fiber#fiber-effect) — 注册可逆效果
- [`ctx.plugin`](./registry#ctx-plugin) — 加载子插件
- [`ctx.inject`](./registry#ctx-inject) — 获取依赖的插件
- [`ctx.get`](#ctx-get) — 获取服务
- [`ctx.set`](#ctx-set) — 设置服务
- [`ctx.provide`](#ctx-provide) — 声明服务
### ctx.extend(meta?)
## 实例属性
```ts website-api
extend(meta = {}): this
```
### ctx.fiber
Create a child context with extra metadata on top of the current scope.
The child prototypally inherits every property of this context; own properties of `meta` shadow the inherited ones. The parent is not mutated.
- **类型:** [`Fiber`](./fiber)
- `meta` — own properties (including symbol keys) to define on the child.
当前上下文的作用域对象。
**Returns** a child context inheriting from this one.
## 实例方法
### ctx.extend(meta)
- **meta:** `object`
- **返回值:** `Context`
构造一个以当前上下文为原型的新上下文实例。
### ctx.intercept(name, config)
- **name:** `string` 服务名称
- **config:** `object` 配置拦截
- **返回值:** `Context`
为指定服务添加一层配置拦截,返回新的上下文实例。
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L99)
### ctx.isolate(name, label?)
- **name:** `string` 服务名称
- **label:** `symbol` 隔离域符号(可选)
- **返回值:** `Context`
```ts website-api
isolate(name: string, label?: symbol)
```
创建一个针对指定服务的隔离域,返回新的上下文实例。隔离域中的同名服务互不影响。
Create a child context with an independent service scope for `name`.
Below the returned context, reads and writes of the service `name` resolve against the new label instead of the parent's, so a different implementation can be provided without affecting the parent scope. Passing the same `label` to two `isolate()` calls joins their scopes.
### ctx.get(name)
- `name` — the service name to isolate.
- `label` — scope label to join; defaults to a fresh unique symbol.
- **name:** `string` 服务名称
- **返回值:** `Service | undefined`
**Returns** a child context whose `name` service resolves in the new scope.
获取指定名称的服务实例。
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L121)
### ctx.intercept(name, config)
```ts website-api
intercept<K extends InjectKey>(name: K, config: Context[K] extends { [symbols.config]: infer T } ? T : never): this
intercept(name: string, config: any): this
```
Add service-specific intercept config for plugins started below this context.
Plugins loaded under the returned context see `config` merged into the service's resolved config (ancestor entries first; see `Service[symbols.resolveConfig]`). The parent context is not affected.
- `name` — the service name whose config to intercept.
- `config` — the intercept config to merge for that service.
**Returns** a child context carrying the additional intercept entry.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L139)
## Static members
### Context.effect
```ts website-api
static readonly effect: unique symbol
```
Symbol key under which a disposer exposes its EffectMeta diagnostics tree.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L44)
### Context.filter
```ts website-api
static readonly filter: unique symbol
```
Symbol key for a context's listener filter, consulted on every event dispatch.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L46)
### Context.isolate
```ts website-api
static readonly isolate: unique symbol
```
Symbol key of the isolation map (see the `Context[symbols.isolate]` property).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L48)
### Context.intercept
```ts website-api
static readonly intercept: unique symbol
```
Symbol key of the intercept map (see the `Context[symbols.intercept]` property).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L50)
### Context.is(value)
```ts website-api
static is(value: any): value is Context
```
Returns true for Cordis context proxies and context prototypes.
Works across realms and across multiple copies of cordis, because the brand is keyed by a global symbol rather than by `instanceof`.
- `value` — the value to test.
**Returns** `true` if `value` is a Cordis context, narrowing its type.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L61)
### ctx.get(name, strict?)
```ts website-api
get<K extends string & keyof this>(name: K, strict?: boolean): undefined | this[K]
get(name: string, strict?: boolean): any
```
Read a service from the store without the inject requirement.
- `name` — the service name.
- `strict` — when `true` (default), only return implementations whose providing fiber is currently active.
**Returns** the service value, or `undefined` when not (yet) provided.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/reflect.ts#L16)
### ctx.set(name, value)
- **name:** `string` 服务名称
- **value:** `any` 服务值
```ts website-api
set<K extends string & keyof this>(name: K, value: undefined | this[K]): void
set(name: string, value: any): void
```
设置指定名称的服务。
Overwrite a provided service's value.
Only the fiber that provided the service may set it; setting an unprovided name throws.
### ctx.provide(name, value?, options?)
- `name` — the service name.
- `value` — the new service value.
- **name:** `string` 服务名称
- **value:** `any` 初始值(可选)
- **options:** `object`
- **返回值:** `void`
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/reflect.ts#L28)
声明一个服务。声明后其他插件可以通过 `inject` 依赖它。
### ctx.provide(name, value)
## 静态属性
```ts website-api
provide<K extends string & keyof this>(name: K, value: undefined | this[K]): () => void
provide(name: string, value?: any): () => void
```
### Context.events
Register a service implementation owned by the current fiber.
The service becomes visible to dependents in the same isolation scope once the fiber is active; it is unregistered (waking dependents) when the returned disposer runs or the fiber unloads. Throws if the name is already provided in this scope or declared as an accessor.
内置事件服务的 symbol key。
- `name` — the service name.
- `value` — the service value.
### Context.current
**Returns** a disposer that unregisters the service.
当前活跃的 Context 实例(在异步链中通过 AsyncLocalStorage 追踪)。
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/reflect.ts#L43)
### ctx.accessor(name, options)
```ts website-api
accessor(name: string, options: Omit<Property.Accessor, 'type'>): void
```
Define a computed context property backed by get/set hooks.
The accessor is removed when the current fiber unloads. Throws if the name is already declared.
- `name` — the context property name.
- `options` — the `get` hook and optional `set` hook.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/reflect.ts#L55)
### ctx.mixin(name, mixins)
```ts website-api
mixin<K extends string & keyof this>(name: K, mixins: (keyof this & keyof this[K])[] | Dict<string>): void
mixin<T extends {}>(source: T, mixins: (keyof this & keyof T)[] | Dict<string>): void
```
Expose selected members of a service directly on `ctx`.
Each mixed-in key becomes an accessor that forwards to the service (binding methods to it), so e.g. `ctx.on` forwards to `ctx.events.on`. Mixins are removed when the current fiber unloads.
- `name` — the context property holding the source service.
- `mixins` — keys to forward, or a source-key → ctx-key map.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/reflect.ts#L66)
+107 -85
View File
@@ -1,120 +1,142 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# Events
`ctx.events` 是内置服务,提供事件系统相关的全部 API。
The event system mixed into every context. Harness-defined events are cataloged on [Harness events](../harness/events.md).
## 实例方法
### ctx.parallel(name, ...args)
### ctx.on(event, listener, options?) {#ctx-on}
- **event:** `string` 事件名称
- **listener:** `Function` 事件监听器
- **options:** `object`
- **prepend:** `boolean` 是否注册为前置(默认 `false`
- **global:** `boolean` 是否注册为全局(默认 `false`
- **返回值:** `() => void` 取消注册函数
注册一个事件监听器。返回的函数可用于手动取消注册,但通常不需要——插件卸载时会自动清理。
```typescript
ctx.on('agent/turn-end', (data) => {
console.log('turn ended:', data)
})
```ts website-api
parallel<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promise<void>
parallel<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promise<void>
```
### ctx.emit(thisArg?, event, ...args) {#ctx-emit}
Dispatch an event, running all listeners concurrently.
- **thisArg:** `any` 监听器的 `this` 参数(可选)
- **event:** `string` 事件名称
- **args:** `any[]` 事件参数
- **返回值:** `void`
- `name` — the event name.
- `args` — arguments passed to every listener.
同步触发所有匹配的监听器(并行,不等待异步完成)。
**Returns** a promise resolving once every listener has settled.
### ctx.parallel(thisArg?, event, ...args)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L43)
- 签名同 `emit`
- **返回值:** `Promise<void>`
### ctx.emit(name, ...args)
异步触发所有匹配的监听器(并行等待)。
### ctx.bail(thisArg?, event, ...args) {#ctx-bail}
- **返回值:** `any`
同步依次触发监听器。第一个返回非 `undefined`/`null`/`false` 值的监听器停止链并返回该值。
### ctx.serial(thisArg?, event, ...args) {#ctx-serial}
- **返回值:** `Promise<any>`
异步依次触发监听器。语义同 `bail` 的异步版本。
### ctx.waterfall(thisArg?, event, ...args) {#ctx-waterfall}
- **返回值:** `Promise<any>`
管道模式:每个监听器接收前一个的输出。监听器内部必须调用 `next()` 才会传递给下一个。
```typescript
// 注册
ctx.on('llm/pre-request', async (messages, next) => {
messages.push(extraMsg)
return next(messages) // 必须调用
})
// 触发
const result = await ctx.waterfall('llm/pre-request', initialMessages)
```ts website-api
emit<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void
emit<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): void
```
::: warning
不调用 `next()` 即为否决 (veto)——管道终止。这是设计行为,用于拦截/网关。
:::
Dispatch an event synchronously, ignoring listener return values.
## Harness 内置事件
- `name` — the event name.
- `args` — arguments passed to every listener.
### agent/pre-step
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L52)
- **触发模式:** serial
- **参数:** `{ agentId, turnIndex }`
### ctx.serial(name, ...args)
Agent 执行一步之前触发。
```ts website-api
serial<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
serial<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
```
### agent/post-step
Dispatch an event, awaiting listeners in order until one bails.
- **触发模式:** emit
- **参数:** `{ agentId, turnIndex, blocks }`
- `name` — the event name.
- `args` — arguments passed to each listener.
Agent 执行一步之后触发。
**Returns** the first bail value (non-null, non-false, non-undefined), if any.
### tool/call
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L62)
- **触发模式:** emit
- **参数:** `{ name, args, callId }`
### ctx.bail(name, ...args)
Tool 被模型调用时触发。
```ts website-api
bail<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
bail<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
```
### tool/result
Dispatch an event, calling listeners in order until one bails.
- **触发模式:** emit
- **参数:** `{ name, result, callId }`
- `name` — the event name.
- `args` — arguments passed to each listener.
Tool 返回结果时触发。
**Returns** the first bail value (non-null, non-false, non-undefined), if any.
### session/event
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L72)
- **触发模式:** emit
- **参数:** `SessionEvent`
### ctx.waterfall(name, ...args)
会话事件被记录时触发。
```ts website-api
waterfall<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
waterfall<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
```
### compact/start
Dispatch an event whose last argument is a `next` continuation.
Each listener wraps the rest of the chain: calling `next()` invokes the next listener (finally the built-in behavior); not calling it vetoes.
- **触发模式:** emit
- `name` — the event name.
- `args` — listener arguments; the final one is the innermost `next`.
上下文压缩开始。
**Returns** the outermost listener's return value.
### compact/end
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L85)
- **触发模式:** emit
### ctx.on(name, listener, options?)
上下文压缩结束。
```ts website-api
on<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
```
Register an event listener owned by the current fiber.
- `name` — the event name to listen for.
- `listener` — called with the dispatch arguments.
- `options` — listener options; a boolean is shorthand for `prepend`.
**Returns** a disposer removing the listener; `true` if it was still registered.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L96)
### ctx.once(name, listener, options?)
```ts website-api
once<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
```
Same as `on()`, but the listener disposes itself after its first call.
- `name` — the event name to listen for.
- `listener` — called at most once with the dispatch arguments.
- `options` — listener options; a boolean is shorthand for `prepend`.
**Returns** a disposer removing the listener; `true` if it was still registered.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L105)
## EventOptions
Options accepted by `ctx.on()` and `ctx.once()`.
```ts website-api
interface EventOptions {
/** Add the listener before existing listeners for the same event. */
prepend?: boolean
/** Receive the event regardless of context filter checks. */
global?: boolean
}
```
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L111)
## DispatchMode
Event dispatch strategy used by the event service.
`emit` runs synchronous listeners without awaiting them, `parallel` awaits all listeners together, `serial` awaits them in order until one bails, `bail` stops on the first synchronous bail value, and `waterfall` composes listeners around a final `next` callback.
```ts website-api
type DispatchMode = 'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall'
```
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L31)
+229 -74
View File
@@ -1,108 +1,263 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# Fiber
Fiber(作用域)是插件实例的运行时容器,管理其生命周期和效果。
A fiber is one loaded plugin instance: its lifecycle state, validated config, and registered effects. `ctx.fiber` is the current fiber; `ctx.effect()` delegates to it.
## 状态机
### ctx.fiber
```
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
↘ FAILED
```ts website-api
fiber: Fiber
```
| 状态 | 数值 | 含义 |
|------|------|------|
| PENDING | 0 | 依赖未就绪,等待中 |
| LOADING | 1 | 正在执行 `apply` |
| ACTIVE | 2 | 运行中 |
| FAILED | 3 | `apply` 抛出异常 |
| UNLOADING | 4 | 正在撤销效果 |
| DISPOSED | 5 | 已完全卸载 |
The fiber (plugin runtime instance) that owns this context.
## 实例属性
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L11)
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)
### fiber.uid
- **类型:** `number`
```ts website-api
public uid: number | null
```
Fiber 的唯一标识符。
Unique id within the registry; 0 for the root fiber, `null` once disposed.
### fiber.status
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L156)
- **类型:** `number`
### fiber.ctx
当前状态(见状态机)。
```ts website-api
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)
### fiber.config
- **类型:** `object`
传递给插件的配置对象。
### fiber.error
- **类型:** `Error | undefined`
如果状态是 FAILED,包含导致失败的异常。
## 实例方法
### fiber.effect(callback) {#fiber-effect}
- **callback:** `() => (() => void) | void`
- **返回值:** `() => void`
注册一个效果。`callback` 在 Fiber 激活时执行;如果返回函数,该函数在 Fiber dispose 时执行。
```typescript
ctx.effect(() => {
const timer = setInterval(tick, 1000)
return () => clearInterval(timer)
})
```ts website-api
public config: any
```
等价地可以通过 `ctx.effect()` 调用(ctx 代理到当前 fiber)。
The validated plugin config (updated by `update()`).
### fiber.dispose()
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L160)
- **返回值:** `Promise<void>`
### fiber.state
手动 dispose 该 Fiber。按注册逆序撤销所有效果,递归 dispose 所有子 Fiber。
```typescript
const child = ctx.plugin(somePlugin)
// 之后:
await child.dispose()
```ts website-api
public state
```
### fiber.update(config)
Current lifecycle state; transitions emit `internal/status`.
- **config:** `object` 新配置
- **返回值:** `void`
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L162)
热更新配置。如果新旧配置不同,触发 dispose + 重新 apply。
### fiber.dispose
```ts website-api
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)
### fiber.store
```ts website-api
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)
### fiber.inertia
```ts website-api
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)
### fiber.name
```ts website-api
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)
### fiber.assertActive()
```ts website-api
assertActive()
```
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)
### fiber.effect(execute, label?)
```ts website-api
effect(execute: () => SyncEffect, label?: string): Disposable<Promise<void>>
effect(execute: () => Effect, label?: string): AsyncDisposable<Promise<void>>
```
Register a cleanup-aware effect on this fiber.
`execute` runs immediately; the disposers it produces are collected and run (in reverse order) either when the returned disposer is called or when the fiber unloads, whichever comes first. Calling the disposer twice is a no-op. Throws `CordisError('INACTIVE_EFFECT')` if the fiber is already disposed, and `TypeError` if `execute` returns an invalid shape.
- `execute` — the effect body; see {@link Effect} for accepted shapes.
- `label` — effect label shown in `getEffects()` diagnostics.
**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)
### fiber.getEffects()
```ts website-api
getEffects()
```
Return metadata for currently registered effects.
**Returns** one {@link EffectMeta} tree per labeled live effect.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L436)
### fiber.await()
```ts website-api
async await()
```
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)
### fiber.restart()
- **返回值:** `void`
强制重启:dispose 后重新加载。
### fiber.then(resolve, reject?)
- **返回值:** `Promise<void>`
使 Fiber 可以被 `await`:等到状态进入 ACTIVE 或 FAILED。
```typescript
const fiber = ctx.plugin(myPlugin)
await fiber // 等待插件加载完成
```ts website-api
async restart()
```
## 访问当前 Fiber
Dispose and immediately reload this plugin with its current config.
```typescript
export function apply(ctx: Context) {
const fiber = ctx.fiber // 当前插件的 Fiber
console.log(fiber.status) // 1 (LOADING, 因为正在 apply 中)
**Returns** a promise resolving once the reload settled.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L574)
### fiber.update(config, noSave?)
```ts website-api
update(config: any, noSave = false)
```
Validate and apply new config, then restart the plugin.
Runs the `internal/update` waterfall first, so update hooks (and HMR) can veto or replace the restart.
- `config` — the new raw config; validated before anything restarts.
- `noSave` — hint for persistence hooks not to write the change back.
**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)
## Effect
Effect body result accepted by `ctx.effect()` and plugin startup.
Either a single disposer, a promise of one, or a (possibly async) iterable yielding several — generator effects register each yielded disposer as it is produced.
```ts website-api
type Effect<T = any> =
| SyncEffect<T>
| AsyncEffect<T>
```
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L82)
## Disposable
Function returned by an effect to release resources during disposal.
Disposers run in reverse registration order when the owning fiber unloads; they may be async, in which case unloading awaits them.
```ts website-api
type Disposable<T = any> = () => T
```
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L73)
## EffectMeta
Tree node used to expose nested effect labels for diagnostics.
```ts website-api
interface EffectMeta {
/** Human-readable effect label, e.g. `ctx.on("event")` or `ctx.provide("name")`. */
label: string
/** Metadata of nested effects registered while this effect ran. */
children: EffectMeta[]
}
```
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L95)
## CordisError
Framework error with a stable machine-readable code.
```ts website-api
class CordisError extends Error {
/**
* @param code — the stable error code; also the default message.
* @param message — optional human-readable override.
*/
constructor(public code: CordisError.Code, message?: string)
}
namespace CordisError {
export type Code = keyof typeof Code
export const Code = {
INACTIVE_EFFECT: 'cannot create effect on inactive context',
} as const
}
```
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L127)
## ValidationError
Error raised when plugin configuration fails standard-schema validation.
```ts website-api
class ValidationError extends TypeError {
name = 'ValidationError'
/**
* Build the aggregated message from schema issues.
*
* @param issues — the standard-schema issues, one message line each.
*/
constructor(issues: readonly StandardSchemaV1.Issue[])
}
```
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L18)
+97 -63
View File
@@ -1,87 +1,121 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# Registry
插件注册表,管理插件的加载和依赖解析。
Plugin loading and dependency injection.
## 实例方法
### ctx.inject(deps, callback)
### ctx.plugin(plugin, config?) {#ctx-plugin}
- **plugin:** `Plugin` 插件(函数、对象或类)
- **config:** `object` 传递给插件的配置(可选)
- **返回值:** `Fiber`
加载一个子插件,返回其 Fiber。子 Fiber 的生命周期绑定到父上下文。
```typescript
// 函数插件
ctx.plugin(myPlugin, { key: 'value' })
// 类插件
ctx.plugin(MyService)
// 返回的 Fiber 可以 await 或 dispose
const fiber = ctx.plugin(myPlugin)
await fiber
```ts website-api
inject(deps: Inject, callback: Plugin.Function<void>): Fiber & PromiseLike<Fiber>
```
### ctx.inject(names, callback) {#ctx-inject}
Run a callback once the requested services are available.
Shorthand for `ctx.plugin({ inject, apply: callback })`: the callback is unloaded and re-run whenever a required service changes.
- **names:** `string[]` 服务名列表
- **callback:** `(ctx: Context) => void`
- **返回值:** `() => void`
- `deps` — required services, as an array or a name → config map.
- `callback` — plugin body called with `(ctx, config)`.
等待指定服务全部就绪后执行 callback。如果服务消失,callback 的效果会自动撤销;服务恢复后重新执行。
**Returns** the fiber; awaiting it settles once loading finished.
```typescript
ctx.inject(['tools', 'llm'], (ctx) => {
// tools 和 llm 都就绪了
ctx.tools.register(/* ... */)
})
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L175)
### ctx.plugin(plugin, ...args)
```ts website-api
plugin<P extends Plugin>(plugin: P, ...args: Spread<GetPluginConfig<P>>): Fiber & PromiseLike<Fiber>
```
这是 `export const inject = [...]` 声明的底层 API。大多数情况下直接使用声明式写法即可。
Load a plugin in the current context.
## 插件形态
- `plugin` — a function, class, or `{ apply }` object plugin.
- `args` — the plugin config, validated against its `Config` schema.
`ctx.plugin()` 接受三种插件形态:
**Returns** the fiber; awaiting it settles once loading finished (rejecting on config or startup errors).
### 函数插件
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L184)
```typescript
function myPlugin(ctx: Context, config?: Config) {
// ...
}
myPlugin.name = 'my-plugin'
myPlugin.inject = ['tools']
```
## Plugin
### 对象插件
Supported plugin entrypoint shapes.
```typescript
const myPlugin = {
name: 'my-plugin',
inject: ['tools'],
apply(ctx: Context, config?: Config) {
// ...
},
}
```
```ts website-api
type Plugin<T = any> =
| Plugin.Function<T>
| Plugin.Constructor<T>
| Plugin.Object<T>
### 类插件(Service
namespace Plugin {
/** Shared metadata understood by the plugin registry and related tooling. */
export interface Base<T = any> {
/** Display name used for fiber diagnostics and logger names. */
name?: string
/** Standard-schema validator applied to config before the plugin starts. */
Config?: StandardSchemaV1<any, T>
/** Services the plugin requires; it only loads while all are available. */
inject?: Inject
/** Service name(s) the plugin provides (read by `Service` and by loaders). */
provide?: string | string[]
/** Service names whose intercept config the plugin declares it consumes. */
intercept?: Dict<boolean>
}
```typescript
class MyService extends Service {
static inject = ['tools']
constructor(ctx: Context) {
super(ctx, 'myService')
export interface Transform<S, T> {
/** Marks the transform object as a schema/config transform. */
schema?: true
/** Convert user-facing config to runtime config. */
Config: (config: S) => T
}
/** Function plugin called with `(ctx, config)`. */
export interface Function<T = any> extends Base<T> {
(ctx: Context, config: T): any
}
/** Class plugin constructed with `(ctx, config)`. */
export interface Constructor<T = any> extends Base<T> {
new (ctx: Context, config: T): any
}
/** Object plugin with an `apply(ctx, config)` method. */
export interface Object<T = any> extends Base<T> {
apply(ctx: Context, config: T): any
}
/** Mutable registry record shared by all fibers of one plugin callback. */
export interface Runtime {
/** Display name copied from the first registered plugin shape. */
name?: string
/** Every live fiber of this plugin (one per `ctx.plugin()` call). */
fibers: DisposableList<Fiber>
/** The executable entrypoint all fibers share (registry identity key). */
callback: globalThis.Function
/** Standard-schema validator applied to each fiber's config. */
Config?: StandardSchemaV1
}
}
```
## 插件元信息
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L91)
| 属性 | 类型 | 说明 |
|------|------|------|
| `name` | `string` | 插件名称(日志用) |
| `inject` | `string[] \| { required?: string[], optional?: string[] }` | 依赖声明 |
| `Config` | `Schema \| object` | 配置 schema 或默认值 |
## Inject
Service dependency declaration accepted by plugins and the `@Inject` decorator.
Array form requests services without intercept config. Object form maps each service name to optional intercept config for the plugin context.
```ts website-api
type Inject<M = Dict> = (keyof M)[] | { [K in keyof M]?: M[K] }
namespace Inject {
/**
* Convert array/object/class-inherited inject metadata into a plain map.
*
* @param inject — the declaration to normalize; `null`/`undefined` add nothing.
* @param result — the map to fill (service name → intercept config or `null`).
* @returns `result`.
*/
export function resolve(inject: Inject | null | undefined, result: Dict = Object.create(null))
}
```
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L18)
+79 -84
View File
@@ -1,97 +1,92 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# Service
Service 基类,用于创建对外暴露能力的插件。
Base class for context services: subclass it and load the subclass as a plugin to register `ctx.<name>`.
## 基本用法
Base class for services that expose a named API on `ctx`.
Subclasses call `super(ctx, name)` from their constructor. The service is registered immediately and is automatically removed with the owning fiber.
```typescript
import { Service, type Context } from 'cordis'
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L11)
declare module 'cordis' {
interface Context {
myService: MyService
}
}
### service.name
export default class MyService extends Service {
constructor(ctx: Context) {
super(ctx, 'myService')
}
// 公开方法
doSomething() {
// ...
}
}
```ts website-api
public name!: string
```
加载后,其他插件可通过 `ctx.myService` 访问。
The service name this instance is registered under.
## 构造函数
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L30)
### new Service(ctx, name)
## Static members
- **ctx:** `Context` 上下文
- **name:** `string` 服务名(注册到 `ctx[name]`
### Service.init
## 实例属性
### service.ctx
- **类型:** `Context`
该服务绑定的上下文。
### service\[Service.tracker\]
- **类型:** `object`
服务追踪信息(名称、绑定状态等)。
## 生命周期
Service 子类可以覆写以下方法:
### start()
服务激活时调用。在这里初始化资源。
### stop()
服务停用时调用。在这里释放资源。
## 静态属性
### Service.inject
- **类型:** `string[] | { required?: string[], optional?: string[] }`
声明本服务依赖的其他服务。
## 与 inject 的关系
当一个 Service 被加载:
1. 框架为该服务名创建声明 (`ctx.provide`)
2. 实例赋值到 `ctx[name]`
3. 依赖该服务的所有 Fiber 从 PENDING 转为 LOADING
当 Service 被卸载:
1. `ctx[name]` 被置为 `undefined`
2. 依赖它的 Fiber 被 dispose
3. 当新的 provider 出现时,dependant Fiber 重新加载
## 示例:Harness 中的 Service
```typescript
// dsh-tools 的 ToolRegistry 就是一个 Service
export class ToolRegistry extends Service {
constructor(ctx: Context) {
super(ctx, 'tools')
}
register(tool: ToolDefinition): () => void {
// ...注册逻辑
return dispose
}
}
```ts website-api
static readonly init: unique symbol
```
Symbol key of an instance method run after construction (class plugins).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L13)
### Service.check
```ts website-api
static readonly check: unique symbol
```
Symbol key of the availability predicate passed to `ctx.provide()`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L15)
### Service.config
```ts website-api
static readonly config: unique symbol
```
Symbol key of the phantom intercept-config type parameter.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L17)
### Service.invoke
```ts website-api
static readonly invoke: unique symbol
```
Symbol key of the call body making a service callable (e.g. `ctx.logger()`).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L19)
### Service.extend
```ts website-api
static readonly extend: unique symbol
```
Symbol key of the helper deriving an extended service instance.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L21)
### Service.tracker
```ts website-api
static readonly tracker: unique symbol
```
Symbol key of the tracker metadata used for context tracing.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L23)
### Service.resolveConfig
```ts website-api
static readonly resolveConfig: unique symbol
```
Symbol key of the intercept-config resolution helper below.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L25)
+56
View File
@@ -0,0 +1,56 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# ctx.agentLoop
`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.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L68)
### ctx.agentLoop.create(id, options?)
```ts website-api
create(id: AgentId, options: AgentOptions = {}): 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.
- `id` — the agent id; also seeds the generated session id.
- `options` — loop options (model, limits, …); defaults applied per option.
**Returns** the running agent, owned by the calling fiber (no handle).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L142)
### ctx.agentLoop.createAgent(options)
```ts website-api
createAgent(options: CreateAgentOptions): 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.
- `options` — agent id, caller-supplied session id, optional seed/meta, and agent options.
**Returns** the handle whose dispose tears down exactly this agent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L166)
### ctx.agentLoop.resume(options)
```ts website-api
async resume(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.
- `options` — the persisted session id to reload, plus agent id/options.
**Returns** the handle for the agent resumed on the reconstructed session.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L194)
-85
View File
@@ -1,85 +0,0 @@
# Agent (dsh-agent)
Agent 实例管理和生命周期。
**包名:** `@deepseek-ai/dsh-agent`
**服务名:** `ctx.agents`
## Agent Service
### ctx.agents.create(options)
- **options:** `AgentOptions`
- **返回值:** `Agent`
创建一个新的 Agent 实例。
### ctx.agents.get(id)
- **id:** `AgentId`
- **返回值:** `Agent | undefined`
获取指定 ID 的 Agent 实例。
## AgentOptions
```typescript
interface AgentOptions {
/** Agent IDbranded */
id?: AgentId
/** 使用的模型名 */
model: string
/** 系统提示词(支持 {{model}} 变量) */
persona?: string
/** 关联的 session */
session?: Session
}
```
## Agent 实例
### agent.id
- **类型:** `AgentId`
Agent 的唯一标识符(branded string)。
### agent.model
- **类型:** `string`
Agent 使用的模型名。
### agent.step(input)
- **input:** `ContentBlock[]`
- **返回值:** `Promise<StepResult>`
执行一步:将输入发送给模型,获取响应,执行 tool calls。这是 agent-loop 内部使用的核心方法。
## Agent Loop
Agent 的执行循环由 `dsh-agent-loop` 管理。它:
1. 组装 system prompt + 历史消息 + 当前输入
2. 调用 LLM(通过 `ctx.llm`
3. 解析响应中的 tool calls
4. 执行 tools
5. 将 tool results 追加到 session
6. 如果 finish reason 是 `tool-calls`,回到步骤 2
### 扩展点
- `agent/pre-step` 事件 — 在每一步 LLM 调用前触发
- `agent/post-step` 事件 — 在每一步完成后触发
- `llm/pre-request` waterfall — 可修改发送给模型的消息
## AgentId
Opaque branded string
```typescript
import { AgentId } from '@deepseek-ai/dsh-agent'
const id = AgentId('main')
```
+91
View File
@@ -0,0 +1,91 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# ctx.agents
`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.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L117)
### ctx.agents.setFactory(factory)
```ts website-api
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.
- `factory` — the loop-owned factory {@link create}/{@link resume} delegate to.
**Returns** the disposer that clears the factory slot.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L132)
### ctx.agents.create(options)
```ts website-api
create(options: CreateAgentOptions): 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.
- `options` — agent id, session id/seed/metadata, and agent options.
**Returns** the handle whose dispose tears down exactly this agent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L150)
### ctx.agents.resume(options)
```ts website-api
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.
- `options` — the persisted session id plus agent id and options.
**Returns** the handle for the resumed agent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L162)
### ctx.agents.register(agent)
```ts website-api
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.
- `agent` — the already-constructed agent to record in the store.
**Returns** the disposer that removes the agent and emits `agent/disposed`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L174)
### ctx.agents.get(id)
```ts website-api
get(id: AgentId): Agent | undefined
```
Look up a live agent.
- `id` — the agent id to look up.
**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)
### ctx.agents.list()
```ts website-api
list(): Agent[]
```
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)
+115 -58
View File
@@ -1,81 +1,138 @@
# Bash (dsh-bash)
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
Bash 命令执行接口。
# ctx.bash
**接口包:** `@deepseek-ai/dsh-bash`
**实现:** `@deepseek-ai/dsh-bash-local`
**消费者:** `@deepseek-ai/dsh-tool-bash`(内置于 agent-core
`BashExecutor` (abstract seam) — provided by `@deepseek-ai/dsh-bash`.
## Bash Service
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()`).
### ctx.bash.execute(request)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L59)
- **request:** `BashRequest`
- **返回值:** `Promise<BashResult>`
### ctx.bash.resolve(request)
执行一个 bash 命令。
## BashRequest
```typescript
interface BashRequest {
/** 要执行的命令 */
command: string
/** 工作目录 */
workdir?: string
/** 超时时间 (ms) */
timeoutMs?: number
}
```ts website-api
abstract resolve(request: BashExecRequest): BashExecSpec
```
## BashResult
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).
```typescript
interface BashResult {
/** 退出码 */
exitCode: number
/** stdout 输出 */
stdout: string
/** stderr 输出 */
stderr: string
/** 是否超时 */
timedOut: boolean
}
- `request` — the caller's request; omitted fields get this implementation's defaults, capped fields are clamped.
**Returns** the fully-specified spec to hand to {@link run}/{@link start}.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L84)
### ctx.bash.run(spec)
```ts website-api
abstract run(spec: BashExecSpec): Promise<BashRunResult>
```
## 配置 (dsh-bash-local)
Run a command in the foreground; resolves when it finishes.
```typescript
interface Config {
/** 命令超时时间,默认 120000 (2 分钟) */
timeoutMs: number
}
- `spec` — a resolved spec from {@link resolve}, never a raw request.
**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)
### ctx.bash.start(spec)
```ts website-api
abstract start(spec: BashExecSpec): BashTask
```
`cordis.yml` 中:
Start a background task and return its handle immediately.
```yaml
- name: '@deepseek-ai/dsh-bash-local'
config:
timeoutMs: 60000
- `spec` — a resolved spec from {@link resolve}, never a raw request.
**Returns** the live task handle; completion fires {@link onTaskDone}.
[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
```
## 模型可用的 Tools
Look up a background task by id.
`dsh-tool-bash` 向模型暴露以下 tools(由 `agent-core` 捆绑):
- `id` — the task id to look up.
| Tool | 说明 |
|------|------|
| `bash` | 执行命令(同步,等待完成) |
| `bash_output` | 获取后台命令的输出 |
| `bash_kill` | 终止后台命令 |
**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)
Bash 是 Harness 的"能力三件套"典型案例:
### ctx.bash.ownerOf(id)
- `dsh-bash`(接口):定义 `ctx.bash``BashRequest`/`BashResult` 类型
- `dsh-bash-local`(实现):通过 `child_process.spawn` 在本地执行
- `dsh-tool-bash`(消费者):将能力包装为模型可调用的 tool
```ts website-api
abstract ownerOf(id: BashTaskId): OwnerToken | undefined
```
换一个沙箱执行器只需替换 `dsh-bash-local`,接口和 tool 不变。
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)
+28
View File
@@ -0,0 +1,28 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# ctx.codeRuntime
`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()`).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/code-runtime/code-runtime/src/index.ts#L59)
### ctx.codeRuntime.run(request)
```ts website-api
abstract run(request: CodeRunRequest): Promise<CodeRunResult>
```
Execute one program against the request's bindings and capture what it emitted. See the class doc for the resolution contract (error is a result field; rejection means seam misuse only).
- `request` — the program, its bindings, and the abort signal; the request carries everything the runtime acts on, with no hidden defaults.
**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)
+55
View File
@@ -0,0 +1,55 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# ctx.compact
`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.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact/src/index.ts#L65)
### ctx.compact.compactIfNeeded(agent, fullSystemPrompt, sessionPrefix, signal)
```ts website-api
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).
- `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.
**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)
### ctx.compact.compactRegion(session, start, end, agent, signal?)
```ts website-api
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.
- `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.
**Returns** what the compaction did (the replaced range and its summary node).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact/src/index.ts#L151)
+546
View File
@@ -0,0 +1,546 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# 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).
## agent/*
### agent/created
**Mode:** `emit`
```ts website-api
'agent/created'(agent: Agent): void
```
An agent was registered in the AgentRegistry and is ready to receive messages.
- `agent` — the newly registered agent, already resolvable in the registry.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L265)
### agent/disposed
**Mode:** `emit`
```ts website-api
'agent/disposed'(agent: Agent): void
```
An agent was disposed and removed from the registry; its fiber and any in-flight turn have been torn down.
- `agent` — the agent that was torn down; its handle is now inert.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L272)
### agent/error
**Mode:** `emit`
```ts website-api
'agent/error'(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.
- `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.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L476)
### 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
```
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).
- `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.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L357)
### agent/prompt-submit
**Mode:** `waterfall`
```ts website-api
'agent/prompt-submit'(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.
- `agent` — the agent draining its inbox.
- `content` — the drained message's blocks, as queued.
- `source` — the message's resolved source.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L370)
### agent/queued
**Mode:** `emit`
```ts website-api
'agent/queued'(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.
- `agent` — the agent whose inbox received the message.
- `content` — the enqueued content blocks, verbatim.
- `info` — the resolved source plus whether it entered as steering.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L290)
### agent/request
**Mode:** `waterfall`
```ts website-api
'agent/request'(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.
- `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.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L394)
### agent/session-prefix
**Mode:** `waterfall`
```ts website-api
'agent/session-prefix'(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.
- `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.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L441)
### agent/session-start
**Mode:** `emit`
```ts website-api
'agent/session-start'(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).
- `agent` — the agent whose session lifecycle began.
- `source` — why the session started (fresh startup, resume, …).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L305)
### agent/status
**Mode:** `emit`
```ts website-api
'agent/status'(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` — the agent whose status flipped.
- `status` — the status just entered (the transition's destination).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L281)
### agent/step-result
**Mode:** `waterfall`
```ts website-api
'agent/step-result'(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, …).
- `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.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L451)
### agent/turn-continuation
**Mode:** `waterfall`
```ts website-api
'agent/turn-continuation'(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.
- `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.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L464)
## fs/*
### fs/edit-intent
**Mode:** `waterfall`
```ts website-api
'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').
- `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)
### fs/observed
**Mode:** `emit`
```ts website-api
'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.
- `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)
### fs/write-intent
**Mode:** `waterfall`
```ts website-api
'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.
- `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)
## llm/*
### llm/stream
**Mode:** `waterfall`
```ts website-api
'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
```
Waterfall around every streaming model call (retry, replay, routing). Bound to the LlmService; call `next()` to reach the resolved adapter's stream, or yield your own chunks to short-circuit.
- `options` — the full request. A LOOP-built request arrives deep-frozen (mutation throws): its content is a pure function of the session log (the reconstructability RFC), so listeners read it, never rewrite it. A hand-built one-shot (compaction summarize) is the caller's own object and stays mutable here.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L39)
## session/*
### session/created
**Mode:** `emit`
```ts website-api
'session/created'(session: Session): void
```
A session was created in the store.
- `session` — the session just entered and announced.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L39)
### session/event
**Mode:** `emit`
```ts website-api
'session/event'(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.
- `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)
### session/flush
**Mode:** `parallel`
```ts website-api
'session/flush'(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.
- `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)
## subagent/*
### subagent/end
**Mode:** `emit`
```ts website-api
'subagent/end'(info: SubagentRunEndInfo): void
```
A subagent run settled — emitted when SubagentRun.result resolves (any stop reason). Paired with Events['subagent/start'].
- `info` — the run identity plus stop reason and final output.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L98)
### subagent/provider-added
**Mode:** `emit`
```ts website-api
'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".
- `provider` — the provider that just registered, live in the registry.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L72)
### subagent/provider-removed
**Mode:** `emit`
```ts website-api
'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.
- `name` — the registry name that no longer resolves.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L83)
### subagent/start
**Mode:** `emit`
```ts website-api
'subagent/start'(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'].
- `info` — which provider started which child agent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L91)
## system-prompt/*
### system-prompt/assemble
**Mode:** `waterfall`
```ts website-api
'system-prompt/assemble'(this: 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.
- `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 {@link AssembleContext} the caller passed to {@link SystemPrompt.assemble} (e.g. which agent the prompt is for), so a listener can filter or extend per agent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L38)
### system-prompt/change
**Mode:** `emit`
```ts website-api
'system-prompt/change'(): void
```
A section, tool provider, or variable provider was registered or unregistered (the assembly inputs changed).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L44)
## tools/*
### tools/change
**Mode:** `emit`
```ts website-api
'tools/change'(): void
```
A tool was registered or unregistered (the available tool set changed).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L132)
### tools/execute
**Mode:** `waterfall`
```ts website-api
'tools/execute'(this: 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.
- `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)
### tools/post-execute
**Mode:** `waterfall`
```ts website-api
'tools/post-execute'(this: ToolRegistry, exec: ToolExecution, result: 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).
- `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)
### tools/pre-execute
**Mode:** `waterfall`
```ts website-api
'tools/pre-execute'(this: 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)`).
- `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)
## workflow/*
### workflow/agent-end
**Mode:** `emit`
```ts website-api
'workflow/agent-end'(info: WorkflowRunInfo, agent: WorkflowAgentEndInfo): void
```
One `agent()` call settled (clean result, child failure, or run cancellation). Paired with Events['workflow/agent-start'] by `agent.seq`, exactly once per started call on every stop path — on an engine termination path (a worker killed past its grace) the end is engine-synthesized with outcome `'cancelled'`.
- `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)
### workflow/agent-start
**Mode:** `emit`
```ts website-api
'workflow/agent-start'(info: WorkflowRunInfo, agent: WorkflowAgentInfo): void
```
One `agent()` call started a child run. Paired with Events['workflow/agent-end'] by `agent.seq`.
- `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)
### workflow/end
**Mode:** `emit`
```ts website-api
'workflow/end'(info: WorkflowRunInfo, result: WorkflowResultInfo): void
```
A workflow run settled (any stop reason). Fired when WorkflowRun.result resolves. Paired with Events['workflow/start'].
- `info` — the run's identity snapshot.
- `result` — the outcome data (stop reason, error, agent count) — deliberately WITHOUT the result value (see {@link WorkflowResultInfo}).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L106)
### workflow/log
**Mode:** `emit`
```ts website-api
'workflow/log'(info: WorkflowRunInfo, message: string): void
```
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)
### workflow/phase
**Mode:** `emit`
```ts website-api
'workflow/phase'(info: WorkflowRunInfo, title: string): void
```
The script entered a phase (a `phase(title)` call) — progress grouping for observers; no execution semantics.
- `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)
### workflow/start
**Mode:** `emit`
```ts website-api
'workflow/start'(info: WorkflowRunInfo): void
```
A workflow run started — the script's meta block validated, the body about to execute. Paired with Events['workflow/end'].
- `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)
+110 -62
View File
@@ -1,78 +1,126 @@
# Filesystem (dsh-fs)
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
文件系统操作接口。
# ctx.fs
**接口包:** `@deepseek-ai/dsh-fs`
**实现:** `@deepseek-ai/dsh-fs-local` + `@deepseek-ai/dsh-fs-policy`
**消费者:** `@deepseek-ai/dsh-tool-fs`
`FileSystem` (abstract seam) — provided by `@deepseek-ai/dsh-fs`.
## FS Service
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`).
### ctx.fs.read(path, options?)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L172)
- **path:** `string`
- **options:** `{ offset?: number; limit?: number }`
- **返回值:** `Promise<string>`
### ctx.fs.resolve(path, opts?)
读取文件内容。
### ctx.fs.write(path, content)
- **path:** `string`
- **content:** `string`
- **返回值:** `Promise<void>`
写入文件(覆盖)。
### ctx.fs.edit(path, edits)
- **path:** `string`
- **edits:** `Edit[]`
- **返回值:** `Promise<void>`
对文件执行精确的字符串替换编辑。
### ctx.fs.stat(path)
- **path:** `string`
- **返回值:** `Promise<FileStat>`
获取文件/目录信息。
## 配置 (dsh-fs-local)
```typescript
interface Config {
/** 工作目录(相对路径的基准) */
cwd: string
}
```ts website-api
abstract resolve(path: string, opts?: { cwd?: string }): Promise<FsTarget>
```
## 策略门 (dsh-fs-policy)
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.
`dsh-fs-policy` 是一个可选的中间层插件,实现 read-before-write/edit 策略——模型必须先读取文件才能写入或编辑。这防止模型盲目覆盖文件。
- `path` — the path to resolve; relative paths resolve against `opts.cwd`.
- `opts` — `cwd` overrides the backend's default base for relative paths.
`cordis.yml` 中,它位于 `fs-local``tool-fs` 之间:
**Returns** the stable target; the same file yields the same `targetKey`.
```yaml
- name: '@deepseek-ai/dsh-fs-local'
config:
cwd: !!js process.cwd()
- name: '@deepseek-ai/dsh-fs-policy'
- name: '@deepseek-ai/dsh-tool-fs'
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L194)
### ctx.fs.stat(target, signal?)
```ts website-api
abstract stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined>
```
## 模型可用的 Tools
Return target metadata, or `undefined` when the target does not exist.
| Tool | 说明 |
|------|------|
| `read` | 读取文件内容(支持 offset/limit |
| `write` | 写入文件(需要先 read) |
| `edit` | 精确字符串替换(需要先 read) |
- `target` — the resolved target to stat.
- `signal` — aborts the metadata round-trip.
## 三件套结构
**Returns** metadata only, never content; undefined for an absent target.
- `dsh-fs`:接口定义
- `dsh-fs-local`:本地文件系统实现
- `dsh-fs-policy`:策略门(read-before-write 检查)
- `dsh-tool-fs`:模型 tool 层
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L202)
### ctx.fs.readText(target, signal?)
```ts website-api
abstract readText(target: FsTarget, signal?: AbortSignal): Promise<string>
```
Read the whole regular text file as a single decoded string.
- `target` — the resolved target to read.
- `signal` — aborts the read.
**Returns** the full decoded UTF-8 content.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L210)
### ctx.fs.streamText(target, signal?)
```ts website-api
abstract streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>>
```
Stream the whole regular text file as decoded text chunks (same text semantics as readText, for large files). The backend owns cross-chunk UTF-8 decoding and binary rejection so the policy layer never touches raw bytes.
- `target` — the resolved target to read.
- `signal` — aborts the stream, including between chunks.
**Returns** the chunk iterable, decoded and validated like {@link readText}.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L221)
### ctx.fs.listDir(target, signal?)
```ts website-api
abstract listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]>
```
List direct children of a directory in stable name order. Returns resolved child targets plus cheap metadata only; never reads file contents.
- `target` — the resolved directory target.
- `signal` — aborts the listing.
**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)
### ctx.fs.writeText(target, content, expected?, signal?)
```ts website-api
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.
- `target` — the resolved target to write.
- `content` — the full new file content.
- `expected` — the write intent guarding the write; omit for unconditional.
- `signal` — aborts before the atomic rename takes effect.
**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)
### ctx.fs.editText(target, edit, expected?, signal?)
```ts website-api
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`.
- `target` — the resolved target to edit.
- `edit` — the literal search/replace request.
- `expected` — the version guard; omit for an unconditional edit.
- `signal` — aborts before the atomic rename takes effect.
**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)
+27 -101
View File
@@ -1,124 +1,50 @@
# LLM (dsh-llm)
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
LLM 服务接口和适配器注册。
# ctx.llm
**包名:** `@deepseek-ai/dsh-llm`
**服务名:** `ctx.llm`
`LlmService` — provided by `@deepseek-ai/dsh-llm`.
## LLM Service
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)
### ctx.llm.registerAdapter(models, adapter)
- **models:** `string[]` 该适配器支持的模型名列表
- **adapter:** `LlmAdapter` 适配器实例
- **返回值:** `() => void` disposer
注册一个 LLM 适配器。当请求中指定的模型名在 `models` 列表中时,路由到该适配器。
```typescript
ctx.llm.registerAdapter(['deepseek-v4-flash', 'deepseek-v4-pro'], adapter)
```ts website-api
registerAdapter(models: string[], adapter: LlmAdapter): () => void
```
## LlmAdapter
Register an adapter for the given model names. Throws `LlmError` with code `DUPLICATE_ADAPTER` if any model already has an adapter (all-or-nothing). Disposed with the fiber.
适配器基类。子类必须实现 `stream()` 方法。
- `models` — every model name this adapter should serve.
- `adapter` — the adapter that streams calls for those models.
### stream(options)
**Returns** the disposer that unregisters all of them.
- **options:** `GenerateOptions`
- **返回值:** `AsyncIterable<StreamChunk>`
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L103)
将统一请求格式转换为具体 API 的流式调用。
### ctx.llm.models()
## GenerateOptions
```typescript
interface GenerateOptions {
model: string
messages: Message[]
tools?: ToolSpec[]
system?: string
maxTokens?: number
temperature?: number
}
```ts website-api
models(): string[]
```
| 字段 | 说明 |
|------|------|
| `model` | 请求的模型名 |
| `messages` | 对话历史 |
| `tools` | 当前可用的 tool 列表(JSON Schema 格式) |
| `system` | 系统提示词 |
| `maxTokens` | 最大输出 token |
| `temperature` | 采样温度 |
Model names with a registered adapter.
## StreamChunk
**Returns** the registered names, in registration order.
流式响应的增量 chunk 类型:
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L124)
```typescript
type StreamChunk =
| { type: 'block-start'; index: number; blockType: 'text' | 'tool-call' }
| { type: 'text-delta'; index: number; text: string }
| { type: 'tool-call-delta'; index: number; id: CallId; name: string; argumentsDelta: string }
| { type: 'block-end'; index: number; block: ContentBlock }
| { type: 'usage'; usage: TokenUsage }
| { type: 'finish'; reason: FinishReason }
### ctx.llm.stream(options)
```ts website-api
stream(options: GenerateOptions): AsyncIterable<StreamChunk>
```
### 协议规则
Stream one model call as raw chunks (token-level deltas). Throws `LlmError` with code `NO_ADAPTER` if no adapter is registered for `options.model`. Dispatches through the `llm/stream` waterfall.
1. 每个内容块以 `block-start` 开始,以 `block-end` 结束
2. `index` 从 0 递增
3. `text-delta` 只在 `blockType: 'text'` 的块中
4. `tool-call-delta` 只在 `blockType: 'tool-call'` 的块中
5. `usage``finish` 之前
6. `finish` 必须是最后一个 chunk
- `options` — the full request; `options.model` selects the adapter.
## CallId
**Returns** the chunk stream, possibly wrapped by `llm/stream` listeners.
Tool call 的 opaque branded ID
```typescript
import { CallId } from '@deepseek-ai/dsh-llm'
const id = CallId('call-abc123')
```
## TokenUsage
```typescript
interface TokenUsage {
inputTokens: number
outputTokens: number
}
```
## FinishReason
```typescript
type FinishReason =
| { kind: 'stop' }
| { kind: 'tool-calls' }
| { kind: 'max-tokens' }
```
## Message
对话消息类型:
```typescript
interface Message {
role: 'user' | 'assistant'
content: ContentBlock[]
}
```
## ContentBlock
```typescript
type ContentBlock =
| { type: 'text'; text: string }
| { type: 'tool-call'; id: CallId; name: string; arguments: string }
| { type: 'tool-result'; callId: CallId; content: ContentBlock[]; isError?: boolean }
```
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L141)
@@ -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.sessionPersistence
`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).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L102)
### ctx.sessionPersistence.create(meta)
```ts website-api
abstract create(meta: SessionHeader): Promise<void>
```
Register a new session's metadata. A backend MAY defer the physical write until the first append (lazy materialization), in which case a created-but-never-appended session is absent from list — abandoned sessions leave nothing behind.
- `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)
### ctx.sessionPersistence.append(id, events)
```ts website-api
abstract append(id: SessionId, events: readonly SessionEvent[]): Promise<void>
```
Durably persist a batch of events (called from the write-behind drain at the `session/flush` checkpoint). Honors the append-only and contiguous-seq contracts: the first event's `seq` MUST equal the stored next-seq (after `load` has durably closed any interrupted turn). Rejects non-JSON- serializable `event.data` with an error naming the offending event type.
- `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)
### ctx.sessionPersistence.load(id)
```ts website-api
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.
- `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.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L152)
### ctx.sessionPersistence.list()
```ts website-api
abstract list(): Promise<SessionHeader[]>
```
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)
-56
View File
@@ -1,56 +0,0 @@
# Session (dsh-session)
会话事件流管理。
**包名:** `@deepseek-ai/dsh-session`
**服务名:** `ctx.session`
## 概述
Session 是 Agent 的对话状态容器。所有模型可见的内容都必须经过 session 事件流记录——这是"model-visible = logged"原则的实现。
## SessionSurface
会话的外部接口,用于查询当前状态。
### surface.messages
- **类型:** `Message[]`
当前会话的完整消息列表(经过 compaction 处理后的视图)。
### surface.events
- **类型:** `SessionEvent[]`
原始事件流。
## SessionEvent
会话中所有变更以事件形式记录:
```typescript
type SessionEvent =
| { type: 'user/message'; content: ContentBlock[] }
| { type: 'assistant/message'; content: ContentBlock[] }
| { type: 'tool/call'; name: string; args: unknown; callId: CallId }
| { type: 'tool/result'; callId: CallId; content: ContentBlock[]; isError?: boolean }
| { type: 'compact/start'; range: [number, number] }
| { type: 'compact/end'; summary: string }
| { type: 'todo/write'; items: TodoItem[] }
// ... 更多事件类型
```
## 设计原则
### Model-visible = Logged
任何到达模型请求的内容都必须能从 session log 重建。如果你要引入新的模型可见输入,必须先定义对应的 session event。
### 事件是 append-only
Session 事件流是只追加的。修改历史(如 compaction)通过新事件(compact/start + compact/end)表达,而不是修改旧事件。
### 持久化
Session 事件流可以通过 `dsh-session-persistence` 持久化到磁盘(JSONL 或 SQLite),实现跨进程恢复。
+110
View File
@@ -0,0 +1,110 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# ctx.sessions
`SessionStore` — provided by `@deepseek-ai/dsh-session`.
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)
### ctx.sessions.create(id?, options?)
```ts website-api
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`).
- `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)
### ctx.sessions.prepare(id?, options?)
```ts website-api
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.
- `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)
### ctx.sessions.enter(session)
```ts website-api
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.
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 {@link prepare}d session not yet in the store.
**Returns** the detach disposer (`onAppend = undefined` + store removal).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L499)
### ctx.sessions.announce(session)
```ts website-api
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).
- `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)
### ctx.sessions.get(id)
```ts website-api
get(id: SessionId): Session | undefined
```
Look up a live session.
- `id` — the session id to look up.
**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)
### ctx.sessions.list()
```ts website-api
list(): Session[]
```
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)
### ctx.sessions.fork(source, boundary?, childSessionId?)
```ts website-api
fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
```
Create a live child session from a turn-enclosed prefix of a live source. `boundary` is an inclusive source event seq; omitted means the source's current last event. A non-empty selected slice must end at `turn/end`.
- `source` — Live source session object or id.
- `boundary` — Inclusive source event seq to fork through; omitted means the source's current last event, and omitted on an empty source forks an empty child.
- `childSessionId` — Optional child session id; omitted delegates to `SessionStore`'s id policy.
**Returns** The created live child session.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L547)
-85
View File
@@ -1,85 +0,0 @@
# Subagent (dsh-subagent)
子代理委派接口。
**接口包:** `@deepseek-ai/dsh-subagent`
**实现:** `@deepseek-ai/dsh-subagent-spawn` / `@deepseek-ai/dsh-subagent-fork`
**消费者:** `@deepseek-ai/dsh-tool-subagent`
## Subagent Service
### ctx.subagent.run(request)
- **request:** `SubagentRequest`
- **返回值:** `Promise<SubagentResult>`
委派一个任务给子代理执行。
## SubagentRequest
```typescript
interface SubagentRequest {
/** 使用的 provider 名称 */
provider: string
/** 委派给子代理的提示 */
prompt: string
/** 子代理使用的模型(可选,默认继承父) */
model?: string
}
```
## SubagentResult
```typescript
interface SubagentResult {
/** 子代理的最终回复 */
response: string
}
```
## Provider 模式
Subagent 支持多种"后端"provider),通过配置选择:
### spawn
创建一个全新的子代理实例,没有父级的对话历史:
```yaml
- name: '@deepseek-ai/dsh-subagent-spawn'
config:
providerName: spawn
```
### fork
创建一个携带父级已完成 turn 前缀的子代理,子代理"知道"父级的对话上下文:
```yaml
- name: '@deepseek-ai/dsh-subagent-fork'
config:
providerName: fork
```
## 模型可用的 Tools
通过 `dsh-tool-subagent` 暴露。可以加载多次,每次绑定不同 provider:
```yaml
# 暴露为 "subagent" tool,使用 spawn 后端
- name: '@deepseek-ai/dsh-tool-subagent'
config:
provider: spawn
toolName: subagent
# 暴露为 "subagent_fork" tool,使用 fork 后端
- name: '@deepseek-ai/dsh-tool-subagent'
config:
provider: fork
toolName: subagent_fork
```
## 使用场景
- **spawn** — 独立子任务(如"搜索这个问题"),子代理不需要知道父级上下文
- **fork** — 需要上下文的子任务(如"基于我们刚才讨论的,去实现这个"),子代理继承父级的对话前缀
+64
View File
@@ -0,0 +1,64 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# ctx.subagents
`SubagentService` — provided by `@deepseek-ai/dsh-subagent`.
The `subagents` service: a registry of named SubagentProviders and a capability-checked start surface.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L144)
### ctx.subagents.registerProvider(provider)
```ts website-api
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.
- `provider` — the provider; its `name` is the registry key.
**Returns** the disposer that unregisters the provider.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L160)
### ctx.subagents.getProvider(name)
```ts website-api
getProvider(name: string): SubagentProvider | undefined
```
Look up a registered provider by name (`undefined` if absent).
- `name` — the provider name as registered.
**Returns** the provider, or undefined when the name is unknown.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L188)
### ctx.subagents.list()
```ts website-api
list(): string[]
```
The names of all registered providers (insertion order).
**Returns** the registered provider names.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L196)
### ctx.subagents.start(name, request)
```ts website-api
start(name: string, request: SubagentStartRequest): 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.
- `name` — the provider to run on.
- `request` — the child's prompt, capabilities, and options.
**Returns** the live run (its `result` resolves when the child settles).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L211)
@@ -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.systemPrompt
`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).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L291)
### ctx.systemPrompt.section(section)
```ts website-api
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.
- `section` — the section to contribute (name, order, text or provider).
**Returns** the disposer that removes the section.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L340)
### ctx.systemPrompt.tools(provider)
```ts website-api
tools(provider: () => ToolSchema[]): () => 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`.
- `provider` — evaluated at every {@link assemble} for fresh schemas.
**Returns** the disposer that removes the provider.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L373)
### ctx.systemPrompt.variable(name, provider)
```ts website-api
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.
- `name` — the reference name (matches `[a-z][a-z0-9_]*`).
- `provider` — evaluated at every {@link assemble} for the value.
**Returns** the disposer that removes the variable.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L403)
### ctx.systemPrompt.assemble(context?)
```ts website-api
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.
- `context` — what this assembly is for (defaults to an empty context; see {@link AssembleContext}).
**Returns** the assembly after the waterfall has run.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L447)
+36 -95
View File
@@ -1,122 +1,63 @@
# Tools (dsh-tools)
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
Tool 注册表和 `defineTool` DSL。
# ctx.tools
**包名:** `@deepseek-ai/dsh-tools`
**服务名:** `ctx.tools`
`ToolRegistry` — provided by `@deepseek-ai/dsh-tools`.
## ToolRegistry
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.
### ctx.tools.register(tool)
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L345)
- **tool:** `ToolDefinition`
- **返回值:** `() => void` disposer
### ctx.tools.register(definition)
注册一个 tool。返回的 disposer 可手动撤销注册(通常不需要,插件卸载时自动撤销)。
## defineTool\<S\>(options)
类型安全的 tool 定义辅助函数。
```typescript
import { defineTool } from '@deepseek-ai/dsh-tools'
const tool = defineTool({
name: 'read_file',
description: 'Read a file from disk.',
parameters: {
path: { type: 'string', required: true, description: 'Absolute file path' },
offset: { type: 'number' },
limit: { type: 'number', description: 'Max lines to read' },
},
async execute(args) {
// args: { path: string; offset?: number; limit?: number }
},
})
```ts website-api
register(definition: ToolDefinition): () => void
```
### DefineToolOptions\<S\>
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.
| 字段 | 类型 | 说明 |
|------|------|------|
| `name` | `string` | Tool 名称(全局唯一) |
| `description` | `string` | 发送给模型的描述 |
| `parameters` | `SchemaSpec` | 参数 schema(见下文) |
| `execute` | `(args: InferArgs<S>, exec: ToolExecution) => Promise<ToolExecuteReturn>` | 执行函数 |
| `presentCall?` | `(args: InferArgs<S>) => ToolCallView \| undefined` | UI 展示(纯函数) |
| `presentResult?` | `(args: InferArgs<S>, result: ToolResult) => ToolResultView \| undefined` | 结果 UI 展示(纯函数) |
- `definition` — the tool's schema plus its execute (and optional presentation) functions.
## SchemaSpec
**Returns** the disposer that unregisters the tool.
参数 schema DSL。每个属性是一个 `SchemaProp`
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L420)
```typescript
interface SchemaProp {
type: 'string' | 'number' | 'boolean' | 'object' | 'array'
required?: true
description?: string
enum?: string[]
properties?: SchemaSpec // type: 'object' 时
items?: SchemaProp // type: 'array' 时
}
### ctx.tools.get(name)
```ts website-api
get(name: string): ToolDefinition | undefined
```
### 类型推导 (InferArgs)
Look up a registered tool.
`InferArgs<S>` 自动从 `SchemaSpec` 推导 TypeScript 类型:
- `name` — the tool name as registered.
- `required: true` → 必填字段
-`required` → 可选字段(`?`
- `type: 'object'` + `properties` → 递归推导嵌套对象
- `type: 'array'` + `items` → 推导为数组
**Returns** the definition, or undefined when no tool has that name.
## ToolDefinition
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L447)
运行时 tool 定义(`defineTool` 的返回值):
### ctx.tools.schemas()
```typescript
interface ToolDefinition {
name: string
description: string
parameters: Record<string, unknown> // JSON Schema
execute(args: unknown, exec: ToolExecution): Promise<ToolExecuteReturn>
presentCall?(args: unknown): ToolCallView | undefined
presentResult?(args: unknown, result: ToolResult): ToolResultView | undefined
}
```ts website-api
schemas(): ToolSchema[]
```
## ToolExecuteReturn
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.
```typescript
type ToolExecuteReturn =
| ContentBlock[] // 仅内容
| { content: ContentBlock[]; meta?: unknown } // 内容 + 元信息
**Returns** one deep-cloned schema per registered tool, in registration order.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L462)
### ctx.tools.execute(exec)
```ts website-api
async execute(exec: ToolExecution): Promise<ToolExecutionResult>
```
## ToolArgsError
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.
当模型生成的参数不匹配 schema 时抛出:
- `exec` — the call to run (name, parsed arguments, caller agent, signal).
```typescript
class ToolArgsError extends HarnessError {
code: 'INVALID_ARGS'
violations: string[]
}
```
**Returns** the final result after every waterfall; failures resolve as `isError` results, never rejections.
框架自动捕获并转换为 `isError` 结果返回给模型。
## validateArgs(spec, args)
- **spec:** `SchemaSpec`
- **args:** `unknown`
- **返回值:** `string[]` 违规信息列表(空 = 合法)
手动校验参数。`defineTool` 内部使用,通常不需要直接调用。
## schemaSpecToJsonSchema(spec)
- **spec:** `SchemaSpec`
- **返回值:** `JsonSchemaObject`
将 SchemaSpec 转换为标准 JSON Schema。用于发送给模型的 wire format。
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L487)
@@ -0,0 +1,37 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# ctx.userInteraction
`UserInteractionService` — provided by `@deepseek-ai/dsh-user-interaction`.
`ctx.userInteraction`: one active UI provider plus an `ask()` surface.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/user-interaction/src/index.ts#L82)
### ctx.userInteraction.registerProvider(provider)
```ts website-api
registerProvider(provider: UserInteractionProvider): () => void
```
Register the UI provider. Only one provider may be active in a context.
- `provider` — UI-side implementation that collects answers.
**Returns** Disposer that unregisters this provider.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/user-interaction/src/index.ts#L95)
### ctx.userInteraction.ask(request)
```ts website-api
async ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer>
```
Ask the active UI provider and wait for the user's answer.
- `request` — Questions, owner agent, and abort signal.
**Returns** The answer chosen or typed by the human.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/user-interaction/src/index.ts#L114)
+74
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.web
`WebService` — provided by `@deepseek-ai/dsh-web`.
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 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)
### ctx.web.registerSearchProvider(provider)
```ts website-api
registerSearchProvider(provider: WebSearchProvider): () => void
```
Register a search provider. Throws WebError `WEB_DUPLICATE_PROVIDER` if its id is already registered for search. Returns a disposer; disposed with the calling fiber.
- `provider` — the provider; its `id` is the registry key.
**Returns** the disposer that unregisters the provider.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/web/web/src/index.ts#L116)
### ctx.web.registerFetchProvider(provider)
```ts website-api
registerFetchProvider(provider: WebFetchProvider): () => void
```
Register a fetch provider. Throws WebError `WEB_DUPLICATE_PROVIDER` if its id is already registered for fetch. Returns a disposer; disposed with the calling fiber.
- `provider` — the provider; its `id` is the registry key.
**Returns** the disposer that unregisters the provider.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/web/web/src/index.ts#L127)
### ctx.web.search(request, exec?)
```ts website-api
async search(request: WebSearchRequest, exec?: WebExecContext): 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.
**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)
### ctx.web.fetch(request, exec?)
```ts website-api
async fetch(request: WebFetchRequest, exec?: WebExecContext): 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.
**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)
+28
View File
@@ -0,0 +1,28 @@
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
# ctx.workflows
`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.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L210)
### ctx.workflows.start(request)
```ts website-api
abstract start(request: WorkflowStartRequest): WorkflowRun
```
Parse and execute a workflow script.
- `request` — the script, its `args`, the parent agent, and an optional cancel signal.
**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)
+23 -11
View File
@@ -1,25 +1,37 @@
# API 参考
本节提供 DeepSeek Harness 的完整 API 参考文档,分为两部分:
本节 DeepSeek Harness 的 API 参考。除本页外,`cordis/``harness/` 下的所有页面**由脚本从源码生成**(`pnpm run gen-website-api`,CI 校验新鲜度),签名与说明永远与代码一致;生成页目前为英文,中文版将随统一翻译流程提供。
## 框架 API
Cordis 微内核提供的基础能力,所有插件开发都建立在这些 API 之上:
- [Context](./cordis/context) — 上下文对象,所有服务和方法的入口
- [Events](./cordis/events) — 事件系统 APIemit / on / bail / serial / waterfall
- [Fiber](./cordis/fiber) — 作用域生命周期(状态机、effect、dispose
- [Events](./cordis/events) — 事件系统 APIon / emit / bail / serial / waterfall
- [Fiber](./cordis/fiber) — 插件生命周期(状态机、effect、dispose
- [Registry](./cordis/registry) — 插件注册(plugin / inject
- [Service](./cordis/service) — 服务基类
## Harness API
DeepSeek Harness SDK 提供的扩展 API,用于构建 Agent 能力
每个 `ctx.*` 服务一页,按服务名索引
- [Tools (dsh-tools)](./harness/tools) — Tool 注册、defineTool DSL、Schema 类型系统
- [LLM (dsh-llm)](./harness/llm) — LLM 服务、适配器注册、StreamChunk 协议
- [Session (dsh-session)](./harness/session) — 会话事件流、消息类型
- [Agent (dsh-agent)](./harness/agent) — Agent 实例管理、生命周期
- [Bash (dsh-bash)](./harness/bash) — Bash 执行接口
- [Filesystem (dsh-fs)](./harness/fs) — 文件系统接口
- [Subagent (dsh-subagent)](./harness/subagent) — 子代理委派接口
- [ctx.agentLoop](./harness/agent-loop) — ReAct 循环的创建与恢复
- [ctx.agents](./harness/agents) — Agent 注册表与工厂
- [ctx.bash](./harness/bash) — Bash 执行接口(抽象缝)
- [ctx.codeRuntime](./harness/code-runtime) — 代码执行接口(抽象缝)
- [ctx.compact](./harness/compact) — 上下文压缩接口(抽象缝)
- [ctx.fs](./harness/fs) — 文件系统接口(抽象缝)
- [ctx.llm](./harness/llm) — LLM 服务与适配器注册
- [ctx.sessionPersistence](./harness/session-persistence) — 会话持久化接口(抽象缝)
- [ctx.sessions](./harness/sessions) — 会话存储
- [ctx.subagents](./harness/subagents) — 子代理委派
- [ctx.systemPrompt](./harness/system-prompt) — 系统提示词组装
- [ctx.tools](./harness/tools) — Tool 注册表
- [ctx.userInteraction](./harness/user-interaction) — 用户交互接口
- [ctx.web](./harness/web) — Web 搜索与抓取
- [ctx.workflows](./harness/workflows) — 动态工作流引擎(抽象缝)
事件总表:[Harness events](./harness/events) — 全部事件按作用域分组,含触发模式与载荷签名。
想学"怎么写一个 tool / 插件"?教程在[开发指南](../develop/basic/);本节只做精确的接口参考。