website: fix nine review findings (generator coverage, loader facts, mode semantics)

Generator (all four structural gaps):
- harness service pages now render public properties/accessors, not just
  methods (ctx.codeRuntime.language/isolation were missing);
- the class page merges the same-named interface half, so ctx.root/baseUrl/
  events/logger/reflect/registry appear on Context (vendor root JSDoc gains
  prose alongside @experimental);
- Pick<…> heritage on a Context merge resolves to the picked class members,
  giving ctx.effect a documented signature on the Fiber page;
- {@link} tags normalize to code spans; merge sections get their own h2 so
  reflect members no longer nest under 'Static members'.

verify-website-yaml: reject the unloadable 'group:' pseudo-name (tree.import
only special-cases 'cordis:'; no builtin is registered here) and recurse into
@cordisjs/plugin-group nested entry lists instead.

Prose corrected against loader/cordis source: service.md isolation example
uses the real group plugin + group: true + the required isolate map;
config.md documents concurrent entry startup (Promise.all; order via inject)
and the real hmr defaults (root ['.'], base/ignored/debounce); events.md
fixes emit (synchronous, not parallel), bail (null/false also delegate), and
serial (stops at the first bail value).
This commit is contained in:
lintianle
2026-07-16 21:15:44 +08:00
parent 4c49677469
commit fcd9d8c391
16 changed files with 230 additions and 42 deletions

View File

@@ -70,8 +70,8 @@ interface MemberDoc {
/** A cordis-page section: which declarations it renders. */
type Section =
| { kind: 'class'; file: string; symbol: string; prefix?: string }
| { kind: 'context-merge'; file: string }
| { kind: 'class'; file: string; symbol: string; prefix?: string; heading?: string }
| { kind: 'context-merge'; file: string; heading?: string }
| { kind: 'decl'; file: string; symbol: string }
/** One generated cordis page. */
@@ -95,7 +95,7 @@ const CORDIS_PAGES: CordisPage[] = [
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' },
{ kind: 'context-merge', file: 'vendor/cordis/src/reflect.ts', heading: 'Service store and mixins' },
],
},
{
@@ -114,7 +114,7 @@ const CORDIS_PAGES: CordisPage[] = [
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: 'class', file: 'vendor/cordis/src/fiber.ts', symbol: 'Fiber', heading: 'The Fiber class' },
{ 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' },
@@ -265,14 +265,53 @@ function memberDoc(
}
}
/** Members of the `interface Context` merge in `rel`, overloads grouped. */
/** Resolve an `extends Pick<Class, 'a' | 'b'>` heritage clause on the Context
* merge to the named members of `Class` declared in the same file — the fiber
* merge (`interface Context extends Pick<Fiber, 'effect'>`) is the motivating
* case: without this, `ctx.effect` had no documented signature anywhere. */
function heritageMembers(
stmt: ts.InterfaceDeclaration,
sf: ts.SourceFile,
groups: Map<string, (ts.MethodSignature | ts.PropertySignature | ts.MethodDeclaration)[]>,
): void {
for (const clause of stmt.heritageClauses ?? []) {
for (const type of clause.types) {
if (!ts.isIdentifier(type.expression) || type.expression.text !== 'Pick') continue
const [target, keys] = type.typeArguments ?? []
if (!target || !keys || !ts.isTypeReferenceNode(target)) continue
const targetName = target.typeName.getText(sf)
const cls = sf.statements.find(
(s): s is ts.ClassDeclaration => ts.isClassDeclaration(s) && s.name?.text === targetName,
)
if (!cls) continue
const picked = new Set<string>()
const collect = (node: ts.TypeNode): void => {
if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal)) picked.add(node.literal.text)
if (ts.isUnionTypeNode(node)) node.types.forEach(collect)
}
collect(keys)
for (const member of cls.members) {
if (!ts.isMethodDeclaration(member)) continue
const name = member.name.getText(sf)
if (!picked.has(name)) continue
const group = groups.get(name) ?? []
group.push(member)
groups.set(name, group)
}
}
}
}
/** Members of the `interface Context` merge in `rel`, overloads grouped;
* `Pick<…>` heritage resolved to the picked class members. */
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)[]>()
const groups = new Map<string, (ts.MethodSignature | ts.PropertySignature | ts.MethodDeclaration)[]>()
for (const stmt of body.statements) {
if (!ts.isInterfaceDeclaration(stmt) || stmt.name.text !== 'Context') continue
heritageMembers(stmt, sf, groups)
for (const member of stmt.members) {
if (!ts.isMethodSignature(member) && !ts.isPropertySignature(member)) continue
if (ts.isComputedPropertyName(member.name)) continue
@@ -286,7 +325,10 @@ function contextMergeMembers(rel: string, violations: string[]): MemberDoc[] {
memberDoc(`ctx.${name} (${rel})`, name, group, rel, violations))
}
/** Instance + static members of one class, as two rendered lists. */
/** Instance + static members of one class, as two rendered lists. The class's
* same-named top-level interface half (declaration merging — vendor Context
* declares `root`/`events`/`logger`/… on the interface) is folded into the
* instance list, so neither half of a merged symbol goes undocumented. */
function classMembers(rel: string, className: string, violations: string[]): {
doc: string
instance: MemberDoc[]
@@ -300,7 +342,8 @@ function classMembers(rel: string, className: string, violations: string[]): {
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)[]>()
type Renderable = ts.MethodDeclaration | ts.PropertyDeclaration | ts.GetAccessorDeclaration | ts.PropertySignature
const instance = new Map<string, Renderable[]>()
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)
@@ -316,7 +359,17 @@ function classMembers(rel: string, className: string, violations: string[]): {
statics.set(name, group)
}
}
type Renderable = ts.MethodDeclaration | ts.PropertyDeclaration | ts.GetAccessorDeclaration
const iface = sf.statements.find(
(s): s is ts.InterfaceDeclaration => ts.isInterfaceDeclaration(s) && s.name.text === className,
)
for (const member of iface?.members ?? []) {
if (!ts.isPropertySignature(member)) continue
if (ts.isComputedPropertyName(member.name)) continue
const name = member.name.getText(sf)
const group = instance.get(name) ?? []
group.push(member)
instance.set(name, group)
}
const toDocs = (groups: Map<string, Renderable[]>, prefix: string): MemberDoc[] =>
[...groups.entries()].map(([name, group]) =>
memberDoc(`${prefix}${name} (${rel})`, name, group, rel, violations))
@@ -417,9 +470,12 @@ function collectHarnessServices(violations: string[]): HarnessService[] {
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[]>()
const groups = new Map<string, (ts.MethodDeclaration | ts.PropertyDeclaration | ts.GetAccessorDeclaration)[]>()
for (const member of cls.members) {
if (!ts.isMethodDeclaration(member)) continue
// Public properties are API too: ctx.codeRuntime.language/isolation
// are readonly descriptors consumers key presentation off.
const renderable = ts.isMethodDeclaration(member) || ts.isPropertyDeclaration(member) || ts.isGetAccessorDeclaration(member)
if (!renderable) continue
if (!isPublicInstance(member)) continue
const name = member.name.getText(sf)
const group = groups.get(name) ?? []
@@ -492,9 +548,19 @@ function sourceLink(source: string): string {
return `[Source](${GITHUB}/${file}#L${line})`
}
/** Render prose paragraphs (one per line of `doc`). */
/** Normalize JSDoc inline `{@link X}` / `{@link X|label}` / `{@link X label}`
* tags to plain Markdown code spans — left verbatim they leak into the built
* page as literal `{@link …}` text. */
function unlink(text: string): string {
return text.replace(/\{@link\s+([^}|\s]+)\s*(?:[|\s]\s*([^}]*))?\}/g, (_m, target: string, label?: string) => {
const name = label?.trim()
return name && name !== '' ? name : `\`${target}\``
})
}
/** Render prose paragraphs (one per line of `doc`), JSDoc links normalized. */
function prose(doc: string): string[] {
return doc.split('\n').filter(l => l.trim() !== '')
return unlink(doc).split('\n').filter(l => l.trim() !== '')
}
/** Render one member section at heading depth 3. */
@@ -507,10 +573,10 @@ function renderMember(prefix: string, m: MemberDoc): string[] {
lines.push('```', '')
lines.push(...prose(m.doc), '')
if (m.params.length > 0) {
for (const p of m.params) lines.push(`- \`${p.name}\`${p.text}`)
for (const p of m.params) lines.push(`- \`${p.name}\`${unlink(p.text)}`)
lines.push('')
}
if (m.returns) lines.push(`**Returns** ${m.returns}`, '')
if (m.returns) lines.push(`**Returns** ${unlink(m.returns)}`, '')
lines.push(sourceLink(m.source), '')
return lines
}
@@ -519,6 +585,7 @@ function renderMember(prefix: string, m: MemberDoc): string[] {
function renderCordisPage(page: CordisPage, violations: string[]): string {
const lines: string[] = [BANNER, '', `# ${page.title}`, '', page.intro, '']
for (const section of page.sections) {
if (section.kind !== 'decl' && section.heading) lines.push(`## ${section.heading}`, '')
if (section.kind === 'context-merge') {
for (const m of contextMergeMembers(section.file, violations)) {
lines.push(...renderMember('ctx.', m))
@@ -577,7 +644,7 @@ function renderEventsPage(events: HarnessEvent[]): string {
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}`)
for (const p of e.params) lines.push(`- \`${p.name}\`${unlink(p.text)}`)
lines.push('')
}
lines.push(sourceLink(e.source), '')

View File

@@ -201,8 +201,15 @@ function checkEntryList(
}
// Illustrative local plugin — nothing on disk to check against.
if (name.startsWith('./') || name.startsWith('../')) return
// Loader built-in group: its config is a nested entry list.
// A `group:`-style pseudo-name is NOT loadable: tree.import() only
// special-cases the `cordis:` prefix, and nothing in this repo registers
// loader builtins — reject it and point at the real group plugin.
if (name.startsWith('group:')) {
flag(`${at}: \`${name}\` is not loadable (no loader builtin is registered); use \`@cordisjs/plugin-group\` with \`group: true\``)
return
}
// The vendored group plugin: its config is a nested entry list.
if (name === '@cordisjs/plugin-group') {
if (Array.isArray(entry['config'])) checkEntryList(entry['config'], known, block, violations)
return
}

2
vendor/README.md vendored
View File

@@ -35,7 +35,7 @@ Keep this log exhaustive — every divergence from upstream must be listed.
3. **All `tsconfig.json` files**: regenerated to extend the repo-root `tsconfig.base.json`, emit TypeScript intermediates to `lib/types`, and declare project references.
4. **Vendored TypeScript source internal specifiers**: changed local relative imports/exports from upstream's specifier shape to explicit `.ts` specifiers so TypeScript rewrites emitted JS to `.js` while declarations keep explicit, NodeNext-safe `.ts` specifiers. This includes `loader/src/config/isolate.ts` using `declare module './entry.ts'`.
5. **`schemastery/tsdown.config.ts` and `logger-console/tsdown.config.ts`**: ours, not upstream files — per-package build-shape overrides (dual ESM+CJS output; separate node/browser entries) for the repo-root tsdown build. They read the JS emitted under `lib/types` and then write the publish runtime entries under `lib/`. Like the regenerated tsconfigs, they are not part of the upstream sync surface.
6. **`cordis/src/*.ts` JSDoc enrichment**: added `@param`/`@returns` tags and contract documentation (disposal semantics, waterfall veto, bail conditions, error cases) across the public plugin-author surface — `Context`, `EventsService`, `Fiber`, `RegistryService`, `ReflectService`, `Service`, `LoggerService` and their `declare module './context.ts'` overloads. Comment-only; no code changes. Motivation: the website API-reference generator renders these docs and hard-errors on undocumented members. Retire this entry when the enrichment is upstreamed to the fork.
6. **`cordis/src/*.ts` JSDoc enrichment**: added `@param`/`@returns` tags and contract documentation (disposal semantics, waterfall veto, bail conditions, error cases) across the public plugin-author surface — `Context` (class, statics, and the `Context` interface properties incl. `root`), `EventsService`, `Fiber`, `RegistryService`, `ReflectService`, `Service`, `LoggerService` and their `declare module './context.ts'` overloads. Comment-only; no code changes. Motivation: the website API-reference generator renders these docs and hard-errors on undocumented members. Retire this entry when the enrichment is upstreamed to the fork.
## Sync procedure

View File

@@ -18,7 +18,7 @@ export interface Context {
[symbols.isolate]: Dict<symbol>
/** Intercept map: service name → config merged into that service's per-plugin config. */
[symbols.intercept]: Dict
/** @experimental */
/** The root context of the application (every child context shares it). @experimental */
root: this
/** Base URL used to resolve relative plugin/module specifiers, if the runtime sets one. */
baseUrl?: string

View File

@@ -57,6 +57,66 @@ Plugins loaded under the returned context see `config` merged into the service's
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L139)
### ctx.root
```ts website-api
root: this
```
The root context of the application (every child context shares it). @experimental
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L22)
### ctx.baseUrl
```ts website-api
baseUrl?: string
```
Base URL used to resolve relative plugin/module specifiers, if the runtime sets one.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L24)
### ctx.events
```ts website-api
events: EventsService
```
The event bus. Its methods are also mixed onto `ctx` (`ctx.on`, `ctx.emit`, ...).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L26)
### ctx.logger
```ts website-api
logger: LoggerService
```
The logging service. Call `ctx.logger(name)` for a named logger.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L28)
### ctx.reflect
```ts website-api
reflect: ReflectService
```
The reflection layer backing the context proxy (`ctx.get`, `ctx.provide`, ...).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L30)
### ctx.registry
```ts website-api
registry: RegistryService
```
The plugin registry. Its methods are mixed onto `ctx` (`ctx.plugin`, `ctx.inject`).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L32)
## Static members
### Context.effect
@@ -114,6 +174,8 @@ Works across realms and across multiple copies of cordis, because the brand is k
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L61)
## Service store and mixins
### ctx.get(name, strict?)
```ts website-api

View File

@@ -4,6 +4,23 @@
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.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 `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)
### ctx.fiber
```ts website-api
@@ -14,6 +31,8 @@ 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)
## The Fiber class
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()`.
@@ -121,7 +140,7 @@ 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.
- `execute` — the effect body; see `Effect` for accepted shapes.
- `label` — effect label shown in `getEffects()` diagnostics.
**Returns** a disposer that tears the effect down and settles once done.
@@ -136,7 +155,7 @@ getEffects()
Return metadata for currently registered effects.
**Returns** one {@link EffectMeta} tree per labeled live effect.
**Returns** one `EffectMeta` tree per labeled live effect.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L436)

View File

@@ -16,7 +16,7 @@ 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.
- `factory` — the loop-owned factory `create`/`resume` delegate to.
**Returns** the disposer that clears the factory slot.

View File

@@ -23,7 +23,7 @@ Resolve a caller's BashExecRequest into a fully-specified BashExecSpec, applying
- `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}.
**Returns** the fully-specified spec to hand to `run`/`start`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L84)
@@ -35,7 +35,7 @@ abstract run(spec: BashExecSpec): Promise<BashRunResult>
Run a command in the foreground; resolves when it finishes.
- `spec` — a resolved spec from {@link resolve}, never a raw request.
- `spec` — a resolved spec from `resolve`, never a raw request.
**Returns** the outcome; nonzero exits, timeout kills, and abort kills resolve with a descriptive result rather than reject.
@@ -49,9 +49,9 @@ abstract start(spec: BashExecSpec): BashTask
Start a background task and return its handle immediately.
- `spec` — a resolved spec from {@link resolve}, never a raw request.
- `spec` — a resolved spec from `resolve`, never a raw request.
**Returns** the live task handle; completion fires {@link onTaskDone}.
**Returns** the live task handle; completion fires `onTaskDone`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L99)

View File

@@ -13,6 +13,26 @@ Semantics every implementation must honor:
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/code-runtime/code-runtime/src/index.ts#L59)
### ctx.codeRuntime.language
```ts website-api
abstract readonly language: string
```
The source language run expects `program` to be written in, as a lowercase identifier. Informational, not gating — a consumer that generates language-specific presentation (typed SDK stubs, usage instructions) switches on it and fails loud on a language it cannot present. Well-known value: `'typescript'`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/code-runtime/code-runtime/src/index.ts#L67)
### ctx.codeRuntime.isolation
```ts website-api
abstract readonly isolation: string
```
The execution substrate, as a lowercase identifier. Informational, not gating — a descriptor so deployments and diagnostics can tell backends apart, not a security claim. Well-known values: `'worker-thread'`, `'process'`, `'container'`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/code-runtime/code-runtime/src/index.ts#L75)
### ctx.codeRuntime.run(request)
```ts website-api

View File

@@ -381,7 +381,7 @@ A subagent run started — emitted after the provider is resolved and its capabi
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.
- `context` — the per-assembly `AssembleContext` the caller passed to `SystemPrompt.assemble` (e.g. which agent the prompt is for), so a listener can filter or extend per agent.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L38)
@@ -497,7 +497,7 @@ One `agent()` call started a child run. Paired with Events['workflow/agent-end']
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}).
- `result` — the outcome data (stop reason, error, agent count) — deliberately WITHOUT the result value (see `WorkflowResultInfo`).
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L106)

View File

@@ -72,7 +72,7 @@ Stream the whole regular text file as decoded text chunks (same text semantics a
- `target` — the resolved target to read.
- `signal` — aborts the stream, including between chunks.
**Returns** the chunk iterable, decoded and validated like {@link readText}.
**Returns** the chunk iterable, decoded and validated like `readText`.
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L221)

View File

@@ -49,7 +49,7 @@ 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.
- `session` — a `prepare`d session not yet in the store.
**Returns** the detach disposer (`onAppend = undefined` + store removal).

View File

@@ -30,7 +30,7 @@ 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.
- `provider` — evaluated at every `assemble` for fresh schemas.
**Returns** the disposer that removes the provider.
@@ -45,7 +45,7 @@ variable(name: string, provider: (context: AssembleContext) => string | undefine
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.
- `provider` — evaluated at every `assemble` for the value.
**Returns** the disposer that removes the variable.
@@ -59,7 +59,7 @@ 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}).
- `context` — what this assembly is for (defaults to an empty context; see `AssembleContext`).
**Returns** the assembly after the waterfall has run.

View File

@@ -45,7 +45,7 @@ Cordis 提供多种事件触发模式,适用于不同场景:
### emit — 广播
所有监听器并行执行,不关心返回值
同步依次调用所有监听器,不等待、不关心返回值(监听器如果是 async其 Promise 被忽略)
```ts
import type { Context } from 'cordis'
@@ -71,7 +71,7 @@ ctx.on('my-plugin/turn-end', (agentId, turnIndex) => {
### bail — 短路
依次调用监听器,第一个返回非 `undefined` 值的结果作为最终值
同步依次调用监听器,第一个返回**`undefined`/`null`/`false`** 值的监听器终止链并作为最终值(返回 `undefined`/`null`/`false` 则继续下一个)
```ts
import type { Context } from 'cordis'
@@ -99,7 +99,7 @@ ctx.on('some-check', (input) => {
### serial — 顺序执行
所有监听器按注册顺序依次执行(异步安全)
按注册顺序逐个 `await` 监听器,遇到第一个 bail 值(非 `undefined`/`null`/`false`)即停止并返回它;全部返回空值则执行到底。相当于 `bail` 的异步版
```ts
import type { Context } from 'cordis'

View File

@@ -132,11 +132,14 @@ export function apply(ctx: Context) {
## 服务隔离
`cordis.yml` 支持服务隔离——同一个服务可以有多个实例,不同插件组看到不同实例:
`cordis.yml` 支持服务隔离——同一个服务可以有多个实例,不同插件组看到不同实例。用 `@cordisjs/plugin-group` 建组(`group: true` 标记组条目),并在组上声明 `isolate`,把该服务隔离进组内作用域
```yaml
- id: group-a
name: 'group:'
name: '@cordisjs/plugin-group'
group: true
isolate:
bash: true
config:
- name: '@deepseek-ai/dsh-bash-local'
config:
@@ -144,7 +147,10 @@ export function apply(ctx: Context) {
- name: './src/plugin-a.ts'
- id: group-b
name: 'group:'
name: '@cordisjs/plugin-group'
group: true
isolate:
bash: true
config:
- name: '@deepseek-ai/dsh-bash-local'
config:
@@ -152,7 +158,7 @@ export function apply(ctx: Context) {
- name: './src/plugin-b.ts'
```
`plugin-a``plugin-b` 各自看到自己组内的 bash 实例,互不影响。
`plugin-a``plugin-b` 各自看到自己组内的 bash 实例,互不影响。`isolate: { bash: true }` 是必需的:不隔离的话,两个组在同一作用域注册同名服务,第二个会直接报重复注册错误。
## Harness 内置服务一览

View File

@@ -332,7 +332,12 @@ config:
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `root` | string[] | **必填** | 监听文件变更的目录列表 |
| `root` | string[] | `['.']` | 监听文件变更的目录列表 |
| `base` | string | — | 解析 `root` 的基准目录(默认取配置文件所在目录) |
| `ignored` | string[] | `['**/node_modules', '**/.*', 'cache', 'data']` | 忽略的 glob 列表 |
| `debounce` | number | `100` | 变更合并窗口(毫秒) |
其余字段透传给 chokidar`Config` 继承 `ChokidarOptions`)。
::: tip
hmr 仅用于开发环境。它需要 `node --expose-internals` 启动参数,`demo:*` 脚本已自动添加。
@@ -342,7 +347,9 @@ hmr 仅用于开发环境。它需要 `node --expose-internals` 启动参数,`
## 加载顺序
`cordis.yml`顺序就是加载顺序。推荐:
`cordis.yml`条目是**并发启动**的loader 对全部条目 `Promise.all`),文件顺序不决定加载顺序。真正的先后关系由依赖协调:插件声明的 `inject` 服务就绪之前,插件不会启动;服务出现后自动继续。所以**不要依赖书写顺序传递时序**——需要"先有 A 再有 B"就让 B `inject` A 提供的服务。
文件顺序只是给人读的。推荐按角色分组书写:
1. **hmr** — 热替换(仅开发时需要)
2. **LLM 适配器** — 模型后端