mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge pull request #270 from deepseek-harness/worktree/docs-website
feat(docs): build maintainable documentation site
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# Agent Note: Project canonical documentation into the website
|
||||
|
||||
Status: implemented
|
||||
|
||||
## Problem
|
||||
|
||||
The repository needs a navigable documentation website without turning the website directory into a second documentation source. Copying package guides, architecture pages, or generated catalogs into a site-specific tree allows the two copies to drift, while pointing VitePress directly at the repository root couples public URLs and navigation to the internal file layout. Repository-relative links also need different destinations on the website: published pages stay inside the site, but source files and unpublished contributor documents belong on GitHub.
|
||||
|
||||
## Decision
|
||||
|
||||
Canonical Markdown remains in the repository tier that owns it. Product-facing guides live under `docs/user/`, generated reference remains in the existing generated catalogs, and architectural and cookbook pages remain at their existing `docs/` paths.
|
||||
|
||||
`website/docs.ts` is an explicit publication manifest. Each entry maps one canonical source file to a stable public route, sidebar, section, and order. Adding or removing a published page is therefore a reviewable manifest change rather than an implicit directory crawl.
|
||||
|
||||
`scripts/project-doc-site.ts` projects the manifest into the ignored `website/.generated/` directory before VitePress starts or builds. The generated tree follows public routes so VitePress navigation, locale detection, and local search share the same route vocabulary. Each page receives an `editSource` frontmatter field pointing to its canonical repository file; the edit-link callback reads only that page data, so public URLs remain independent of the source layout.
|
||||
|
||||
Locale home projections retain only the canonical YAML frontmatter. The repository-facing body can keep its H1 and bilingual source links, while the VitePress home theme owns the rendered hero and features and the site navigation owns locale switching.
|
||||
|
||||
The projector parses Markdown links without reserializing the document. A link to another published source becomes a site-relative route; a link to an unpublished repository file becomes a GitHub source link; a repository image becomes a raw GitHub URL. Missing relative targets fail projection. Unit tests pin these transformations, and `docs:check` runs the projector tests plus a production VitePress build as part of `doc-sync` and the parallel documentation gates.
|
||||
|
||||
Mermaid renders the canonical diagrams. The website workspace explicitly declares the five packages that `vitepress-plugin-mermaid` asks Vite to prebundle because pnpm's strict dependency isolation otherwise makes those transitive packages unavailable to the local development server; Knip records this runtime-only use as an intentional dependency exception.
|
||||
|
||||
Site publication is separate from site construction. The repository contains local development and build commands, but no hosting or deployment workflow until a public destination is chosen.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Commit copied Markdown under `website/`.** This makes VitePress setup direct, but every copied guide or API table gains two owners and requires a synchronization convention that cannot identify which copy is authoritative.
|
||||
|
||||
**Make `website/` the canonical home for every published page.** This keeps one copy but moves architecture, generated reference, and contributor-facing material away from their repository ownership tiers merely to satisfy a renderer.
|
||||
|
||||
**Discover every Markdown file automatically.** This minimizes manifest maintenance but publishes internal documents accidentally, exposes source moves as URL changes, and produces navigation from incidental directory order.
|
||||
|
||||
**Use filesystem symlinks.** Symlinks preserve a single source but do not solve public routing or repository-relative links, and their behavior is less predictable across local development, package tooling, and hosted CI environments.
|
||||
|
||||
**Build only in a deployment workflow.** A deployment job can reveal rendering failures after merge. Keeping the production build in `doc-sync` makes the same failure visible locally and in ordinary CI even when no public deployment exists.
|
||||
|
||||
## Consequences
|
||||
|
||||
Documentation facts have one editable home, public routes remain stable across source moves, and the site can include generated references without committing another generated copy. Local development watches canonical inputs and regenerates the disposable projection.
|
||||
|
||||
The publication manifest is a maintained allowlist, and link projection adds a small repository-specific build adapter. A new kind of Markdown link behavior needs a projector test. Mermaid support also increases the client bundle size, but preserves diagrams already used by the canonical documentation.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-20-generated-cordis-core-api.md: 848dec2dba6f432c706798c40abe98e8937da651
|
||||
2026-07-20-generated-cordis-core-api.zh.md: c40a480224f4e1387b71ade9264458cd84403584
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Generate the Cordis core API reference
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-20-generated-cordis-core-api.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Plugin authors need the detailed Cordis APIs behind `ctx`, event dispatch, fibers, plugin registration, and services. The generated [Harness event and service catalogs](2026-06-20-generated-cordis-catalog.md) intentionally summarize inherited Cordis members, so they do not replace a method-level Cordis reference. Keeping a second hand-written copy under the website would drift from the vendored source and make the renderer an additional documentation owner.
|
||||
|
||||
## Decision
|
||||
|
||||
`scripts/cordis-core-api.ts` reads the public declarations and original JSDoc from `vendor/cordis/src` with the TypeScript compiler API. An explicit page manifest generates five files under [`docs/cordis-catalog/core/`](../../../../docs/cordis-catalog/core/context.md): Context, Events, Fiber, Registry, and Service. `scripts/gen-cordis-catalog.ts` writes these pages together with the Harness event and service catalogs, and `verify-cordis-catalog` rejects stale output.
|
||||
|
||||
The generator validates that documented classes and methods retain descriptive JSDoc, including parameter and non-void return contracts. It emits declaration-only `ts cordis-catalog` fences with the original JSDoc, then renders the same description, parameters, and return contract as readable Markdown. Source links point to the vendored files, and the five pages cross-link to one another. The Harness catalogs remain the exhaustive inventory of repository-declared events and `ctx.*` services; the core pages document how the inherited Cordis APIs operate.
|
||||
|
||||
`website/docs.ts` publishes the five canonical files under matching `/reference/cordis-api/` and `/en/reference/cordis-api/` routes. Both locales use the English generated source until the generator emits translated pages, so changing language preserves navigation structure and route identity.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Restore the old website files as canonical Markdown.** This would recover the pages quickly, but their signatures and prose could drift from the vendored implementation and the website would regain a second documentation source.
|
||||
|
||||
**Expand the inherited tier of the Harness catalogs in place.** Those catalogs answer which Harness events and services exist. Mixing full framework class references into the same pages would obscure that inventory and reverse their deliberate terse inherited tier.
|
||||
|
||||
**Publish vendored source declarations directly.** Source files are authoritative but do not provide stable topic pages, curated public ordering, or website navigation, and they expose implementation bodies that are not part of the reference contract.
|
||||
|
||||
## Consequences
|
||||
|
||||
The five Cordis API pages follow vendor updates through one deterministic generator and share the repository's documentation freshness gate. The website gains a dedicated Cordis API section without copied site content, while root and English navigation remain structurally identical.
|
||||
|
||||
The page manifest is curated, so a newly public Cordis core type needs an explicit generator entry. Generated prose is English-only, and source JSDoc quality directly limits reference quality; Chinese output requires generator-level translation rather than hand-editing the generated files.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: 生成 Cordis 核心 API 参考文档
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-20-generated-cordis-core-api.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
插件作者需要了解 `ctx`、事件派发、Fiber、插件注册和 Service 背后的详细 Cordis API。已有的 [Harness 事件与服务目录](2026-06-20-generated-cordis-catalog.md)有意只简要概括继承自 Cordis 的成员,因此无法替代方法级 Cordis 参考文档。如果在网站下维护另一份手写副本,它会与 vendored 源码产生漂移,也会让渲染器成为额外的文档所有者。
|
||||
|
||||
## 决策
|
||||
|
||||
`scripts/cordis-core-api.ts` 使用 TypeScript Compiler API,从 `vendor/cordis/src` 读取公开声明和原始 JSDoc。一个显式页面清单在 [`docs/cordis-catalog/core/`](../../../../docs/cordis-catalog/core/context.md) 下生成五个文件:Context、Events、Fiber、Registry 和 Service。`scripts/gen-cordis-catalog.ts` 将这些页面与 Harness 事件和服务目录一同写入,`verify-cordis-catalog` 会拒绝过期产物。
|
||||
|
||||
生成器会验证所记录的类和方法保留描述性 JSDoc,包括参数和非 void 返回值契约。它生成包含原始 JSDoc 且仅含声明的 `ts cordis-catalog` 代码围栏,再将同一份说明、参数和返回值契约渲染为便于阅读的 Markdown。源码链接指向 vendored 文件,五个页面之间相互交叉链接。Harness 目录仍是仓库声明的事件与 `ctx.*` 服务的完整清单;核心页面负责说明继承自 Cordis 的 API 如何工作。
|
||||
|
||||
`website/docs.ts` 将五个规范源文件发布到结构对应的 `/reference/cordis-api/` 和 `/en/reference/cordis-api/` 路由。在生成器产出翻译页面之前,两个 locale 都使用英文生成源,因此切换语言时导航结构和路由标识保持不变。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**将旧网站文件恢复为规范 Markdown。** 这能快速恢复页面,但其签名和说明可能与 vendored 实现漂移,网站也会重新成为第二个文档来源。
|
||||
|
||||
**直接扩充 Harness 目录中的继承层。** 这些目录回答有哪些 Harness 事件与服务。将完整的框架类参考混入同一页面会模糊这份清单的定位,并推翻继承层保持精简的既有决定。
|
||||
|
||||
**直接发布 vendored 源码声明。** 源文件具有权威性,但不能提供稳定的主题页面、经过筛选的公开顺序或网站导航,还会暴露不属于参考契约的实现体。
|
||||
|
||||
## 影响
|
||||
|
||||
五个 Cordis API 页面通过同一个确定性生成器跟随 vendor 更新,并复用仓库的文档新鲜度检查。网站无需复制内容即可获得独立的 Cordis API 章节,中文入口和英文入口的导航结构保持一致。
|
||||
|
||||
页面清单需要人工维护,因此新增公开 Cordis 核心类型时必须显式添加生成器条目。当前生成说明只有英文,且源码 JSDoc 的质量直接决定参考文档质量;中文产物需要在生成器层实现翻译,不能手工编辑生成文件。
|
||||
82
.agents/skills/dsh-doc-site-sync/SKILL.md
Normal file
82
.agents/skills/dsh-doc-site-sync/SKILL.md
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
name: dsh-doc-site-sync
|
||||
description: Use when publishing, updating, moving, or removing DeepSeek Harness documentation website pages; editing website/docs.ts mappings or navigation; diagnosing a page missing from the VitePress site; fixing projected documentation links; or running the docs:dev, docs:check, and doc-sync workflow after website-content changes.
|
||||
---
|
||||
|
||||
# Synchronizing the DeepSeek Harness Documentation Site
|
||||
|
||||
Keep repository Markdown as the only editable content source. Treat the website as a tested projection: [website/docs.ts](../../../website/docs.ts) selects public pages, [scripts/project-doc-site.ts](../../../scripts/project-doc-site.ts) rewrites them into the disposable `website/.generated/` tree, and VitePress builds that tree.
|
||||
|
||||
Repository translations follow the sibling pairing contract: English `foo.md`, Chinese `foo.zh.md`, and `foo.i18n.yaml` live together. Never create `zh-CN/` or other locale directories for website content. The site route trees are independent of that source layout: `foo.zh.md` projects to the root route and `foo.md` projects to the matching `/en/` route.
|
||||
|
||||
## Read the owning contracts
|
||||
|
||||
- Read [docs/AGENTS.md](../../../docs/AGENTS.md) and use [dsh-doc-standards](../dsh-doc-standards/SKILL.md) when deciding where content belongs or changing product documentation prose.
|
||||
- Use [dsh-translate-docs](../dsh-translate-docs/SKILL.md) whenever an edited source has a bilingual counterpart.
|
||||
- Read the current `DocsPage` type and entries in [website/docs.ts](../../../website/docs.ts) before changing the manifest; do not rely on a remembered field set.
|
||||
- Read [website/.vitepress/config.ts](../../../website/.vitepress/config.ts) before adding a new section, sidebar collection, locale, or top-level navigation item.
|
||||
|
||||
## Classify the change
|
||||
|
||||
- **Edit an already published page:** change only its canonical Markdown source. Do not touch the manifest unless its route or navigation metadata changes.
|
||||
- **Publish a new page:** create it in its owning `docs/` tier, then add one manifest entry.
|
||||
- **Rename, move, or remove a page:** update the canonical file, manifest entry, and inbound repository links atomically. Remove stale manifest entries; `docs:check` rejects missing sources.
|
||||
- **Publish a generated catalog:** map the generated `docs/` file, but change its generator or source metadata rather than editing the catalog by hand.
|
||||
- **Change site structure:** update the manifest for ordinary pages; update VitePress configuration only when the existing sidebar, section, or locale model cannot express the change.
|
||||
|
||||
Never edit or commit `website/.generated/`, `website/.cache/`, or `website/.dist/`. Never copy a maintained `docs/` page into `website/`.
|
||||
|
||||
## Add or update a manifest entry
|
||||
|
||||
Set every `DocsPage` field deliberately:
|
||||
|
||||
- `source`: repository-relative canonical Markdown path. For a complete bilingual pair, add the English `.md` path through `pairedPages()`; it derives the sibling `.zh.md`, the content locales, and counterpart aliases.
|
||||
- `route`: public VitePress path including the `.md` suffix.
|
||||
- `label`: sidebar label, not necessarily the document H1.
|
||||
- `sidebar`: reuse `zh-guide`, `zh-develop`, or `en-docs` unless the information architecture genuinely needs another collection.
|
||||
- `section`: reuse an existing section when possible. If adding one, also place it in `sectionOrder` in the VitePress config.
|
||||
- `order`: stable order within the section.
|
||||
- `sourceAliases`: optional additional repository paths that should resolve to this page when links are projected. It does not create another public route.
|
||||
|
||||
Use `mirroredPages()` only for a source that intentionally falls back to the same available language in both route trees. Convert that entry to `pairedPages()` when its counterpart is added. Keep the manifest an explicit public allowlist. Do not publish RFCs, postmortems, testing guides, `AGENTS.md`, or maintainer workflows merely because they exist under `docs/`; add internal material only when the user explicitly changes the publication boundary.
|
||||
|
||||
## Preserve link behavior
|
||||
|
||||
Write normal repository-relative Markdown links in canonical docs. The projector applies these rules:
|
||||
|
||||
- A target present in the manifest becomes a site-relative route.
|
||||
- An existing target outside the manifest becomes a GitHub source link, including supported line suffixes.
|
||||
- External URLs, site-absolute URLs, email links, and fragment-only links remain unchanged.
|
||||
- A missing repository-relative target fails projection instead of silently producing a broken link.
|
||||
|
||||
Do not write website-specific routes into canonical Markdown just to satisfy VitePress. Use `sourceAliases` for directory-style repository links that should resolve to a mapped index page.
|
||||
|
||||
## Preview and validate
|
||||
|
||||
Run local preview while editing:
|
||||
|
||||
```sh
|
||||
pnpm docs:dev
|
||||
```
|
||||
|
||||
The dev server watches mapped source files and reprojects them. Restart it after changing the manifest if the new source is not picked up automatically.
|
||||
|
||||
Run the focused website gate before treating the mapping as valid:
|
||||
|
||||
```sh
|
||||
pnpm docs:check
|
||||
```
|
||||
|
||||
Before committing a documentation-site change, run:
|
||||
|
||||
```sh
|
||||
pnpm run doc-sync
|
||||
pnpm run lint
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Use [dsh-pre-push-checks](../dsh-pre-push-checks/SKILL.md) before pushing. Report the canonical files changed, manifest entries added or removed, public routes affected, and the exact checks run.
|
||||
|
||||
## Keep deployment separate
|
||||
|
||||
Synchronizing content into the VitePress build does not publish it to the internet. Do not add GitHub Pages permissions, deployment workflows, custom domains, or public hosting unless the user explicitly requests deployment and confirms the hosting policy.
|
||||
4
.agents/skills/dsh-doc-site-sync/agents/openai.yaml
Normal file
4
.agents/skills/dsh-doc-site-sync/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "DSH Documentation Site Sync"
|
||||
short_description: "Publish repository docs through the DSH website manifest"
|
||||
default_prompt: "Use $dsh-doc-site-sync to publish or update a DeepSeek Harness documentation page on the website."
|
||||
@@ -37,7 +37,7 @@ examples/ Runnable cordis.yml leaves over packages/examples bundles (see exam
|
||||
.agents/ Agent workflows and Agent Notes (`notes/`)
|
||||
docs/ architecture, generated catalogs, postmortems, cookbook (see docs/AGENTS.md)
|
||||
scripts/ repo gates and generators
|
||||
website/ VitePress docs site (zh-CN); api/ pages generated from source
|
||||
website/ VitePress projection of selected bilingual docs/ sources
|
||||
```
|
||||
|
||||
Package groups: [packages/README.md](packages/README.md).
|
||||
|
||||
@@ -15,9 +15,10 @@ Each fact has one home: the tier whose job it is. Elsewhere, link to that home;
|
||||
| [Agent Notes](../.agents/notes/README.md) | Decision records: the why, what-was-given-up, and concise verification contract; `implemented/` notes describe shipped reality in present tense | Migration plans, acceptance-task checklists, fixture walkthroughs, and spec-speak ("should…") once the decision has shipped |
|
||||
| [postmortem/](postmortem/README.md) | Incident stories — the only tier where war-story narrative belongs | — |
|
||||
| [cookbook/](cookbook/adding-a-package.md) | Step-by-step how-tos with numbered verify steps | Design rationale (→ the Agent Note each guide links) |
|
||||
| [user/](user/index.md) | Product-facing guides published by the documentation website | Generated reference tables, contributor procedures, decision history |
|
||||
| Package README | The per-package contract: config, semantics, limitations, extension points, and [Model Experience](cookbook/adding-a-package.md#4-write-the-package-readme) | JSDoc restatement, generated-catalog restatement (event/tool tables), other packages' concerns |
|
||||
| [development.md](development.md) | First-stop contributor onboarding: local setup, daily workflow, and CI shape at summary level; a bilingual pair under the [i18n contract](i18n/README.md) | Runtime/version rationale (→ Agent Notes), gate-by-gate enumerations that drift from `package.json` scripts |
|
||||
| Generated catalogs: [cordis events](cordis-catalog/events.md), [cordis services](cordis-catalog/services.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive enumerations regenerated from source, freshness-gated | Hand edits of any kind |
|
||||
| Generated catalogs: [cordis events](cordis-catalog/events.md), [cordis services](cordis-catalog/services.md), [Cordis core API](cordis-catalog/core/context.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive enumerations regenerated from source, freshness-gated | Hand edits of any kind |
|
||||
| Skills (`.agents/skills/`) | Reusable workflows and specialized decision standards | Product and runtime contracts (→ docs or source) |
|
||||
|
||||
Placement: bugs → postmortems; rationale → Agent Notes; procedures → cookbooks; type shapes → core data; package contracts → READMEs; standing orders → root `AGENTS.md` with a rationale link.
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.
|
||||
Run `pnpm run gen-cordis-catalog` to regenerate. -->
|
||||
|
||||
# Context
|
||||
|
||||
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).
|
||||
The context is the core Cordis object: every service, event, and lifecycle API is reached through `ctx`. Event methods are documented on [Events](events.md), effects and the current fiber on [Fiber](fiber.md), and plugin loading 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.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L42)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L42)
|
||||
|
||||
### ctx.extend(meta?)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Create a child context with extra metadata on top of the current scope.
|
||||
*
|
||||
@@ -25,17 +27,18 @@ extend(meta = {}): this
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
- `meta` — own properties (including symbol keys) to define on the child.
|
||||
|
||||
**Returns** a child context inheriting from this one.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L99)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L99)
|
||||
|
||||
### ctx.isolate(name, label?)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Create a child context with an independent service scope for `name`.
|
||||
*
|
||||
@@ -52,6 +55,7 @@ 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.
|
||||
|
||||
- `name` — the service name to isolate.
|
||||
@@ -59,11 +63,11 @@ Below the returned context, reads and writes of the service `name` resolve again
|
||||
|
||||
**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)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L121)
|
||||
|
||||
### ctx.intercept(name, config)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Add service-specific intercept config for plugins started below this
|
||||
* context.
|
||||
@@ -81,6 +85,7 @@ 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.
|
||||
@@ -88,123 +93,123 @@ Plugins loaded under the returned context see `config` merged into the service's
|
||||
|
||||
**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)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L139)
|
||||
|
||||
### ctx.root
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The root context of the application (every child context shares it). @experimental */
|
||||
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)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L22)
|
||||
|
||||
### ctx.baseUrl
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Base URL used to resolve relative plugin/module specifiers, if the runtime sets one. */
|
||||
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)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L24)
|
||||
|
||||
### ctx.events
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The event bus. Its methods are also mixed onto `ctx` (`ctx.on`, `ctx.emit`, ...). */
|
||||
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)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L26)
|
||||
|
||||
### ctx.logger
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The logging service. Call `ctx.logger(name)` for a named logger. */
|
||||
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)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L28)
|
||||
|
||||
### ctx.reflect
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The reflection layer backing the context proxy (`ctx.get`, `ctx.provide`, ...). */
|
||||
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)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L30)
|
||||
|
||||
### ctx.registry
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The plugin registry. Its methods are mixed onto `ctx` (`ctx.plugin`, `ctx.inject`). */
|
||||
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)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L32)
|
||||
|
||||
## Static members
|
||||
|
||||
### Context.effect
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key under which a disposer exposes its {@link EffectMeta} diagnostics tree. */
|
||||
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)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L44)
|
||||
|
||||
### Context.filter
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key for a context's listener filter, consulted on every event dispatch. */
|
||||
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)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L46)
|
||||
|
||||
### Context.isolate
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key of the isolation map (see the `Context[symbols.isolate]` property). */
|
||||
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)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L48)
|
||||
|
||||
### Context.intercept
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key of the intercept map (see the `Context[symbols.intercept]` property). */
|
||||
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)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L50)
|
||||
|
||||
### Context.is(value)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Returns true for Cordis context proxies and context prototypes.
|
||||
*
|
||||
@@ -218,19 +223,20 @@ 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)
|
||||
[Source](../../../vendor/cordis/src/context.ts#L61)
|
||||
|
||||
## Service store and mixins
|
||||
|
||||
### ctx.get(name, strict?)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Read a service from the store without the inject requirement.
|
||||
*
|
||||
@@ -250,11 +256,11 @@ Read a service from the store without the inject requirement.
|
||||
|
||||
**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)
|
||||
[Source](../../../vendor/cordis/src/reflect.ts#L16)
|
||||
|
||||
### ctx.set(name, value)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Overwrite a provided service's value.
|
||||
*
|
||||
@@ -269,16 +275,17 @@ 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.
|
||||
|
||||
- `name` — the service name.
|
||||
- `value` — the new service value.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/reflect.ts#L28)
|
||||
[Source](../../../vendor/cordis/src/reflect.ts#L28)
|
||||
|
||||
### ctx.provide(name, value)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Register a service implementation owned by the current fiber.
|
||||
*
|
||||
@@ -296,6 +303,7 @@ provide(name: string, value?: any): () => void
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
- `name` — the service name.
|
||||
@@ -303,11 +311,11 @@ The service becomes visible to dependents in the same isolation scope once the f
|
||||
|
||||
**Returns** a disposer that unregisters the service.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/reflect.ts#L43)
|
||||
[Source](../../../vendor/cordis/src/reflect.ts#L43)
|
||||
|
||||
### ctx.accessor(name, options)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Define a computed context property backed by get/set hooks.
|
||||
*
|
||||
@@ -321,16 +329,17 @@ 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)
|
||||
[Source](../../../vendor/cordis/src/reflect.ts#L55)
|
||||
|
||||
### ctx.mixin(name, mixins)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Expose selected members of a service directly on `ctx`.
|
||||
*
|
||||
@@ -346,9 +355,10 @@ mixin<T extends {}>(source: T, mixins: (keyof this & keyof T)[] | Dict<string>):
|
||||
```
|
||||
|
||||
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)
|
||||
[Source](../../../vendor/cordis/src/reflect.ts#L66)
|
||||
@@ -1,12 +1,13 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.
|
||||
Run `pnpm run gen-cordis-catalog` to regenerate. -->
|
||||
|
||||
# Events
|
||||
|
||||
The event system mixed into every context. Harness-defined events are cataloged on [Harness events](../harness/events.md).
|
||||
The event-dispatch API mixed into every context. Harness event declarations and their dispatch modes are generated separately in the [Cordis events catalog](../events.md).
|
||||
|
||||
### ctx.parallel(name, ...args)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Dispatch an event, running all listeners concurrently.
|
||||
*
|
||||
@@ -25,11 +26,11 @@ Dispatch an event, running all listeners concurrently.
|
||||
|
||||
**Returns** a promise resolving once every listener has settled.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L43)
|
||||
[Source](../../../vendor/cordis/src/events.ts#L43)
|
||||
|
||||
### ctx.emit(name, ...args)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Dispatch an event synchronously, ignoring listener return values.
|
||||
*
|
||||
@@ -45,11 +46,11 @@ Dispatch an event synchronously, ignoring listener return values.
|
||||
- `name` — the event name.
|
||||
- `args` — arguments passed to every listener.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L52)
|
||||
[Source](../../../vendor/cordis/src/events.ts#L52)
|
||||
|
||||
### ctx.serial(name, ...args)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Dispatch an event, awaiting listeners in order until one bails.
|
||||
*
|
||||
@@ -68,11 +69,11 @@ Dispatch an event, awaiting listeners in order until one bails.
|
||||
|
||||
**Returns** the first bail value (non-null, non-false, non-undefined), if any.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L62)
|
||||
[Source](../../../vendor/cordis/src/events.ts#L62)
|
||||
|
||||
### ctx.bail(name, ...args)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Dispatch an event, calling listeners in order until one bails.
|
||||
*
|
||||
@@ -91,11 +92,11 @@ Dispatch an event, calling listeners in order until one bails.
|
||||
|
||||
**Returns** the first bail value (non-null, non-false, non-undefined), if any.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L72)
|
||||
[Source](../../../vendor/cordis/src/events.ts#L72)
|
||||
|
||||
### ctx.waterfall(name, ...args)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Dispatch an event whose last argument is a `next` continuation.
|
||||
*
|
||||
@@ -111,6 +112,7 @@ waterfall<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
- `name` — the event name.
|
||||
@@ -118,11 +120,11 @@ Each listener wraps the rest of the chain: calling `next()` invokes the next lis
|
||||
|
||||
**Returns** the outermost listener's return value.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L85)
|
||||
[Source](../../../vendor/cordis/src/events.ts#L85)
|
||||
|
||||
### ctx.on(name, listener, options?)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Register an event listener owned by the current fiber.
|
||||
*
|
||||
@@ -142,11 +144,11 @@ Register an event listener owned by the current fiber.
|
||||
|
||||
**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)
|
||||
[Source](../../../vendor/cordis/src/events.ts#L96)
|
||||
|
||||
### ctx.once(name, listener, options?)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Same as `on()`, but the listener disposes itself after its first call.
|
||||
*
|
||||
@@ -166,13 +168,13 @@ Same as `on()`, but the listener disposes itself after its first call.
|
||||
|
||||
**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)
|
||||
[Source](../../../vendor/cordis/src/events.ts#L105)
|
||||
|
||||
## EventOptions
|
||||
|
||||
Options accepted by `ctx.on()` and `ctx.once()`.
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Options accepted by `ctx.on()` and `ctx.once()`. */
|
||||
interface EventOptions {
|
||||
/** Add the listener before existing listeners for the same event. */
|
||||
@@ -182,14 +184,15 @@ interface EventOptions {
|
||||
}
|
||||
```
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L111)
|
||||
[Source](../../../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
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Event dispatch strategy used by the event service.
|
||||
*
|
||||
@@ -201,4 +204,4 @@ Event dispatch strategy used by the event service.
|
||||
type DispatchMode = 'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall'
|
||||
```
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L31)
|
||||
[Source](../../../vendor/cordis/src/events.ts#L31)
|
||||
@@ -1,12 +1,13 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.
|
||||
Run `pnpm run gen-cordis-catalog` to regenerate. -->
|
||||
|
||||
# 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.
|
||||
A fiber is one loaded plugin instance: its lifecycle state, validated config, and registered effects. `ctx.fiber` is the current fiber, and `ctx.effect()` delegates to it.
|
||||
|
||||
### ctx.effect(execute, label?)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Register a cleanup-aware effect on this fiber.
|
||||
*
|
||||
@@ -25,6 +26,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 `Effect` for accepted shapes.
|
||||
@@ -32,117 +34,118 @@ Register a cleanup-aware effect on this fiber.
|
||||
|
||||
**Returns** a disposer that tears the effect down and settles once done.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L419)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L419)
|
||||
|
||||
### ctx.fiber
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The fiber (plugin runtime instance) that owns this context. */
|
||||
fiber: Fiber
|
||||
```
|
||||
|
||||
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)
|
||||
[Source](../../../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()`.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L183)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L183)
|
||||
|
||||
### fiber.uid
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Unique id within the registry; 0 for the root fiber, `null` once disposed. */
|
||||
public uid: number | null
|
||||
```
|
||||
|
||||
Unique id within the registry; 0 for the root fiber, `null` once disposed.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L185)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L185)
|
||||
|
||||
### fiber.ctx
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The context this fiber's plugin runs in (extends the parent context). */
|
||||
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#L187)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L187)
|
||||
|
||||
### fiber.config
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The validated plugin config (updated by `update()`). */
|
||||
public config: any
|
||||
```
|
||||
|
||||
The validated plugin config (updated by `update()`).
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L189)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L189)
|
||||
|
||||
### fiber.state
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Current lifecycle state; transitions emit `internal/status`. */
|
||||
public state
|
||||
```
|
||||
|
||||
Current lifecycle state; transitions emit `internal/status`.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L191)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L191)
|
||||
|
||||
### fiber.dispose
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Dispose this fiber: unload the plugin, then settle once cleanup finished. */
|
||||
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#L193)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L193)
|
||||
|
||||
### fiber.store
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Snapshot of required service implementations while loaded; `undefined` otherwise. */
|
||||
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#L195)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L195)
|
||||
|
||||
### fiber.inertia
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The in-flight load/unload transition, if one is currently running. */
|
||||
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#L197)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L197)
|
||||
|
||||
### fiber.name
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The plugin's display name, inherited from the nearest named ancestor, else `'root'`. */
|
||||
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#L340)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L340)
|
||||
|
||||
### fiber.assertActive()
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Throw if the fiber has already been disposed.
|
||||
*
|
||||
@@ -156,11 +159,11 @@ 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#L355)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L355)
|
||||
|
||||
### fiber.effect(execute, label?)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Register a cleanup-aware effect on this fiber.
|
||||
*
|
||||
@@ -179,6 +182,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 `Effect` for accepted shapes.
|
||||
@@ -186,11 +190,11 @@ Register a cleanup-aware effect on this fiber.
|
||||
|
||||
**Returns** a disposer that tears the effect down and settles once done.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L419)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L419)
|
||||
|
||||
### fiber.getEffects()
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Return metadata for currently registered effects.
|
||||
*
|
||||
@@ -203,11 +207,11 @@ Return metadata for currently registered effects.
|
||||
|
||||
**Returns** one `EffectMeta` tree per labeled live effect.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L572)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L572)
|
||||
|
||||
### fiber.await()
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Wait for current lifecycle work and rethrow startup errors.
|
||||
*
|
||||
@@ -221,11 +225,11 @@ 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#L701)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L701)
|
||||
|
||||
### fiber.restart()
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Dispose and immediately reload this plugin with its current config.
|
||||
*
|
||||
@@ -239,11 +243,11 @@ Dispose and immediately reload this plugin with its current config.
|
||||
|
||||
**Returns** a promise resolving once the reload settled.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L715)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L715)
|
||||
|
||||
### fiber.update(config, noSave?)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Validate and apply new config, then restart the plugin.
|
||||
*
|
||||
@@ -259,6 +263,7 @@ 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.
|
||||
@@ -266,14 +271,15 @@ Runs the `internal/update` waterfall first, so update hooks (and HMR) can veto o
|
||||
|
||||
**Returns** nothing; the restart runs behind the `internal/update` waterfall.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L733)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L733)
|
||||
|
||||
## 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
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Effect body result accepted by `ctx.effect()` and plugin startup.
|
||||
*
|
||||
@@ -286,14 +292,15 @@ type Effect<T = any> =
|
||||
| AsyncEffect<T>
|
||||
```
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L82)
|
||||
[Source](../../../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
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Function returned by an effect to release resources during disposal.
|
||||
*
|
||||
@@ -303,13 +310,13 @@ Disposers run in reverse registration order when the owning fiber unloads; they
|
||||
type Disposable<T = any> = () => T
|
||||
```
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L73)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L73)
|
||||
|
||||
## EffectMeta
|
||||
|
||||
Tree node used to expose nested effect labels for diagnostics.
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Tree node used to expose nested effect labels for diagnostics. */
|
||||
interface EffectMeta {
|
||||
/** Human-readable effect label, e.g. `ctx.on("event")` or `ctx.provide("name")`. */
|
||||
@@ -319,13 +326,13 @@ interface EffectMeta {
|
||||
}
|
||||
```
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L95)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L95)
|
||||
|
||||
## CordisError
|
||||
|
||||
Framework error with a stable machine-readable code.
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Framework error with a stable machine-readable code. */
|
||||
class CordisError extends Error {
|
||||
/**
|
||||
@@ -345,13 +352,13 @@ namespace CordisError {
|
||||
}
|
||||
```
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L156)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L156)
|
||||
|
||||
## ValidationError
|
||||
|
||||
Error raised when plugin configuration fails standard-schema validation.
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Error raised when plugin configuration fails standard-schema validation. */
|
||||
class ValidationError extends TypeError {
|
||||
name = 'ValidationError'
|
||||
@@ -365,4 +372,4 @@ class ValidationError extends TypeError {
|
||||
}
|
||||
```
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L18)
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L18)
|
||||
@@ -1,4 +1,5 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.
|
||||
Run `pnpm run gen-cordis-catalog` to regenerate. -->
|
||||
|
||||
# Registry
|
||||
|
||||
@@ -6,7 +7,7 @@ Plugin loading and dependency injection.
|
||||
|
||||
### ctx.inject(deps, callback)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Run a callback once the requested services are available.
|
||||
*
|
||||
@@ -21,6 +22,7 @@ inject(deps: Inject, callback: Plugin.Function<void>): Fiber & PromiseLike<Fiber
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
- `deps` — required services, as an array or a name → config map.
|
||||
@@ -28,11 +30,11 @@ Shorthand for `ctx.plugin({ inject, apply: callback })`: the callback is unloade
|
||||
|
||||
**Returns** the fiber; awaiting it settles once loading finished.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L175)
|
||||
[Source](../../../vendor/cordis/src/registry.ts#L175)
|
||||
|
||||
### ctx.plugin(plugin, ...args)
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Load a plugin in the current context.
|
||||
*
|
||||
@@ -51,13 +53,13 @@ Load a plugin in the current context.
|
||||
|
||||
**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)
|
||||
[Source](../../../vendor/cordis/src/registry.ts#L184)
|
||||
|
||||
## Plugin
|
||||
|
||||
Supported plugin entrypoint shapes.
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Supported plugin entrypoint shapes. */
|
||||
type Plugin<T = any> =
|
||||
| Plugin.Function<T>
|
||||
@@ -116,14 +118,15 @@ namespace Plugin {
|
||||
}
|
||||
```
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L91)
|
||||
[Source](../../../vendor/cordis/src/registry.ts#L91)
|
||||
|
||||
## 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
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Service dependency declaration accepted by plugins and the `@Inject`
|
||||
* decorator.
|
||||
@@ -146,4 +149,4 @@ namespace Inject {
|
||||
}
|
||||
```
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L18)
|
||||
[Source](../../../vendor/cordis/src/registry.ts#L18)
|
||||
@@ -1,100 +1,102 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.
|
||||
Run `pnpm run gen-cordis-catalog` to regenerate. -->
|
||||
|
||||
# Service
|
||||
|
||||
Base class for context services: subclass it and load the subclass as a plugin to register `ctx.<name>`.
|
||||
The base class for context services. A subclass loaded as a plugin registers itself as `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.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L11)
|
||||
[Source](../../../vendor/cordis/src/service.ts#L11)
|
||||
|
||||
### service.name
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** The service name this instance is registered under. */
|
||||
public name!: string
|
||||
```
|
||||
|
||||
The service name this instance is registered under.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L30)
|
||||
[Source](../../../vendor/cordis/src/service.ts#L30)
|
||||
|
||||
## Static members
|
||||
|
||||
### Service.init
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key of an instance method run after construction (class plugins). */
|
||||
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)
|
||||
[Source](../../../vendor/cordis/src/service.ts#L13)
|
||||
|
||||
### Service.check
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key of the availability predicate passed to `ctx.provide()`. */
|
||||
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)
|
||||
[Source](../../../vendor/cordis/src/service.ts#L15)
|
||||
|
||||
### Service.config
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key of the phantom intercept-config type parameter. */
|
||||
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)
|
||||
[Source](../../../vendor/cordis/src/service.ts#L17)
|
||||
|
||||
### Service.invoke
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key of the call body making a service callable (e.g. `ctx.logger()`). */
|
||||
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)
|
||||
[Source](../../../vendor/cordis/src/service.ts#L19)
|
||||
|
||||
### Service.extend
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key of the helper deriving an extended service instance. */
|
||||
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)
|
||||
[Source](../../../vendor/cordis/src/service.ts#L21)
|
||||
|
||||
### Service.tracker
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key of the tracker metadata used for context tracing. */
|
||||
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)
|
||||
[Source](../../../vendor/cordis/src/service.ts#L23)
|
||||
|
||||
### Service.resolveConfig
|
||||
|
||||
```ts website-api
|
||||
```ts cordis-catalog
|
||||
/** Symbol key of the intercept-config resolution helper below. */
|
||||
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)
|
||||
[Source](../../../vendor/cordis/src/service.ts#L25)
|
||||
@@ -7,7 +7,7 @@ Every cordis event a plugin can listen to: exact signature, dispatch mode, and o
|
||||
|
||||
This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them.
|
||||
|
||||
The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely.
|
||||
The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely. The event-dispatch methods themselves are generated in the [Cordis core Events API](core/events.md).
|
||||
|
||||
Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)), **parallel** (awaited fan-out; all listeners run), **serial** (awaited in registration order until one returns a bail value — anything other than `null`, `false`, or `undefined`).
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Every `ctx.<key>` service a plugin can call: the exact public interface with ori
|
||||
|
||||
This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them.
|
||||
|
||||
The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer `ctx` surface a plugin also sees — pinned vendor source, summarized tersely.
|
||||
The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer `ctx` surface a plugin also sees — pinned vendor source, summarized tersely. Detailed Context, Fiber, Registry, and Service APIs are generated in the [Cordis core API](core/context.md).
|
||||
|
||||
## `ctx.agentLoop` — `AgentLoop`
|
||||
|
||||
|
||||
6
docs/user/develop/basic/config.i18n.yaml
Normal file
6
docs/user/develop/basic/config.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
config.md: 26d2d48ebede74194fbf306aa97d214bdb99b722
|
||||
config.zh.md: 9ed389b16779f25c633d0c8772f8658197ba4322
|
||||
118
docs/user/develop/basic/config.md
Normal file
118
docs/user/develop/basic/config.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Plugin configuration
|
||||
|
||||
English | [中文](config.zh.md)
|
||||
|
||||
Accept configuration supplied through `cordis.yml`.
|
||||
|
||||
## Define the Config type
|
||||
|
||||
Export a `Config` type and a same-named Schemastery schema. Put defaults directly on the schema fields:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import Schema from 'schemastery'
|
||||
|
||||
export const name = 'my-plugin'
|
||||
|
||||
export interface Config {
|
||||
greeting: string
|
||||
maxRetries: number
|
||||
verbose?: boolean
|
||||
}
|
||||
|
||||
export const Config: Schema<Config> = Schema.object({
|
||||
greeting: Schema.string().default('Hello'),
|
||||
maxRetries: Schema.number().default(3),
|
||||
verbose: Schema.boolean().default(false),
|
||||
})
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
console.log(config.greeting) // User value or schema default.
|
||||
}
|
||||
```
|
||||
|
||||
Configure it in `cordis.yml`:
|
||||
|
||||
```yaml
|
||||
- name: './src/my-plugin.ts'
|
||||
config:
|
||||
greeting: 'Hi there'
|
||||
maxRetries: 5
|
||||
```
|
||||
|
||||
When loading the plugin, Cordis uses the exported schema to validate configuration and fill defaults. Do not export a plain object as `Config`; it does not implement the Standard Schema interface required by Cordis.
|
||||
|
||||
## Schema validation
|
||||
|
||||
Use Schemastery to express stricter validation:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import Schema from 'schemastery'
|
||||
|
||||
export const name = 'validated-plugin'
|
||||
|
||||
export interface Config {
|
||||
apiKey: string
|
||||
timeout: number
|
||||
mode: 'fast' | 'accurate'
|
||||
}
|
||||
|
||||
export const Config = Schema.object({
|
||||
apiKey: Schema.string().required(),
|
||||
timeout: Schema.number().default(30000),
|
||||
mode: Schema.union(['fast', 'accurate']).default('fast'),
|
||||
})
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
// config is validated and type-safe.
|
||||
}
|
||||
```
|
||||
|
||||
The schema runs while the plugin loads. Invalid configuration fails the load with an actionable error.
|
||||
|
||||
## Design principles
|
||||
|
||||
### Do not hardcode tunable values
|
||||
|
||||
Harness requires **anything that two deployments may want to set differently to be a configuration field**.
|
||||
|
||||
```ts
|
||||
// Wrong: hardcoded timeout.
|
||||
const TIMEOUT = 30000
|
||||
|
||||
// Correct: configurable.
|
||||
export interface Config {
|
||||
timeoutMs: number // Defaults to 30000.
|
||||
}
|
||||
```
|
||||
|
||||
The test is whether `cordis.yml` can change the value without a code edit.
|
||||
|
||||
### Fail loudly on invalid configuration
|
||||
|
||||
If configuration refers to an unregistered LLM provider route or another nonexistent resource, fail early instead of silently skipping it:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
export interface ModelConfig {
|
||||
provider: string
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: ModelConfig) {
|
||||
if (!ctx.llm.listProviders().some(provider => provider.id === config.provider)) {
|
||||
throw new Error(`LLM provider "${config.provider}" is not registered`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Work with HMR
|
||||
|
||||
A configuration edit hot-replaces the plugin: the framework unloads the old instance and loads a new one. Because registrations are effects and clean themselves up, replacement does not retain the old instance's registrations.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Plugins and lifecycle](../framework/) — understand the full plugin lifecycle
|
||||
- [Services and dependencies](../framework/service.md) — provide a service to other plugins
|
||||
@@ -1,24 +1,33 @@
|
||||
# 插件配置
|
||||
|
||||
[English](config.md) | 中文
|
||||
|
||||
让你的插件接受用户在 `cordis.yml` 中传入的配置。
|
||||
|
||||
## 定义 Config 类型
|
||||
|
||||
在插件中导出一个 `Config` 类型,`apply` 的第二个参数就是用户配置:
|
||||
在插件中导出一个 `Config` 类型和同名的 Schemastery schema;默认值直接写在 schema 中:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import Schema from 'schemastery'
|
||||
|
||||
export const name = 'my-plugin'
|
||||
|
||||
export interface Config {
|
||||
greeting?: string
|
||||
maxRetries?: number
|
||||
greeting: string
|
||||
maxRetries: number
|
||||
verbose?: boolean
|
||||
}
|
||||
|
||||
export const Config: Schema<Config> = Schema.object({
|
||||
greeting: Schema.string().default('Hello'),
|
||||
maxRetries: Schema.number().default(3),
|
||||
verbose: Schema.boolean().default(false),
|
||||
})
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
console.log(config.greeting ?? 'Hello') // 用户配置或默认值
|
||||
console.log(config.greeting) // User value or schema default.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -31,32 +40,32 @@ export function apply(ctx: Context, config: Config) {
|
||||
maxRetries: 5
|
||||
```
|
||||
|
||||
只导出类型时,配置原样传入,默认值由代码自己兜底(如上面的 `??`)。想让框架代管默认值和校验,导出一个 schema(见下节)。
|
||||
插件加载时,Cordis 会通过导出的 schema 校验配置,并填充未提供字段的默认值。不要导出普通对象作为 `Config`,因为它不满足 Cordis 要求的 Standard Schema 接口。
|
||||
|
||||
## Schema 校验
|
||||
|
||||
对于需要默认值和严格校验的场景,额外导出一个 Schemastery schema(仓库约定以 `z` 引入)。加载时框架先用它校验并填充默认值,再把结果传给 `apply`:
|
||||
对于需要严格校验的场景,使用 Schemastery 定义 schema:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import z from 'schemastery'
|
||||
import Schema from 'schemastery'
|
||||
|
||||
export const name = 'validated-plugin'
|
||||
|
||||
export interface Config {
|
||||
apiKey: string
|
||||
timeout?: number
|
||||
mode?: 'fast' | 'accurate'
|
||||
timeout: number
|
||||
mode: 'fast' | 'accurate'
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
apiKey: z.string().required(),
|
||||
timeout: z.number().default(30000),
|
||||
mode: z.union(['fast', 'accurate'] as const).default('fast'),
|
||||
export const Config = Schema.object({
|
||||
apiKey: Schema.string().required(),
|
||||
timeout: Schema.number().default(30000),
|
||||
mode: Schema.union(['fast', 'accurate']).default('fast'),
|
||||
})
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
// config 已经过校验,类型安全,默认值已填充
|
||||
// config is validated and type-safe.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -69,13 +78,12 @@ Schema 在插件加载时执行校验。如果配置不合法,插件会加载
|
||||
Harness 的约定:**任何两个部署可能想要不同值的东西,都应该是配置字段**。
|
||||
|
||||
```ts
|
||||
// 错误 — 硬编码超时时间
|
||||
// Wrong: hardcoded timeout.
|
||||
const TIMEOUT = 30000
|
||||
|
||||
// 正确 — 可配置
|
||||
// Correct: configurable.
|
||||
export interface Config {
|
||||
/** 默认 30000 */
|
||||
timeoutMs?: number
|
||||
timeoutMs: number // Defaults to 30000.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -83,26 +91,23 @@ export interface Config {
|
||||
|
||||
### 配置错误要响亮
|
||||
|
||||
如果配置引用了不存在的东西(比如一个未注册的 LLM 提供方路由),应该尽早报错,而不是静默跳过:
|
||||
如果配置引用了未注册的 LLM 提供方路由或其他不存在的资源,应该尽早报错,而不是静默跳过:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
export interface Config {
|
||||
export interface ModelConfig {
|
||||
provider: string
|
||||
model: string
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
export function apply(ctx: Context, config: ModelConfig) {
|
||||
if (!ctx.llm.listProviders().some(provider => provider.id === config.provider)) {
|
||||
throw new Error(`LLM provider "${config.provider}" is not registered`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
模型目录只用于发现;适配器可能接受目录之外的模型 ID,因此不能把 `listModels()` 当作请求白名单。
|
||||
|
||||
## 配合 HMR
|
||||
|
||||
配置变更会触发插件热替换:修改 `cordis.yml` 中某个插件的 `config`,框架会卸载旧实例、加载新实例。由于注册都是效果(自动清理),这个过程是安全的。
|
||||
@@ -110,4 +115,4 @@ export function apply(ctx: Context, config: Config) {
|
||||
## 下一步
|
||||
|
||||
- [插件与生命周期](../framework/) — 深入了解插件的完整生命周期
|
||||
- [服务与依赖](../framework/service) — 让你的插件对外提供服务
|
||||
- [服务与依赖](../framework/service.md) — 让你的插件对外提供服务
|
||||
6
docs/user/develop/basic/index.i18n.yaml
Normal file
6
docs/user/develop/basic/index.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
index.md: 5fa46806bc195ad2566fc0a29b45eb1dd7a68179
|
||||
index.zh.md: a6d238c12841c8c25b00376ee032e5db50fc6b4e
|
||||
151
docs/user/develop/basic/index.md
Normal file
151
docs/user/develop/basic/index.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Your first plugin
|
||||
|
||||
English | [中文](index.zh.md)
|
||||
|
||||
This guide creates a minimal Harness plugin and loads it into an agent.
|
||||
|
||||
## What is a plugin?
|
||||
|
||||
In Harness, a plugin is a TypeScript module that exports an `apply` function. The framework calls `apply` when loading the plugin and passes a `ctx` context object through which the plugin registers capabilities:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const name = 'my-plugin'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// Register capabilities here.
|
||||
}
|
||||
```
|
||||
|
||||
That is the complete shape.
|
||||
|
||||
## Create the plugin file
|
||||
|
||||
Create `src/my-plugin.ts` in your project:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const name = 'hello-plugin'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// Required dependencies are ready before apply runs.
|
||||
console.log('[hello-plugin] plugin loaded!')
|
||||
}
|
||||
```
|
||||
|
||||
## Register it in cordis.yml
|
||||
|
||||
Add an entry to `cordis.yml`:
|
||||
|
||||
```yaml
|
||||
- id: hello
|
||||
name: './src/my-plugin.ts'
|
||||
```
|
||||
|
||||
After startup, the console prints `[hello-plugin] plugin loaded!`.
|
||||
|
||||
## Automatic cleanup
|
||||
|
||||
Anything registered through `ctx`—event listeners, tools, or timers—is cleaned up when the plugin unloads. You do not need to call removeListener or clearInterval manually.
|
||||
|
||||
For a resource that needs explicit cleanup, such as a network connection, use `ctx.effect()` to provide its disposer:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.effect(() => {
|
||||
const timer = setInterval(() => {
|
||||
console.log('heartbeat')
|
||||
}, 5000)
|
||||
|
||||
// The returned function runs when the plugin unloads.
|
||||
return () => clearInterval(timer)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Declare dependencies
|
||||
|
||||
If the plugin consumes another service such as `tools` or `llm`, declare it in `inject`:
|
||||
|
||||
```ts ignore-check
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const name = 'my-tool-plugin'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// ctx.tools is ready here.
|
||||
ctx.tools.register(/* ... */)
|
||||
}
|
||||
```
|
||||
|
||||
The framework waits for every required service before loading the plugin.
|
||||
|
||||
## Three plugin forms
|
||||
|
||||
In addition to a function module, a plugin can use object or class form.
|
||||
|
||||
### Object form
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export default {
|
||||
name: 'my-plugin',
|
||||
inject: ['tools'],
|
||||
apply(ctx: Context) {
|
||||
// ...
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Class form
|
||||
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
export default class MyService extends Service {
|
||||
static inject = ['tools']
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'myService')
|
||||
// Perform synchronous initialization in the constructor.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Function form is sufficient in most cases. Use class form when the plugin provides a service to other plugins; see [services and dependencies](../framework/service.md).
|
||||
|
||||
## Complete example
|
||||
|
||||
`examples/echo-agent/src/echo-tool.ts` is a plugin that registers a tool:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'echo-tool'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'echo',
|
||||
description: 'Echo the given text back, uppercased.',
|
||||
parameters: {
|
||||
text: { type: 'string', required: true },
|
||||
},
|
||||
async execute(args) {
|
||||
return [{ type: 'text', text: `ECHO: ${args.text.toUpperCase()}` }]
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Build a tool](./tool.md) — learn the tool definition DSL
|
||||
- [Plugin configuration](./config.md) — accept user configuration
|
||||
@@ -1,5 +1,7 @@
|
||||
# 第一个插件
|
||||
|
||||
[English](index.md) | 中文
|
||||
|
||||
本文带你编写一个最小的 Harness 插件并加载到 Agent 中。
|
||||
|
||||
## 插件是什么
|
||||
@@ -12,7 +14,7 @@ import type { Context } from 'cordis'
|
||||
export const name = 'my-plugin'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// 在这里注册能力
|
||||
// Register capabilities here.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -28,8 +30,8 @@ import type { Context } from 'cordis'
|
||||
export const name = 'hello-plugin'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// apply 函数体在插件加载时执行
|
||||
console.log('[hello-plugin] 插件已加载!')
|
||||
// Required dependencies are ready before apply runs.
|
||||
console.log('[hello-plugin] plugin loaded!')
|
||||
}
|
||||
```
|
||||
|
||||
@@ -42,7 +44,7 @@ export function apply(ctx: Context) {
|
||||
name: './src/my-plugin.ts'
|
||||
```
|
||||
|
||||
启动后你会在控制台看到 `[hello-plugin] 插件已加载!`。
|
||||
启动后你会在控制台看到 `[hello-plugin] plugin loaded!`。
|
||||
|
||||
## 自动清理
|
||||
|
||||
@@ -59,7 +61,7 @@ export function apply(ctx: Context) {
|
||||
console.log('heartbeat')
|
||||
}, 5000)
|
||||
|
||||
// 返回的函数会在插件卸载时被调用
|
||||
// The returned function runs when the plugin unloads.
|
||||
return () => clearInterval(timer)
|
||||
})
|
||||
}
|
||||
@@ -69,23 +71,15 @@ export function apply(ctx: Context) {
|
||||
|
||||
如果你的插件需要使用其他服务(如 `tools`、`llm`),需要声明 `inject`:
|
||||
|
||||
```ts
|
||||
```ts ignore-check
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'my-tool-plugin'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// ctx.tools 现在可用
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'demo',
|
||||
description: 'Demo tool.',
|
||||
parameters: {},
|
||||
async execute() {
|
||||
return []
|
||||
},
|
||||
}))
|
||||
// ctx.tools is ready here.
|
||||
ctx.tools.register(/* ... */)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -99,7 +93,6 @@ export function apply(ctx: Context) {
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export default {
|
||||
name: 'my-plugin',
|
||||
@@ -114,23 +107,18 @@ export default {
|
||||
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export default class MyService extends Service {
|
||||
static inject = ['tools']
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'myService')
|
||||
}
|
||||
|
||||
// 服务的公开方法
|
||||
greet(name: string) {
|
||||
return `Hello, ${name}!`
|
||||
// Perform synchronous initialization in the constructor.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
大多数情况下,函数形式足够了。类形式用于需要对外提供服务的插件(见 [服务与依赖](../framework/service))。
|
||||
大多数情况下,函数形式足够了。类形式用于需要对外提供服务的插件(见 [服务与依赖](../framework/service.md))。
|
||||
|
||||
## 完整示例
|
||||
|
||||
@@ -159,5 +147,5 @@ export function apply(ctx: Context) {
|
||||
|
||||
## 下一步
|
||||
|
||||
- [开发一个 Tool](tool) — 详细了解 tool 定义 DSL
|
||||
- [插件配置](config) — 让插件接受用户配置
|
||||
- [开发一个 Tool](./tool.md) — 详细了解 tool 定义 DSL
|
||||
- [插件配置](./config.md) — 让插件接受用户配置
|
||||
6
docs/user/develop/basic/tool.i18n.yaml
Normal file
6
docs/user/develop/basic/tool.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
tool.md: 416733bcb584fa5303a8b3ba5e6e904302e7f992
|
||||
tool.zh.md: fce9a7d9b973853c8b4fb9ae2c034e749d8da999
|
||||
208
docs/user/develop/basic/tool.md
Normal file
208
docs/user/develop/basic/tool.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Build a tool
|
||||
|
||||
English | [中文](tool.zh.md)
|
||||
|
||||
A tool is a capability the model can call. This guide builds one with `defineTool`.
|
||||
|
||||
## Minimal example
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'my-tool'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'greet',
|
||||
description: 'Greet someone by name.',
|
||||
parameters: {
|
||||
name: { type: 'string', required: true, description: 'The name to greet' },
|
||||
},
|
||||
async execute(args) {
|
||||
// args is inferred as { name: string }.
|
||||
return [{ type: 'text', text: `Hello, ${args.name}!` }]
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
## Parameter definitions
|
||||
|
||||
`parameters` uses a compact format that the framework converts to the JSON Schema sent to the model.
|
||||
|
||||
### Primitive types
|
||||
|
||||
```ts
|
||||
export const parameters = {
|
||||
path: { type: 'string', required: true },
|
||||
limit: { type: 'number' },
|
||||
recursive: { type: 'boolean' },
|
||||
}
|
||||
// Inferred type: { path: string; limit?: number; recursive?: boolean }
|
||||
```
|
||||
|
||||
### Enums
|
||||
|
||||
```ts
|
||||
export const parameters = {
|
||||
mode: { type: 'string', required: true, enum: ['read', 'write', 'append'] },
|
||||
}
|
||||
// Inferred type: { mode: string } (enum values are validated at runtime)
|
||||
```
|
||||
|
||||
### Nested objects
|
||||
|
||||
```ts
|
||||
export const parameters = {
|
||||
options: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
timeout: { type: 'number' },
|
||||
retries: { type: 'number' },
|
||||
},
|
||||
},
|
||||
}
|
||||
// Inferred type: { options?: { timeout?: number; retries?: number } }
|
||||
```
|
||||
|
||||
### Arrays
|
||||
|
||||
```ts
|
||||
export const parameters = {
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
}
|
||||
// Inferred type: { tags?: string[] }
|
||||
```
|
||||
|
||||
### Property fields
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|------|------|------|
|
||||
| `type` | `'string' \| 'number' \| 'boolean' \| 'object' \| 'array'` | Value type |
|
||||
| `required` | `true` | Marks the property required and affects inference |
|
||||
| `description` | `string` | Description sent to the model |
|
||||
| `enum` | `string[]` | Allowed string values |
|
||||
| `properties` | `SchemaSpec` | Nested properties for an object |
|
||||
| `items` | `SchemaProp` | Element schema for an array |
|
||||
|
||||
## The execute function
|
||||
|
||||
`execute` receives validated, inferred `args` and an `exec` execution context:
|
||||
|
||||
```ts
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const tool = defineTool({
|
||||
name: 'example',
|
||||
description: 'Return an example result.',
|
||||
parameters: {},
|
||||
async execute(args, exec) {
|
||||
// args: inferred from parameters
|
||||
// exec: ToolExecution context
|
||||
|
||||
// Return a ContentBlock array.
|
||||
void args
|
||||
void exec
|
||||
return [{ type: 'text', text: 'result here' }]
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Return value
|
||||
|
||||
`execute` returns a `ContentBlock[]` that becomes the tool result visible to the model:
|
||||
|
||||
```ts ignore-check
|
||||
// Text result
|
||||
return [{ type: 'text', text: 'file content here...' }]
|
||||
|
||||
// Multiple blocks
|
||||
return [
|
||||
{ type: 'text', text: 'Found 3 matches:' },
|
||||
{ type: 'text', text: matchResults.join('\n') },
|
||||
]
|
||||
```
|
||||
|
||||
### Argument validation
|
||||
|
||||
Before calling `execute`, `defineTool` validates model-generated arguments. Invalid input raises `ToolArgsError`; the framework turns it into an `isError` result so the model can correct its call.
|
||||
|
||||
Do not repeat type validation inside `execute`.
|
||||
|
||||
## Presentation
|
||||
|
||||
A tool can define UI presentation methods for terminal and ACP clients:
|
||||
|
||||
```ts ignore-check
|
||||
defineTool({
|
||||
name: 'bash',
|
||||
// ...
|
||||
presentCall(args) {
|
||||
return {
|
||||
card: 'terminal',
|
||||
title: args.command,
|
||||
}
|
||||
},
|
||||
presentResult(args, result) {
|
||||
return {
|
||||
card: 'terminal',
|
||||
output: result.content.map(b => b.type === 'text' ? b.text : '').join(''),
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
`presentCall` and `presentResult` are **pure functions**. Streaming UI and session replay may call them more than once.
|
||||
|
||||
## Registration and unloading
|
||||
|
||||
`ctx.tools.register()` returns a disposer, but a registration made through `ctx` is already tracked by the framework. Unloading the plugin removes the tool automatically, so the plugin does not call the disposer itself.
|
||||
|
||||
```ts ignore-check
|
||||
// This is sufficient:
|
||||
ctx.tools.register(defineTool({ /* ... */ }))
|
||||
|
||||
// No saved disposer or extra cleanup registration is needed.
|
||||
```
|
||||
|
||||
## Complete example
|
||||
|
||||
This tool counts files in a directory:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import { readdir } from 'node:fs/promises'
|
||||
|
||||
export const name = 'file-counter'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'count_files',
|
||||
description: 'Count files in a directory.',
|
||||
parameters: {
|
||||
path: { type: 'string', required: true, description: 'Directory path' },
|
||||
extension: { type: 'string', description: 'Filter by extension (e.g. ".ts")' },
|
||||
},
|
||||
async execute(args) {
|
||||
const entries = await readdir(args.path, { withFileTypes: true })
|
||||
let files = entries.filter(e => e.isFile())
|
||||
if (args.extension) {
|
||||
files = files.filter(f => f.name.endsWith(args.extension!))
|
||||
}
|
||||
return [{ type: 'text', text: `Found ${files.length} files.` }]
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Plugin configuration](./config.md) — make the tool configurable
|
||||
- [Capability layering](../practice/) — understand the interface/implementation/consumer pattern
|
||||
@@ -1,5 +1,7 @@
|
||||
# 开发一个 Tool
|
||||
|
||||
[English](tool.md) | 中文
|
||||
|
||||
Tool 是模型可以调用的能力。本文介绍如何用 `defineTool` 编写一个 tool。
|
||||
|
||||
## 最小示例
|
||||
@@ -19,7 +21,7 @@ export function apply(ctx: Context) {
|
||||
name: { type: 'string', required: true, description: 'The name to greet' },
|
||||
},
|
||||
async execute(args) {
|
||||
// args 自动推导为 { name: string }
|
||||
// args is inferred as { name: string }.
|
||||
return [{ type: 'text', text: `Hello, ${args.name}!` }]
|
||||
},
|
||||
}))
|
||||
@@ -33,33 +35,27 @@ export function apply(ctx: Context) {
|
||||
### 基本类型
|
||||
|
||||
```ts
|
||||
import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
const parameters = {
|
||||
export const parameters = {
|
||||
path: { type: 'string', required: true },
|
||||
limit: { type: 'number' },
|
||||
recursive: { type: 'boolean' },
|
||||
} satisfies SchemaSpec
|
||||
// 推导类型: { path: string; limit?: number; recursive?: boolean }
|
||||
}
|
||||
// Inferred type: { path: string; limit?: number; recursive?: boolean }
|
||||
```
|
||||
|
||||
### 枚举
|
||||
|
||||
```ts
|
||||
import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
const parameters = {
|
||||
export const parameters = {
|
||||
mode: { type: 'string', required: true, enum: ['read', 'write', 'append'] },
|
||||
} satisfies SchemaSpec
|
||||
// 推导类型: { mode: string } (运行时校验 enum 值)
|
||||
}
|
||||
// Inferred type: { mode: string } (enum values are validated at runtime)
|
||||
```
|
||||
|
||||
### 嵌套对象
|
||||
|
||||
```ts
|
||||
import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
const parameters = {
|
||||
export const parameters = {
|
||||
options: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -67,22 +63,20 @@ const parameters = {
|
||||
retries: { type: 'number' },
|
||||
},
|
||||
},
|
||||
} satisfies SchemaSpec
|
||||
// 推导类型: { options?: { timeout?: number; retries?: number } }
|
||||
}
|
||||
// Inferred type: { options?: { timeout?: number; retries?: number } }
|
||||
```
|
||||
|
||||
### 数组
|
||||
|
||||
```ts
|
||||
import type { SchemaSpec } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
const parameters = {
|
||||
export const parameters = {
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
} satisfies SchemaSpec
|
||||
// 推导类型: { tags?: string[] }
|
||||
}
|
||||
// Inferred type: { tags?: string[] }
|
||||
```
|
||||
|
||||
### 每个属性的字段
|
||||
@@ -103,15 +97,17 @@ const parameters = {
|
||||
```ts
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
defineTool({
|
||||
name: 'demo',
|
||||
description: 'Demo tool.',
|
||||
export const tool = defineTool({
|
||||
name: 'example',
|
||||
description: 'Return an example result.',
|
||||
parameters: {},
|
||||
async execute(args, exec) {
|
||||
// args: 根据 parameters 自动推导的类型
|
||||
// exec: ToolExecution 对象,提供执行上下文
|
||||
// args: inferred from parameters
|
||||
// exec: ToolExecution context
|
||||
|
||||
// 返回 ContentBlock 数组
|
||||
// Return a ContentBlock array.
|
||||
void args
|
||||
void exec
|
||||
return [{ type: 'text', text: 'result here' }]
|
||||
},
|
||||
})
|
||||
@@ -121,23 +117,15 @@ defineTool({
|
||||
|
||||
`execute` 必须返回一个 `ContentBlock[]`,告诉模型 tool 的执行结果:
|
||||
|
||||
```ts
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
```ts ignore-check
|
||||
// Text result
|
||||
return [{ type: 'text', text: 'file content here...' }]
|
||||
|
||||
declare const matchResults: string[]
|
||||
|
||||
// 文本结果
|
||||
function textResult(): ContentBlock[] {
|
||||
return [{ type: 'text', text: 'file content here...' }]
|
||||
}
|
||||
|
||||
// 多个 block
|
||||
function multiBlockResult(): ContentBlock[] {
|
||||
return [
|
||||
{ type: 'text', text: 'Found 3 matches:' },
|
||||
{ type: 'text', text: matchResults.join('\n') },
|
||||
]
|
||||
}
|
||||
// Multiple blocks
|
||||
return [
|
||||
{ type: 'text', text: 'Found 3 matches:' },
|
||||
{ type: 'text', text: matchResults.join('\n') },
|
||||
]
|
||||
```
|
||||
|
||||
### 参数校验
|
||||
@@ -150,22 +138,14 @@ function multiBlockResult(): ContentBlock[] {
|
||||
|
||||
Tool 可以定义 UI 渲染方法,用于在终端或 ACP 客户端中展示 tool call 和 result:
|
||||
|
||||
```ts
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
```ts ignore-check
|
||||
defineTool({
|
||||
name: 'bash',
|
||||
description: 'Run a shell command.',
|
||||
parameters: {
|
||||
command: { type: 'string', required: true },
|
||||
},
|
||||
async execute(args) {
|
||||
return [{ type: 'text', text: `ran: ${args.command}` }]
|
||||
},
|
||||
// ...
|
||||
presentCall(args) {
|
||||
return {
|
||||
card: 'terminal',
|
||||
title: args.command.slice(0, 60),
|
||||
title: args.command,
|
||||
}
|
||||
},
|
||||
presentResult(args, result) {
|
||||
@@ -183,25 +163,11 @@ defineTool({
|
||||
|
||||
`ctx.tools.register()` 返回值就是 disposer。但由于你在 `ctx` 上调用,框架已经自动追踪了这个注册——插件卸载时会自动移除 tool。你不需要手动调用 disposer。
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
```ts ignore-check
|
||||
// This is sufficient:
|
||||
ctx.tools.register(defineTool({ /* ... */ }))
|
||||
|
||||
declare const ctx: Context
|
||||
|
||||
// 这样就够了:
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'noop',
|
||||
description: 'Do nothing.',
|
||||
parameters: {},
|
||||
async execute() {
|
||||
return []
|
||||
},
|
||||
}))
|
||||
|
||||
// 不需要:
|
||||
// const dispose = ctx.tools.register(...)
|
||||
// ctx.effect(() => dispose)
|
||||
// No saved disposer or extra cleanup registration is needed.
|
||||
```
|
||||
|
||||
## 完整实战示例
|
||||
@@ -238,5 +204,5 @@ export function apply(ctx: Context) {
|
||||
|
||||
## 下一步
|
||||
|
||||
- [插件配置](config) — 让你的 tool 可配置
|
||||
- [插件配置](./config.md) — 让你的 tool 可配置
|
||||
- [能力三件套](../practice/) — 了解 seam/impl/consumer 模式
|
||||
6
docs/user/develop/framework/events.i18n.yaml
Normal file
6
docs/user/develop/framework/events.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
events.md: 0c57681a55ea0200fe8f33293176fc94f09a4ce5
|
||||
events.zh.md: 3e14739d4a97ba014d545c9f226000507aaeacef
|
||||
143
docs/user/develop/framework/events.md
Normal file
143
docs/user/develop/framework/events.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Event system
|
||||
|
||||
English | [中文](events.zh.md)
|
||||
|
||||
Events are the core communication mechanism between Cordis plugins. Harness uses them extensively for loosely coupled extension points.
|
||||
|
||||
## Basic use
|
||||
|
||||
### Listen for an event
|
||||
|
||||
```ts ignore-check
|
||||
ctx.on('event-name', (payload) => {
|
||||
// Handle the event.
|
||||
})
|
||||
```
|
||||
|
||||
### Emit an event
|
||||
|
||||
```ts ignore-check
|
||||
ctx.emit('event-name', payload)
|
||||
```
|
||||
|
||||
## Event modes
|
||||
|
||||
Cordis provides several event modes for different interaction contracts.
|
||||
|
||||
### emit — broadcast
|
||||
|
||||
Every listener runs synchronously and return values are ignored:
|
||||
|
||||
```ts ignore-check
|
||||
// Emit
|
||||
ctx.emit('my-plugin/ready', { id: 'worker-1' })
|
||||
|
||||
// Listen
|
||||
ctx.on('my-plugin/ready', ({ id }) => {
|
||||
console.log(`${id} is ready`)
|
||||
})
|
||||
```
|
||||
|
||||
### bail — short circuit
|
||||
|
||||
Listeners run in order; the first non-`undefined` result becomes the final result:
|
||||
|
||||
```ts ignore-check
|
||||
// Dispatch
|
||||
const result = ctx.bail('some-check', input)
|
||||
|
||||
// Listen: a returned value stops later listeners.
|
||||
ctx.on('some-check', (input) => {
|
||||
if (shouldBlock(input)) return 'blocked'
|
||||
// Return undefined to continue to the next listener.
|
||||
})
|
||||
```
|
||||
|
||||
### serial — ordered execution
|
||||
|
||||
Listeners run in registration order and asynchronous results are awaited. The first listener to return a non-empty value stops further execution:
|
||||
|
||||
```ts ignore-check
|
||||
await ctx.serial('setup-phase', context)
|
||||
```
|
||||
|
||||
### waterfall — pipeline
|
||||
|
||||
Each listener may wrap the downstream result to form a processing chain. A listener **must call `next()` to delegate downstream**; omitting the call vetoes the pipeline:
|
||||
|
||||
```ts ignore-check
|
||||
// Dispatch
|
||||
const output = await ctx.waterfall('my-plugin/transform', input, async () => input)
|
||||
|
||||
// Listen: next() is mandatory.
|
||||
ctx.on('my-plugin/transform', async (_input, next) => {
|
||||
const downstream = await next()
|
||||
return downstream.trim()
|
||||
})
|
||||
```
|
||||
|
||||
::: warning
|
||||
A waterfall listener **must call `next()`**. Omitting it vetoes the pipeline by design, enabling interception and gateway behavior.
|
||||
:::
|
||||
|
||||
## Typed events
|
||||
|
||||
Harness uses TypeScript declaration merging for type-safe events:
|
||||
|
||||
```ts
|
||||
import 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'my-plugin/ready': (payload: { id: string }) => void
|
||||
'my-plugin/check': (input: string) => boolean | undefined
|
||||
'my-plugin/transform': (input: string, next: () => Promise<string>) => Promise<string>
|
||||
}
|
||||
}
|
||||
|
||||
// ctx.on('my-plugin/ready', ...) and ctx.emit('my-plugin/ready', ...)
|
||||
// are now inferred correctly.
|
||||
```
|
||||
|
||||
## Cordis events and session records
|
||||
|
||||
Harness Cordis events use `namespace/action` names, including `agent/pre-step`, `agent/request`, `agent/step-result`, `tools/result`, and `session/event`. The generated [event catalog](../../../cordis-catalog/events.md) records complete signatures and modes.
|
||||
|
||||
`turn/*`, `step/*`, `tool/call`, `tool/result`, and `compact/*` are durable session-event types, not same-named Cordis events. To observe them, listen to `session/event` and inspect `event.type`.
|
||||
|
||||
## Event listeners are effects
|
||||
|
||||
A listener registered with `ctx.on()` is removed automatically when its plugin unloads:
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// This listener is removed when the plugin disposes.
|
||||
ctx.on('tools/result', handler)
|
||||
}
|
||||
```
|
||||
|
||||
## Example: logging plugin
|
||||
|
||||
This plugin logs tool calls and results:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'tool-logger'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.on('tools/result', (exec, result) => {
|
||||
console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
|
||||
const text = result.content
|
||||
.map(block => block.type === 'text' ? block.text : '')
|
||||
.join('')
|
||||
console.log(`[tool result] ${text.slice(0, 100)}`)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Capability layering](../practice/) — understand events within capability interfaces
|
||||
- [LLM adapters](../practice/llm-adapter.md) — implement a complete LLM backend
|
||||
143
docs/user/develop/framework/events.zh.md
Normal file
143
docs/user/develop/framework/events.zh.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# 事件系统
|
||||
|
||||
[English](events.md) | 中文
|
||||
|
||||
事件是 Cordis 插件间通信的核心机制。Harness 大量使用事件来实现松耦合的扩展点。
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 监听事件
|
||||
|
||||
```ts ignore-check
|
||||
ctx.on('event-name', (payload) => {
|
||||
// Handle the event.
|
||||
})
|
||||
```
|
||||
|
||||
### 触发事件
|
||||
|
||||
```ts ignore-check
|
||||
ctx.emit('event-name', payload)
|
||||
```
|
||||
|
||||
## 事件模式
|
||||
|
||||
Cordis 提供多种事件触发模式,适用于不同场景:
|
||||
|
||||
### emit — 广播
|
||||
|
||||
所有监听器同步执行,不关心返回值:
|
||||
|
||||
```ts ignore-check
|
||||
// Emit
|
||||
ctx.emit('my-plugin/ready', { id: 'worker-1' })
|
||||
|
||||
// Listen
|
||||
ctx.on('my-plugin/ready', ({ id }) => {
|
||||
console.log(`${id} is ready`)
|
||||
})
|
||||
```
|
||||
|
||||
### bail — 短路
|
||||
|
||||
依次调用监听器,第一个返回非 `undefined` 值的结果作为最终值:
|
||||
|
||||
```ts ignore-check
|
||||
// Dispatch
|
||||
const result = ctx.bail('some-check', input)
|
||||
|
||||
// Listen: a returned value stops later listeners.
|
||||
ctx.on('some-check', (input) => {
|
||||
if (shouldBlock(input)) return 'blocked'
|
||||
// Return undefined to continue to the next listener.
|
||||
})
|
||||
```
|
||||
|
||||
### serial — 顺序执行
|
||||
|
||||
监听器按注册顺序依次执行,并等待异步结果;第一个返回非空值的监听器会终止后续执行:
|
||||
|
||||
```ts ignore-check
|
||||
await ctx.serial('setup-phase', context)
|
||||
```
|
||||
|
||||
### waterfall — 管道
|
||||
|
||||
每个监听器可以包装下游返回值,形成处理链。**必须调用 `next()` 传递给下游**,不调用即为否决:
|
||||
|
||||
```ts ignore-check
|
||||
// Dispatch
|
||||
const output = await ctx.waterfall('my-plugin/transform', input, async () => input)
|
||||
|
||||
// Listen: next() is mandatory.
|
||||
ctx.on('my-plugin/transform', async (_input, next) => {
|
||||
const downstream = await next()
|
||||
return downstream.trim()
|
||||
})
|
||||
```
|
||||
|
||||
::: warning
|
||||
Waterfall 监听器**必须调用 `next()`**。不调用 `next` 等于否决整个管道,这是故意为之的设计——用于实现拦截/网关逻辑。
|
||||
:::
|
||||
|
||||
## Typed Events
|
||||
|
||||
Harness 使用 TypeScript 声明合并来为事件提供类型安全:
|
||||
|
||||
```ts
|
||||
import 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'my-plugin/ready': (payload: { id: string }) => void
|
||||
'my-plugin/check': (input: string) => boolean | undefined
|
||||
'my-plugin/transform': (input: string, next: () => Promise<string>) => Promise<string>
|
||||
}
|
||||
}
|
||||
|
||||
// ctx.on('my-plugin/ready', ...) and ctx.emit('my-plugin/ready', ...)
|
||||
// are now inferred correctly.
|
||||
```
|
||||
|
||||
## Cordis 事件与会话记录
|
||||
|
||||
Harness 的 Cordis 事件遵循 `namespace/action` 命名,例如 `agent/pre-step`、`agent/request`、`agent/step-result`、`tools/result` 和 `session/event`。完整签名与触发模式见[Events 目录](../../../cordis-catalog/events.md)。
|
||||
|
||||
`turn/*`、`step/*`、`tool/call`、`tool/result` 和 `compact/*` 是持久化的会话事件类型,不是同名 Cordis 事件。需要观察它们时,监听 `session/event` 并检查 `event.type`。
|
||||
|
||||
## 事件也是效果
|
||||
|
||||
通过 `ctx.on()` 注册的监听器会在插件卸载时自动移除:
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// This listener is removed when the plugin disposes.
|
||||
ctx.on('tools/result', handler)
|
||||
}
|
||||
```
|
||||
|
||||
## 实战示例:日志插件
|
||||
|
||||
一个记录所有 tool 调用的简单插件:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'tool-logger'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.on('tools/result', (exec, result) => {
|
||||
console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
|
||||
const text = result.content
|
||||
.map(block => block.type === 'text' ? block.text : '')
|
||||
.join('')
|
||||
console.log(`[tool result] ${text.slice(0, 100)}`)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
- [能力三件套](../practice/) — 事件在 capability seam 中的角色
|
||||
- [LLM 适配器](../practice/llm-adapter.md) — 实现一个完整的 LLM 后端
|
||||
6
docs/user/develop/framework/index.i18n.yaml
Normal file
6
docs/user/develop/framework/index.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
index.md: 79e925b54509da41535735527e283850384257ec
|
||||
index.zh.md: 62be8c706510704f7b07286f166f14fa81235a0a
|
||||
136
docs/user/develop/framework/index.md
Normal file
136
docs/user/develop/framework/index.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Plugins and lifecycle
|
||||
|
||||
English | [中文](index.zh.md)
|
||||
|
||||
This page describes the Cordis plugin model and lifecycle state machine.
|
||||
|
||||
## Fiber state machine
|
||||
|
||||
Every loaded plugin owns a **Fiber** scope with the following states:
|
||||
|
||||
```
|
||||
PENDING → LOADING → ACTIVE
|
||||
↘ FAILED
|
||||
ACTIVE → UNLOADING → DISPOSED
|
||||
```
|
||||
|
||||
| State | Meaning |
|
||||
|------|------|
|
||||
| PENDING | Declared, but required dependencies are not ready |
|
||||
| LOADING | Dependencies are ready and `apply` is running |
|
||||
| ACTIVE | The plugin is running |
|
||||
| FAILED | `apply` threw an error |
|
||||
| UNLOADING | The plugin is unloading and disposing resources |
|
||||
| DISPOSED | The plugin is fully unloaded |
|
||||
|
||||
## Dependency-driven loading
|
||||
|
||||
A plugin with `inject` waits for every required service before loading:
|
||||
|
||||
```ts ignore-check
|
||||
export const inject = ['tools', 'llm']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// ctx.tools and ctx.llm are ready here.
|
||||
}
|
||||
```
|
||||
|
||||
If a required service disappears, for example during provider replacement, the plugin unloads automatically (ACTIVE → DISPOSED) and loads again when the service returns.
|
||||
|
||||
## Automatic cleanup
|
||||
|
||||
Every registration made through `ctx` is undone when the plugin unloads:
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// Event listener: removed automatically on unload.
|
||||
ctx.on('some-event', handler)
|
||||
|
||||
// Custom resource: the returned disposer runs on unload.
|
||||
ctx.effect(() => {
|
||||
const connection = createConnection()
|
||||
return () => connection.close()
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The framework tracks and disposes all of these operations:
|
||||
- `ctx.on(event, handler)` — event listener
|
||||
- `ctx.tools.register(tool)` — tool registration
|
||||
- `ctx.llm.registerAdapter(names, adapter)` — LLM adapter registration
|
||||
- `ctx.effect(() => cleanup)` — custom resource
|
||||
|
||||
During unload, disposer invocation starts in reverse registration order, but multiple async disposers run concurrently and have no serial completion guarantee. Put order-dependent cleanup in one disposer returned from a single `ctx.effect()` and await its steps serially there.
|
||||
|
||||
## Nested contexts
|
||||
|
||||
`ctx.plugin()` creates a child Fiber that inherits the parent context but has an independent lifecycle:
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// Register a child plugin.
|
||||
ctx.plugin(childPlugin)
|
||||
|
||||
// The child has its own Fiber and unloads with its parent.
|
||||
}
|
||||
```
|
||||
|
||||
## Dispose semantics
|
||||
|
||||
To stop a plugin instance early:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
declare const ctx: Context
|
||||
declare function myPlugin(ctx: Context): void
|
||||
|
||||
const fiber = ctx.plugin(myPlugin)
|
||||
|
||||
// Dispose it manually later.
|
||||
await fiber.dispose()
|
||||
```
|
||||
|
||||
`dispose` guarantees:
|
||||
1. All registrations owned by the plugin are removed.
|
||||
2. Child plugins are recursively unloaded.
|
||||
3. The returned promise resolves after all asynchronous cleanup finishes.
|
||||
|
||||
## Hot replacement (HMR)
|
||||
|
||||
With `@cordisjs/plugin-hmr` loaded from `cordis.yml`, editing a plugin source file triggers:
|
||||
|
||||
1. Unload the old plugin and clean up its registrations.
|
||||
2. Load the new code.
|
||||
3. Run the new `apply`.
|
||||
|
||||
Because plugin registrations clean themselves up, hot replacement does not retain registrations from the old instance.
|
||||
|
||||
## Example lifecycle
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
console.log('plugin loading')
|
||||
|
||||
ctx.effect(() => {
|
||||
console.log('effect registered')
|
||||
return () => console.log('effect cleaned up')
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Loading prints:
|
||||
```
|
||||
plugin loading
|
||||
effect registered
|
||||
```
|
||||
|
||||
Unloading prints:
|
||||
```
|
||||
effect cleaned up
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Services and dependencies](./service.md) — expose a capability to other plugins
|
||||
- [Event system](./events.md) — communicate between plugins
|
||||
@@ -1,5 +1,7 @@
|
||||
# 插件与生命周期
|
||||
|
||||
[English](index.md) | 中文
|
||||
|
||||
深入了解 Cordis 插件模型和生命周期状态机。
|
||||
|
||||
## Fiber 状态机
|
||||
@@ -25,15 +27,11 @@ ACTIVE → UNLOADING → DISPOSED
|
||||
|
||||
声明了 `inject` 的插件不会立即加载,而是等待依赖的服务就绪:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
```ts ignore-check
|
||||
export const inject = ['tools', 'llm']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// 到这里时,ctx.tools 和 ctx.llm 一定存在
|
||||
// ctx.tools and ctx.llm are ready here.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -43,23 +41,12 @@ export function apply(ctx: Context) {
|
||||
|
||||
通过 `ctx` 做的任何注册,在插件卸载时都会自动撤销:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Events {
|
||||
'my-plugin/some-event'(): void
|
||||
}
|
||||
}
|
||||
|
||||
declare function handler(): void
|
||||
declare function createConnection(): { close(): void }
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// 事件监听——卸载时自动移除
|
||||
ctx.on('my-plugin/some-event', handler)
|
||||
// Event listener: removed automatically on unload.
|
||||
ctx.on('some-event', handler)
|
||||
|
||||
// 自定义资源——卸载时调用返回的函数
|
||||
// Custom resource: the returned disposer runs on unload.
|
||||
ctx.effect(() => {
|
||||
const connection = createConnection()
|
||||
return () => connection.close()
|
||||
@@ -73,22 +60,18 @@ export function apply(ctx: Context) {
|
||||
- `ctx.llm.registerAdapter(names, adapter)` — LLM 适配器注册
|
||||
- `ctx.effect(() => cleanup)` — 自定义资源
|
||||
|
||||
插件卸载时,这些注册按倒序逐个撤销。
|
||||
插件卸载时,处置器按注册顺序的反向发起,但多个异步处置器会并发执行,不保证逐个完成。存在顺序依赖的清理步骤必须放进同一个 `ctx.effect()` 返回的处置器中,由该处置器负责串行等待。
|
||||
|
||||
## 嵌套上下文
|
||||
|
||||
`ctx.plugin()` 创建子 Fiber,它继承父上下文但有独立的生命周期:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
declare function childPlugin(ctx: Context): void
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
// 注册一个子插件
|
||||
// Register a child plugin.
|
||||
ctx.plugin(childPlugin)
|
||||
|
||||
// 子插件有自己的 Fiber,父卸载时子也卸载
|
||||
// The child has its own Fiber and unloads with its parent.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -104,7 +87,7 @@ declare function myPlugin(ctx: Context): void
|
||||
|
||||
const fiber = ctx.plugin(myPlugin)
|
||||
|
||||
// 之后可以手动 dispose
|
||||
// Dispose it manually later.
|
||||
await fiber.dispose()
|
||||
```
|
||||
|
||||
@@ -125,11 +108,7 @@ await fiber.dispose()
|
||||
|
||||
## 实战:理解生命周期
|
||||
|
||||
`apply` 函数体就是加载钩子;卸载没有专门的事件——把清理逻辑放进 `ctx.effect()` 的返回函数即可:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
```ts ignore-check
|
||||
export function apply(ctx: Context) {
|
||||
console.log('plugin loading')
|
||||
|
||||
@@ -153,5 +132,5 @@ effect cleaned up
|
||||
|
||||
## 下一步
|
||||
|
||||
- [服务与依赖](service) — 让你的插件对外提供能力
|
||||
- [事件系统](events) — 插件间通信的核心机制
|
||||
- [服务与依赖](./service.md) — 让你的插件对外提供能力
|
||||
- [事件系统](./events.md) — 插件间通信的核心机制
|
||||
6
docs/user/develop/framework/service.i18n.yaml
Normal file
6
docs/user/develop/framework/service.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
service.md: 1bf28cb3c7dfdfbd6d0babfa3b1688ac65eea01e
|
||||
service.zh.md: 17785c056ab9a0a21974e6ed8bbe7f7de05fa00e
|
||||
148
docs/user/develop/framework/service.md
Normal file
148
docs/user/develop/framework/service.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Services and dependencies
|
||||
|
||||
English | [中文](service.zh.md)
|
||||
|
||||
A service is a capability one plugin exposes to other plugins. `inject` declares the services a plugin requires.
|
||||
|
||||
## What is a service?
|
||||
|
||||
In Harness, `tools`, `llm`, and `agents` are services. Each is a named capability mounted on `ctx`:
|
||||
|
||||
```ts ignore-check
|
||||
ctx.tools // ToolRegistry service
|
||||
ctx.llm // LLM service
|
||||
ctx.agents // Agent service
|
||||
```
|
||||
|
||||
Any plugin can provide a service for other plugins to consume.
|
||||
|
||||
## Consume a service
|
||||
|
||||
Declare `inject` to use an existing service:
|
||||
|
||||
```ts ignore-check
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// ctx.tools exists and is ready here.
|
||||
ctx.tools.register(/* ... */)
|
||||
}
|
||||
```
|
||||
|
||||
When `apply` runs, every service declared by `inject` is ready. If a service is not ready, the plugin waits instead of running.
|
||||
|
||||
## Provide a service
|
||||
|
||||
### Extend Service
|
||||
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
export default class MetricsService extends Service {
|
||||
static inject = ['llm'] // A service may depend on other services.
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'metrics') // 'metrics' is the service name.
|
||||
}
|
||||
|
||||
// Public service method.
|
||||
record(event: string, value: number) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After loading this plugin, consumers access the service as `ctx.metrics`:
|
||||
|
||||
```ts ignore-check
|
||||
export const inject = ['metrics']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.metrics.record('tool_call', 1)
|
||||
}
|
||||
```
|
||||
|
||||
### Declare its type
|
||||
|
||||
Use TypeScript declaration merging to type `ctx.metrics`:
|
||||
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
metrics: MetricsService
|
||||
}
|
||||
}
|
||||
|
||||
export default class MetricsService extends Service {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'metrics')
|
||||
}
|
||||
|
||||
record(event: string, value: number) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency behavior
|
||||
|
||||
### Required and optional dependencies
|
||||
|
||||
```ts ignore-check
|
||||
// Required: the plugin does not load while the service is absent.
|
||||
export const inject = ['tools']
|
||||
|
||||
// Optional: omit inject and query with ctx.get() at the use site.
|
||||
export function apply(ctx: Context) {
|
||||
const metrics = ctx.get('metrics')
|
||||
metrics?.record('plugin_loaded', 1)
|
||||
}
|
||||
```
|
||||
|
||||
### When a service disappears
|
||||
|
||||
If a required service disappears while the application is running, for example because its provider unloads:
|
||||
|
||||
1. Dependent plugins dispose automatically.
|
||||
2. They load again when the service returns.
|
||||
|
||||
This prevents a plugin from calling a service that no longer exists.
|
||||
|
||||
## Service isolation
|
||||
|
||||
`cordis.yml` can isolate services so separate plugin groups see separate instances of the same service:
|
||||
|
||||
```yaml
|
||||
- id: group-a
|
||||
name: '@cordisjs/plugin-group'
|
||||
group: true
|
||||
isolate:
|
||||
bash: true
|
||||
config:
|
||||
- name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
timeoutMs: 5000
|
||||
- name: './src/plugin-a.ts'
|
||||
|
||||
- id: group-b
|
||||
name: '@cordisjs/plugin-group'
|
||||
group: true
|
||||
isolate:
|
||||
bash: true
|
||||
config:
|
||||
- name: '@deepseek-ai/dsh-bash-local'
|
||||
config:
|
||||
timeoutMs: 60000
|
||||
- name: './src/plugin-b.ts'
|
||||
```
|
||||
|
||||
`plugin-a` and `plugin-b` each see the Bash instance in their own group, with no cross-group effect.
|
||||
|
||||
## Built-in Harness services
|
||||
|
||||
The repository generates the service names, public methods, and source locations in the [service catalog](../../../cordis-catalog/services.md). Use that catalog and the service's TypeScript interface while developing a plugin; do not maintain a second static list.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Event system](./events.md) — communicate between plugins without tight coupling
|
||||
- [Capability layering](../practice/) — use services as capability interfaces
|
||||
@@ -1,22 +1,17 @@
|
||||
# 服务与依赖
|
||||
|
||||
[English](service.md) | 中文
|
||||
|
||||
服务 (Service) 是插件对外暴露能力的方式。依赖 (inject) 是插件声明自己需要哪些服务。
|
||||
|
||||
## 什么是服务
|
||||
|
||||
在 Harness 中,`tools`、`llm`、`agents` 都是服务。服务是挂载在 `ctx` 上的命名能力:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-agent'
|
||||
|
||||
declare const ctx: Context
|
||||
|
||||
ctx.tools // ToolRegistry 服务
|
||||
ctx.llm // LLM 服务
|
||||
ctx.agents // Agent 注册表服务
|
||||
```ts ignore-check
|
||||
ctx.tools // ToolRegistry service
|
||||
ctx.llm // LLM service
|
||||
ctx.agents // Agent service
|
||||
```
|
||||
|
||||
任何插件都可以提供一个新服务,供其他插件使用。
|
||||
@@ -25,22 +20,12 @@ ctx.agents // Agent 注册表服务
|
||||
|
||||
声明 `inject` 来使用已有服务:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
```ts ignore-check
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// ctx.tools 在这里一定存在且就绪
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'demo',
|
||||
description: 'Demo tool.',
|
||||
parameters: {},
|
||||
async execute() {
|
||||
return []
|
||||
},
|
||||
}))
|
||||
// ctx.tools exists and is ready here.
|
||||
ctx.tools.register(/* ... */)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -52,16 +37,15 @@ export function apply(ctx: Context) {
|
||||
|
||||
```ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
import type {} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
export default class MetricsService extends Service {
|
||||
static inject = ['llm'] // 本服务也可以依赖其他服务
|
||||
static inject = ['llm'] // A service may depend on other services.
|
||||
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'metrics') // 'metrics' 是服务名
|
||||
super(ctx, 'metrics') // 'metrics' is the service name.
|
||||
}
|
||||
|
||||
// 服务的公开方法
|
||||
// Public service method.
|
||||
record(event: string, value: number) {
|
||||
// ...
|
||||
}
|
||||
@@ -70,9 +54,7 @@ export default class MetricsService extends Service {
|
||||
|
||||
加载这个插件后,其他插件就可以通过 `ctx.metrics` 访问它:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
```ts ignore-check
|
||||
export const inject = ['metrics']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
@@ -104,18 +86,14 @@ export default class MetricsService extends Service {
|
||||
|
||||
## 依赖的行为
|
||||
|
||||
### 必选依赖 vs 可选读取
|
||||
### 必选依赖 vs 可选依赖
|
||||
|
||||
`inject` 声明的依赖都是必选的:服务不存在时,插件不会加载。如果只想"有则用之",用 `ctx.get()` 读取——服务不存在时返回 `undefined`,插件照常加载:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
// 必选:服务不存在时,插件不会加载
|
||||
```ts ignore-check
|
||||
// Required: the plugin does not load while the service is absent.
|
||||
export const inject = ['tools']
|
||||
|
||||
// Optional: omit inject and query with ctx.get() at the use site.
|
||||
export function apply(ctx: Context) {
|
||||
// 可选读取:不声明 inject,服务不存在时返回 undefined
|
||||
const metrics = ctx.get('metrics')
|
||||
metrics?.record('plugin_loaded', 1)
|
||||
}
|
||||
@@ -132,7 +110,7 @@ export function apply(ctx: Context) {
|
||||
|
||||
## 服务隔离
|
||||
|
||||
`cordis.yml` 支持服务隔离——同一个服务可以有多个实例,不同插件组看到不同实例。用 `@cordisjs/plugin-group` 建组(`group: true` 标记组条目),并在组上声明 `isolate`,把该服务隔离进组内作用域:
|
||||
`cordis.yml` 支持服务隔离——同一个服务可以有多个实例,不同插件组看到不同实例:
|
||||
|
||||
```yaml
|
||||
- id: group-a
|
||||
@@ -158,24 +136,13 @@ export function apply(ctx: Context) {
|
||||
- name: './src/plugin-b.ts'
|
||||
```
|
||||
|
||||
`plugin-a` 和 `plugin-b` 各自看到自己组内的 bash 实例,互不影响。`isolate: { bash: true }` 是必需的:不隔离的话,两个组在同一作用域注册同名服务,第二个会直接报重复注册错误。
|
||||
`plugin-a` 和 `plugin-b` 各自看到自己组内的 bash 实例,互不影响。
|
||||
|
||||
## Harness 内置服务一览
|
||||
## Harness 内置服务
|
||||
|
||||
| 服务名 | 提供者 | 用途 |
|
||||
|--------|--------|------|
|
||||
| `tools` | dsh-tools | Tool 注册表 |
|
||||
| `llm` | dsh-llm | LLM 调用 + 适配器注册 |
|
||||
| `agents` | dsh-agent | Agent 注册表 |
|
||||
| `agentLoop` | dsh-agent-loop | Agent 创建与循环执行 |
|
||||
| `sessions` | dsh-session | 会话存储与事件流 |
|
||||
| `systemPrompt` | dsh-system-prompt | 系统提示词组装 |
|
||||
| `bash` | dsh-bash(实现:dsh-bash-local) | Bash 命令执行 |
|
||||
| `fs` | dsh-fs(实现:dsh-fs-local) | 文件系统操作 |
|
||||
| `subagents` | dsh-subagent | 子代理委派 |
|
||||
| `sessionPersistence` | dsh-session-persistence(实现:-jsonl / -sqlite) | 会话持久化 |
|
||||
服务名、公开方法和源码位置由仓库自动生成,见[服务目录](../../../cordis-catalog/services.md)。开发插件时应以该目录和服务接口的 TypeScript 类型为准,不要复制一份静态清单。
|
||||
|
||||
## 下一步
|
||||
|
||||
- [事件系统](events) — 插件间松耦合通信
|
||||
- [事件系统](./events.md) — 插件间松耦合通信
|
||||
- [能力三件套](../practice/) — 服务在 seam 模式中的应用
|
||||
6
docs/user/develop/practice/index.i18n.yaml
Normal file
6
docs/user/develop/practice/index.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
index.md: 0261b49b071167f7c2a33f78bbc1959cc6f1879f
|
||||
index.zh.md: 5819344430fcbde31bf825e9815120983e44e3f6
|
||||
158
docs/user/develop/practice/index.md
Normal file
158
docs/user/develop/practice/index.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# Three-layer capability design
|
||||
|
||||
English | [中文](index.zh.md)
|
||||
|
||||
When a capability is general enough to need replaceable implementations, such as Bash execution, Harness splits it into three packages: an **interface**, an **implementation**, and a **consumer**. Each layer can evolve or be replaced independently.
|
||||
|
||||
## Bash example
|
||||
|
||||
The Bash execution capability consists of:
|
||||
|
||||
- **Interface** (`dsh-bash`) — defines Bash request and result shapes
|
||||
- **Implementation** (`dsh-bash-local`) — executes commands on the local machine
|
||||
- **Consumer** (`dsh-tool-bash`) — exposes the capability as a model-callable tool
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
|
||||
│ dsh-bash │────▶│ dsh-bash-local │ │ dsh-tool-bash│
|
||||
│ (interface) │ │ (implementation) │ │(consumer/tool)│
|
||||
└─────────────┘ └──────────────────┘ └──────────────┘
|
||||
▲ │
|
||||
└────────────────────────────────────────────┘
|
||||
inject: ['bash']
|
||||
```
|
||||
|
||||
## Benefits of the split
|
||||
|
||||
### Replace implementations
|
||||
|
||||
One interface can have multiple implementations selected through `cordis.yml`:
|
||||
|
||||
```yaml
|
||||
# Local execution
|
||||
- name: '@deepseek-ai/dsh-bash-local'
|
||||
|
||||
# Or a future remote sandbox implementation
|
||||
# - name: '@deepseek-ai/dsh-bash-remote'
|
||||
# config:
|
||||
# endpoint: 'https://sandbox.example.com'
|
||||
```
|
||||
|
||||
The interface and tool remain unchanged while the implementation changes.
|
||||
|
||||
### Evolve independently
|
||||
|
||||
- The interface changes rarely after its contract stabilizes.
|
||||
- Implementations can improve performance and security independently.
|
||||
- Consumers can change how they present the capability to the model.
|
||||
|
||||
### Decouple dependencies
|
||||
|
||||
- The implementation depends on the interface.
|
||||
- The consumer depends on the interface.
|
||||
- The implementation and consumer **do not depend on each other**.
|
||||
|
||||
## Built-in three-layer capabilities
|
||||
|
||||
| Capability | Interface | Implementation | Consumer |
|
||||
|------|-------------|------|---------------|
|
||||
| Bash | `dsh-bash` | `dsh-bash-local` | `dsh-tool-bash` |
|
||||
| Filesystem | `dsh-fs` | `dsh-fs-local` + `dsh-fs-policy` | `dsh-tool-fs` |
|
||||
| Web | `dsh-web` | `dsh-web-fetch-local` / `dsh-web-search-*` | `dsh-tool-web` |
|
||||
| Subagent | `dsh-subagent` | `dsh-subagent-spawn` / `dsh-subagent-fork` | `dsh-tool-subagent` |
|
||||
| Compaction | `dsh-compact` | `dsh-compact-basic` | The implementation consumes agent-loop extension events |
|
||||
|
||||
## Develop a three-layer capability
|
||||
|
||||
### Step 1: define the interface
|
||||
|
||||
```ts ignore-check
|
||||
// packages/my-cap/my-cap/src/index.ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
myCap: MyCapService
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class MyCapService extends Service {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'myCap')
|
||||
}
|
||||
|
||||
/** Execute the capability. */
|
||||
abstract execute(request: MyCapRequest): Promise<MyCapResult>
|
||||
}
|
||||
|
||||
export interface MyCapRequest {
|
||||
input: string
|
||||
}
|
||||
|
||||
export interface MyCapResult {
|
||||
output: string
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: write an implementation
|
||||
|
||||
```ts ignore-check
|
||||
// packages/my-cap/my-cap-local/src/index.ts
|
||||
import type { Context } from 'cordis'
|
||||
import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/dsh-my-cap'
|
||||
|
||||
class MyCapLocal extends MyCapService {
|
||||
async execute(request: MyCapRequest): Promise<MyCapResult> {
|
||||
// Concrete implementation.
|
||||
return { output: request.input.toUpperCase() }
|
||||
}
|
||||
}
|
||||
|
||||
export const name = 'my-cap-local'
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.plugin(MyCapLocal)
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: write a consumer
|
||||
|
||||
```ts ignore-check
|
||||
// packages/my-cap/tool-my-cap/src/index.ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
export const name = 'tool-my-cap'
|
||||
export const inject = ['tools', 'myCap']
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'my_cap',
|
||||
description: 'Execute my capability.',
|
||||
parameters: {
|
||||
input: { type: 'string', required: true },
|
||||
},
|
||||
async execute(args) {
|
||||
const result = await ctx.myCap.execute({ input: args.input })
|
||||
return [{ type: 'text', text: result.output }]
|
||||
},
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
### Compose them in cordis.yml
|
||||
|
||||
```yaml
|
||||
- name: '@deepseek-ai/dsh-my-cap-local'
|
||||
- name: '@deepseek-ai/dsh-tool-my-cap'
|
||||
```
|
||||
|
||||
## Design points
|
||||
|
||||
- **Do not split preemptively** — use three packages only when the capability needs replaceable implementations. A simple tool plugin does not.
|
||||
- **The interface owns Request/Result types** — implementations and consumers depend only on the interface package.
|
||||
- **Explicit > implicit** — resolve defaults in an explicit `resolve(request): Spec` step rather than hiding `?? default` expressions inside `run()`.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [LLM adapter](./llm-adapter.md) — implement an LLM backend, a common capability interface extension
|
||||
@@ -1,5 +1,7 @@
|
||||
# 能力的三层拆分
|
||||
|
||||
[English](index.md) | 中文
|
||||
|
||||
当一个能力(插件)足够通用(比如"执行 bash 命令"),Harness 会把它拆成三个包:**接口**、**实现**、**消费者**。这样可以独立替换其中任何一层。
|
||||
|
||||
## 以 Bash 为例
|
||||
@@ -13,7 +15,7 @@
|
||||
```
|
||||
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
|
||||
│ dsh-bash │────▶│ dsh-bash-local │ │ dsh-tool-bash│
|
||||
│ (接口) │ │ (实现) │ │ (消费者/tool)│
|
||||
│ (interface) │ │ (implementation) │ │(consumer/tool)│
|
||||
└─────────────┘ └──────────────────┘ └──────────────┘
|
||||
▲ │
|
||||
└────────────────────────────────────────────┘
|
||||
@@ -27,10 +29,10 @@
|
||||
同一个接口可以有多种实现。用户通过 `cordis.yml` 选择:
|
||||
|
||||
```yaml
|
||||
# 本地执行
|
||||
# Local execution
|
||||
- name: '@deepseek-ai/dsh-bash-local'
|
||||
|
||||
# 或:远程沙箱执行(未来)
|
||||
# Or a future remote sandbox implementation
|
||||
# - name: '@deepseek-ai/dsh-bash-remote'
|
||||
# config:
|
||||
# endpoint: 'https://sandbox.example.com'
|
||||
@@ -58,13 +60,13 @@
|
||||
| 文件系统 | `dsh-fs` | `dsh-fs-local` + `dsh-fs-policy` | `dsh-tool-fs` |
|
||||
| Web | `dsh-web` | `dsh-web-fetch-local` / `dsh-web-search-*` | `dsh-tool-web` |
|
||||
| 子代理 | `dsh-subagent` | `dsh-subagent-spawn` / `dsh-subagent-fork` | `dsh-tool-subagent` |
|
||||
| 压缩 | `dsh-compact` | `dsh-compact-basic` | (内置于 agent-loop) |
|
||||
| 压缩 | `dsh-compact` | `dsh-compact-basic` | 由实现插件消费 agent-loop 的扩展事件 |
|
||||
|
||||
## 开发你自己的三件套
|
||||
|
||||
### 第一步:定义接口
|
||||
|
||||
```ts
|
||||
```ts ignore-check
|
||||
// packages/my-cap/my-cap/src/index.ts
|
||||
import { Service, type Context } from 'cordis'
|
||||
|
||||
@@ -79,7 +81,7 @@ export abstract class MyCapService extends Service {
|
||||
super(ctx, 'myCap')
|
||||
}
|
||||
|
||||
/** 执行能力的核心方法 */
|
||||
/** Execute the capability. */
|
||||
abstract execute(request: MyCapRequest): Promise<MyCapResult>
|
||||
}
|
||||
|
||||
@@ -101,7 +103,7 @@ import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/
|
||||
|
||||
class MyCapLocal extends MyCapService {
|
||||
async execute(request: MyCapRequest): Promise<MyCapResult> {
|
||||
// 具体实现
|
||||
// Concrete implementation.
|
||||
return { output: request.input.toUpperCase() }
|
||||
}
|
||||
}
|
||||
@@ -115,7 +117,7 @@ export function apply(ctx: Context) {
|
||||
|
||||
### 第三步:编写消费者 (tool)
|
||||
|
||||
```ts
|
||||
```ts ignore-check
|
||||
// packages/my-cap/tool-my-cap/src/index.ts
|
||||
import type { Context } from 'cordis'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
@@ -140,7 +142,7 @@ export function apply(ctx: Context) {
|
||||
|
||||
### 在 cordis.yml 中组合
|
||||
|
||||
```yaml ignore-check
|
||||
```yaml
|
||||
- name: '@deepseek-ai/dsh-my-cap-local'
|
||||
- name: '@deepseek-ai/dsh-tool-my-cap'
|
||||
```
|
||||
@@ -153,4 +155,4 @@ export function apply(ctx: Context) {
|
||||
|
||||
## 下一步
|
||||
|
||||
- [LLM 适配器](llm-adapter) — 实现一个 LLM 后端(最常见的 seam 扩展)
|
||||
- [LLM 适配器](./llm-adapter.md) — 实现一个 LLM 后端(最常见的 seam 扩展)
|
||||
6
docs/user/develop/practice/llm-adapter.i18n.yaml
Normal file
6
docs/user/develop/practice/llm-adapter.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
llm-adapter.md: f34fc9e1d5b59a323bb562764821ef910025880e
|
||||
llm-adapter.zh.md: 3c781ae8a1a011e2f73d5f6de43f6f75e1fb549f
|
||||
185
docs/user/develop/practice/llm-adapter.md
Normal file
185
docs/user/develop/practice/llm-adapter.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# LLM adapters
|
||||
|
||||
English | [中文](llm-adapter.zh.md)
|
||||
|
||||
This guide connects a new LLM provider to Harness.
|
||||
|
||||
## Overview
|
||||
|
||||
An LLM adapter extends `LlmAdapter` and implements `stream()`, translating Harness's provider-neutral request into a provider API call and translating the response back into Harness chunks.
|
||||
|
||||
## Minimal implementation
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import Schema from 'schemastery'
|
||||
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
class MyAdapter extends LlmAdapter {
|
||||
private apiKey: string
|
||||
|
||||
constructor(apiKey: string) {
|
||||
super()
|
||||
this.apiKey = apiKey
|
||||
}
|
||||
|
||||
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
// 1. Convert options.messages to the provider format.
|
||||
// 2. Call the streaming API.
|
||||
// 3. Convert the response into StreamChunk values.
|
||||
}
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
apiKey: string
|
||||
models: string[]
|
||||
}
|
||||
|
||||
export const Config: Schema<Config> = Schema.object({
|
||||
apiKey: Schema.string().required(),
|
||||
models: Schema.array(Schema.string()).required(),
|
||||
})
|
||||
|
||||
export const name = 'my-llm-adapter'
|
||||
export const inject = ['llm']
|
||||
|
||||
export function apply(ctx: Context, config: Config) {
|
||||
const adapter = new MyAdapter(config.apiKey)
|
||||
ctx.llm.registerAdapter(config.models, adapter)
|
||||
}
|
||||
```
|
||||
|
||||
## StreamChunk protocol
|
||||
|
||||
`stream()` yields chunks using this protocol:
|
||||
|
||||
```ts
|
||||
import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
async function* exampleChunks(): AsyncIterable<StreamChunk> {
|
||||
// 1. Start each content block with block-start.
|
||||
yield { type: 'block-start', index: 0, blockType: 'text' }
|
||||
|
||||
// 2. Stream text through text-delta.
|
||||
yield { type: 'text-delta', index: 0, text: 'Hello' }
|
||||
yield { type: 'text-delta', index: 0, text: ' world' }
|
||||
|
||||
// 3. End each content block with block-end and the complete block.
|
||||
yield {
|
||||
type: 'block-end',
|
||||
index: 0,
|
||||
block: { type: 'text', text: 'Hello world' },
|
||||
}
|
||||
|
||||
// 4. Tool-call block.
|
||||
yield { type: 'block-start', index: 1, blockType: 'tool-call' }
|
||||
yield {
|
||||
type: 'tool-call-delta',
|
||||
index: 1,
|
||||
id: CallId('call-123'),
|
||||
name: 'bash',
|
||||
argumentsDelta: '{"command":"ls"}',
|
||||
}
|
||||
yield {
|
||||
type: 'block-end',
|
||||
index: 1,
|
||||
block: {
|
||||
type: 'tool-call',
|
||||
id: CallId('call-123'),
|
||||
name: 'bash',
|
||||
arguments: '{"command":"ls"}',
|
||||
},
|
||||
}
|
||||
|
||||
// 5. Token usage.
|
||||
yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } }
|
||||
|
||||
// 6. Finish reason.
|
||||
yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
// Alternatively, { kind: 'tool-calls' } requests tool execution.
|
||||
}
|
||||
```
|
||||
|
||||
### Key rules
|
||||
|
||||
- Every `block-start` has a matching `block-end`.
|
||||
- `index` increases from 0 and identifies content-block order.
|
||||
- A `tool-call-delta` carries raw JSON text in `argumentsDelta`, either all at once or over multiple chunks.
|
||||
- `finish` is the final chunk.
|
||||
- Emit `usage` before `finish`.
|
||||
|
||||
## GenerateOptions
|
||||
|
||||
`stream()` receives the exported `GenerateOptions` type. It includes the model, conversation history, system prompt, tool schemas, generation parameters, stop sequences, and abort signal; treat the TypeScript type exported by `@deepseek-ai/dsh-llm` as authoritative. Map supported fields to the provider API. If the provider cannot honor a field, throw `LlmError` with a stable code instead of silently dropping it.
|
||||
|
||||
## Register an adapter
|
||||
|
||||
```ts ignore-check
|
||||
ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter)
|
||||
```
|
||||
|
||||
The first argument lists the model names handled by the adapter. If `cordis.yml` selects `model: model-name-1`, the service routes that request to this adapter.
|
||||
|
||||
## Use it from cordis.yml
|
||||
|
||||
```yaml
|
||||
- id: my-llm
|
||||
name: './src/my-llm-adapter.ts'
|
||||
config:
|
||||
apiKey: !!js process.env.MY_API_KEY
|
||||
models:
|
||||
- my-model-v1
|
||||
- my-model-v2
|
||||
|
||||
- id: stdio-agent
|
||||
name: '@deepseek-ai/dsh-stdio-demo'
|
||||
config:
|
||||
model: my-model-v1 # References the model registered above.
|
||||
```
|
||||
|
||||
## Reference implementations
|
||||
|
||||
The repository contains complete implementations:
|
||||
|
||||
- `packages/llm/llm-deepseek/` — DeepSeek API adapter using the OpenAI-compatible format
|
||||
- `packages/llm/llm-pi-ai/` — Pi AI adapter using a different API format
|
||||
- `examples/echo-agent/src/mock-llm.ts` — minimal local teaching adapter
|
||||
|
||||
Start with the mock adapter to study a complete chunk sequence without network behavior.
|
||||
|
||||
## Error handling
|
||||
|
||||
Adapters throw transport and protocol failures as `LlmError` values with stable codes. The agent loop preserves the error and code for diagnostics and policy; it does not convert an ordinary `Error` automatically. Every provider HTTP request must also merge `attributionHeaders()` and forward `options.signal`.
|
||||
|
||||
```ts
|
||||
import {
|
||||
attributionHeaders,
|
||||
LlmAdapter,
|
||||
LlmError,
|
||||
type GenerateOptions,
|
||||
type StreamChunk,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
class HttpAdapter extends LlmAdapter {
|
||||
constructor(private readonly endpoint: string) {
|
||||
super()
|
||||
}
|
||||
|
||||
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
const response = await fetch(this.endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
...attributionHeaders(),
|
||||
},
|
||||
body: JSON.stringify({ model: options.model, messages: options.messages }),
|
||||
...options.signal ? { signal: options.signal } : {},
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new LlmError(`Provider API error: ${response.status}`, 'PROVIDER_HTTP_ERROR')
|
||||
}
|
||||
// A real adapter parses the response and emits the complete chunk sequence.
|
||||
yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,5 +1,7 @@
|
||||
# LLM 适配器
|
||||
|
||||
[English](llm-adapter.md) | 中文
|
||||
|
||||
本文介绍如何为 Harness 接入一个新的 LLM 提供方。
|
||||
|
||||
## 概述
|
||||
@@ -10,6 +12,7 @@ LLM 适配器是一个继承 `LlmAdapter` 的类,实现 `stream()` 方法,
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import Schema from 'schemastery'
|
||||
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
class MyAdapter extends LlmAdapter {
|
||||
@@ -21,9 +24,9 @@ class MyAdapter extends LlmAdapter {
|
||||
}
|
||||
|
||||
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
// 1. 将 options.messages 转换为你的 API 格式
|
||||
// 2. 调用 API(流式)
|
||||
// 3. 将 API 响应转换为 StreamChunk 序列
|
||||
// 1. Convert options.messages to the provider format.
|
||||
// 2. Call the streaming API.
|
||||
// 3. Convert the response into StreamChunk values.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +35,11 @@ export interface Config {
|
||||
models: string[]
|
||||
}
|
||||
|
||||
export const Config: Schema<Config> = Schema.object({
|
||||
apiKey: Schema.string().required(),
|
||||
models: Schema.array(Schema.string()).required(),
|
||||
})
|
||||
|
||||
export const name = 'my-llm-adapter'
|
||||
export const inject = ['llm']
|
||||
|
||||
@@ -48,22 +56,22 @@ export function apply(ctx: Context, config: Config) {
|
||||
```ts
|
||||
import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
async function* demo(): AsyncIterable<StreamChunk> {
|
||||
// 1. 每个内容块以 block-start 开始
|
||||
async function* exampleChunks(): AsyncIterable<StreamChunk> {
|
||||
// 1. Start each content block with block-start.
|
||||
yield { type: 'block-start', index: 0, blockType: 'text' }
|
||||
|
||||
// 2. 文本块使用 text-delta
|
||||
// 2. Stream text through text-delta.
|
||||
yield { type: 'text-delta', index: 0, text: 'Hello' }
|
||||
yield { type: 'text-delta', index: 0, text: ' world' }
|
||||
|
||||
// 3. 每个内容块以 block-end 结束(携带完整 block)
|
||||
// 3. End each content block with block-end and the complete block.
|
||||
yield {
|
||||
type: 'block-end',
|
||||
index: 0,
|
||||
block: { type: 'text', text: 'Hello world' },
|
||||
}
|
||||
|
||||
// 4. Tool call 块
|
||||
// 4. Tool-call block.
|
||||
yield { type: 'block-start', index: 1, blockType: 'tool-call' }
|
||||
yield {
|
||||
type: 'tool-call-delta',
|
||||
@@ -83,12 +91,12 @@ async function* demo(): AsyncIterable<StreamChunk> {
|
||||
},
|
||||
}
|
||||
|
||||
// 5. Token 用量
|
||||
// 5. Token usage.
|
||||
yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } }
|
||||
|
||||
// 6. 结束原因
|
||||
// 6. Finish reason.
|
||||
yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
// 或: { kind: 'tool-calls' } 表示模型想调用 tool
|
||||
// Alternatively, { kind: 'tool-calls' } requests tool execution.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -102,33 +110,11 @@ async function* demo(): AsyncIterable<StreamChunk> {
|
||||
|
||||
## GenerateOptions
|
||||
|
||||
`stream()` 接收的请求包含:
|
||||
|
||||
```ts
|
||||
import type { GenerateOptions } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
declare const options: GenerateOptions
|
||||
|
||||
options.model // 模型名
|
||||
options.messages // 对话历史 (Message[])
|
||||
options.tools // 可用的 tool schema 列表 (ToolSchema[])
|
||||
options.system // 系统提示词
|
||||
options.maxTokens // 最大输出 token
|
||||
options.temperature // 温度
|
||||
options.signal // 取消信号(必须响应)
|
||||
```
|
||||
|
||||
你的适配器需要将这些映射到具体 API 的参数。
|
||||
`stream()` 接收仓库导出的 `GenerateOptions`。它包含模型名、对话历史、系统提示词、tool schema、生成参数、停止序列和中止信号;完整字段以 `@deepseek-ai/dsh-llm` 导出的 TypeScript 类型为准。适配器必须将支持的字段映射到具体 API;无法支持的字段应抛出带稳定 code 的 `LlmError`,不能静默丢弃。
|
||||
|
||||
## 注册适配器
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
import type { LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
declare const ctx: Context
|
||||
declare const adapter: LlmAdapter
|
||||
|
||||
```ts ignore-check
|
||||
ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter)
|
||||
```
|
||||
|
||||
@@ -148,7 +134,7 @@ ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter)
|
||||
- id: stdio-agent
|
||||
name: '@deepseek-ai/dsh-stdio-demo'
|
||||
config:
|
||||
model: my-model-v1 # 引用上面注册的模型名
|
||||
model: my-model-v1 # References the model registered above.
|
||||
```
|
||||
|
||||
## 实战参考
|
||||
@@ -163,20 +149,37 @@ mock 适配器是学习 StreamChunk 协议的最佳起点——它用纯本地
|
||||
|
||||
## 错误处理
|
||||
|
||||
适配器中的异常会被 agent-loop 捕获并转化为 `LlmError`,告知上层。不需要在 `stream()` 内部做错误恢复——让异常冒泡即可。
|
||||
适配器应将传输和协议故障作为带稳定 code 的 `LlmError` 抛出;agent loop 会保留该错误及其 code,供诊断和策略使用。不要依赖普通 `Error` 被自动转换。每个提供方 HTTP 请求还必须合并 `attributionHeaders()`,并传递 `options.signal`。
|
||||
|
||||
```ts
|
||||
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import {
|
||||
attributionHeaders,
|
||||
LlmAdapter,
|
||||
LlmError,
|
||||
type GenerateOptions,
|
||||
type StreamChunk,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
|
||||
class HttpAdapter extends LlmAdapter {
|
||||
private endpoint = 'https://api.example.com/v1/chat'
|
||||
constructor(private readonly endpoint: string) {
|
||||
super()
|
||||
}
|
||||
|
||||
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
const response = await fetch(this.endpoint, { method: 'POST' })
|
||||
const response = await fetch(this.endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
...attributionHeaders(),
|
||||
},
|
||||
body: JSON.stringify({ model: options.model, messages: options.messages }),
|
||||
...options.signal ? { signal: options.signal } : {},
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status}`)
|
||||
throw new LlmError(`Provider API error: ${response.status}`, 'PROVIDER_HTTP_ERROR')
|
||||
}
|
||||
// ... 正常流式处理
|
||||
// A real adapter parses the response and emits the complete chunk sequence.
|
||||
yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
}
|
||||
}
|
||||
```
|
||||
6
docs/user/guide/config.i18n.yaml
Normal file
6
docs/user/guide/config.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
config.md: a3f56018fd43cc803c1710f97c29a77340a0b257
|
||||
config.zh.md: af661b9d7ef72e4085551202169e975bd0c3ec99
|
||||
59
docs/user/guide/config.md
Normal file
59
docs/user/guide/config.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Configuration
|
||||
|
||||
English | [中文](config.zh.md)
|
||||
|
||||
Harness uses `cordis.yml` to describe which plugins an agent loads and the configuration passed to each one. The file composes capabilities; the generated configuration catalog records the fields and defaults each package actually supports, avoiding a second hand-maintained reference.
|
||||
|
||||
## Start from a real configuration
|
||||
|
||||
The repository examples are runnable configurations and the most reliable starting points for a new project:
|
||||
|
||||
- [echo-agent](../../../examples/echo-agent/cordis.yml) uses a local mock model and needs no API key.
|
||||
- [repl-agent](../../../examples/repl-agent/cordis.yml) combines the DeepSeek model, Bash, filesystem, compaction, subagents, and workflows.
|
||||
- [acp-agent](../../../examples/acp-agent/cordis.yml) connects to editor clients over ACP.
|
||||
|
||||
A minimal configuration is a list of plugin entries:
|
||||
|
||||
```yaml
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
models:
|
||||
- deepseek-v4-flash
|
||||
|
||||
- id: stdio-agent
|
||||
name: '@deepseek-ai/dsh-stdio-demo'
|
||||
config:
|
||||
model: deepseek-v4-flash
|
||||
```
|
||||
|
||||
## Plugin entries
|
||||
|
||||
`name` identifies an npm package or a local module relative to `cordis.yml`; `id` gives the plugin instance a stable identity; and `config` supplies plugin-specific configuration. Set `disabled: true` to skip an entry temporarily.
|
||||
|
||||
```yaml
|
||||
- id: local-tool
|
||||
name: './src/my-tool.ts'
|
||||
disabled: false
|
||||
config:
|
||||
toolName: my_tool
|
||||
```
|
||||
|
||||
Plugins load in file order. Place plugins that depend on services after the applications or capability plugins that provide them. Missing models, tools, and plugins fail as early as possible instead of being silently ignored.
|
||||
|
||||
## JavaScript values and environment variables
|
||||
|
||||
The Cordis loader evaluates runtime expressions tagged with `!!js`. Keep API keys and other secrets in the gitignored `.env` file at the repository root, never in committed configuration.
|
||||
|
||||
```yaml
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
cwd: !!js process.cwd()
|
||||
```
|
||||
|
||||
The tag is `!!js`, not `!js`.
|
||||
|
||||
## Exact configuration reference
|
||||
|
||||
The generated [plugin configuration catalog](../../config-catalog.md) lists every current field, type, and default. For composition concepts, continue to the [architecture](../../architecture.md) and [capability interfaces](../../capability-seams.md). To create a configuration, copy the closest entry from the [examples overview](../../../examples/README.md) and adapt it.
|
||||
59
docs/user/guide/config.zh.md
Normal file
59
docs/user/guide/config.zh.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# 配置文件
|
||||
|
||||
[English](config.md) | 中文
|
||||
|
||||
Harness 使用 `cordis.yml` 描述 Agent 加载哪些插件以及每个插件的参数。配置文件负责组合能力;每个包真正支持的字段和默认值由源码生成的配置目录负责记录,避免两份手写表格逐渐不一致。
|
||||
|
||||
## 从真实配置开始
|
||||
|
||||
仓库中的示例就是可以运行的配置,也是新项目最可靠的起点:
|
||||
|
||||
- [echo-agent](../../../examples/echo-agent/cordis.yml) 使用本地 mock 模型,不需要 API key。
|
||||
- [repl-agent](../../../examples/repl-agent/cordis.yml) 组合 DeepSeek 模型、Bash、文件系统、压缩、子代理和工作流。
|
||||
- [acp-agent](../../../examples/acp-agent/cordis.yml) 通过 ACP 接入编辑器客户端。
|
||||
|
||||
最小配置由一组插件条目组成:
|
||||
|
||||
```yaml
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
models:
|
||||
- deepseek-v4-flash
|
||||
|
||||
- id: stdio-agent
|
||||
name: '@deepseek-ai/dsh-stdio-demo'
|
||||
config:
|
||||
model: deepseek-v4-flash
|
||||
```
|
||||
|
||||
## 插件条目
|
||||
|
||||
`name` 指定 npm 包或相对于 `cordis.yml` 的本地模块,`id` 为插件实例提供稳定标识,`config` 传入插件自己的配置。需要临时跳过某个条目时可设置 `disabled: true`。
|
||||
|
||||
```yaml
|
||||
- id: local-tool
|
||||
name: './src/my-tool.ts'
|
||||
disabled: false
|
||||
config:
|
||||
toolName: my_tool
|
||||
```
|
||||
|
||||
插件按文件中的顺序加载。依赖其他服务的插件应该排在提供这些服务的应用或能力插件之后;引用不存在的模型、工具或插件会尽早报错,而不是被静默忽略。
|
||||
|
||||
## JavaScript 值和环境变量
|
||||
|
||||
Cordis loader 使用 `!!js` 标签读取运行时表达式。API key 等凭据应放在仓库根目录、已被 Git 忽略的 `.env` 中,不能提交到配置文件。
|
||||
|
||||
```yaml
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
cwd: !!js process.cwd()
|
||||
```
|
||||
|
||||
标签是 `!!js`,不是 `!js`。
|
||||
|
||||
## 精确配置参考
|
||||
|
||||
每个插件当前支持的字段、类型和默认值见自动生成的[插件配置目录](../../config-catalog.md)。理解插件如何组合可继续阅读[架构说明](../../architecture.md)和[能力接口](../../capability-seams.md);要创建自己的配置,优先复制并修改[示例目录说明](../../../examples/README.md)中最接近的例子。
|
||||
6
docs/user/guide/index.i18n.yaml
Normal file
6
docs/user/guide/index.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
index.md: a20b1041e13b01b6b1d01a5baa8975d3e68c6aa0
|
||||
index.zh.md: 56ec50352218e2e28ad2dd7a6ef387376de75606
|
||||
49
docs/user/guide/index.md
Normal file
49
docs/user/guide/index.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Introduction
|
||||
|
||||
English | [中文](index.zh.md)
|
||||
|
||||
DeepSeek Harness is a **plugin-based agent development framework** built on the [Cordis](https://github.com/cordiverse/cordis) microkernel. Its central idea is simple: **everything is a plugin**.
|
||||
|
||||
## What it is
|
||||
|
||||
Harness implements every capability an AI agent needs—including LLM calls, tool execution, session management, and subtask delegation—as a composable plugin. A `cordis.yml` file declares which plugins to load and how to configure them, assembling a complete agent.
|
||||
|
||||
```yaml
|
||||
# Select the LLM backend
|
||||
- name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
|
||||
# Select the application template
|
||||
- name: '@deepseek-ai/dsh-stdio-demo'
|
||||
config:
|
||||
model: deepseek-v4-flash
|
||||
```
|
||||
|
||||
## Who it is for
|
||||
|
||||
### Application users
|
||||
|
||||
To run an existing agent application, such as a coding assistant or conversational agent:
|
||||
|
||||
1. Copy an example template.
|
||||
2. Add an API key.
|
||||
3. Run it.
|
||||
|
||||
No code is required. See the [quick start](./quickstart.md).
|
||||
|
||||
### Plugin developers
|
||||
|
||||
To add a custom tool, a new LLM adapter, or another execution backend, write a plugin. Harness provides explicit extension interfaces and a type-safe development experience. See [development](../develop/basic/).
|
||||
|
||||
## Core features
|
||||
|
||||
- **Configuration only** — `cordis.yml` selects the capability set; changing a model or adding a tool is a configuration edit.
|
||||
- **Hot replacement (HMR)** — edit plugin code during development without restarting the process.
|
||||
|
||||
## Technology
|
||||
|
||||
- **Runtime**: Node.js ^22.19 or >= 24
|
||||
- **Language**: TypeScript (ESM)
|
||||
- **Framework**: Cordis
|
||||
- **Package manager**: pnpm workspaces (the repository pins pnpm 11)
|
||||
@@ -1,5 +1,7 @@
|
||||
# 介绍
|
||||
|
||||
[English](index.md) | 中文
|
||||
|
||||
DeepSeek Harness 是一个**插件化的 Agent 开发框架**,基于 [Cordis](https://github.com/cordiverse/cordis) 微内核构建。它的核心理念是:**一切皆插件**。
|
||||
|
||||
## 它是什么
|
||||
@@ -7,12 +9,12 @@ DeepSeek Harness 是一个**插件化的 Agent 开发框架**,基于 [Cordis](
|
||||
Harness 将一个 AI Agent(智能体) 所需要的所有能力——LLM 调用、工具执行、会话管理、子任务分配——全部构建为可组合的插件。你通过一个 `cordis.yml` 配置文件来声明加载哪些插件、使用什么参数,就能组装出一个完整的 Agent。
|
||||
|
||||
```yaml
|
||||
# 选择 LLM 后端
|
||||
# Select the LLM backend
|
||||
- name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
|
||||
# 选择应用模板
|
||||
# Select the application template
|
||||
- name: '@deepseek-ai/dsh-stdio-demo'
|
||||
config:
|
||||
model: deepseek-v4-flash
|
||||
@@ -28,7 +30,7 @@ Harness 将一个 AI Agent(智能体) 所需要的所有能力——LLM 调
|
||||
2. 填写 API key
|
||||
3. 运行
|
||||
|
||||
不需要写任何代码。详见 [快速开始](quickstart)。
|
||||
不需要写任何代码。详见 [快速开始](./quickstart.md)。
|
||||
|
||||
### 插件开发者
|
||||
|
||||
@@ -41,7 +43,7 @@ Harness 将一个 AI Agent(智能体) 所需要的所有能力——LLM 调
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **运行时**: Node.js >= 24
|
||||
- **运行时**: Node.js ^22.19 或 >= 24
|
||||
- **语言**: TypeScript (ESM)
|
||||
- **框架**: Cordis
|
||||
- **包管理**: pnpm workspaces
|
||||
- **包管理**: pnpm workspaces(仓库固定使用 pnpm 11)
|
||||
6
docs/user/guide/quickstart.i18n.yaml
Normal file
6
docs/user/guide/quickstart.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
quickstart.md: acae2ac095e057971043c2bcece7a52d3ebc1c2c
|
||||
quickstart.zh.md: 54643fe54e62dbbd3696362cb43ff8569577c53b
|
||||
99
docs/user/guide/quickstart.md
Normal file
99
docs/user/guide/quickstart.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Quick start
|
||||
|
||||
English | [中文](quickstart.zh.md)
|
||||
|
||||
This guide gets an agent running in five minutes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Node.js](https://nodejs.org/) ^22.19 or >= 24
|
||||
- [pnpm](https://pnpm.io/) 11 (use Corepack to select the repository-pinned version)
|
||||
|
||||
```sh
|
||||
# Check versions
|
||||
node -v # v22.19.x, or v24.x and newer
|
||||
corepack enable
|
||||
pnpm -v # 11.x
|
||||
```
|
||||
|
||||
## Step 1: run echo-agent
|
||||
|
||||
echo-agent needs no API key and runs after dependencies are installed.
|
||||
|
||||
```sh
|
||||
# Clone the repository
|
||||
git clone https://github.com/deepseek-harness/deepseek-harness.git
|
||||
cd deepseek-harness
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Start echo-agent
|
||||
pnpm run demo:echo
|
||||
```
|
||||
|
||||
The process prints:
|
||||
|
||||
```
|
||||
echo-agent ready. Type a message ("echo <text>" triggers the tool).
|
||||
>
|
||||
```
|
||||
|
||||
Enter:
|
||||
|
||||
```
|
||||
> echo hello world
|
||||
```
|
||||
|
||||
The model issues a tool call, and the echo tool returns the text in uppercase:
|
||||
|
||||
```
|
||||
[tool call] echo({"text":"hello world"})
|
||||
[tool result] ECHO: HELLO WORLD
|
||||
```
|
||||
|
||||
Your local environment is ready.
|
||||
|
||||
## Step 2: use a real model
|
||||
|
||||
Next, connect a real DeepSeek model and run the complete command-line agent.
|
||||
|
||||
### Get an API key
|
||||
|
||||
Get an API key from [DeepSeek Platform](https://platform.deepseek.com/).
|
||||
|
||||
### Configure the environment
|
||||
|
||||
Create a gitignored `.env` file in the repository root:
|
||||
|
||||
```sh
|
||||
DEEPSEEK_API_KEY=sk-your-key-here
|
||||
```
|
||||
|
||||
### Start repl-agent
|
||||
|
||||
```sh
|
||||
pnpm run demo:repl
|
||||
```
|
||||
|
||||
```
|
||||
agent REPL ready. Give it a coding task.
|
||||
>
|
||||
```
|
||||
|
||||
This is a complete coding assistant that can read and write files, run commands, and delegate subtasks.
|
||||
|
||||
Try a task:
|
||||
|
||||
```
|
||||
> Create hello.js in the current directory, print "Hello from Harness!", and run it
|
||||
```
|
||||
|
||||
## What happened
|
||||
|
||||
echo-agent and repl-agent use the same application framework (`@deepseek-ai/dsh-stdio-demo`). Their `cordis.yml` files select different plugins and configuration. Custom agents use the same composition model.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Configuration](./config.md) — understand the `cordis.yml` format
|
||||
- [Develop a plugin](../develop/basic/) — build your own tool or backend
|
||||
@@ -1,16 +1,19 @@
|
||||
# 快速开始
|
||||
|
||||
[English](quickstart.md) | 中文
|
||||
|
||||
本指南带你在 5 分钟内跑起一个 Agent。
|
||||
|
||||
## 环境准备
|
||||
|
||||
- [Node.js](https://nodejs.org/) >= 24
|
||||
- [pnpm](https://pnpm.io/) >= 9
|
||||
- [Node.js](https://nodejs.org/) ^22.19 或 >= 24
|
||||
- [pnpm](https://pnpm.io/) 11(建议通过 Corepack 使用仓库固定的版本)
|
||||
|
||||
```sh
|
||||
# 确认版本
|
||||
node -v # v24.x 或更高
|
||||
pnpm -v # 9.x 或更高
|
||||
# Check versions
|
||||
node -v # v22.19.x, or v24.x and newer
|
||||
corepack enable
|
||||
pnpm -v # 11.x
|
||||
```
|
||||
|
||||
## 第一步:运行 echo-agent
|
||||
@@ -18,16 +21,14 @@ pnpm -v # 9.x 或更高
|
||||
echo-agent 不需要 API key,装好依赖就能跑。
|
||||
|
||||
```sh
|
||||
# 克隆仓库
|
||||
# Clone the repository
|
||||
git clone https://github.com/deepseek-harness/deepseek-harness.git
|
||||
cd deepseek-harness
|
||||
|
||||
# 安装依赖
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
# 如果看到 ERR_PNPM_IGNORED_BUILDS,可以忽略——安装已经成功了。
|
||||
# 想消除这个提示可以跑一次: pnpm approve-builds
|
||||
|
||||
# 启动 echo-agent
|
||||
# Start echo-agent
|
||||
pnpm run demo:echo
|
||||
```
|
||||
|
||||
@@ -85,7 +86,7 @@ agent REPL ready. Give it a coding task.
|
||||
试着给它一个任务:
|
||||
|
||||
```
|
||||
> 在当前目录创建一个 hello.js,内容是打印 "Hello from Harness!",然后运行它
|
||||
> Create hello.js in the current directory, print "Hello from Harness!", and run it
|
||||
```
|
||||
|
||||
## 回头看
|
||||
@@ -94,5 +95,5 @@ echo-agent 和 repl-agent 用的是同一个应用框架(`@deepseek-ai/dsh-stdio
|
||||
|
||||
## 下一步
|
||||
|
||||
- [配置文件](config) — 了解 `cordis.yml` 的完整语法
|
||||
- [配置文件](./config.md) — 了解 `cordis.yml` 的完整语法
|
||||
- [开发插件](../develop/basic/) — 编写你自己的 tool 或后端
|
||||
6
docs/user/index.i18n.yaml
Normal file
6
docs/user/index.i18n.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
index.md: e9a1f03785c7472c47550ec59ea0165d28d3d9a6
|
||||
index.zh.md: 907f1452c9ff50d619989c18dcf2727addb2573d
|
||||
25
docs/user/index.md
Normal file
25
docs/user/index.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
layout: home
|
||||
hero:
|
||||
name: DeepSeek Harness
|
||||
text: Plugin-based agent development framework
|
||||
tagline: Built on the Cordis microkernel; everything is a plugin
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Quick start
|
||||
link: /en/guide/quickstart
|
||||
- theme: alt
|
||||
text: Develop plugins
|
||||
link: /en/develop/basic/
|
||||
features:
|
||||
- title: Plugin architecture
|
||||
details: Built on the Cordis plugin system. Every capability is registered by a plugin, takes effect when loaded, and is reverted when unloaded.
|
||||
- title: Configuration as composition
|
||||
details: One cordis.yml determines the agent's complete capability set. Change a model or add a tool by editing configuration.
|
||||
- title: Ready to use
|
||||
details: Includes LLM calls, file access, Bash execution, subagent delegation, and the rest of the core toolchain. Copy a template to get started.
|
||||
---
|
||||
|
||||
# DeepSeek Harness
|
||||
|
||||
English | [中文](index.zh.md)
|
||||
@@ -7,15 +7,19 @@ hero:
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 快速开始
|
||||
link: /zh-CN/guide/quickstart
|
||||
link: /guide/quickstart
|
||||
- theme: alt
|
||||
text: 开发插件
|
||||
link: /zh-CN/develop/basic/
|
||||
link: /develop/basic/
|
||||
features:
|
||||
- title: 插件化架构
|
||||
details: 基于 Cordis 效果系统,所有能力通过插件注册,加载即生效、卸载即还原。
|
||||
details: 基于 Cordis 插件系统,所有能力通过插件注册,加载即生效、卸载即还原。
|
||||
- title: 配置即组合
|
||||
details: 一个 cordis.yml 决定整个 Agent 的能力组合——换模型、加工具,只需改一行配置。
|
||||
- title: 开箱即用
|
||||
details: 内置 LLM 调用、文件读写、Bash 执行、子代理委派等完整工具链,复制模板即可运行。
|
||||
---
|
||||
|
||||
# DeepSeek Harness
|
||||
|
||||
[English](index.md) | 中文
|
||||
@@ -12,6 +12,7 @@ export default tseslint.config(
|
||||
'**/.sessions/**',
|
||||
'.claude/**', // harness-local state (worktrees, skills) — other checkouts, not this one's sources
|
||||
'**/.doc-typecheck-*/**',
|
||||
'website/.generated/**',
|
||||
'vendor/**', // vendored source keeps upstream style and idioms
|
||||
'native/**', // imported landlock-run subtree: self-contained workspace with its own gates (native/README.md)
|
||||
'**/*.js',
|
||||
@@ -22,7 +23,7 @@ export default tseslint.config(
|
||||
|
||||
// --- our packages: full strictness -------------------------------------
|
||||
{
|
||||
files: ['packages/*/*/src/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts'],
|
||||
files: ['packages/*/*/src/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'website/**/*.ts'],
|
||||
extends: [
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
],
|
||||
@@ -109,7 +110,7 @@ export default tseslint.config(
|
||||
|
||||
// --- file-local duplication (all owned TypeScript) ---------------------
|
||||
{
|
||||
files: ['packages/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts'],
|
||||
files: ['packages/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'website/**/*.ts'],
|
||||
plugins: { sonarjs },
|
||||
rules: {
|
||||
// Cross-file clones are covered separately by jscpd.
|
||||
@@ -126,7 +127,7 @@ export default tseslint.config(
|
||||
|
||||
// --- formatting (everything we own) -------------------------------------
|
||||
{
|
||||
files: ['packages/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'eslint.config.mjs'],
|
||||
files: ['packages/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'website/**/*.ts', 'eslint.config.mjs'],
|
||||
plugins: { '@stylistic': stylistic },
|
||||
rules: {
|
||||
'@stylistic/indent': ['error', 2],
|
||||
|
||||
12
knip.json
12
knip.json
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||
"exclude": ["duplicates"],
|
||||
"ignoreBinaries": ["bwrap", "python3", "sandbox-exec"],
|
||||
"ignoreWorkspaces": ["vendor/*", "python/sdk-runtime", "website"],
|
||||
"ignoreWorkspaces": ["vendor/*", "python/sdk-runtime"],
|
||||
"workspaces": {
|
||||
".": {
|
||||
"project": ["scripts/**/*.ts"]
|
||||
@@ -18,6 +18,16 @@
|
||||
"project": ["**/*.ts"],
|
||||
"ignoreDependencies": ["@deepseek-ai/.+", "@cordisjs/.+"]
|
||||
},
|
||||
"website": {
|
||||
"project": ["**/*.ts"],
|
||||
"ignoreDependencies": [
|
||||
"@braintree/sanitize-url",
|
||||
"cytoscape",
|
||||
"cytoscape-cose-bilkent",
|
||||
"dayjs",
|
||||
"debug"
|
||||
]
|
||||
},
|
||||
"packages/*/*": {
|
||||
"entry": ["tests/**/*.spec.ts"],
|
||||
"project": ["src/**/*.ts", "tests/**/*.ts"]
|
||||
|
||||
13
package.json
13
package.json
@@ -48,6 +48,12 @@
|
||||
"verify-translation-prompt": "tsx scripts/verify-translation-prompt.ts",
|
||||
"verify-translation-pairing": "tsx scripts/verify-translation-pairing.ts",
|
||||
"verify-doc-budgets": "tsx scripts/verify-doc-budgets.ts",
|
||||
"docs:dev": "pnpm --filter @deepseek-ai/website run dev",
|
||||
"docs:build": "pnpm --filter @deepseek-ai/website run build",
|
||||
"docs:preview": "pnpm --filter @deepseek-ai/website run preview",
|
||||
"docs:check": "pnpm exec vitest run scripts/project-doc-site.spec.ts && pnpm run docs:build",
|
||||
"website:dev": "pnpm run docs:dev",
|
||||
"website:build": "pnpm run docs:build",
|
||||
"verify-package-readme-limitations": "tsx scripts/verify-package-readme-limitations.ts",
|
||||
"verify-node-next-types": "tsx scripts/verify-node-next-types.ts",
|
||||
"verify-runtime-closure": "tsx scripts/verify-runtime-closure.ts",
|
||||
@@ -69,13 +75,8 @@
|
||||
"gen-scoped-events": "tsx scripts/gen-scoped-events.ts",
|
||||
"verify-scoped-events": "tsx scripts/gen-scoped-events.ts --check",
|
||||
"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-scoped-events && 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-package-readme-model-experience && pnpm run verify-mermaid && pnpm run verify-agent-note-classification && pnpm run verify-agent-note-format && pnpm run verify-type-equiv && pnpm run verify-translation-prompt && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets && pnpm run verify-package-readme-limitations && 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-scoped-events && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-package-readme-model-experience && pnpm run verify-mermaid && pnpm run verify-agent-note-classification && pnpm run verify-agent-note-format && pnpm run verify-type-equiv && pnpm run verify-translation-prompt && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets && pnpm run verify-package-readme-limitations && pnpm run docs:check",
|
||||
"hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-cordis-config && pnpm run verify-node-next-types && pnpm run verify-runtime-closure",
|
||||
"demo:echo": "node --expose-internals --import tsx packages/examples/stdio-demo/src/bin.ts examples/echo-agent/cordis.yml",
|
||||
"demo:repl": "node --expose-internals --import tsx packages/examples/stdio-demo/src/bin.ts examples/repl-agent/cordis.yml",
|
||||
|
||||
362
pnpm-lock.yaml
generated
362
pnpm-lock.yaml
generated
@@ -2874,15 +2874,33 @@ importers:
|
||||
|
||||
website:
|
||||
devDependencies:
|
||||
markdown-it-mathjax3:
|
||||
specifier: ^4.3.2
|
||||
version: 4.3.2
|
||||
'@braintree/sanitize-url':
|
||||
specifier: 7.1.2
|
||||
version: 7.1.2
|
||||
cytoscape:
|
||||
specifier: 3.34.0
|
||||
version: 3.34.0
|
||||
cytoscape-cose-bilkent:
|
||||
specifier: 4.1.0
|
||||
version: 4.1.0(cytoscape@3.34.0)
|
||||
dayjs:
|
||||
specifier: 1.11.21
|
||||
version: 1.11.21
|
||||
debug:
|
||||
specifier: 4.4.3
|
||||
version: 4.4.3
|
||||
mermaid:
|
||||
specifier: 11.16.0
|
||||
version: 11.16.0
|
||||
vite:
|
||||
specifier: ^5.4.14
|
||||
version: 5.4.21(@types/node@25.9.3)(lightningcss@1.32.0)
|
||||
vitepress:
|
||||
specifier: ^1.6.3
|
||||
version: 1.6.4(@algolia/client-search@5.55.2)(@types/node@25.9.3)(lightningcss@1.32.0)(markdown-it-mathjax3@4.3.2)(postcss@8.5.15)(search-insights@2.17.3)(typescript@6.0.3)
|
||||
vue:
|
||||
specifier: ^3.5.13
|
||||
version: 3.5.39(typescript@6.0.3)
|
||||
specifier: ^1.6.4
|
||||
version: 1.6.4(@algolia/client-search@5.55.2)(@types/node@25.9.3)(lightningcss@1.32.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@6.0.3)
|
||||
vitepress-plugin-mermaid:
|
||||
specifier: ^2.0.17
|
||||
version: 2.0.17(mermaid@11.16.0)(vitepress@1.6.4(@algolia/client-search@5.55.2)(@types/node@25.9.3)(lightningcss@1.32.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@6.0.3))
|
||||
|
||||
packages:
|
||||
|
||||
@@ -3145,6 +3163,9 @@ packages:
|
||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@braintree/sanitize-url@6.0.4':
|
||||
resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==}
|
||||
|
||||
'@braintree/sanitize-url@7.1.2':
|
||||
resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==}
|
||||
|
||||
@@ -3663,6 +3684,9 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@mermaid-js/mermaid-mindmap@9.3.0':
|
||||
resolution: {integrity: sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==}
|
||||
|
||||
'@mermaid-js/parser@1.2.0':
|
||||
resolution: {integrity: sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==}
|
||||
|
||||
@@ -4786,10 +4810,6 @@ packages:
|
||||
resolution: {integrity: sha512-OyacJsaeuLUvGWOynNqYc6sx88XvyoG39wMT8SYqL3l9wwaorDW/LPRbUPfhzw0bWsUWzNCZTnFYOrWFBKsUaw==}
|
||||
engines: {node: '>= 14.0.0'}
|
||||
|
||||
ansi-colors@4.1.3:
|
||||
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -4853,9 +4873,6 @@ packages:
|
||||
resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
boolbase@1.0.0:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
|
||||
bowser@2.14.1:
|
||||
resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==}
|
||||
|
||||
@@ -4905,13 +4922,6 @@ packages:
|
||||
character-entities@2.0.2:
|
||||
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
|
||||
|
||||
cheerio-select@1.6.0:
|
||||
resolution: {integrity: sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==}
|
||||
|
||||
cheerio@1.0.0-rc.10:
|
||||
resolution: {integrity: sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
chokidar@4.0.3:
|
||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
@@ -4926,18 +4936,10 @@ packages:
|
||||
comma-separated-tokens@2.0.3:
|
||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||
|
||||
commander@13.1.0:
|
||||
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
commander@15.0.0:
|
||||
resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==}
|
||||
engines: {node: '>=22.12.0'}
|
||||
|
||||
commander@6.2.1:
|
||||
resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
commander@7.2.0:
|
||||
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -5005,17 +5007,10 @@ packages:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
css-select@4.3.0:
|
||||
resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
|
||||
|
||||
css-tree@3.2.1:
|
||||
resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
|
||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
|
||||
|
||||
css-what@6.2.2:
|
||||
resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
@@ -5233,26 +5228,9 @@ packages:
|
||||
resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
|
||||
dom-serializer@1.4.1:
|
||||
resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==}
|
||||
|
||||
domelementtype@2.3.0:
|
||||
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||
|
||||
domhandler@3.3.0:
|
||||
resolution: {integrity: sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
domhandler@4.3.1:
|
||||
resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
dompurify@3.4.11:
|
||||
resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==}
|
||||
|
||||
domutils@2.8.0:
|
||||
resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
|
||||
|
||||
dts-resolver@3.0.0:
|
||||
resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==}
|
||||
engines: {node: ^22.18.0 || >=24.0.0}
|
||||
@@ -5292,9 +5270,6 @@ packages:
|
||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
entities@2.2.0:
|
||||
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
|
||||
|
||||
entities@7.0.1:
|
||||
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
|
||||
engines: {node: '>=0.12'}
|
||||
@@ -5331,10 +5306,6 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
escape-goat@3.0.0:
|
||||
resolution: {integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
escape-html@1.0.3:
|
||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||
|
||||
@@ -5377,10 +5348,6 @@ packages:
|
||||
jiti:
|
||||
optional: true
|
||||
|
||||
esm@3.2.25:
|
||||
resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
espree@10.4.0:
|
||||
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -5643,12 +5610,6 @@ packages:
|
||||
html-void-elements@3.0.0:
|
||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||
|
||||
htmlparser2@5.0.1:
|
||||
resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==}
|
||||
|
||||
htmlparser2@6.1.0:
|
||||
resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==}
|
||||
|
||||
http-errors@2.0.1:
|
||||
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -5853,11 +5814,6 @@ packages:
|
||||
jszip@3.10.1:
|
||||
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||
|
||||
juice@8.1.0:
|
||||
resolution: {integrity: sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
hasBin: true
|
||||
|
||||
jwa@2.0.1:
|
||||
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
|
||||
|
||||
@@ -6056,9 +6012,6 @@ packages:
|
||||
mark.js@8.11.1:
|
||||
resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==}
|
||||
|
||||
markdown-it-mathjax3@4.3.2:
|
||||
resolution: {integrity: sha512-TX3GW5NjmupgFtMJGRauioMbbkGsOXAAt1DZ/rzzYmTHqzkO1rNAdiMD4NiruurToPApn2kYy76x02QN26qr2w==}
|
||||
|
||||
markdown-table@3.0.4:
|
||||
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
||||
|
||||
@@ -6076,10 +6029,6 @@ packages:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
mathjax-full@3.2.2:
|
||||
resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==}
|
||||
deprecated: Version 4 replaces this package with the scoped package @mathjax/src
|
||||
|
||||
mdast-util-find-and-replace@3.0.2:
|
||||
resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
|
||||
|
||||
@@ -6123,9 +6072,6 @@ packages:
|
||||
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
mensch@0.3.4:
|
||||
resolution: {integrity: sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==}
|
||||
|
||||
merge-descriptors@2.0.0:
|
||||
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -6133,9 +6079,6 @@ packages:
|
||||
mermaid@11.16.0:
|
||||
resolution: {integrity: sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==}
|
||||
|
||||
mhchemparser@4.2.1:
|
||||
resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==}
|
||||
|
||||
micromark-core-commonmark@2.0.3:
|
||||
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
|
||||
|
||||
@@ -6228,11 +6171,6 @@ packages:
|
||||
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
mime@2.6.0:
|
||||
resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
hasBin: true
|
||||
|
||||
minimatch@10.2.5:
|
||||
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
@@ -6254,9 +6192,6 @@ packages:
|
||||
mitt@3.0.1:
|
||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||
|
||||
mj-context-menu@0.6.1:
|
||||
resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==}
|
||||
|
||||
mri@1.2.0:
|
||||
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -6352,21 +6287,12 @@ packages:
|
||||
engines: {node: '>=10.5.0'}
|
||||
deprecated: Use your platform's native DOMException instead
|
||||
|
||||
node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
|
||||
node-fetch@3.3.2:
|
||||
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
nth-check@2.1.1:
|
||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||
non-layered-tidy-tree-layout@2.0.2:
|
||||
resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
@@ -6434,12 +6360,6 @@ packages:
|
||||
pako@1.0.11:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
|
||||
parse5-htmlparser2-tree-adapter@6.0.1:
|
||||
resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==}
|
||||
|
||||
parse5@6.0.1:
|
||||
resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
|
||||
|
||||
parse5@8.0.1:
|
||||
resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==}
|
||||
|
||||
@@ -6720,9 +6640,6 @@ packages:
|
||||
sisteransi@1.0.5:
|
||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||
|
||||
slick@1.12.2:
|
||||
resolution: {integrity: sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==}
|
||||
|
||||
smol-toml@1.6.1:
|
||||
resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==}
|
||||
engines: {node: '>= 18'}
|
||||
@@ -6742,10 +6659,6 @@ packages:
|
||||
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
speech-rule-engine@4.1.4:
|
||||
resolution: {integrity: sha512-i/VCLG1fvRc95pMHRqG4aQNscv+9aIsqA2oI7ZQS51sTdUcDHYX6cpT8/tqZ+enjs1tKVwbRBWgxut9SWn+f9g==}
|
||||
hasBin: true
|
||||
|
||||
stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
|
||||
@@ -6836,9 +6749,6 @@ packages:
|
||||
resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
|
||||
tr46@6.0.0:
|
||||
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -6990,10 +6900,6 @@ packages:
|
||||
resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==}
|
||||
hasBin: true
|
||||
|
||||
valid-data-url@3.0.1:
|
||||
resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
vary@1.1.2:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -7083,6 +6989,12 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vitepress-plugin-mermaid@2.0.17:
|
||||
resolution: {integrity: sha512-IUzYpwf61GC6k0XzfmAmNrLvMi9TRrVRMsUyCA8KNXhg/mQ1VqWnO0/tBVPiX5UoKF1mDUwqn5QV4qAJl6JnUg==}
|
||||
peerDependencies:
|
||||
mermaid: 10 || 11
|
||||
vitepress: ^1.0.0 || ^1.0.0-alpha
|
||||
|
||||
vitepress@1.6.4:
|
||||
resolution: {integrity: sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==}
|
||||
hasBin: true
|
||||
@@ -7152,17 +7064,10 @@ packages:
|
||||
resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
web-resource-inliner@6.0.1:
|
||||
resolution: {integrity: sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
web-streams-polyfill@3.3.3:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
webidl-conversions@8.0.1:
|
||||
resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -7175,9 +7080,6 @@ packages:
|
||||
resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -7188,9 +7090,6 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
wicked-good-xpath@1.3.0:
|
||||
resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==}
|
||||
|
||||
word-wrap@1.2.5:
|
||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -7669,6 +7568,9 @@ snapshots:
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2': {}
|
||||
|
||||
'@braintree/sanitize-url@6.0.4':
|
||||
optional: true
|
||||
|
||||
'@braintree/sanitize-url@7.1.2': {}
|
||||
|
||||
'@bramus/specificity@2.4.2':
|
||||
@@ -8069,6 +7971,17 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@mermaid-js/mermaid-mindmap@9.3.0':
|
||||
dependencies:
|
||||
'@braintree/sanitize-url': 6.0.4
|
||||
cytoscape: 3.34.0
|
||||
cytoscape-cose-bilkent: 4.1.0(cytoscape@3.34.0)
|
||||
cytoscape-fcose: 2.2.0(cytoscape@3.34.0)
|
||||
d3: 7.9.0
|
||||
khroma: 2.1.0
|
||||
non-layered-tidy-tree-layout: 2.0.2
|
||||
optional: true
|
||||
|
||||
'@mermaid-js/parser@1.2.0':
|
||||
dependencies:
|
||||
'@chevrotain/types': 11.1.2
|
||||
@@ -9092,8 +9005,6 @@ snapshots:
|
||||
'@algolia/requester-fetch': 5.55.2
|
||||
'@algolia/requester-node-http': 5.55.2
|
||||
|
||||
ansi-colors@4.1.3: {}
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-regex@6.2.2: {}
|
||||
@@ -9154,8 +9065,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
boolbase@1.0.0: {}
|
||||
|
||||
bowser@2.14.1: {}
|
||||
|
||||
brace-expansion@2.1.2:
|
||||
@@ -9194,24 +9103,6 @@ snapshots:
|
||||
|
||||
character-entities@2.0.2: {}
|
||||
|
||||
cheerio-select@1.6.0:
|
||||
dependencies:
|
||||
css-select: 4.3.0
|
||||
css-what: 6.2.2
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 4.3.1
|
||||
domutils: 2.8.0
|
||||
|
||||
cheerio@1.0.0-rc.10:
|
||||
dependencies:
|
||||
cheerio-select: 1.6.0
|
||||
dom-serializer: 1.4.1
|
||||
domhandler: 4.3.1
|
||||
htmlparser2: 6.1.0
|
||||
parse5: 6.0.1
|
||||
parse5-htmlparser2-tree-adapter: 6.0.1
|
||||
tslib: 2.8.1
|
||||
|
||||
chokidar@4.0.3:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
@@ -9224,12 +9115,8 @@ snapshots:
|
||||
|
||||
comma-separated-tokens@2.0.3: {}
|
||||
|
||||
commander@13.1.0: {}
|
||||
|
||||
commander@15.0.0: {}
|
||||
|
||||
commander@6.2.1: {}
|
||||
|
||||
commander@7.2.0: {}
|
||||
|
||||
commander@8.3.0: {}
|
||||
@@ -9297,21 +9184,11 @@ snapshots:
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
css-select@4.3.0:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
css-what: 6.2.2
|
||||
domhandler: 4.3.1
|
||||
domutils: 2.8.0
|
||||
nth-check: 2.1.1
|
||||
|
||||
css-tree@3.2.1:
|
||||
dependencies:
|
||||
mdn-data: 2.27.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
css-what@6.2.2: {}
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
cytoscape-cose-bilkent@4.1.0(cytoscape@3.34.0):
|
||||
@@ -9541,32 +9418,10 @@ snapshots:
|
||||
|
||||
diff@9.0.0: {}
|
||||
|
||||
dom-serializer@1.4.1:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 4.3.1
|
||||
entities: 2.2.0
|
||||
|
||||
domelementtype@2.3.0: {}
|
||||
|
||||
domhandler@3.3.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
domhandler@4.3.1:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
dompurify@3.4.11:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
domutils@2.8.0:
|
||||
dependencies:
|
||||
dom-serializer: 1.4.1
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 4.3.1
|
||||
|
||||
dts-resolver@3.0.0(oxc-resolver@11.20.0):
|
||||
optionalDependencies:
|
||||
oxc-resolver: 11.20.0
|
||||
@@ -9595,8 +9450,6 @@ snapshots:
|
||||
|
||||
encodeurl@2.0.0: {}
|
||||
|
||||
entities@2.2.0: {}
|
||||
|
||||
entities@7.0.1: {}
|
||||
|
||||
entities@8.0.0: {}
|
||||
@@ -9668,8 +9521,6 @@ snapshots:
|
||||
'@esbuild/win32-ia32': 0.28.1
|
||||
'@esbuild/win32-x64': 0.28.1
|
||||
|
||||
escape-goat@3.0.0: {}
|
||||
|
||||
escape-html@1.0.3: {}
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
@@ -9743,8 +9594,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
esm@3.2.25: {}
|
||||
|
||||
espree@10.4.0:
|
||||
dependencies:
|
||||
acorn: 8.17.0
|
||||
@@ -10056,20 +9905,6 @@ snapshots:
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
|
||||
htmlparser2@5.0.1:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 3.3.0
|
||||
domutils: 2.8.0
|
||||
entities: 2.2.0
|
||||
|
||||
htmlparser2@6.1.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 4.3.1
|
||||
domutils: 2.8.0
|
||||
entities: 2.2.0
|
||||
|
||||
http-errors@2.0.1:
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
@@ -10256,16 +10091,6 @@ snapshots:
|
||||
readable-stream: 2.3.8
|
||||
setimmediate: 1.0.5
|
||||
|
||||
juice@8.1.0:
|
||||
dependencies:
|
||||
cheerio: 1.0.0-rc.10
|
||||
commander: 6.2.1
|
||||
mensch: 0.3.4
|
||||
slick: 1.12.2
|
||||
web-resource-inliner: 6.0.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
jwa@2.0.1:
|
||||
dependencies:
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
@@ -10440,13 +10265,6 @@ snapshots:
|
||||
|
||||
mark.js@8.11.1: {}
|
||||
|
||||
markdown-it-mathjax3@4.3.2:
|
||||
dependencies:
|
||||
juice: 8.1.0
|
||||
mathjax-full: 3.2.2
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
markdown-table@3.0.4: {}
|
||||
|
||||
marked@16.4.2: {}
|
||||
@@ -10455,13 +10273,6 @@ snapshots:
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mathjax-full@3.2.2:
|
||||
dependencies:
|
||||
esm: 3.2.25
|
||||
mhchemparser: 4.2.1
|
||||
mj-context-menu: 0.6.1
|
||||
speech-rule-engine: 4.1.4
|
||||
|
||||
mdast-util-find-and-replace@3.0.2:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
@@ -10580,8 +10391,6 @@ snapshots:
|
||||
|
||||
media-typer@1.1.0: {}
|
||||
|
||||
mensch@0.3.4: {}
|
||||
|
||||
merge-descriptors@2.0.0: {}
|
||||
|
||||
mermaid@11.16.0:
|
||||
@@ -10608,8 +10417,6 @@ snapshots:
|
||||
ts-dedent: 2.3.0
|
||||
uuid: 14.0.1
|
||||
|
||||
mhchemparser@4.2.1: {}
|
||||
|
||||
micromark-core-commonmark@2.0.3:
|
||||
dependencies:
|
||||
decode-named-character-reference: 1.3.0
|
||||
@@ -10807,8 +10614,6 @@ snapshots:
|
||||
dependencies:
|
||||
mime-db: 1.54.0
|
||||
|
||||
mime@2.6.0: {}
|
||||
|
||||
minimatch@10.2.5:
|
||||
dependencies:
|
||||
brace-expansion: 5.0.6
|
||||
@@ -10825,8 +10630,6 @@ snapshots:
|
||||
|
||||
mitt@3.0.1: {}
|
||||
|
||||
mj-context-menu@0.6.1: {}
|
||||
|
||||
mri@1.2.0: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
@@ -10901,19 +10704,14 @@ snapshots:
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
node-fetch@3.3.2:
|
||||
dependencies:
|
||||
data-uri-to-buffer: 4.0.1
|
||||
fetch-blob: 3.2.0
|
||||
formdata-polyfill: 4.0.10
|
||||
|
||||
nth-check@2.1.1:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
non-layered-tidy-tree-layout@2.0.2:
|
||||
optional: true
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
@@ -11015,12 +10813,6 @@ snapshots:
|
||||
|
||||
pako@1.0.11: {}
|
||||
|
||||
parse5-htmlparser2-tree-adapter@6.0.1:
|
||||
dependencies:
|
||||
parse5: 6.0.1
|
||||
|
||||
parse5@6.0.1: {}
|
||||
|
||||
parse5@8.0.1:
|
||||
dependencies:
|
||||
entities: 8.0.0
|
||||
@@ -11379,8 +11171,6 @@ snapshots:
|
||||
|
||||
sisteransi@1.0.5: {}
|
||||
|
||||
slick@1.12.2: {}
|
||||
|
||||
smol-toml@1.6.1: {}
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
@@ -11391,12 +11181,6 @@ snapshots:
|
||||
|
||||
speakingurl@14.0.1: {}
|
||||
|
||||
speech-rule-engine@4.1.4:
|
||||
dependencies:
|
||||
'@xmldom/xmldom': 0.9.10
|
||||
commander: 13.1.0
|
||||
wicked-good-xpath: 1.3.0
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
statuses@2.0.2: {}
|
||||
@@ -11477,8 +11261,6 @@ snapshots:
|
||||
dependencies:
|
||||
tldts: 7.4.5
|
||||
|
||||
tr46@0.0.3: {}
|
||||
|
||||
tr46@6.0.0:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
@@ -11608,8 +11390,6 @@ snapshots:
|
||||
|
||||
uuid@14.0.1: {}
|
||||
|
||||
valid-data-url@3.0.1: {}
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
vfile-message@4.0.3:
|
||||
@@ -11672,7 +11452,14 @@ snapshots:
|
||||
tsx: 4.22.4
|
||||
yaml: 2.9.0
|
||||
|
||||
vitepress@1.6.4(@algolia/client-search@5.55.2)(@types/node@25.9.3)(lightningcss@1.32.0)(markdown-it-mathjax3@4.3.2)(postcss@8.5.15)(search-insights@2.17.3)(typescript@6.0.3):
|
||||
vitepress-plugin-mermaid@2.0.17(mermaid@11.16.0)(vitepress@1.6.4(@algolia/client-search@5.55.2)(@types/node@25.9.3)(lightningcss@1.32.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@6.0.3)):
|
||||
dependencies:
|
||||
mermaid: 11.16.0
|
||||
vitepress: 1.6.4(@algolia/client-search@5.55.2)(@types/node@25.9.3)(lightningcss@1.32.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
'@mermaid-js/mermaid-mindmap': 9.3.0
|
||||
|
||||
vitepress@1.6.4(@algolia/client-search@5.55.2)(@types/node@25.9.3)(lightningcss@1.32.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@6.0.3):
|
||||
dependencies:
|
||||
'@docsearch/css': 3.8.2
|
||||
'@docsearch/js': 3.8.2(@algolia/client-search@5.55.2)(search-insights@2.17.3)
|
||||
@@ -11693,7 +11480,6 @@ snapshots:
|
||||
vite: 5.4.21(@types/node@25.9.3)(lightningcss@1.32.0)
|
||||
vue: 3.5.39(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
markdown-it-mathjax3: 4.3.2
|
||||
postcss: 8.5.15
|
||||
transitivePeerDependencies:
|
||||
- '@algolia/client-search'
|
||||
@@ -11797,21 +11583,8 @@ snapshots:
|
||||
|
||||
walk-up-path@4.0.0: {}
|
||||
|
||||
web-resource-inliner@6.0.1:
|
||||
dependencies:
|
||||
ansi-colors: 4.1.3
|
||||
escape-goat: 3.0.0
|
||||
htmlparser2: 5.0.1
|
||||
mime: 2.6.0
|
||||
node-fetch: 2.7.0
|
||||
valid-data-url: 3.0.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
web-streams-polyfill@3.3.3: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
webidl-conversions@8.0.1: {}
|
||||
|
||||
whatwg-mimetype@5.0.0: {}
|
||||
@@ -11824,11 +11597,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@noble/hashes'
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
@@ -11838,8 +11606,6 @@ snapshots:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
|
||||
wicked-good-xpath@1.3.0: {}
|
||||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
wordwrap@1.0.0: {}
|
||||
|
||||
49
scripts/cordis-core-api.spec.ts
Normal file
49
scripts/cordis-core-api.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/** Tests for the generated Cordis core API reference. */
|
||||
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
CORDIS_CORE_API_PAGES,
|
||||
renderCordisCoreApiPage,
|
||||
renderCordisCoreApiPages,
|
||||
type CordisCoreApiPage,
|
||||
} from './cordis-core-api.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('Cordis core API generation', () => {
|
||||
it('renders the five detailed pages from pinned vendor declarations', () => {
|
||||
const pages = renderCordisCoreApiPages()
|
||||
expect([...pages.keys()]).toEqual(CORDIS_CORE_API_PAGES.map(page => page.out))
|
||||
expect(pages.get('docs/cordis-catalog/core/context.md')).toContain('### ctx.extend(meta?)')
|
||||
expect(pages.get('docs/cordis-catalog/core/events.md')).toContain('## DispatchMode')
|
||||
expect(pages.get('docs/cordis-catalog/core/fiber.md')).toContain('## EffectMeta')
|
||||
expect(pages.get('docs/cordis-catalog/core/registry.md')).toContain('## Plugin')
|
||||
expect(pages.get('docs/cordis-catalog/core/service.md')).toContain('### Service.resolveConfig')
|
||||
|
||||
const fiber = pages.get('docs/cordis-catalog/core/fiber.md') ?? ''
|
||||
expect(fiber).toContain('```\n\nRegister a cleanup-aware effect on this fiber.')
|
||||
expect(fiber).toContain('- `execute` — the effect body; see `Effect` for accepted shapes.')
|
||||
expect(fiber).toContain('**Returns** a disposer that tears the effect down and settles once done.')
|
||||
})
|
||||
|
||||
it('rejects a public core class without source JSDoc', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-cordis-core-api-'))
|
||||
roots.push(root)
|
||||
mkdirSync(join(root, 'vendor/cordis/src'), { recursive: true })
|
||||
writeFileSync(join(root, 'vendor/cordis/src/service.ts'), 'export class Service {\n run(): string { return "ok" }\n}\n')
|
||||
const page: CordisCoreApiPage = {
|
||||
out: 'docs/cordis-catalog/core/service.md',
|
||||
title: 'Service',
|
||||
intro: 'Service API.',
|
||||
sections: [{ kind: 'class', file: 'vendor/cordis/src/service.ts', symbol: 'Service' }],
|
||||
}
|
||||
expect(() => renderCordisCoreApiPage(page, root)).toThrow('class Service')
|
||||
})
|
||||
})
|
||||
433
scripts/cordis-core-api.ts
Normal file
433
scripts/cordis-core-api.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
/** Generate detailed Cordis core API pages from pinned vendor declarations. */
|
||||
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import ts from 'typescript'
|
||||
import { checkParams, checkReturns, parseJsDoc, parseTags, pointer, rawJsDoc, reportViolations } from './jsdoc.ts'
|
||||
import { cordisModuleBody } from './cordis-walk.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const FENCE = 'ts cordis-catalog'
|
||||
|
||||
/** One declaration group rendered on a Cordis core API page. */
|
||||
type CordisCoreApiSection =
|
||||
| { 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 core API page. */
|
||||
export interface CordisCoreApiPage {
|
||||
out: string
|
||||
title: string
|
||||
intro: string
|
||||
sections: CordisCoreApiSection[]
|
||||
}
|
||||
|
||||
/** Explicit editorial grouping for the pinned Cordis core surface. */
|
||||
export const CORDIS_CORE_API_PAGES: CordisCoreApiPage[] = [
|
||||
{
|
||||
out: 'docs/cordis-catalog/core/context.md',
|
||||
title: 'Context',
|
||||
intro: 'The context is the core Cordis object: every service, event, and lifecycle API is reached through `ctx`. Event methods are documented on [Events](events.md), effects and the current fiber on [Fiber](fiber.md), and plugin loading 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', heading: 'Service store and mixins' },
|
||||
],
|
||||
},
|
||||
{
|
||||
out: 'docs/cordis-catalog/core/events.md',
|
||||
title: 'Events',
|
||||
intro: 'The event-dispatch API mixed into every context. Harness event declarations and their dispatch modes are generated separately in the [Cordis events catalog](../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: 'docs/cordis-catalog/core/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, and `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', 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' },
|
||||
{ kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'CordisError' },
|
||||
{ kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'ValidationError' },
|
||||
],
|
||||
},
|
||||
{
|
||||
out: 'docs/cordis-catalog/core/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: 'docs/cordis-catalog/core/service.md',
|
||||
title: 'Service',
|
||||
intro: 'The base class for context services. A subclass loaded as a plugin registers itself as `ctx.<name>`.',
|
||||
sections: [
|
||||
{ kind: 'class', file: 'vendor/cordis/src/service.ts', symbol: 'Service' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
interface MemberDoc {
|
||||
name: string
|
||||
heading: string
|
||||
signatures: string[]
|
||||
jsDoc: string
|
||||
doc: string
|
||||
params: { name: string; text: string }[]
|
||||
returns: string | null
|
||||
source: string
|
||||
}
|
||||
|
||||
interface RenderContext {
|
||||
scanRoot: string
|
||||
cache: Map<string, { sf: ts.SourceFile; text: string }>
|
||||
violations: string[]
|
||||
}
|
||||
|
||||
function load(ctx: RenderContext, rel: string): { sf: ts.SourceFile; text: string } {
|
||||
const cached = ctx.cache.get(rel)
|
||||
if (cached !== undefined) return cached
|
||||
const text = readFileSync(resolve(ctx.scanRoot, rel), 'utf8')
|
||||
const entry = { sf: ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true), text }
|
||||
ctx.cache.set(rel, entry)
|
||||
return entry
|
||||
}
|
||||
|
||||
function sourceJsDoc(text: string, sf: ts.SourceFile, node: ts.Node): string {
|
||||
const raw = rawJsDoc(text, node)
|
||||
if (raw === '') return ''
|
||||
const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf))
|
||||
const lineStart = sf.getPositionOfLineAndCharacter(line, 0)
|
||||
const indent = text.slice(lineStart, node.getStart(sf))
|
||||
return raw.split('\n')
|
||||
.map((sourceLine, index) => index > 0 && sourceLine.startsWith(indent)
|
||||
? sourceLine.slice(indent.length)
|
||||
: sourceLine)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
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 signature = tail
|
||||
? full.slice(0, full.length - tail.getText(sf).length).replace(/[=\s]+$/, '')
|
||||
: full
|
||||
return signature.replace(/\s*;?\s*$/, '').replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
function headingParams(parameters: readonly ts.ParameterDeclaration[], sf: ts.SourceFile): string {
|
||||
const names = parameters
|
||||
.filter(parameter => !(ts.isIdentifier(parameter.name) && parameter.name.text === 'this'))
|
||||
.map((parameter) => {
|
||||
const rest = parameter.dotDotDotToken ? '...' : ''
|
||||
const optional = parameter.questionToken || parameter.initializer ? '?' : ''
|
||||
return `${rest}${parameter.name.getText(sf)}${optional}`
|
||||
})
|
||||
return `(${names.join(', ')})`
|
||||
}
|
||||
|
||||
function isPublicInstance(member: ts.ClassElement): boolean {
|
||||
const modifiers = ts.getCombinedModifierFlags(member)
|
||||
if (modifiers & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected | ts.ModifierFlags.Static)) return false
|
||||
if (!member.name || ts.isComputedPropertyName(member.name) || ts.isPrivateIdentifier(member.name)) return false
|
||||
return !member.name.getText().startsWith('_')
|
||||
}
|
||||
|
||||
function isPublicStatic(member: ts.ClassElement): boolean {
|
||||
const modifiers = ts.getCombinedModifierFlags(member)
|
||||
if (modifiers & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected)) return false
|
||||
if (!(modifiers & ts.ModifierFlags.Static)) return false
|
||||
if (!member.name || ts.isComputedPropertyName(member.name) || ts.isPrivateIdentifier(member.name)) return false
|
||||
return !member.name.getText().startsWith('_')
|
||||
}
|
||||
|
||||
type Member = ts.MethodDeclaration
|
||||
| ts.MethodSignature
|
||||
| ts.PropertyDeclaration
|
||||
| ts.PropertySignature
|
||||
| ts.GetAccessorDeclaration
|
||||
|
||||
function memberDoc(ctx: RenderContext, where: string, name: string, group: Member[], rel: string): MemberDoc {
|
||||
const { sf, text } = load(ctx, rel)
|
||||
const first = group[0]
|
||||
if (first === undefined) throw new Error(`cordis-core-api: empty member group for ${name}.`)
|
||||
const rawDocs = group.map(member => sourceJsDoc(text, sf, member))
|
||||
const docIndex = rawDocs.findIndex(raw => parseJsDoc(raw).doc !== '')
|
||||
const raw = docIndex === -1 ? '' : (rawDocs[docIndex] ?? '')
|
||||
const doc = parseJsDoc(raw).doc
|
||||
if (doc === '') ctx.violations.push(`${where} has no JSDoc prose.`)
|
||||
const { params: tags, returns } = parseTags(raw)
|
||||
const functionMembers = group.filter((member): member is ts.MethodDeclaration | ts.MethodSignature =>
|
||||
ts.isMethodDeclaration(member) || ts.isMethodSignature(member))
|
||||
const docCarrier = functionMembers[docIndex === -1 ? 0 : docIndex]
|
||||
const params: { name: string; text: string }[] = []
|
||||
if (docCarrier !== undefined) {
|
||||
checkParams(where, 'cordis-core-api', docCarrier.parameters, tags, sf,
|
||||
parameter => ts.isIdentifier(parameter.name) && parameter.name.text === 'this', ctx.violations)
|
||||
if (docCarrier.type !== undefined) {
|
||||
checkReturns(where, docCarrier.type, returns, sf, ctx.violations)
|
||||
} else if (returns === null && ts.isMethodDeclaration(docCarrier)) {
|
||||
ctx.violations.push(`${where} has no return type annotation; document the result with @returns.`)
|
||||
}
|
||||
for (const parameter of docCarrier.parameters) {
|
||||
if (!ts.isIdentifier(parameter.name) || parameter.name.text === 'this') continue
|
||||
const text = tags.get(parameter.name.text)
|
||||
if (text !== undefined) params.push({ name: parameter.name.text, text })
|
||||
}
|
||||
}
|
||||
const headingSource = docCarrier ?? functionMembers[0]
|
||||
const signatures = ts.isMethodDeclaration(first) && functionMembers.length > 1
|
||||
? functionMembers.filter(member => ts.isMethodDeclaration(member) && member.body === undefined)
|
||||
: group
|
||||
return {
|
||||
name,
|
||||
heading: headingSource === undefined ? '' : headingParams(headingSource.parameters, sf),
|
||||
signatures: signatures.map(member => signatureOf(member, sf)),
|
||||
jsDoc: raw,
|
||||
doc,
|
||||
params,
|
||||
returns,
|
||||
source: pointer(rel, sf, first),
|
||||
}
|
||||
}
|
||||
|
||||
function heritageMembers(
|
||||
statement: ts.InterfaceDeclaration,
|
||||
sf: ts.SourceFile,
|
||||
groups: Map<string, (ts.MethodSignature | ts.PropertySignature | ts.MethodDeclaration)[]>,
|
||||
): void {
|
||||
for (const clause of statement.heritageClauses ?? []) {
|
||||
for (const type of clause.types) {
|
||||
if (!ts.isIdentifier(type.expression) || type.expression.text !== 'Pick') continue
|
||||
const [target, keys] = type.typeArguments ?? []
|
||||
if (target === undefined || keys === undefined || !ts.isTypeReferenceNode(target)) continue
|
||||
const targetName = target.typeName.getText(sf)
|
||||
const cls = sf.statements.find(
|
||||
(entry): entry is ts.ClassDeclaration => ts.isClassDeclaration(entry) && entry.name?.text === targetName,
|
||||
)
|
||||
if (cls === undefined) 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function contextMergeMembers(ctx: RenderContext, rel: string): MemberDoc[] {
|
||||
const { sf } = load(ctx, rel)
|
||||
const body = cordisModuleBody(sf)
|
||||
if (body === null) throw new Error(`cordis-core-api: ${rel} has no Context module merge.`)
|
||||
const groups = new Map<string, (ts.MethodSignature | ts.PropertySignature | ts.MethodDeclaration)[]>()
|
||||
for (const statement of body.statements) {
|
||||
if (!ts.isInterfaceDeclaration(statement) || statement.name.text !== 'Context') continue
|
||||
heritageMembers(statement, sf, groups)
|
||||
for (const member of statement.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, `ctx.${name} (${rel})`, name, group, rel))
|
||||
}
|
||||
|
||||
function classMembers(ctx: RenderContext, rel: string, className: string): {
|
||||
doc: string
|
||||
instance: MemberDoc[]
|
||||
statics: MemberDoc[]
|
||||
source: string
|
||||
} {
|
||||
const { sf, text } = load(ctx, rel)
|
||||
const cls = sf.statements.find(
|
||||
(statement): statement is ts.ClassDeclaration =>
|
||||
ts.isClassDeclaration(statement) && statement.name?.text === className,
|
||||
)
|
||||
if (cls === undefined) throw new Error(`cordis-core-api: class ${className} not found in ${rel}.`)
|
||||
const doc = parseJsDoc(rawJsDoc(text, cls)).doc
|
||||
if (doc === '') ctx.violations.push(`class ${className} (${pointer(rel, sf, cls)}) has no JSDoc.`)
|
||||
const instance = new Map<string, Member[]>()
|
||||
const statics = new Map<string, Member[]>()
|
||||
for (const member of cls.members) {
|
||||
if (!ts.isMethodDeclaration(member) && !ts.isPropertyDeclaration(member) && !ts.isGetAccessorDeclaration(member)) 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)
|
||||
}
|
||||
}
|
||||
const declaration = sf.statements.find(
|
||||
(statement): statement is ts.InterfaceDeclaration =>
|
||||
ts.isInterfaceDeclaration(statement) && statement.name.text === className,
|
||||
)
|
||||
for (const member of declaration?.members ?? []) {
|
||||
if (!ts.isPropertySignature(member) || ts.isComputedPropertyName(member.name)) continue
|
||||
const name = member.name.getText(sf)
|
||||
const group = instance.get(name) ?? []
|
||||
group.push(member)
|
||||
instance.set(name, group)
|
||||
}
|
||||
const render = (groups: Map<string, Member[]>, prefix: string): MemberDoc[] =>
|
||||
[...groups.entries()].map(([name, group]) => memberDoc(ctx, `${prefix}${name} (${rel})`, name, group, rel))
|
||||
return {
|
||||
doc,
|
||||
instance: render(instance, `${className}#`),
|
||||
statics: render(statics, `${className}.`),
|
||||
source: pointer(rel, sf, cls),
|
||||
}
|
||||
}
|
||||
|
||||
function stripBodies(node: ts.Node, sf: ts.SourceFile): string {
|
||||
const cuts: { start: number; end: number }[] = []
|
||||
const visit = (entry: ts.Node): void => {
|
||||
const functionLike = ts.isMethodDeclaration(entry)
|
||||
|| ts.isConstructorDeclaration(entry)
|
||||
|| ts.isFunctionDeclaration(entry)
|
||||
|| ts.isGetAccessorDeclaration(entry)
|
||||
|| ts.isSetAccessorDeclaration(entry)
|
||||
if (functionLike && entry.body !== undefined) {
|
||||
const signatureEnd = (entry.type ?? entry.parameters.at(-1) ?? entry).getEnd()
|
||||
cuts.push({ start: signatureEnd, end: entry.body.getEnd() })
|
||||
return
|
||||
}
|
||||
entry.forEachChild(visit)
|
||||
}
|
||||
visit(node)
|
||||
const base = node.getStart(sf)
|
||||
let output = node.getText(sf)
|
||||
for (const cut of cuts.sort((left, right) => right.start - left.start)) {
|
||||
const head = output.slice(0, cut.start - base)
|
||||
const between = output.slice(cut.start - base, cut.end - base)
|
||||
const bodyBrace = between.indexOf('{')
|
||||
output = head + between.slice(0, bodyBrace).trimEnd() + output.slice(cut.end - base)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
function declarationPaste(ctx: RenderContext, rel: string, symbol: string): { doc: string; code: string; source: string } {
|
||||
const { sf, text } = load(ctx, rel)
|
||||
const matches = sf.statements.filter((statement) => {
|
||||
const named = ts.isInterfaceDeclaration(statement)
|
||||
|| ts.isTypeAliasDeclaration(statement)
|
||||
|| ts.isClassDeclaration(statement)
|
||||
|| ts.isEnumDeclaration(statement)
|
||||
|| ts.isModuleDeclaration(statement)
|
||||
return named && statement.name?.getText(sf) === symbol
|
||||
})
|
||||
const first = matches[0]
|
||||
if (first === undefined) throw new Error(`cordis-core-api: declaration ${symbol} not found in ${rel}.`)
|
||||
const doc = parseJsDoc(sourceJsDoc(text, sf, first)).doc
|
||||
const code = matches.map((statement) => {
|
||||
const jsDoc = sourceJsDoc(text, sf, statement)
|
||||
const declaration = stripBodies(statement, sf).replace(/^export\s+(default\s+)?/, '')
|
||||
return jsDoc === '' ? declaration : `${jsDoc}\n${declaration}`
|
||||
}).join('\n\n')
|
||||
return { doc, code, source: pointer(rel, sf, first) }
|
||||
}
|
||||
|
||||
function sourceLink(source: string): string {
|
||||
const [file, line] = source.split(':')
|
||||
return `[Source](../../../${file}${line === undefined ? '' : `#L${line}`})`
|
||||
}
|
||||
|
||||
function unlink(text: string): string {
|
||||
return text.replace(/\{@link\s+([^}|\s]+)\s*(?:[|\s]\s*([^}]*))?\}/g, (_match, target: string, label?: string) => {
|
||||
const name = label?.trim()
|
||||
return name && name !== '' ? name : `\`${target}\``
|
||||
})
|
||||
}
|
||||
|
||||
function prose(doc: string): string[] {
|
||||
const paragraphs = unlink(doc)
|
||||
.split(/\n\s*\n/)
|
||||
.map(paragraph => paragraph.replace(/\s*\n\s*/g, ' ').trim())
|
||||
.filter(paragraph => paragraph !== '')
|
||||
return paragraphs.flatMap((paragraph, index) => index === 0 ? [paragraph] : ['', paragraph])
|
||||
}
|
||||
|
||||
function renderMember(prefix: string, member: MemberDoc): string[] {
|
||||
const lines = [`### ${prefix}${member.name}${member.heading}`, '', `\`\`\`${FENCE}`]
|
||||
if (member.jsDoc !== '') lines.push(member.jsDoc)
|
||||
lines.push(...member.signatures, '```', '')
|
||||
if (member.doc !== '') lines.push(...prose(member.doc), '')
|
||||
for (const parameter of member.params) lines.push(`- \`${parameter.name}\` — ${unlink(parameter.text)}`)
|
||||
if (member.params.length > 0) lines.push('')
|
||||
if (member.returns !== null && member.returns !== '') lines.push(`**Returns** ${unlink(member.returns)}`, '')
|
||||
lines.push(sourceLink(member.source), '')
|
||||
return lines
|
||||
}
|
||||
|
||||
/** Render one detailed Cordis core API page and reject undocumented members. */
|
||||
export function renderCordisCoreApiPage(
|
||||
page: CordisCoreApiPage,
|
||||
scanRoot: string = root,
|
||||
): string {
|
||||
const ctx: RenderContext = { scanRoot, cache: new Map(), violations: [] }
|
||||
const lines = [
|
||||
'<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.',
|
||||
' Run `pnpm run gen-cordis-catalog` to regenerate. -->',
|
||||
'',
|
||||
`# ${page.title}`,
|
||||
'',
|
||||
page.intro,
|
||||
'',
|
||||
]
|
||||
for (const section of page.sections) {
|
||||
if (section.kind !== 'decl' && section.heading !== undefined) lines.push(`## ${section.heading}`, '')
|
||||
if (section.kind === 'context-merge') {
|
||||
for (const member of contextMergeMembers(ctx, section.file)) lines.push(...renderMember('ctx.', member))
|
||||
} else if (section.kind === 'class') {
|
||||
const cls = classMembers(ctx, section.file, section.symbol)
|
||||
if (cls.doc !== '') lines.push(...prose(cls.doc), '')
|
||||
lines.push(sourceLink(cls.source), '')
|
||||
const prefix = section.prefix ?? `${section.symbol.toLowerCase()}.`
|
||||
for (const member of cls.instance) lines.push(...renderMember(prefix, member))
|
||||
if (cls.statics.length > 0) {
|
||||
lines.push('## Static members', '')
|
||||
for (const member of cls.statics) lines.push(...renderMember(`${section.symbol}.`, member))
|
||||
}
|
||||
} else {
|
||||
const declaration = declarationPaste(ctx, section.file, section.symbol)
|
||||
lines.push(`## ${section.symbol}`, '')
|
||||
if (declaration.doc !== '') lines.push(...prose(declaration.doc), '')
|
||||
lines.push(`\`\`\`${FENCE}`, declaration.code, '```', '', sourceLink(declaration.source), '')
|
||||
}
|
||||
}
|
||||
reportViolations('gen-cordis-catalog', ctx.violations)
|
||||
return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`
|
||||
}
|
||||
|
||||
/** Render every detailed Cordis core API page. */
|
||||
export function renderCordisCoreApiPages(scanRoot: string = root): Map<string, string> {
|
||||
return new Map(CORDIS_CORE_API_PAGES.map(page => [page.out, renderCordisCoreApiPage(page, scanRoot)]))
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
/**
|
||||
* Shared AST walkers for the cordis documentation generators
|
||||
* (`gen-cordis-catalog.ts`, `gen-website-api.ts`): locating the cordis module
|
||||
* merge in a source file, enumerating its `interface Events` members, and
|
||||
* resolving the `interface Context` service keys to their service classes.
|
||||
* One walk, two renderers — the catalog and the website page carry different
|
||||
* prose but must agree on WHAT exists.
|
||||
* AST walkers for the Cordis catalog generator: locate the Cordis module merge
|
||||
* in a source file, enumerate its `interface Events` members, and resolve the
|
||||
* `interface Context` service keys to their service classes.
|
||||
*/
|
||||
|
||||
import ts from 'typescript'
|
||||
|
||||
@@ -192,7 +192,7 @@ function remapBlockPaths(output: string, blocks: Block[]): string {
|
||||
})
|
||||
}
|
||||
|
||||
const markdownGlobs = ['README.md', '.agents/notes/**/*.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md', 'website/zh-CN/**/*.md']
|
||||
const markdownGlobs = ['README.md', '.agents/notes/**/*.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md']
|
||||
|
||||
const files: string[] = []
|
||||
for (const pattern of markdownGlobs) {
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
* curated table below. `--check` verifies both committed artifacts.
|
||||
*/
|
||||
|
||||
import { globSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { resolve, sep } from 'node:path'
|
||||
import { globSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, resolve, sep } from 'node:path'
|
||||
import ts from 'typescript'
|
||||
import { renderCordisCoreApiPages } from './cordis-core-api.ts'
|
||||
import { checkParams, checkReturns, parseJsDoc, parseTags, pointer, rawJsDoc, reportViolations, type Mode } from './jsdoc.ts'
|
||||
import { cordisModuleBody, eventMembers, serviceClasses } from './cordis-walk.ts'
|
||||
|
||||
@@ -266,8 +267,7 @@ interface InheritedEntry {
|
||||
source: string
|
||||
}
|
||||
|
||||
// cordisModuleBody / eventMembers / serviceClasses live in cordis-walk.ts,
|
||||
// shared with gen-website-api.ts — one walk, two renderers.
|
||||
// cordisModuleBody / eventMembers / serviceClasses live in cordis-walk.ts.
|
||||
|
||||
/** The signature text of a method-signature member (everything but a body). */
|
||||
function memberSignature(member: ts.TypeElement | ts.ClassElement, sf: ts.SourceFile): string {
|
||||
@@ -505,7 +505,7 @@ export function renderEvents(events: EventEntry[]): string {
|
||||
'',
|
||||
GATE_NOTICE,
|
||||
'',
|
||||
'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely.',
|
||||
'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely. The event-dispatch methods themselves are generated in the [Cordis core Events API](core/events.md).',
|
||||
'',
|
||||
'Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)), **parallel** (awaited fan-out; all listeners run), **serial** (awaited in registration order until one returns a bail value — anything other than `null`, `false`, or `undefined`).',
|
||||
'',
|
||||
@@ -540,7 +540,7 @@ export function renderServices(services: ServiceEntry[]): string {
|
||||
'',
|
||||
GATE_NOTICE,
|
||||
'',
|
||||
'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer `ctx` surface a plugin also sees — pinned vendor source, summarized tersely.',
|
||||
'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer `ctx` surface a plugin also sees — pinned vendor source, summarized tersely. Detailed Context, Fiber, Registry, and Service APIs are generated in the [Cordis core API](core/context.md).',
|
||||
'',
|
||||
]
|
||||
for (const s of services) lines.push(...renderService(s))
|
||||
@@ -564,6 +564,7 @@ function main(): void {
|
||||
const outputs: [string, string][] = [
|
||||
[OUT_EVENTS, renderEvents(collectEvents())],
|
||||
[OUT_SERVICES, renderServices(collectServices())],
|
||||
...renderCordisCoreApiPages(),
|
||||
]
|
||||
if (process.argv.includes('--check')) {
|
||||
const stale: string[] = []
|
||||
@@ -580,15 +581,19 @@ function main(): void {
|
||||
if (committed !== content) stale.push(out)
|
||||
}
|
||||
if (stale.length === 0) {
|
||||
console.log(`gen-cordis-catalog: ${OUT_EVENTS} and ${OUT_SERVICES} are up to date.`)
|
||||
console.log(`gen-cordis-catalog: ${outputs.length} generated file(s) are up to date.`)
|
||||
process.exit(0)
|
||||
}
|
||||
console.error(`gen-cordis-catalog: ${stale.join(' and ')} ${stale.length === 1 ? 'is' : 'are'} stale. Run \`pnpm run gen-cordis-catalog\` and commit the result.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
for (const [out, content] of outputs) writeFileSync(resolve(root, out), content)
|
||||
console.log(`gen-cordis-catalog: wrote ${OUT_EVENTS} and ${OUT_SERVICES}.`)
|
||||
for (const [out, content] of outputs) {
|
||||
const destination = resolve(root, out)
|
||||
mkdirSync(dirname(destination), { recursive: true })
|
||||
writeFileSync(destination, content)
|
||||
}
|
||||
console.log(`gen-cordis-catalog: wrote ${outputs.length} generated file(s).`)
|
||||
}
|
||||
|
||||
// Run only when invoked as a script, not when imported by a test.
|
||||
|
||||
@@ -1,757 +0,0 @@
|
||||
/**
|
||||
* 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 and retain the
|
||||
* declaration's original source JSDoc. doc-typecheck only processes its known
|
||||
* info strings, so these bare (non-compilable) 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'
|
||||
import { cordisModuleBody, eventMembers, serviceClasses } from './cordis-walk.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'
|
||||
|
||||
/** Return sorted repository-relative glob matches with stable URL separators. */
|
||||
function repoGlob(pattern: string): string[] {
|
||||
return globSync(pattern, { cwd: root }).map(rel => rel.replaceAll('\\', '/')).sort()
|
||||
}
|
||||
|
||||
/** 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[]
|
||||
/** Original source JSDoc, dedented only from its containing declaration. */
|
||||
jsDoc: 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; heading?: string }
|
||||
| { kind: 'context-merge'; file: string; heading?: 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', heading: 'Service store and mixins' },
|
||||
],
|
||||
},
|
||||
{
|
||||
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', 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' },
|
||||
{ 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 module-merge walk (cordisModuleBody / eventMembers / serviceClasses) is
|
||||
// shared with gen-cordis-catalog.ts via cordis-walk.ts.
|
||||
|
||||
/** Original JSDoc with only the source container's indentation removed. */
|
||||
function sourceJSDoc(text: string, sf: ts.SourceFile, node: ts.Node): string {
|
||||
const raw = rawJsDoc(text, node)
|
||||
if (raw === '') return ''
|
||||
const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf))
|
||||
const lineStart = sf.getPositionOfLineAndCharacter(line, 0)
|
||||
const indent = text.slice(lineStart, node.getStart(sf))
|
||||
return raw.split('\n')
|
||||
.map((sourceLine, index) => index > 0 && sourceLine.startsWith(indent)
|
||||
? sourceLine.slice(indent.length)
|
||||
: sourceLine)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
/** 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 => sourceJSDoc(text, sf, 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)),
|
||||
jsDoc: raw,
|
||||
doc,
|
||||
params,
|
||||
returns: returnsText,
|
||||
source: pointer(rel, sf, first),
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 = cordisModuleBody(sf)
|
||||
if (!body) throw new Error(`gen-website-api: ${rel} has no context module merge`)
|
||||
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
|
||||
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. 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[]
|
||||
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.`)
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
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))
|
||||
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 firstJSDoc = sourceJSDoc(text, sf, first)
|
||||
const doc = parseJsDoc(firstJSDoc).doc
|
||||
const code = matches.map((statement) => {
|
||||
const jsDoc = sourceJSDoc(text, sf, statement)
|
||||
const declaration = stripBodies(statement, sf).replace(/^export\s+(default\s+)?/, '')
|
||||
return jsDoc === '' ? declaration : `${jsDoc}\n${declaration}`
|
||||
}).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 repoGlob('packages/*/*/src/index.ts')) {
|
||||
const { sf, text } = load(rel)
|
||||
if (!text.includes('interface Context')) continue
|
||||
const body = cordisModuleBody(sf)
|
||||
if (!body) continue
|
||||
const pkgJson = resolve(root, dirname(dirname(rel)), 'package.json')
|
||||
// Manifest shape is repo-owned; `name` is the one field read here.
|
||||
const manifest = JSON.parse(readFileSync(pkgJson, 'utf8')) as { name: string }
|
||||
const pkg = manifest.name
|
||||
for (const { key, type, cls, abstract, doc: clsDoc } of serviceClasses(body, sf, rel, violations)) {
|
||||
const groups = new Map<string, (ts.MethodDeclaration | ts.PropertyDeclaration | ts.GetAccessorDeclaration)[]>()
|
||||
for (const member of cls.members) {
|
||||
// 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) ?? []
|
||||
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
|
||||
/** Original source event JSDoc, dedented from its module/interface. */
|
||||
jsDoc: 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 repoGlob('packages/*/*/src/*.ts')) {
|
||||
const { sf, text } = load(rel)
|
||||
if (!text.includes('interface Events')) continue
|
||||
const body = cordisModuleBody(sf)
|
||||
if (!body) continue
|
||||
for (const { name, member } of eventMembers(body, sf)) {
|
||||
const raw = sourceJSDoc(text, sf, 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), jsDoc: raw, 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})`
|
||||
}
|
||||
|
||||
/** 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 unlink(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)
|
||||
lines.push(m.jsDoc)
|
||||
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}\` — ${unlink(p.text)}`)
|
||||
lines.push('')
|
||||
}
|
||||
if (m.returns) lines.push(`**Returns** ${unlink(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 !== '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))
|
||||
}
|
||||
} 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.jsDoc, e.signature, '```', '')
|
||||
lines.push(...prose(e.doc), '')
|
||||
if (e.params.length > 0) {
|
||||
for (const p of e.params) lines.push(`- \`${p.name}\` — ${unlink(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))
|
||||
|
||||
for (const [rel, content] of files) {
|
||||
if (!rel.endsWith('.md')) continue
|
||||
for (const match of content.matchAll(/^```ts website-api\n([\s\S]*?)\n```$/gm)) {
|
||||
const body = match[1] ?? ''
|
||||
if (!body.startsWith('/**')) {
|
||||
violations.push(`${rel}: a ts website-api fence does not begin with original source JSDoc.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = repoGlob(`${PAGES_DIR}/{cordis,harness}/*.md`)
|
||||
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,6 +1,6 @@
|
||||
/**
|
||||
* Shared fenced-code-block extractor for the Markdown doc gates
|
||||
* (`doc-typecheck.ts`, `verify-website-yaml.ts`). One scanner, per-gate
|
||||
* (currently `doc-typecheck.ts`; future Markdown gates can share it). One scanner, per-gate
|
||||
* classification: each gate maps a fence info string (` ```ts `,
|
||||
* ` ```yaml ignore-check `, …) to its own kind tag and receives every
|
||||
* classified block with its 1-based opening-fence line.
|
||||
|
||||
219
scripts/project-doc-site.spec.ts
Normal file
219
scripts/project-doc-site.spec.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
/** Tests for the documentation website projection adapter. */
|
||||
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { docsPages, type DocsPage } from '../website/docs.ts'
|
||||
import { addProjectionFrontmatter, projectedPageContent, rewriteMarkdown } from './project-doc-site.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function fixture(): { root: string; pages: DocsPage[] } {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-doc-site-'))
|
||||
roots.push(root)
|
||||
mkdirSync(join(root, 'docs'), { recursive: true })
|
||||
mkdirSync(join(root, 'packages'), { recursive: true })
|
||||
writeFileSync(join(root, 'docs/a.md'), '# A\n')
|
||||
writeFileSync(join(root, 'docs/b.md'), '# B\n')
|
||||
writeFileSync(join(root, 'docs/x(y).md'), '# Parentheses\n')
|
||||
writeFileSync(join(root, 'packages/tool.ts'), 'one\ntwo\n')
|
||||
writeFileSync(join(root, 'packages/logo.svg'), '<svg/>\n')
|
||||
return {
|
||||
root,
|
||||
pages: [
|
||||
{ locale: 'root', contentLocale: 'en-US', source: 'docs/a.md', route: 'a.md', label: 'A', sidebar: 'zh-reference', section: 'Test', order: 1 },
|
||||
{ locale: 'root', contentLocale: 'en-US', source: 'docs/b.md', route: 'reference-root/b.md', label: 'B', sidebar: 'zh-reference', section: 'Test', order: 2 },
|
||||
{ locale: 'en', contentLocale: 'en-US', source: 'docs/a.md', route: 'en/a.md', label: 'A', sidebar: 'en-reference', section: 'Test', order: 1 },
|
||||
{ locale: 'en', contentLocale: 'en-US', source: 'docs/b.md', route: 'en/reference/b.md', label: 'B', sidebar: 'en-reference', section: 'Test', order: 2 },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
describe('rewriteMarkdown', () => {
|
||||
it('maps published pages and pins unpublished source links', () => {
|
||||
const { root, pages } = fixture()
|
||||
const source = '[B](b.md#part) [source](../packages/tool.ts:2) [web](https://example.com)\n'
|
||||
expect(rewriteMarkdown(source, {
|
||||
locale: 'en',
|
||||
sourcePath: 'docs/a.md',
|
||||
route: 'en/a.md',
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
})).toBe(
|
||||
'[B](./reference/b.md#part) '
|
||||
+ '[source](https://github.com/deepseek-harness/deepseek-harness/blob/abc123/packages/tool.ts#L2) '
|
||||
+ '[web](https://example.com)\n',
|
||||
)
|
||||
})
|
||||
|
||||
it('selects the published target in the current site locale', () => {
|
||||
const { root, pages } = fixture()
|
||||
expect(rewriteMarkdown('[B](b.md)\n', {
|
||||
locale: 'root',
|
||||
sourcePath: 'docs/a.md',
|
||||
route: 'a.md',
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
})).toBe('[B](./reference-root/b.md)\n')
|
||||
})
|
||||
|
||||
it('uses raw GitHub content for unpublished images', () => {
|
||||
const { root, pages } = fixture()
|
||||
expect(rewriteMarkdown('\n', {
|
||||
locale: 'en',
|
||||
sourcePath: 'docs/a.md',
|
||||
route: 'en/a.md',
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
})).toBe('\n')
|
||||
})
|
||||
|
||||
it('does not rewrite Markdown-looking text inside code fences', () => {
|
||||
const { root, pages } = fixture()
|
||||
const source = '```md\n[B](b.md)\n```\n'
|
||||
expect(rewriteMarkdown(source, {
|
||||
locale: 'en',
|
||||
sourcePath: 'docs/a.md',
|
||||
route: 'en/a.md',
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
})).toBe(source)
|
||||
})
|
||||
|
||||
it('replaces the destination token without changing repeated titles or escapes', () => {
|
||||
const { root, pages } = fixture()
|
||||
const source = '[title](b.md "b.md") [escaped](x\\(y\\).md)\n'
|
||||
expect(rewriteMarkdown(source, {
|
||||
locale: 'en',
|
||||
sourcePath: 'docs/a.md',
|
||||
route: 'en/a.md',
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
})).toBe(
|
||||
'[title](./reference/b.md "b.md") '
|
||||
+ '[escaped](https://github.com/deepseek-harness/deepseek-harness/blob/abc123/docs/x(y).md)\n',
|
||||
)
|
||||
})
|
||||
|
||||
it('routes a pair switcher across locales while ordinary links stay in locale', () => {
|
||||
const { root, pages } = fixture()
|
||||
writeFileSync(join(root, 'docs/a.zh.md'), '# A\n')
|
||||
const paired = pages.filter(page => page.source !== 'docs/a.md')
|
||||
paired.push(
|
||||
{
|
||||
locale: 'root', contentLocale: 'zh-CN', source: 'docs/a.zh.md', sourceAliases: ['docs/a.md'],
|
||||
route: 'guide/a.md', label: 'A', sidebar: 'zh-guide', section: 'Test', order: 1,
|
||||
},
|
||||
{
|
||||
locale: 'en', contentLocale: 'en-US', source: 'docs/a.md', sourceAliases: ['docs/a.zh.md'],
|
||||
route: 'en/guide/a.md', label: 'A', sidebar: 'en-guide', section: 'Test', order: 1,
|
||||
},
|
||||
)
|
||||
expect(rewriteMarkdown('[English](a.md) [B](b.md)\n', {
|
||||
locale: 'root',
|
||||
sourcePath: 'docs/a.zh.md',
|
||||
route: 'guide/a.md',
|
||||
pages: paired,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
})).toBe('[English](../en/guide/a.md) [B](../reference-root/b.md)\n')
|
||||
})
|
||||
|
||||
it('fails loud when a relative target is missing', () => {
|
||||
const { root, pages } = fixture()
|
||||
expect(() => rewriteMarkdown('[missing](missing.md)\n', {
|
||||
locale: 'en',
|
||||
sourcePath: 'docs/a.md',
|
||||
route: 'en/a.md',
|
||||
pages,
|
||||
repoRoot: root,
|
||||
repositoryRef: 'abc123',
|
||||
})).toThrow('links to missing path "missing.md"')
|
||||
})
|
||||
})
|
||||
|
||||
describe('docsPages locale routes', () => {
|
||||
it('publishes every route in both locales and selects paired user sources', () => {
|
||||
const byRoute = new Map(docsPages.map(page => [page.route, page]))
|
||||
for (const page of docsPages.filter(page => page.locale === 'root')) {
|
||||
const counterpart = byRoute.get(`en/${page.route}`)
|
||||
expect(counterpart, page.route).toBeDefined()
|
||||
expect(counterpart?.locale).toBe('en')
|
||||
if (page.source.startsWith('docs/user/')) {
|
||||
expect(page.source).toMatch(/\.zh\.md$/)
|
||||
expect(page.contentLocale).toBe('zh-CN')
|
||||
expect(counterpart?.source).toBe(page.source.replace(/\.zh\.md$/, '.md'))
|
||||
expect(counterpart?.contentLocale).toBe('en-US')
|
||||
} else {
|
||||
expect(counterpart?.source).toBe(page.source)
|
||||
expect(counterpart?.contentLocale).toBe(page.contentLocale)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('publishes the Cordis core API under matching locale structures', () => {
|
||||
const files = ['context.md', 'events.md', 'fiber.md', 'registry.md', 'service.md']
|
||||
for (const file of files) {
|
||||
const root = docsPages.find(page => page.route === `reference/cordis-api/${file}`)
|
||||
const english = docsPages.find(page => page.route === `en/reference/cordis-api/${file}`)
|
||||
expect(root?.source).toBe(`docs/cordis-catalog/core/${file}`)
|
||||
expect(root?.section).toBe('Cordis API')
|
||||
expect(english?.source).toBe(root?.source)
|
||||
expect(english?.section).toBe('Cordis Core API')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('addProjectionFrontmatter', () => {
|
||||
it('adds frontmatter to an ordinary Markdown page', () => {
|
||||
expect(addProjectionFrontmatter('# Guide\n', 'docs/guide.md')).toBe(
|
||||
'---\neditSource: "docs/guide.md"\n---\n\n# Guide\n',
|
||||
)
|
||||
})
|
||||
|
||||
it('extends existing VitePress frontmatter', () => {
|
||||
expect(addProjectionFrontmatter('---\nlayout: home\n---\n', 'docs/index.md')).toBe(
|
||||
'---\neditSource: "docs/index.md"\nlayout: home\n---\n',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('projectedPageContent', () => {
|
||||
const page = (sidebar: DocsPage['sidebar']): DocsPage => ({
|
||||
locale: 'root',
|
||||
contentLocale: 'zh-CN',
|
||||
source: 'docs/index.zh.md',
|
||||
route: 'index.md',
|
||||
label: 'Home',
|
||||
sidebar,
|
||||
section: 'Home',
|
||||
order: 0,
|
||||
})
|
||||
|
||||
it('omits the source-only body from locale home pages', () => {
|
||||
expect(projectedPageContent(
|
||||
'---\nlayout: home\nhero:\n name: Harness\n---\n\n# Harness\n\n[English](index.md) | 中文\n',
|
||||
page(null),
|
||||
)).toBe('---\nlayout: home\nhero:\n name: Harness\n---\n')
|
||||
})
|
||||
|
||||
it('keeps the full body for ordinary pages', () => {
|
||||
const markdown = '---\ntitle: Guide\n---\n\n# Guide\n'
|
||||
expect(projectedPageContent(markdown, page('zh-guide'))).toBe(markdown)
|
||||
})
|
||||
|
||||
it('rejects a locale home source without frontmatter', () => {
|
||||
expect(() => projectedPageContent('# Harness\n', page(null)))
|
||||
.toThrow('locale home source "docs/index.zh.md" must start with YAML frontmatter')
|
||||
})
|
||||
})
|
||||
322
scripts/project-doc-site.ts
Normal file
322
scripts/project-doc-site.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* Build-time projection from canonical repository Markdown into VitePress.
|
||||
*
|
||||
* The generated tree is disposable: sources stay in their owning `docs/`
|
||||
* tier, while this adapter rewrites cross-source links for the public site.
|
||||
*/
|
||||
|
||||
import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, extname, posix, relative, resolve, sep } from 'node:path'
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown'
|
||||
import { gfmFromMarkdown } from 'mdast-util-gfm'
|
||||
import { gfm } from 'micromark-extension-gfm'
|
||||
import type { Nodes } from 'mdast'
|
||||
import { docsPages, type DocsLocale, type DocsPage } from '../website/docs.ts'
|
||||
|
||||
const REPOSITORY_URL = 'https://github.com/deepseek-harness/deepseek-harness'
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const generatedRoot = resolve(root, 'website/.generated')
|
||||
|
||||
interface Replacement {
|
||||
start: number
|
||||
end: number
|
||||
value: string
|
||||
}
|
||||
|
||||
interface DestinationRange {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
type RewritableNode = Extract<Nodes, { type: 'link' | 'image' | 'definition' }>
|
||||
|
||||
/** Inputs for rewriting one canonical Markdown page. */
|
||||
export interface RewriteMarkdownOptions {
|
||||
locale: DocsLocale
|
||||
sourcePath: string
|
||||
route: string
|
||||
pages: DocsPage[]
|
||||
repoRoot: string
|
||||
repositoryRef: string
|
||||
}
|
||||
|
||||
function repoPath(absPath: string, repoRoot: string): string {
|
||||
return relative(repoRoot, absPath).split(sep).join('/')
|
||||
}
|
||||
|
||||
function isExternalOrSiteAbsolute(url: string): boolean {
|
||||
return url.startsWith('#')
|
||||
|| url.startsWith('//')
|
||||
|| url.startsWith('/')
|
||||
|| /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)
|
||||
}
|
||||
|
||||
function skipWhitespace(source: string, start: number): number {
|
||||
let index = start
|
||||
while (/\s/.test(source[index] ?? '')) index += 1
|
||||
return index
|
||||
}
|
||||
|
||||
function labelEnd(source: string): number {
|
||||
const first = source.indexOf('[')
|
||||
if (first === -1) return -1
|
||||
let depth = 0
|
||||
for (let index = first; index < source.length; index += 1) {
|
||||
const char = source[index]
|
||||
if (char === '\\') {
|
||||
index += 1
|
||||
} else if (char === '[') {
|
||||
depth += 1
|
||||
} else if (char === ']') {
|
||||
depth -= 1
|
||||
if (depth === 0) return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function destinationRange(rawNode: string, type: 'link' | 'image' | 'definition'): DestinationRange {
|
||||
const endOfLabel = labelEnd(rawNode)
|
||||
if (endOfLabel === -1) {
|
||||
throw new Error(`project-doc-site: cannot locate label end in ${JSON.stringify(rawNode)}.`)
|
||||
}
|
||||
|
||||
let start: number
|
||||
if (type === 'definition') {
|
||||
const colon = rawNode.indexOf(':', endOfLabel + 1)
|
||||
if (colon === -1) {
|
||||
throw new Error(`project-doc-site: cannot locate definition separator in ${JSON.stringify(rawNode)}.`)
|
||||
}
|
||||
start = skipWhitespace(rawNode, colon + 1)
|
||||
} else {
|
||||
if (rawNode[endOfLabel + 1] !== '(') {
|
||||
throw new Error(`project-doc-site: cannot locate inline destination in ${JSON.stringify(rawNode)}.`)
|
||||
}
|
||||
start = skipWhitespace(rawNode, endOfLabel + 2)
|
||||
}
|
||||
|
||||
if (rawNode[start] === '<') {
|
||||
for (let index = start + 1; index < rawNode.length; index += 1) {
|
||||
if (rawNode[index] === '\\') index += 1
|
||||
else if (rawNode[index] === '>') return { start: start + 1, end: index }
|
||||
}
|
||||
throw new Error(`project-doc-site: cannot locate angle-bracket destination end in ${JSON.stringify(rawNode)}.`)
|
||||
}
|
||||
|
||||
let depth = 0
|
||||
for (let index = start; index < rawNode.length; index += 1) {
|
||||
const char = rawNode[index]
|
||||
if (char === '\\') {
|
||||
index += 1
|
||||
} else if (char === '(') {
|
||||
depth += 1
|
||||
} else if (char === ')') {
|
||||
if (depth === 0) return { start, end: index }
|
||||
depth -= 1
|
||||
} else if (/\s/.test(char ?? '') && depth === 0) {
|
||||
return { start, end: index }
|
||||
}
|
||||
}
|
||||
return { start, end: rawNode.length }
|
||||
}
|
||||
|
||||
function splitTarget(url: string): { path: string; suffix: string } {
|
||||
const boundary = url.search(/[?#]/)
|
||||
if (boundary === -1) return { path: url, suffix: '' }
|
||||
return { path: url.slice(0, boundary), suffix: url.slice(boundary) }
|
||||
}
|
||||
|
||||
function decodePath(path: string): string {
|
||||
try {
|
||||
return decodeURIComponent(path)
|
||||
} catch {
|
||||
throw new Error(`project-doc-site: malformed percent escape in ${JSON.stringify(path)}.`)
|
||||
}
|
||||
}
|
||||
|
||||
function routeTarget(fromRoute: string, toRoute: string, suffix: string): string {
|
||||
const target = posix.relative(posix.dirname(fromRoute), toRoute)
|
||||
return `${target.startsWith('.') ? target : `./${target}`}${suffix}`
|
||||
}
|
||||
|
||||
function sourceMap(pages: DocsPage[]): Map<string, Map<DocsLocale, DocsPage>> {
|
||||
const map = new Map<string, Map<DocsLocale, DocsPage>>()
|
||||
for (const page of pages) {
|
||||
for (const source of [page.source, ...(page.sourceAliases ?? [])]) {
|
||||
const localized = map.get(source) ?? new Map<DocsLocale, DocsPage>()
|
||||
if (localized.has(page.locale)) {
|
||||
throw new Error(`project-doc-site: duplicate source or alias ${JSON.stringify(source)} for locale ${JSON.stringify(page.locale)}.`)
|
||||
}
|
||||
localized.set(page.locale, page)
|
||||
map.set(source, localized)
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
function counterpartSource(source: string): string {
|
||||
return source.endsWith('.zh.md')
|
||||
? source.replace(/\.zh\.md$/, '.md')
|
||||
: source.replace(/\.md$/, '.zh.md')
|
||||
}
|
||||
|
||||
function resolveRepositoryTarget(sourceAbs: string, rawPath: string, repoRoot: string): { absPath: string; line?: number } {
|
||||
const decoded = decodePath(rawPath)
|
||||
let absPath = resolve(dirname(sourceAbs), decoded)
|
||||
if (existsSync(absPath)) return { absPath }
|
||||
|
||||
const lineMatch = decoded.match(/:(\d+)$/)
|
||||
if (lineMatch !== null) {
|
||||
const lineText = lineMatch[1]
|
||||
if (lineText === undefined) throw new Error('project-doc-site: line suffix matched without a line number.')
|
||||
absPath = resolve(dirname(sourceAbs), decoded.slice(0, -lineMatch[0].length))
|
||||
if (existsSync(absPath)) return { absPath, line: Number.parseInt(lineText, 10) }
|
||||
}
|
||||
|
||||
if (extname(decoded) === '') {
|
||||
const markdown = resolve(dirname(sourceAbs), `${decoded}.md`)
|
||||
if (existsSync(markdown)) return { absPath: markdown }
|
||||
const index = resolve(dirname(sourceAbs), decoded, 'index.md')
|
||||
if (existsSync(index)) return { absPath: index }
|
||||
}
|
||||
|
||||
throw new Error(`project-doc-site: ${repoPath(sourceAbs, repoRoot)} links to missing path ${JSON.stringify(rawPath)}.`)
|
||||
}
|
||||
|
||||
function githubTarget(
|
||||
absPath: string,
|
||||
line: number | undefined,
|
||||
suffix: string,
|
||||
repositoryRef: string,
|
||||
repoRoot: string,
|
||||
image: boolean,
|
||||
): string {
|
||||
const path = repoPath(absPath, repoRoot)
|
||||
if (image) return `https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/${repositoryRef}/${path}${suffix}`
|
||||
const kind = lstatSync(absPath).isDirectory() ? 'tree' : 'blob'
|
||||
const lineSuffix = line === undefined ? suffix : `#L${line}`
|
||||
return `${REPOSITORY_URL}/${kind}/${repositoryRef}/${path}${lineSuffix}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite repository-relative links without reserializing Markdown.
|
||||
*
|
||||
* @param source Markdown text from the canonical file.
|
||||
* @param options Source, route, manifest, and repository context.
|
||||
* @returns Markdown whose published links resolve inside the site or to GitHub.
|
||||
*/
|
||||
export function rewriteMarkdown(source: string, options: RewriteMarkdownOptions): string {
|
||||
const sourceAbs = resolve(options.repoRoot, options.sourcePath)
|
||||
const published = sourceMap(options.pages)
|
||||
const tree = fromMarkdown(source, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] })
|
||||
const replacements: Replacement[] = []
|
||||
|
||||
const rewrite = (node: RewritableNode): void => {
|
||||
if (isExternalOrSiteAbsolute(node.url)) return
|
||||
const { path, suffix } = splitTarget(node.url)
|
||||
if (path === '') return
|
||||
const { absPath, line } = resolveRepositoryTarget(sourceAbs, path, options.repoRoot)
|
||||
const targetPath = repoPath(absPath, options.repoRoot)
|
||||
const isLanguageSwitcher = targetPath === counterpartSource(options.sourcePath)
|
||||
const targetLocale: DocsLocale = isLanguageSwitcher
|
||||
? options.locale === 'root' ? 'en' : 'root'
|
||||
: options.locale
|
||||
const page = published.get(targetPath)?.get(targetLocale)
|
||||
const nextUrl = page === undefined
|
||||
? githubTarget(absPath, line, suffix, options.repositoryRef, options.repoRoot, node.type === 'image')
|
||||
: routeTarget(options.route, page.route, suffix)
|
||||
|
||||
const start = node.position?.start.offset
|
||||
const end = node.position?.end.offset
|
||||
if (start === undefined || end === undefined) {
|
||||
throw new Error(`project-doc-site: link ${JSON.stringify(node.url)} has no source offsets.`)
|
||||
}
|
||||
const rawNode = source.slice(start, end)
|
||||
const rawDestination = destinationRange(rawNode, node.type)
|
||||
replacements.push({
|
||||
start: start + rawDestination.start,
|
||||
end: start + rawDestination.end,
|
||||
value: nextUrl,
|
||||
})
|
||||
}
|
||||
|
||||
const visit = (node: Nodes): void => {
|
||||
if ((node.type === 'link' || node.type === 'image' || node.type === 'definition') && 'url' in node) rewrite(node)
|
||||
if ('children' in node) {
|
||||
for (const child of node.children) visit(child)
|
||||
}
|
||||
}
|
||||
visit(tree)
|
||||
|
||||
let projected = source
|
||||
for (const replacement of replacements.sort((a, b) => b.start - a.start)) {
|
||||
projected = projected.slice(0, replacement.start) + replacement.value + projected.slice(replacement.end)
|
||||
}
|
||||
return projected
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the canonical edit target in VitePress frontmatter.
|
||||
*
|
||||
* @param markdown Projected Markdown content.
|
||||
* @param sourcePath Repository-relative canonical source path.
|
||||
* @returns Markdown with an `editSource` frontmatter field.
|
||||
*/
|
||||
export function addProjectionFrontmatter(markdown: string, sourcePath: string): string {
|
||||
const field = `editSource: ${JSON.stringify(sourcePath)}`
|
||||
if (markdown.startsWith('---\n')) return markdown.replace('---\n', `---\n${field}\n`)
|
||||
return `---\n${field}\n---\n\n${markdown}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the Markdown rendered for one published page.
|
||||
*
|
||||
* @param markdown Rewritten canonical Markdown content.
|
||||
* @param page Publication manifest entry for the content.
|
||||
* @returns Full Markdown for ordinary pages or frontmatter-only Markdown for a locale home page.
|
||||
*/
|
||||
export function projectedPageContent(markdown: string, page: DocsPage): string {
|
||||
if (page.sidebar !== null) return markdown
|
||||
if (!markdown.startsWith('---\n')) {
|
||||
throw new Error(`project-doc-site: locale home source ${JSON.stringify(page.source)} must start with YAML frontmatter.`)
|
||||
}
|
||||
const closingDelimiter = '\n---\n'
|
||||
const closing = markdown.indexOf(closingDelimiter, 4)
|
||||
if (closing === -1) {
|
||||
throw new Error(`project-doc-site: locale home source ${JSON.stringify(page.source)} has unclosed YAML frontmatter.`)
|
||||
}
|
||||
return markdown.slice(0, closing + closingDelimiter.length)
|
||||
}
|
||||
|
||||
/** Canonical Markdown files watched by the local VitePress dev server. */
|
||||
export function docsSourceFiles(): string[] {
|
||||
return [...new Set(docsPages.map(page => resolve(root, page.source)))]
|
||||
}
|
||||
|
||||
/** Rebuild the disposable VitePress source tree from the publication manifest. */
|
||||
export function projectDocs(): void {
|
||||
const routes = new Set<string>()
|
||||
const repositoryRef = process.env.GITHUB_SHA ?? 'master'
|
||||
rmSync(generatedRoot, { recursive: true, force: true })
|
||||
|
||||
for (const page of docsPages) {
|
||||
if (routes.has(page.route)) throw new Error(`project-doc-site: duplicate route ${JSON.stringify(page.route)}.`)
|
||||
routes.add(page.route)
|
||||
const sourceAbs = resolve(root, page.source)
|
||||
if (!existsSync(sourceAbs) || !lstatSync(sourceAbs).isFile()) {
|
||||
throw new Error(`project-doc-site: source ${JSON.stringify(page.source)} does not exist or is not a file.`)
|
||||
}
|
||||
const output = resolve(generatedRoot, page.route)
|
||||
mkdirSync(dirname(output), { recursive: true })
|
||||
const markdown = readFileSync(sourceAbs, 'utf8')
|
||||
const projected = rewriteMarkdown(markdown, {
|
||||
sourcePath: page.source,
|
||||
locale: page.locale,
|
||||
route: page.route,
|
||||
pages: docsPages,
|
||||
repoRoot: root,
|
||||
repositoryRef,
|
||||
})
|
||||
writeFileSync(output, addProjectionFrontmatter(projectedPageContent(projected, page), page.source))
|
||||
}
|
||||
}
|
||||
@@ -214,7 +214,6 @@ function ciPrimaryGates(): Gate[] {
|
||||
...docSyncLeafGates(),
|
||||
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
|
||||
pnpmScript('knip', 'knip'),
|
||||
pnpmScript('website-build', 'website:build', { label: 'website build' }),
|
||||
pnpmScript('build', 'build', { needs: ['typecheck'] }),
|
||||
pnpmScript('publint', 'publint', { needs: ['build'] }),
|
||||
pnpmScript('node-next-types', 'verify-node-next-types', {
|
||||
@@ -234,7 +233,6 @@ function ciStaticGates(): Gate[] {
|
||||
...docSyncLeafGates(),
|
||||
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
|
||||
pnpmScript('knip', 'knip'),
|
||||
pnpmScript('website-build', 'website:build', { label: 'website build' }),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -337,7 +335,6 @@ function docSyncLeafGates(options: {
|
||||
pnpmScript('persistence-catalog', 'verify-persistence-catalog', { label: 'persistence catalog' }),
|
||||
pnpmScript('doc-graphs', 'verify-doc-graphs', { label: 'doc graphs' }),
|
||||
pnpmScript('scoped-events', 'verify-scoped-events', { label: 'scoped events' }),
|
||||
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' }),
|
||||
@@ -350,8 +347,9 @@ function docSyncLeafGates(options: {
|
||||
pnpmScript('translation-prompt', 'verify-translation-prompt', { label: 'translation prompt' }),
|
||||
pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }),
|
||||
pnpmScript('doc-budgets', 'verify-doc-budgets', { label: 'doc budgets' }),
|
||||
// Keep the VitePress build in this single gate because projection rewrites website/.generated.
|
||||
pnpmScript('docs-site', 'docs:check', { label: 'documentation site' }),
|
||||
pnpmScript('package-readme-limitations', 'verify-package-readme-limitations', { label: 'package README limitations' }),
|
||||
pnpmScript('website-yaml', 'verify-website-yaml', { label: 'website yaml' }),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,18 @@
|
||||
"docs/development.md",
|
||||
"docs/i18n/README.md",
|
||||
"docs/i18n/translation-rules.md",
|
||||
"docs/user/develop/basic/config.md",
|
||||
"docs/user/develop/basic/index.md",
|
||||
"docs/user/develop/basic/tool.md",
|
||||
"docs/user/develop/framework/events.md",
|
||||
"docs/user/develop/framework/index.md",
|
||||
"docs/user/develop/framework/service.md",
|
||||
"docs/user/develop/practice/index.md",
|
||||
"docs/user/develop/practice/llm-adapter.md",
|
||||
"docs/user/guide/config.md",
|
||||
"docs/user/guide/index.md",
|
||||
"docs/user/guide/quickstart.md",
|
||||
"docs/user/index.md",
|
||||
".agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md",
|
||||
".agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md",
|
||||
"python/README.md",
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* Reject Markdown prose paragraphs spanning multiple physical lines. The GFM
|
||||
* AST distinguishes paragraphs—including those in lists and blockquotes—from
|
||||
* multiline structural nodes. The checker never rewrites; symlinked instruction
|
||||
* files are deduped. The owning convention is in `docs/AGENTS.md`.
|
||||
* files are deduped. VitePress frontmatter and custom-container delimiters are
|
||||
* masked before parsing. The owning convention is in `docs/AGENTS.md`.
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'node:fs'
|
||||
@@ -35,11 +36,23 @@ interface Violation {
|
||||
text: string
|
||||
}
|
||||
|
||||
function maskVitePressStructure(source: string): string {
|
||||
const lines = source.split('\n')
|
||||
if (lines[0] === '---') {
|
||||
const closing = lines.indexOf('---', 1)
|
||||
if (closing !== -1) {
|
||||
for (let index = 0; index <= closing; index++) lines[index] = ''
|
||||
}
|
||||
}
|
||||
return lines.map(line => line.trimStart().startsWith(':::') ? '' : line).join('\n')
|
||||
}
|
||||
|
||||
/** Find every hard-wrapped prose paragraph in one Markdown file via its AST. */
|
||||
function findViolations(absPath: string): Violation[] {
|
||||
const file = relative(root, absPath)
|
||||
const source = readFileSync(absPath, 'utf8')
|
||||
const tree = parseMarkdown(source)
|
||||
const parsedSource = maskVitePressStructure(source)
|
||||
const tree = parseMarkdown(parsedSource)
|
||||
const out: Violation[] = []
|
||||
|
||||
visitMarkdown(tree, (node: Nodes): boolean | void => {
|
||||
|
||||
@@ -14,7 +14,7 @@ import ts from 'typescript'
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
/** Scan doc-typecheck's full Markdown scope so unmanifested blocks also fail. */
|
||||
const MARKDOWN_GLOBS = ['README.md', '.agents/notes/**/*.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md', 'website/zh-CN/**/*.md']
|
||||
const MARKDOWN_GLOBS = ['README.md', '.agents/notes/**/*.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md']
|
||||
|
||||
/** One manifest entry: a source-equivalence block and its source symbol. */
|
||||
interface ManifestEntry {
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
/**
|
||||
* Doc-sync gate: verify the fenced ```yaml examples in the website against
|
||||
* the loader and the workspace truth. A `cordis.yml` example that names a
|
||||
* plugin that does not exist, or passes a config key the plugin never
|
||||
* declared, is worse than no example — it fails silently for the reader.
|
||||
*
|
||||
* Scope: `website/zh-CN/**/*.md`, EXCLUDING `website/zh-CN/api/**` (the api
|
||||
* pages are generator-owned — their yaml examples are verified at generation
|
||||
* time by a later stream, not re-checked here). Blocks opt out with
|
||||
* ` ```yaml ignore-check ` (same philosophy as doc-typecheck's opt-out: the
|
||||
* count is reported, an unchecked block is a visible decision, not a silent
|
||||
* hole — placeholder plugin names in tutorials are the legitimate case).
|
||||
*
|
||||
* Each checked block is parsed with the loader's REAL schema —
|
||||
* `JSON_SCHEMA` extended with the `!!js` scalar type exactly as
|
||||
* vendor/include/src/index.ts declares it — so `!!js process.env.X` parses
|
||||
* here iff it parses at runtime. Then:
|
||||
*
|
||||
* - Root is an ARRAY → a cordis.yml entry list. Every item must be a mapping
|
||||
* with a string `name` and only the keys `EntryOptions` declares
|
||||
* (vendor/loader/src/config/entry.ts plus the isolate.ts merge:
|
||||
* id, name, config, group, disabled, inject, intercept, isolate).
|
||||
* - `./` / `../` names are illustrative local plugins — existence is not
|
||||
* checkable, skip. `group:*` names are loader built-ins; their `config`
|
||||
* is itself an entry list and is recursed into.
|
||||
* - Any other name must be a real workspace package (`packages/*/*` and
|
||||
* `vendor/*` package.json names).
|
||||
* - For `@deepseek-ai/dsh-*` names the config-catalog generator is the
|
||||
* truth: kind `config` → the yaml `config`'s top-level keys must be
|
||||
* properties of the declared config type (member names of the first
|
||||
* catalog paste ∪ top-level segments of the runtime schema keys);
|
||||
* config-free kinds → a non-empty `config` mapping is a violation;
|
||||
* seam/library kinds → name existence only (loading one directly is
|
||||
* dubious, but that is a docs-prose concern, not this gate's).
|
||||
* - Root is a MAPPING or scalar → a fragment (e.g. a bare `config:` excerpt):
|
||||
* syntax check only.
|
||||
*
|
||||
* This is a checker, not a fixer: it reports `file:line message` and exits 1.
|
||||
*
|
||||
* Run: `tsx scripts/verify-website-yaml.ts`.
|
||||
*/
|
||||
|
||||
import { globSync, readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import * as yaml from 'js-yaml'
|
||||
import ts from 'typescript'
|
||||
import { collectConfigCatalog, type CatalogEntry } from './gen-config-catalog.ts'
|
||||
import { extractFences } from './md-fences.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
/** Mirror of the loader's yaml schema (vendor/include/src/index.ts): the
|
||||
* `!!js` tag parses to an expression wrapper, everything else is JSON. */
|
||||
const JsExpr = new yaml.Type('tag:yaml.org,2002:js', {
|
||||
kind: 'scalar',
|
||||
resolve: data => typeof data === 'string',
|
||||
construct: (data: string) => ({ __jsExpr: data }),
|
||||
})
|
||||
const schema = yaml.JSON_SCHEMA.extend(JsExpr)
|
||||
|
||||
/** The exact key set an entry mapping may carry: `EntryOptions` in
|
||||
* vendor/loader/src/config/entry.ts plus the isolate.ts interface merge. */
|
||||
const ENTRY_KEYS = ['id', 'name', 'config', 'group', 'disabled', 'inject', 'intercept', 'isolate'] as const
|
||||
|
||||
/** One `file:line message` finding. */
|
||||
interface Violation {
|
||||
file: string
|
||||
/** 1-based line of the block's opening fence. */
|
||||
line: number
|
||||
message: string
|
||||
}
|
||||
|
||||
/** One extracted ```yaml block. */
|
||||
interface Block {
|
||||
file: string
|
||||
/** 1-based line of the opening fence. */
|
||||
line: number
|
||||
kind: 'check' | 'ignore'
|
||||
code: string
|
||||
}
|
||||
|
||||
/** Extract every ```yaml / ```yaml ignore-check block from one Markdown file. */
|
||||
function extractBlocks(file: string): Block[] {
|
||||
return extractFences(resolve(root, file), info =>
|
||||
info === 'yaml' ? 'check' : info === 'yaml ignore-check' ? 'ignore' : null)
|
||||
.map(f => ({ file, line: f.line, kind: f.kind, code: f.code }))
|
||||
}
|
||||
|
||||
/** Every workspace package name: `packages/<group>/<pkg>` and `vendor/<pkg>`. */
|
||||
function knownPackages(): Set<string> {
|
||||
const names = new Set<string>()
|
||||
for (const pattern of ['packages/*/*/package.json', 'vendor/*/package.json']) {
|
||||
for (const match of globSync(pattern, { cwd: root })) {
|
||||
const pkg: unknown = JSON.parse(readFileSync(resolve(root, match), 'utf8'))
|
||||
if (typeof pkg === 'object' && pkg !== null && 'name' in pkg && typeof pkg.name === 'string') {
|
||||
names.add(pkg.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
/** The catalog, built once on first `@deepseek-ai/dsh-*` name, keyed by pkg. */
|
||||
let catalogByPkg: Map<string, CatalogEntry> | null = null
|
||||
function catalogFor(pkg: string): CatalogEntry | undefined {
|
||||
catalogByPkg ??= new Map(collectConfigCatalog().map(e => [e.pkg, e]))
|
||||
return catalogByPkg.get(pkg)
|
||||
}
|
||||
|
||||
/** Top-level property names of the first catalog paste (the verbatim config
|
||||
* type declaration), parsed as source text. */
|
||||
function pasteKeys(paste: string): Set<string> {
|
||||
const sf = ts.createSourceFile('paste.ts', paste, ts.ScriptTarget.Latest, true)
|
||||
const keys = new Set<string>()
|
||||
const addMembers = (members: ts.NodeArray<ts.TypeElement>): void => {
|
||||
for (const m of members) {
|
||||
if (ts.isPropertySignature(m) || ts.isMethodSignature(m)) {
|
||||
const name = m.name
|
||||
keys.add(ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : name.getText(sf))
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const stmt of sf.statements) {
|
||||
if (ts.isInterfaceDeclaration(stmt)) addMembers(stmt.members)
|
||||
else if (ts.isTypeAliasDeclaration(stmt) && ts.isTypeLiteralNode(stmt.type)) addMembers(stmt.type.members)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
/** The allowed top-level config keys of a kind-`config` catalog entry: the
|
||||
* first paste's member names ∪ the schema keys' top-level segments
|
||||
* (`agents[].id` → `agents`). Cached per entry. */
|
||||
const allowedKeysCache = new Map<string, Set<string>>()
|
||||
function allowedConfigKeys(entry: CatalogEntry): Set<string> {
|
||||
const cached = allowedKeysCache.get(entry.pkg)
|
||||
if (cached) return cached
|
||||
const keys = pasteKeys(entry.pastes?.[0]?.text ?? '')
|
||||
for (const path of entry.schemaKeys ?? []) {
|
||||
const top = path.split('.')[0]?.replace(/\[\]$/, '')
|
||||
if (top) keys.add(top)
|
||||
}
|
||||
allowedKeysCache.set(entry.pkg, keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
/** A parsed yaml mapping (arrays and `!!js` wrappers excluded). */
|
||||
function asMapping(value: unknown): Record<string, unknown> | null {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) return null
|
||||
if ('__jsExpr' in value) return null
|
||||
return value as Record<string, unknown>
|
||||
}
|
||||
|
||||
/** Check one cordis.yml entry list (recursing into `group:` sub-lists). */
|
||||
function checkEntryList(
|
||||
items: unknown[],
|
||||
known: Set<string>,
|
||||
block: Block,
|
||||
violations: Violation[],
|
||||
): void {
|
||||
const flag = (message: string): void => {
|
||||
violations.push({ file: block.file, line: block.line, message })
|
||||
}
|
||||
items.forEach((item, index) => {
|
||||
const at = `entry ${index + 1}`
|
||||
const entry = asMapping(item)
|
||||
if (!entry) {
|
||||
flag(`${at}: not a mapping`)
|
||||
return
|
||||
}
|
||||
const name = entry['name']
|
||||
if (typeof name !== 'string') {
|
||||
flag(`${at}: missing string \`name\``)
|
||||
return
|
||||
}
|
||||
for (const key of Object.keys(entry)) {
|
||||
if (!(ENTRY_KEYS as readonly string[]).includes(key)) {
|
||||
flag(`${at} (${name}): unknown entry key \`${key}\` (EntryOptions allows: ${[...ENTRY_KEYS].join(', ')})`)
|
||||
}
|
||||
}
|
||||
// Illustrative local plugin — nothing on disk to check against.
|
||||
if (name.startsWith('./') || name.startsWith('../')) return
|
||||
// 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
|
||||
}
|
||||
if (!known.has(name)) {
|
||||
flag(`${at}: unknown plugin \`${name}\` (not a workspace package)`)
|
||||
return
|
||||
}
|
||||
if (!name.startsWith('@deepseek-ai/dsh-')) return
|
||||
const catalog = catalogFor(name)
|
||||
if (!catalog) return
|
||||
const config = asMapping(entry['config'])
|
||||
if (catalog.kind === 'config') {
|
||||
if (!config) return
|
||||
const allowed = allowedConfigKeys(catalog)
|
||||
for (const key of Object.keys(config)) {
|
||||
if (!allowed.has(key)) {
|
||||
flag(`${at}: \`${name}\` has no config key \`${key}\` (known keys: ${[...allowed].sort().join(', ')})`)
|
||||
}
|
||||
}
|
||||
} else if (catalog.kind === 'no-config') {
|
||||
if (config && Object.keys(config).length > 0) {
|
||||
flag(`${at}: \`${name}\` declares no config, but the example passes one`)
|
||||
}
|
||||
}
|
||||
// seam / library: loading one directly is dubious, but that is a prose
|
||||
// concern — this gate only vouches for name existence.
|
||||
})
|
||||
}
|
||||
|
||||
const files = globSync('website/zh-CN/**/*.md', { cwd: root })
|
||||
.filter(f => !f.startsWith('website/zh-CN/api/'))
|
||||
.sort()
|
||||
|
||||
const violations: Violation[] = []
|
||||
const known = knownPackages()
|
||||
let entryLists = 0
|
||||
let fragments = 0
|
||||
let ignored = 0
|
||||
let scanned = 0
|
||||
|
||||
for (const file of files) {
|
||||
for (const block of extractBlocks(file)) {
|
||||
scanned++
|
||||
if (block.kind === 'ignore') {
|
||||
ignored++
|
||||
continue
|
||||
}
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = yaml.load(block.code, { schema })
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message.split('\n')[0] ?? 'parse error' : String(error)
|
||||
violations.push({ file: block.file, line: block.line, message: `yaml parse error: ${message}` })
|
||||
continue
|
||||
}
|
||||
if (Array.isArray(parsed)) {
|
||||
entryLists++
|
||||
checkEntryList(parsed, known, block, violations)
|
||||
} else {
|
||||
// Mapping or scalar root: a fragment (e.g. a bare `config:` excerpt) —
|
||||
// syntax is all there is to check.
|
||||
fragments++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (violations.length === 0) {
|
||||
console.log(
|
||||
`verify-website-yaml: ${scanned} yaml block(s) in ${files.length} file(s): `
|
||||
+ `${entryLists} entry list(s) + ${fragments} fragment(s) checked, ${ignored} ignore-check skipped.`,
|
||||
)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
console.error('verify-website-yaml: invalid yaml examples found:')
|
||||
for (const v of violations) {
|
||||
console.error(` ${v.file}:${v.line} ${v.message}`)
|
||||
}
|
||||
process.exit(1)
|
||||
@@ -9,7 +9,9 @@
|
||||
"examples/*/start.ts",
|
||||
"examples/*/tests/**/*.ts",
|
||||
"packages/*/*/tests/**/*.ts",
|
||||
"scripts/**/*.ts"
|
||||
"scripts/**/*.ts",
|
||||
"website/**/*.ts",
|
||||
"website/.vitepress/**/*.ts"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "./vendor/cosmokit" },
|
||||
|
||||
5
website/.gitignore
vendored
5
website/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
.vitepress/dist/
|
||||
.vitepress/cache/
|
||||
.cache/
|
||||
.dist/
|
||||
.generated/
|
||||
|
||||
191
website/.vitepress/config.ts
Normal file
191
website/.vitepress/config.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/** VitePress configuration for the locally projected documentation site. */
|
||||
|
||||
import type { DefaultTheme, PageData } from 'vitepress'
|
||||
import type { ViteDevServer } from 'vite'
|
||||
import { withMermaid } from 'vitepress-plugin-mermaid'
|
||||
import { docsPages, type DocsPage } from '../docs.ts'
|
||||
import { docsSourceFiles, projectDocs } from '../../scripts/project-doc-site.ts'
|
||||
|
||||
projectDocs()
|
||||
|
||||
const sectionOrder = [
|
||||
'入门',
|
||||
'基础',
|
||||
'框架能力',
|
||||
'实战',
|
||||
'概念',
|
||||
'生成参考',
|
||||
'Cordis API',
|
||||
'数据结构',
|
||||
'开发手册',
|
||||
'Guide',
|
||||
'Basics',
|
||||
'Framework',
|
||||
'Practice',
|
||||
'Concepts',
|
||||
'Generated reference',
|
||||
'Cordis Core API',
|
||||
'Data structures',
|
||||
'Cookbook',
|
||||
]
|
||||
|
||||
function sidebar(collection: DocsPage['sidebar']): DefaultTheme.SidebarItem[] {
|
||||
const pages = docsPages.filter(page => page.sidebar === collection)
|
||||
const sections = new Map<string, DocsPage[]>()
|
||||
for (const page of pages) {
|
||||
const entries = sections.get(page.section) ?? []
|
||||
entries.push(page)
|
||||
sections.set(page.section, entries)
|
||||
}
|
||||
return [...sections.entries()]
|
||||
.sort(([left], [right]) => sectionOrder.indexOf(left) - sectionOrder.indexOf(right))
|
||||
.map(([text, entries]) => ({
|
||||
text,
|
||||
items: entries
|
||||
.sort((left, right) => left.order - right.order)
|
||||
.map(page => ({ text: page.label, link: `/${page.route.replace(/(?:index)?\.md$/, '')}` })),
|
||||
}))
|
||||
}
|
||||
|
||||
function watchCanonicalDocs(server: ViteDevServer): void {
|
||||
const sources = docsSourceFiles()
|
||||
server.watcher.add(sources)
|
||||
server.watcher.on('change', (changed) => {
|
||||
if (!sources.includes(changed)) return
|
||||
projectDocs()
|
||||
})
|
||||
}
|
||||
|
||||
function escapeVueInterpolation(html: string): string {
|
||||
return html.replaceAll('{{', '{{').replaceAll('}}', '}}')
|
||||
}
|
||||
|
||||
const sharedTheme: Pick<DefaultTheme.Config, 'search' | 'socialLinks' | 'editLink'> = {
|
||||
search: {
|
||||
provider: 'local',
|
||||
options: {
|
||||
locales: {
|
||||
root: {
|
||||
translations: {
|
||||
button: {
|
||||
buttonText: '搜索文档',
|
||||
buttonAriaLabel: '搜索文档',
|
||||
},
|
||||
modal: {
|
||||
displayDetails: '显示详细列表',
|
||||
resetButtonTitle: '清除搜索',
|
||||
backButtonTitle: '关闭搜索',
|
||||
noResultsText: '未找到相关结果',
|
||||
footer: {
|
||||
selectText: '选择',
|
||||
selectKeyAriaLabel: '回车键',
|
||||
navigateText: '切换',
|
||||
navigateUpKeyAriaLabel: '上方向键',
|
||||
navigateDownKeyAriaLabel: '下方向键',
|
||||
closeText: '关闭',
|
||||
closeKeyAriaLabel: 'Esc 键',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/deepseek-harness/deepseek-harness' },
|
||||
],
|
||||
editLink: {
|
||||
pattern: ({ frontmatter }: PageData) => {
|
||||
const data: unknown = frontmatter
|
||||
const editSource: unknown = typeof data === 'object' && data !== null ? Reflect.get(data, 'editSource') : undefined
|
||||
if (typeof editSource !== 'string') throw new Error('Projected documentation page has no editSource frontmatter.')
|
||||
return `https://github.com/deepseek-harness/deepseek-harness/edit/master/${editSource}`
|
||||
},
|
||||
text: '在 GitHub 上编辑此页',
|
||||
},
|
||||
}
|
||||
|
||||
export default withMermaid({
|
||||
title: 'DeepSeek Harness',
|
||||
description: '用于构建 Agent Harness 的插件化 SDK',
|
||||
cleanUrls: true,
|
||||
srcDir: '.generated',
|
||||
cacheDir: '.cache',
|
||||
outDir: '.dist',
|
||||
locales: {
|
||||
root: {
|
||||
label: '简体中文',
|
||||
lang: 'zh-CN',
|
||||
themeConfig: {
|
||||
nav: [
|
||||
{ text: '入门', link: '/guide/', activeMatch: '^/guide/' },
|
||||
{ text: '开发', link: '/develop/basic/', activeMatch: '^/develop/' },
|
||||
{ text: '参考', link: '/reference/', activeMatch: '^/reference/' },
|
||||
],
|
||||
sidebar: {
|
||||
'/guide/': sidebar('zh-guide'),
|
||||
'/develop/': sidebar('zh-develop'),
|
||||
'/reference/': sidebar('zh-reference'),
|
||||
},
|
||||
outline: { label: '本页目录' },
|
||||
docFooter: { prev: '上一篇', next: '下一篇' },
|
||||
darkModeSwitchLabel: '外观',
|
||||
lightModeSwitchTitle: '切换到浅色主题',
|
||||
darkModeSwitchTitle: '切换到深色主题',
|
||||
sidebarMenuLabel: '菜单',
|
||||
returnToTopLabel: '返回顶部',
|
||||
langMenuLabel: '切换语言',
|
||||
skipToContentLabel: '跳至内容',
|
||||
},
|
||||
},
|
||||
en: {
|
||||
label: 'English',
|
||||
lang: 'en-US',
|
||||
link: '/en/',
|
||||
themeConfig: {
|
||||
nav: [
|
||||
{ text: 'Guide', link: '/en/guide/', activeMatch: '^/en/guide/' },
|
||||
{ text: 'Develop', link: '/en/develop/basic/', activeMatch: '^/en/develop/' },
|
||||
{ text: 'Reference', link: '/en/reference/', activeMatch: '^/en/reference/' },
|
||||
],
|
||||
sidebar: {
|
||||
'/en/guide/': sidebar('en-guide'),
|
||||
'/en/develop/': sidebar('en-develop'),
|
||||
'/en/reference/': sidebar('en-reference'),
|
||||
},
|
||||
editLink: {
|
||||
pattern: ({ frontmatter }: PageData) => {
|
||||
const data: unknown = frontmatter
|
||||
const editSource: unknown = typeof data === 'object' && data !== null ? Reflect.get(data, 'editSource') : undefined
|
||||
if (typeof editSource !== 'string') throw new Error('Projected documentation page has no editSource frontmatter.')
|
||||
return `https://github.com/deepseek-harness/deepseek-harness/edit/master/${editSource}`
|
||||
},
|
||||
text: 'Edit this page on GitHub',
|
||||
},
|
||||
outline: { label: 'On this page' },
|
||||
docFooter: { prev: 'Previous', next: 'Next' },
|
||||
},
|
||||
},
|
||||
},
|
||||
vite: {
|
||||
plugins: [
|
||||
{
|
||||
name: 'deepseek-harness-doc-projector',
|
||||
configureServer: watchCanonicalDocs,
|
||||
},
|
||||
],
|
||||
},
|
||||
markdown: {
|
||||
config(md) {
|
||||
const renderText = md.renderer.rules.text
|
||||
const renderCode = md.renderer.rules.code_inline
|
||||
if (renderText === undefined || renderCode === undefined) {
|
||||
throw new Error('VitePress Markdown renderer is missing its text or inline-code rule.')
|
||||
}
|
||||
md.renderer.rules.text = (...args) => escapeVueInterpolation(renderText(...args))
|
||||
md.renderer.rules.code_inline = (...args) => escapeVueInterpolation(renderCode(...args))
|
||||
},
|
||||
},
|
||||
mermaid: {},
|
||||
themeConfig: sharedTheme,
|
||||
})
|
||||
@@ -1,134 +0,0 @@
|
||||
{
|
||||
"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.approval",
|
||||
"link": "/zh-CN/api/harness/approval"
|
||||
},
|
||||
{
|
||||
"text": "ctx.bash",
|
||||
"link": "/zh-CN/api/harness/bash"
|
||||
},
|
||||
{
|
||||
"text": "ctx.bashEnv",
|
||||
"link": "/zh-CN/api/harness/bash-env"
|
||||
},
|
||||
{
|
||||
"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.permission",
|
||||
"link": "/zh-CN/api/harness/permission"
|
||||
},
|
||||
{
|
||||
"text": "ctx.sandbox",
|
||||
"link": "/zh-CN/api/harness/sandbox"
|
||||
},
|
||||
{
|
||||
"text": "ctx.sandboxPolicy",
|
||||
"link": "/zh-CN/api/harness/sandbox-policy"
|
||||
},
|
||||
{
|
||||
"text": "ctx.sessionPersistence",
|
||||
"link": "/zh-CN/api/harness/session-persistence"
|
||||
},
|
||||
{
|
||||
"text": "ctx.sessionQuery",
|
||||
"link": "/zh-CN/api/harness/session-query"
|
||||
},
|
||||
{
|
||||
"text": "ctx.sessions",
|
||||
"link": "/zh-CN/api/harness/sessions"
|
||||
},
|
||||
{
|
||||
"text": "ctx.skills",
|
||||
"link": "/zh-CN/api/harness/skills"
|
||||
},
|
||||
{
|
||||
"text": "ctx.spillStore",
|
||||
"link": "/zh-CN/api/harness/spill-store"
|
||||
},
|
||||
{
|
||||
"text": "ctx.subagents",
|
||||
"link": "/zh-CN/api/harness/subagents"
|
||||
},
|
||||
{
|
||||
"text": "ctx.systemPrompt",
|
||||
"link": "/zh-CN/api/harness/system-prompt"
|
||||
},
|
||||
{
|
||||
"text": "ctx.tasks",
|
||||
"link": "/zh-CN/api/harness/tasks"
|
||||
},
|
||||
{
|
||||
"text": "ctx.tokenMeter",
|
||||
"link": "/zh-CN/api/harness/token-meter"
|
||||
},
|
||||
{
|
||||
"text": "ctx.toolResultPrune",
|
||||
"link": "/zh-CN/api/harness/tool-result-prune"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { defineConfig } from 'vitepress'
|
||||
import { zhCN } from './zh-CN'
|
||||
|
||||
export default defineConfig({
|
||||
title: 'DeepSeek Harness',
|
||||
description: '插件化 Agent 开发框架',
|
||||
|
||||
// The design essays (design/revertible-effects, design/context-model) carry
|
||||
// real TeX; math: true wires markdown-it-mathjax3 into the pipeline.
|
||||
// markdown-it-mathjax3 is pinned to ^4 (NOT 5.x): v5 injects a <style> tag
|
||||
// per formula, which Vue's template compiler rejects ("Tags with side
|
||||
// effect … are ignored in client component templates"); v4 emits pure SVG.
|
||||
markdown: { math: true },
|
||||
|
||||
locales: {
|
||||
'zh-CN': zhCN,
|
||||
},
|
||||
|
||||
themeConfig: {
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/deepseek-harness/deepseek-harness' },
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -1,93 +0,0 @@
|
||||
import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress'
|
||||
import apiSidebarData from './api-sidebar.json'
|
||||
|
||||
const guideSidebar: DefaultTheme.SidebarItem[] = [
|
||||
{
|
||||
text: '入门',
|
||||
items: [
|
||||
{ text: '介绍', link: '/zh-CN/guide/' },
|
||||
{ text: '快速开始', link: '/zh-CN/guide/quickstart' },
|
||||
{ text: '配置文件', link: '/zh-CN/guide/config' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const developSidebar: DefaultTheme.SidebarItem[] = [
|
||||
{
|
||||
text: '基础',
|
||||
items: [
|
||||
{ text: '第一个插件', link: '/zh-CN/develop/basic/' },
|
||||
{ text: '开发一个 Tool', link: '/zh-CN/develop/basic/tool' },
|
||||
{ text: '插件配置', link: '/zh-CN/develop/basic/config' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '框架能力',
|
||||
items: [
|
||||
{ text: '插件与生命周期', link: '/zh-CN/develop/framework/' },
|
||||
{ text: '服务与依赖', link: '/zh-CN/develop/framework/service' },
|
||||
{ text: '事件系统', link: '/zh-CN/develop/framework/events' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: '实战',
|
||||
items: [
|
||||
{ text: '能力的三层拆分', link: '/zh-CN/develop/practice/' },
|
||||
{ text: 'LLM 适配器', link: '/zh-CN/develop/practice/llm-adapter' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// 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/' },
|
||||
...apiSidebarData.cordis,
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Harness API',
|
||||
items: apiSidebarData.harness,
|
||||
},
|
||||
]
|
||||
|
||||
const designSidebar: DefaultTheme.SidebarItem[] = [
|
||||
{
|
||||
text: '系统设计',
|
||||
items: [
|
||||
{ text: '概述', link: '/zh-CN/design/' },
|
||||
{ text: '可组合性与插件系统', link: '/zh-CN/design/composability' },
|
||||
{ text: '作用与余作用', link: '/zh-CN/design/effects-coeffects' },
|
||||
{ text: '可逆作用', link: '/zh-CN/design/revertible-effects' },
|
||||
{ text: '响应式余作用', link: '/zh-CN/design/reactive-coeffects' },
|
||||
{ text: '上下文模型', link: '/zh-CN/design/context-model' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const zhCN: LocaleSpecificConfig<DefaultTheme.Config> = {
|
||||
label: '简体中文',
|
||||
lang: 'zh-CN',
|
||||
themeConfig: {
|
||||
nav: [
|
||||
{ text: '入门', link: '/zh-CN/guide/', activeMatch: '/zh-CN/guide/' },
|
||||
{ text: '开发', link: '/zh-CN/develop/basic/', activeMatch: '/zh-CN/develop/' },
|
||||
{ text: 'API', link: '/zh-CN/api/', activeMatch: '/zh-CN/api/' },
|
||||
{ text: '设计', link: '/zh-CN/design/', activeMatch: '/zh-CN/design/' },
|
||||
],
|
||||
sidebar: {
|
||||
'/zh-CN/guide/': guideSidebar,
|
||||
'/zh-CN/develop/': developSidebar,
|
||||
'/zh-CN/api/': apiSidebar,
|
||||
'/zh-CN/design/': designSidebar,
|
||||
},
|
||||
// level [2,3]: the generated API pages put each member at h3 (### ctx.foo)
|
||||
// under an h2 scope/statics group — both belong in the page outline.
|
||||
outline: { label: '本页目录', level: [2, 3] },
|
||||
docFooter: { prev: '上一篇', next: '下一篇' },
|
||||
},
|
||||
}
|
||||
305
website/docs.ts
Normal file
305
website/docs.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* Canonical publication manifest for the documentation website.
|
||||
*
|
||||
* Markdown stays in its owning repository tier. This manifest maps each
|
||||
* canonical source into matching route trees for both site locales; when a
|
||||
* translation is absent, both routes intentionally project the available
|
||||
* source instead of copying Markdown.
|
||||
*/
|
||||
|
||||
/** Locale key used by the VitePress site. */
|
||||
export type DocsLocale = 'root' | 'en'
|
||||
|
||||
/** Sidebar collection rendered for one locale and top-level module. */
|
||||
type DocsSidebar =
|
||||
| 'zh-guide'
|
||||
| 'zh-develop'
|
||||
| 'zh-reference'
|
||||
| 'en-guide'
|
||||
| 'en-develop'
|
||||
| 'en-reference'
|
||||
|
||||
/** A page projected into the VitePress source tree. */
|
||||
export interface DocsPage {
|
||||
/** VitePress locale whose route tree owns this projection. */
|
||||
locale: DocsLocale
|
||||
/** Language of the canonical source currently projected at this route. */
|
||||
contentLocale: 'zh-CN' | 'en-US'
|
||||
/** Repository-relative canonical Markdown source. */
|
||||
source: string
|
||||
/** VitePress route, including the `.md` suffix. */
|
||||
route: string
|
||||
/** Navigation label shown in the sidebar. */
|
||||
label: string
|
||||
/** Sidebar collection that owns the page, or null for a locale home page. */
|
||||
sidebar: DocsSidebar | null
|
||||
/** Section label within the sidebar. */
|
||||
section: string
|
||||
/** Stable order within the section. */
|
||||
order: number
|
||||
/** Additional repository paths that resolve to this page. */
|
||||
sourceAliases?: string[]
|
||||
}
|
||||
|
||||
interface MirroredPage {
|
||||
source: string | Record<DocsLocale, string>
|
||||
route: string
|
||||
contentLocale: DocsPage['contentLocale'] | Record<DocsLocale, DocsPage['contentLocale']>
|
||||
label: Record<DocsLocale, string>
|
||||
sidebar: Record<DocsLocale, DocsSidebar | null>
|
||||
section: Record<DocsLocale, string>
|
||||
order: number
|
||||
sourceAliases?: string[] | Partial<Record<DocsLocale, string[]>>
|
||||
}
|
||||
|
||||
type PairedPage = Omit<MirroredPage, 'source' | 'contentLocale' | 'sourceAliases'> & {
|
||||
/** English side of a sibling `foo.md` / `foo.zh.md` pair. */
|
||||
source: string
|
||||
/** Language-neutral repository aliases, such as the directory of an index page. */
|
||||
sourceAliases?: string[]
|
||||
}
|
||||
|
||||
function localized<T>(value: T | Record<DocsLocale, T>, locale: DocsLocale): T {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
? (value as Record<DocsLocale, T>)[locale]
|
||||
: value
|
||||
}
|
||||
|
||||
function mirroredPages(pages: MirroredPage[]): DocsPage[] {
|
||||
return pages.flatMap(page => (['root', 'en'] as const).map((locale) => {
|
||||
const aliases = page.sourceAliases === undefined
|
||||
? undefined
|
||||
: Array.isArray(page.sourceAliases) ? page.sourceAliases : page.sourceAliases[locale]
|
||||
return {
|
||||
locale,
|
||||
contentLocale: localized(page.contentLocale, locale),
|
||||
source: localized(page.source, locale),
|
||||
route: locale === 'root' ? page.route : `en/${page.route}`,
|
||||
label: page.label[locale],
|
||||
sidebar: page.sidebar[locale],
|
||||
section: page.section[locale],
|
||||
order: page.order,
|
||||
...(aliases === undefined ? {} : { sourceAliases: aliases }),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
function pairedPages(pages: PairedPage[]): DocsPage[] {
|
||||
return mirroredPages(pages.map((page) => {
|
||||
const chineseSource = page.source.replace(/\.md$/, '.zh.md')
|
||||
const sharedAliases = page.sourceAliases ?? []
|
||||
return {
|
||||
...page,
|
||||
source: { root: chineseSource, en: page.source },
|
||||
contentLocale: { root: 'zh-CN', en: 'en-US' },
|
||||
sourceAliases: {
|
||||
root: [...sharedAliases, page.source],
|
||||
en: [...sharedAliases, chineseSource],
|
||||
},
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
const homeAndGuide = pairedPages([
|
||||
{
|
||||
source: 'docs/user/index.md',
|
||||
route: 'index.md',
|
||||
label: { root: 'DeepSeek Harness', en: 'DeepSeek Harness' },
|
||||
sidebar: { root: null, en: null },
|
||||
section: { root: '首页', en: 'Home' },
|
||||
order: 0,
|
||||
},
|
||||
{
|
||||
source: 'docs/user/guide/index.md',
|
||||
route: 'guide/index.md',
|
||||
label: { root: '介绍', en: 'Introduction' },
|
||||
sidebar: { root: 'zh-guide', en: 'en-guide' },
|
||||
section: { root: '入门', en: 'Guide' },
|
||||
order: 1,
|
||||
sourceAliases: ['docs/user/guide'],
|
||||
},
|
||||
{
|
||||
source: 'docs/user/guide/quickstart.md',
|
||||
route: 'guide/quickstart.md',
|
||||
label: { root: '快速开始', en: 'Quick start' },
|
||||
sidebar: { root: 'zh-guide', en: 'en-guide' },
|
||||
section: { root: '入门', en: 'Guide' },
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
source: 'docs/user/guide/config.md',
|
||||
route: 'guide/config.md',
|
||||
label: { root: '配置文件', en: 'Configuration' },
|
||||
sidebar: { root: 'zh-guide', en: 'en-guide' },
|
||||
section: { root: '入门', en: 'Guide' },
|
||||
order: 3,
|
||||
},
|
||||
])
|
||||
|
||||
const develop = pairedPages([
|
||||
{
|
||||
source: 'docs/user/develop/basic/index.md',
|
||||
route: 'develop/basic/index.md',
|
||||
label: { root: '第一个插件', en: 'First plugin' },
|
||||
sidebar: { root: 'zh-develop', en: 'en-develop' },
|
||||
section: { root: '基础', en: 'Basics' },
|
||||
order: 1,
|
||||
sourceAliases: ['docs/user/develop/basic'],
|
||||
},
|
||||
{
|
||||
source: 'docs/user/develop/basic/tool.md',
|
||||
route: 'develop/basic/tool.md',
|
||||
label: { root: '开发一个 Tool', en: 'Build a tool' },
|
||||
sidebar: { root: 'zh-develop', en: 'en-develop' },
|
||||
section: { root: '基础', en: 'Basics' },
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
source: 'docs/user/develop/basic/config.md',
|
||||
route: 'develop/basic/config.md',
|
||||
label: { root: '插件配置', en: 'Plugin configuration' },
|
||||
sidebar: { root: 'zh-develop', en: 'en-develop' },
|
||||
section: { root: '基础', en: 'Basics' },
|
||||
order: 3,
|
||||
},
|
||||
{
|
||||
source: 'docs/user/develop/framework/index.md',
|
||||
route: 'develop/framework/index.md',
|
||||
label: { root: '插件与生命周期', en: 'Plugin lifecycle' },
|
||||
sidebar: { root: 'zh-develop', en: 'en-develop' },
|
||||
section: { root: '框架能力', en: 'Framework' },
|
||||
order: 1,
|
||||
sourceAliases: ['docs/user/develop/framework'],
|
||||
},
|
||||
{
|
||||
source: 'docs/user/develop/framework/service.md',
|
||||
route: 'develop/framework/service.md',
|
||||
label: { root: '服务与依赖', en: 'Services and dependencies' },
|
||||
sidebar: { root: 'zh-develop', en: 'en-develop' },
|
||||
section: { root: '框架能力', en: 'Framework' },
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
source: 'docs/user/develop/framework/events.md',
|
||||
route: 'develop/framework/events.md',
|
||||
label: { root: '事件系统', en: 'Event system' },
|
||||
sidebar: { root: 'zh-develop', en: 'en-develop' },
|
||||
section: { root: '框架能力', en: 'Framework' },
|
||||
order: 3,
|
||||
},
|
||||
{
|
||||
source: 'docs/user/develop/practice/index.md',
|
||||
route: 'develop/practice/index.md',
|
||||
label: { root: '能力的三层拆分', en: 'Capability layering' },
|
||||
sidebar: { root: 'zh-develop', en: 'en-develop' },
|
||||
section: { root: '实战', en: 'Practice' },
|
||||
order: 1,
|
||||
sourceAliases: ['docs/user/develop/practice'],
|
||||
},
|
||||
{
|
||||
source: 'docs/user/develop/practice/llm-adapter.md',
|
||||
route: 'develop/practice/llm-adapter.md',
|
||||
label: { root: 'LLM 适配器', en: 'LLM adapter' },
|
||||
sidebar: { root: 'zh-develop', en: 'en-develop' },
|
||||
section: { root: '实战', en: 'Practice' },
|
||||
order: 2,
|
||||
},
|
||||
])
|
||||
|
||||
const reference = mirroredPages([
|
||||
...([
|
||||
['docs/architecture.md', 'reference/index.md', '架构', 'Architecture'],
|
||||
['docs/cordis-primer.md', 'reference/cordis-primer.md', 'Cordis 入门', 'Cordis primer'],
|
||||
['docs/capability-seams.md', 'reference/capability-seams.md', '能力服务', 'Capability services'],
|
||||
['docs/agent-lifecycle.md', 'reference/agent-lifecycle.md', 'Agent 生命周期', 'Agent lifecycle'],
|
||||
['docs/tool-execution-pipeline.md', 'reference/tool-execution-pipeline.md', 'Tool 执行', 'Tool execution'],
|
||||
] as const).map(([source, route, rootLabel, enLabel], order): MirroredPage => ({
|
||||
source,
|
||||
route,
|
||||
contentLocale: 'en-US',
|
||||
label: { root: rootLabel, en: enLabel },
|
||||
sidebar: { root: 'zh-reference', en: 'en-reference' },
|
||||
section: { root: '概念', en: 'Concepts' },
|
||||
order,
|
||||
})),
|
||||
...([
|
||||
['docs/config-catalog.md', 'reference/config-catalog.md', '插件配置', 'Plugin configuration'],
|
||||
['docs/tool-catalog.md', 'reference/tool-catalog.md', 'Tool Schema', 'Tool schemas'],
|
||||
['docs/cordis-catalog/services.md', 'reference/cordis-catalog/services.md', '服务', 'Services'],
|
||||
['docs/cordis-catalog/events.md', 'reference/cordis-catalog/events.md', '事件', 'Events'],
|
||||
['docs/persistence-catalog.md', 'reference/persistence-catalog.md', '持久化事件', 'Persistence events'],
|
||||
] as const).map(([source, route, rootLabel, enLabel], order): MirroredPage => ({
|
||||
source,
|
||||
route,
|
||||
contentLocale: 'en-US',
|
||||
label: { root: rootLabel, en: enLabel },
|
||||
sidebar: { root: 'zh-reference', en: 'en-reference' },
|
||||
section: { root: '生成参考', en: 'Generated reference' },
|
||||
order,
|
||||
})),
|
||||
...([
|
||||
['context.md', 'Context', 'Context'],
|
||||
['events.md', 'Events', 'Events'],
|
||||
['fiber.md', 'Fiber', 'Fiber'],
|
||||
['registry.md', 'Plugin Registry', 'Plugin Registry'],
|
||||
['service.md', 'Service', 'Service'],
|
||||
] as const).map(([file, rootLabel, enLabel], order): MirroredPage => ({
|
||||
source: `docs/cordis-catalog/core/${file}`,
|
||||
route: `reference/cordis-api/${file}`,
|
||||
contentLocale: 'en-US',
|
||||
label: { root: rootLabel, en: enLabel },
|
||||
sidebar: { root: 'zh-reference', en: 'en-reference' },
|
||||
section: { root: 'Cordis API', en: 'Cordis Core API' },
|
||||
order,
|
||||
})),
|
||||
...([
|
||||
['core.md', '核心数据结构', 'Core data structures'],
|
||||
['scope.md', '作用域', 'Scopes'],
|
||||
['session.md', '会话', 'Sessions'],
|
||||
['system-prompt.md', '系统提示词', 'System prompts'],
|
||||
['tools.md', '工具', 'Tools'],
|
||||
['llm-streaming.md', 'LLM 流式响应', 'LLM streaming'],
|
||||
['bash.md', 'Bash 执行', 'Bash execution'],
|
||||
['filesystem.md', '文件系统', 'Filesystem'],
|
||||
['code-runtime.md', '代码运行时', 'Code runtime'],
|
||||
['compaction.md', '上下文压缩', 'Compaction'],
|
||||
['subagent.md', '子代理', 'Subagents'],
|
||||
['workflow.md', '工作流', 'Workflows'],
|
||||
['skills.md', '技能', 'Skills'],
|
||||
['approval.md', '审批', 'Approvals'],
|
||||
['user-interaction.md', '用户交互', 'User interaction'],
|
||||
['sandbox.md', '沙箱', 'Sandboxing'],
|
||||
['web.md', 'Web 访问', 'Web access'],
|
||||
['persistence.md', '会话持久化', 'Session persistence'],
|
||||
] as const).map(([file, rootLabel, enLabel], order): MirroredPage => ({
|
||||
source: `docs/core-data-structures/${file}`,
|
||||
route: `reference/core-data-structures/${file}`,
|
||||
contentLocale: 'en-US',
|
||||
label: { root: rootLabel, en: enLabel },
|
||||
sidebar: { root: 'zh-reference', en: 'en-reference' },
|
||||
section: { root: '数据结构', en: 'Data structures' },
|
||||
order,
|
||||
...(file === 'core.md' ? { sourceAliases: ['docs/core-data-structures'] } : {}),
|
||||
})),
|
||||
...([
|
||||
['adding-a-package.md', '新增 Package', 'Adding a package'],
|
||||
['adding-a-tool.md', '新增 Tool', 'Adding a tool'],
|
||||
['adding-an-llm-adapter.md', '新增 LLM Adapter', 'Adding an LLM adapter'],
|
||||
['extension-cookbook.md', '扩展模式', 'Extension patterns'],
|
||||
] as const).map(([file, rootLabel, enLabel], order): MirroredPage => ({
|
||||
source: `docs/cookbook/${file}`,
|
||||
route: `reference/cookbook/${file}`,
|
||||
contentLocale: 'en-US',
|
||||
label: { root: rootLabel, en: enLabel },
|
||||
sidebar: { root: 'zh-reference', en: 'en-reference' },
|
||||
section: { root: '开发手册', en: 'Cookbook' },
|
||||
order,
|
||||
})),
|
||||
])
|
||||
|
||||
/** Every canonical page published by the documentation website. */
|
||||
export const docsPages: DocsPage[] = [
|
||||
...homeAndGuide,
|
||||
...develop,
|
||||
...reference,
|
||||
]
|
||||
@@ -4,13 +4,19 @@
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vitepress dev . --port 5173 --open",
|
||||
"dev": "vitepress dev . --host 127.0.0.1 --port 5173",
|
||||
"build": "vitepress build .",
|
||||
"preview": "vitepress preview ."
|
||||
"preview": "vitepress preview . --host 127.0.0.1 --port 4173"
|
||||
},
|
||||
"devDependencies": {
|
||||
"markdown-it-mathjax3": "^4.3.2",
|
||||
"vitepress": "^1.6.3",
|
||||
"vue": "^3.5.13"
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"cytoscape": "3.34.0",
|
||||
"cytoscape-cose-bilkent": "4.1.0",
|
||||
"dayjs": "1.11.21",
|
||||
"debug": "4.4.3",
|
||||
"mermaid": "11.16.0",
|
||||
"vite": "^5.4.14",
|
||||
"vitepress": "^1.6.4",
|
||||
"vitepress-plugin-mermaid": "^2.0.17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
<!-- 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`.
|
||||
|
||||
Concrete agent factory and driver service.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L407)
|
||||
|
||||
### ctx.agentLoop.create(id, options?, meta?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Create an agent and session under one caller-supplied identity, owned by
|
||||
* the accessing fiber. Constructor-driven config calls mint a fresh combined
|
||||
* id before entering this boundary.
|
||||
* @param id - shared agent/session identity.
|
||||
* @param options - concrete loop options.
|
||||
* @param meta - optional fresh-session workspace metadata.
|
||||
* @returns the published running agent.
|
||||
*/
|
||||
create(id: SessionId, options: AgentOptions = {}, meta: Pick<SessionHeader, 'cwd'> = {}): Agent
|
||||
```
|
||||
|
||||
Create an agent and session under one caller-supplied identity, owned by the accessing fiber. Constructor-driven config calls mint a fresh combined id before entering this boundary.
|
||||
|
||||
- `id` — shared agent/session identity.
|
||||
- `options` — concrete loop options.
|
||||
- `meta` — optional fresh-session workspace metadata.
|
||||
|
||||
**Returns** the published running agent.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L542)
|
||||
|
||||
### ctx.agentLoop.createAgent(ownerCtx, options)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Create an owned agent on a caller-supplied session id.
|
||||
* @param ownerCtx - caller context that structurally owns the transaction.
|
||||
* @param options - identities, session seed/metadata, loop options, setup, and cancellation.
|
||||
* @returns the published handle.
|
||||
*/
|
||||
async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>
|
||||
```
|
||||
|
||||
Create an owned agent on a caller-supplied session id.
|
||||
|
||||
- `ownerCtx` — caller context that structurally owns the transaction.
|
||||
- `options` — identities, session seed/metadata, loop options, setup, and cancellation.
|
||||
|
||||
**Returns** the published handle.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L564)
|
||||
|
||||
### ctx.agentLoop.resume(ownerCtx, options)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Resume an owned agent from the configured persistence service.
|
||||
* @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
|
||||
* @param options - persisted identity, loop options, setup, and cancellation.
|
||||
* @returns the published handle.
|
||||
*/
|
||||
async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>
|
||||
```
|
||||
|
||||
Resume an owned agent from the configured persistence service.
|
||||
|
||||
- `ownerCtx` — caller context that owns load, setup, and the live lifecycle.
|
||||
- `options` — persisted identity, loop options, setup, and cancellation.
|
||||
|
||||
**Returns** the published handle.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L596)
|
||||
@@ -1,331 +0,0 @@
|
||||
<!-- 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 service (`ctx.agents`): tracks live agents and carries the initiating Agent through one process-local asynchronous driver chain. Agent *creation* is provided by whichever plugin implements the AgentFactory (`@deepseek-ai/dsh-agent-loop`), registered via setFactory.
|
||||
Initiator methods provide same-process causal attribution only. Ambient presence is neither liveness proof nor authorization; subjects and owners remain explicit, as does identity at worker, process, persistence, and wire boundaries. Returned Promise boundaries drain during teardown, except a nested lineage that starts an owning-fiber unload is excluded from its own drain.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L217)
|
||||
|
||||
### ctx.agents.currentInitiator()
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Read the Agent that initiated the inherited asynchronous driver chain.
|
||||
* Use this optional form for logging, tracing, metrics, or host attribution
|
||||
* that also supports agentless calls. When a parent creates a child, setup
|
||||
* reports the causal parent while `agentCtx.agent` identifies the child.
|
||||
* @returns the inherited Agent, or `undefined` outside an initiator boundary
|
||||
* and inside an explicit clearing boundary.
|
||||
* @throws when this service instance has been disposed.
|
||||
*/
|
||||
currentInitiator(): Agent | undefined
|
||||
```
|
||||
|
||||
Read the Agent that initiated the inherited asynchronous driver chain. Use this optional form for logging, tracing, metrics, or host attribution that also supports agentless calls. When a parent creates a child, setup reports the causal parent while `agentCtx.agent` identifies the child.
|
||||
|
||||
**Returns** the inherited Agent, or `undefined` outside an initiator boundary and inside an explicit clearing boundary.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L256)
|
||||
|
||||
### ctx.agents.requireInitiator()
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Read the initiating Agent and fail when no initiator boundary is active.
|
||||
* Use this for private helpers contractually below a driver, or for a
|
||||
* deployment-owned outbound request whose contract forbids agentless calls.
|
||||
* Generic or direct-call seams use optional lookup or explicit request fields.
|
||||
* @returns the inherited Agent.
|
||||
* @throws when no initiator is active or this service instance has been disposed.
|
||||
*/
|
||||
requireInitiator(): Agent
|
||||
```
|
||||
|
||||
Read the initiating Agent and fail when no initiator boundary is active. Use this for private helpers contractually below a driver, or for a deployment-owned outbound request whose contract forbids agentless calls. Generic or direct-call seams use optional lookup or explicit request fields.
|
||||
|
||||
**Returns** the inherited Agent.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L269)
|
||||
|
||||
### ctx.agents.withInitiator(agent, operation)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Run an operation with one exact Agent as its process-local initiator. The
|
||||
* exact synchronous value or Promise returned by the operation is preserved.
|
||||
* Custom drivers and test harnesses wrap their complete returned foreground
|
||||
* lifetime.
|
||||
* A queue or wire receiver may establish this boundary only after validating
|
||||
* explicit identity and resolving the exact live Agent; this method does neither.
|
||||
* Detached work remains owned by the subsystem that starts it.
|
||||
* @param agent - initiating Agent to inherit; presence is neither liveness proof nor authorization.
|
||||
* @param operation - synchronous or asynchronous operation to invoke.
|
||||
* @returns the exact value returned by `operation`.
|
||||
* @throws when the initiator scope is closing/disposed, or when `operation` throws.
|
||||
*/
|
||||
withInitiator<T>(agent: Agent, operation: () => T): T
|
||||
```
|
||||
|
||||
Run an operation with one exact Agent as its process-local initiator. The exact synchronous value or Promise returned by the operation is preserved. Custom drivers and test harnesses wrap their complete returned foreground lifetime. A queue or wire receiver may establish this boundary only after validating explicit identity and resolving the exact live Agent; this method does neither. Detached work remains owned by the subsystem that starts it.
|
||||
|
||||
- `agent` — initiating Agent to inherit; presence is neither liveness proof nor authorization.
|
||||
- `operation` — synchronous or asynchronous operation to invoke.
|
||||
|
||||
**Returns** the exact value returned by `operation`.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L288)
|
||||
|
||||
### ctx.agents.withoutInitiator(operation)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Run an operation inside a boundary that hides any inherited initiating
|
||||
* Agent. The exact synchronous value or Promise is preserved.
|
||||
* Use this while creating lazy shared timers, queue pumps, pool maintenance,
|
||||
* watchers, or exporters so they do not inherit the first Agent that happens
|
||||
* to initialize them. It clears only initiator attribution, not explicit
|
||||
* fields, and does not own or drain detached resources.
|
||||
* @param operation - synchronous or asynchronous operation to invoke without an initiator.
|
||||
* @returns the exact value returned by `operation`.
|
||||
* @throws when the initiator scope is closing/disposed, or when `operation` throws.
|
||||
*/
|
||||
withoutInitiator<T>(operation: () => T): T
|
||||
```
|
||||
|
||||
Run an operation inside a boundary that hides any inherited initiating Agent. The exact synchronous value or Promise is preserved. Use this while creating lazy shared timers, queue pumps, pool maintenance, watchers, or exporters so they do not inherit the first Agent that happens to initialize them. It clears only initiator attribution, not explicit fields, and does not own or drain detached resources.
|
||||
|
||||
- `operation` — synchronous or asynchronous operation to invoke without an initiator.
|
||||
|
||||
**Returns** the exact value returned by `operation`.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L303)
|
||||
|
||||
### ctx.agents.setFactory(factory)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Register the agent-creation factory (the loop calls this on construction,
|
||||
* effect-scoped). A traced Cordis service is canonicalized to its concrete
|
||||
* target; each create/resume call is then traced through that caller's
|
||||
* context so ownership follows the caller without stacking proxy layers.
|
||||
* Throws if a factory is already registered. Returns the disposer; on
|
||||
* dispose the factory slot is cleared.
|
||||
* @param factory - the loop-owned factory {@link create}/{@link resume} delegate to.
|
||||
* @returns the disposer that clears the factory slot. The exact
|
||||
* Cordis effect disposer (single-shot): composite (generator) effects may
|
||||
* yield it directly — exact identity nests the teardown in order.
|
||||
*/
|
||||
setFactory(factory: AgentFactory): () => void
|
||||
```
|
||||
|
||||
Register the agent-creation factory (the loop calls this on construction, effect-scoped). A traced Cordis service is canonicalized to its concrete target; each create/resume call is then traced through that caller's context so ownership follows the caller without stacking proxy layers. Throws if a factory is already registered. Returns the disposer; on dispose the factory slot is cleared.
|
||||
|
||||
- `factory` — the loop-owned factory `create`/`resume` delegate to.
|
||||
|
||||
**Returns** the disposer that clears the factory slot. The exact Cordis effect disposer (single-shot): composite (generator) effects may yield it directly — exact identity nests the teardown in order.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L319)
|
||||
|
||||
### ctx.agents.create(options)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Create and publish a new agent through the registered factory.
|
||||
* Distinct from {@link register} (which records an already-constructed
|
||||
* agent): this constructs the agent and its session. Rejects if no factory is
|
||||
* registered or creation/setup fails. The resolved {@link AgentHandle} lets
|
||||
* the owner tear down exactly this agent.
|
||||
* @param options - shared identity, session seed/metadata, and agent options.
|
||||
* @returns the handle after setup, rollback-covered publication, and loop start complete.
|
||||
*/
|
||||
async create(options: CreateAgentOptions): Promise<AgentHandle>
|
||||
```
|
||||
|
||||
Create and publish a new agent through the registered factory. Distinct from register (which records an already-constructed agent): this constructs the agent and its session. Rejects if no factory is registered or creation/setup fails. The resolved AgentHandle lets the owner tear down exactly this agent.
|
||||
|
||||
- `options` — shared identity, session seed/metadata, and agent options.
|
||||
|
||||
**Returns** the handle after setup, rollback-covered publication, and loop start complete.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L352)
|
||||
|
||||
### ctx.agents.resume(options)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Load a persisted session and resume an agent on it through the registered
|
||||
* factory. Rejects if no factory is registered; the factory rejects if
|
||||
* session persistence is not configured or persistence/setup fails.
|
||||
* @param options - persisted identity, configuration, and optional setup.
|
||||
* @returns the handle after setup, rollback-covered publication, and loop start complete.
|
||||
*/
|
||||
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 or persistence/setup fails.
|
||||
|
||||
- `options` — persisted identity, configuration, and optional setup.
|
||||
|
||||
**Returns** the handle after setup, rollback-covered publication, and loop start complete.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L371)
|
||||
|
||||
### ctx.agents.register(agent)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* 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 — both with the agent's scope carrier
|
||||
* (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the
|
||||
* emits are scope-filtered regardless of which context invoked `register`
|
||||
* (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always
|
||||
* requires passing the carrier). Returns the disposer.
|
||||
* @param agent - the already-constructed agent to record in the store.
|
||||
* @returns the EXACT Cordis effect disposer (single-shot; a repeat call
|
||||
* returns undefined without awaiting an in-flight teardown). Exact
|
||||
* identity is load-bearing: a composite (generator) effect that owns a
|
||||
* teardown ORDER — the agent factory's lifecycle chain — must yield THIS
|
||||
* function so Cordis nests the unregistration at that yield position;
|
||||
* yielding a wrapper would leave it disposing as a concurrent sibling on
|
||||
* owner unload, unregistering the agent (and emitting `agent/disposed`)
|
||||
* while its final turn is still draining.
|
||||
*/
|
||||
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 — both with the agent's scope carrier (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the emits are scope-filtered regardless of which context invoked `register` (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always requires passing the carrier). Returns the disposer.
|
||||
|
||||
- `agent` — the already-constructed agent to record in the store.
|
||||
|
||||
**Returns** the EXACT Cordis effect disposer (single-shot; a repeat call returns undefined without awaiting an in-flight teardown). Exact identity is load-bearing: a composite (generator) effect that owns a teardown ORDER — the agent factory's lifecycle chain — must yield THIS function so Cordis nests the unregistration at that yield position; yielding a wrapper would leave it disposing as a concurrent sibling on owner unload, unregistering the agent (and emitting `agent/disposed`) while its final turn is still draining.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L397)
|
||||
|
||||
### ctx.agents.enter(agent, owner)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Insert an already-constructed agent without announcing it. This is the
|
||||
* advanced ordered-lifecycle primitive used by the async agent factory: it
|
||||
* first completes setup while the agent is unpublished, then assigns the
|
||||
* returned detach closure into its pre-installed composite teardown before
|
||||
* calling {@link announce}. Ordinary callers use {@link register}.
|
||||
* @param agent - the prepared, unpublished agent.
|
||||
* @param owner - live agent whose scoped context created this agent, or
|
||||
* undefined for a top-level runtime root. This is runtime ownership, not
|
||||
* the resumed session's durable parent lineage.
|
||||
* @returns an idempotent closure that removes this exact entry and emits
|
||||
* `agent/disposed` with listener failures contained. When called from a
|
||||
* synchronous `agent/created` listener, removal and disposal wait until
|
||||
* that creation dispatch unwinds.
|
||||
*/
|
||||
enter(agent: Agent, owner: Agent | undefined): () => void
|
||||
```
|
||||
|
||||
Insert an already-constructed agent without announcing it. This is the advanced ordered-lifecycle primitive used by the async agent factory: it first completes setup while the agent is unpublished, then assigns the returned detach closure into its pre-installed composite teardown before calling announce. Ordinary callers use register.
|
||||
|
||||
- `agent` — the prepared, unpublished agent.
|
||||
- `owner` — live agent whose scoped context created this agent, or undefined for a top-level runtime root. This is runtime ownership, not the resumed session's durable parent lineage.
|
||||
|
||||
**Returns** an idempotent closure that removes this exact entry and emits `agent/disposed` with listener failures contained. When called from a synchronous `agent/created` listener, removal and disposal wait until that creation dispatch unwinds.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L421)
|
||||
|
||||
### ctx.agents.announce(agent)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Announce an agent previously inserted with {@link enter}.
|
||||
* @param agent - the live inserted agent to announce.
|
||||
* @throws if `agent` is not the exact live registry entry for its id, or its
|
||||
* creation announcement already began (including a reentrant call from a
|
||||
* creation listener).
|
||||
*/
|
||||
announce(agent: Agent): void
|
||||
```
|
||||
|
||||
Announce an agent previously inserted with enter.
|
||||
|
||||
- `agent` — the live inserted agent to announce.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L496)
|
||||
|
||||
### ctx.agents.get(id)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Look up a live agent.
|
||||
* @param id - the shared agent/session id to look up.
|
||||
* @returns the agent, or undefined when no live agent has that id.
|
||||
*/
|
||||
get(id: SessionId): Agent | undefined
|
||||
```
|
||||
|
||||
Look up a live agent.
|
||||
|
||||
- `id` — the shared agent/session 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#L530)
|
||||
|
||||
### ctx.agents.isOwnedBy(id, owner)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Test whether a live agent was created through one exact parent agent's
|
||||
* scoped context. Runtime ownership is independent of durable session
|
||||
* lineage and remains unambiguous when unrelated providers reuse an id.
|
||||
* @param id - the candidate child agent's shared agent/session id.
|
||||
* @param owner - the expected runtime creator agent.
|
||||
* @returns true only while the exact child entry is live under that owner.
|
||||
*/
|
||||
isOwnedBy(id: SessionId, owner: Agent): boolean
|
||||
```
|
||||
|
||||
Test whether a live agent was created through one exact parent agent's scoped context. Runtime ownership is independent of durable session lineage and remains unambiguous when unrelated providers reuse an id.
|
||||
|
||||
- `id` — the candidate child agent's shared agent/session id.
|
||||
- `owner` — the expected runtime creator agent.
|
||||
|
||||
**Returns** true only while the exact child entry is live under that owner.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L542)
|
||||
|
||||
### ctx.agents.list()
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* All live agents, in registration order.
|
||||
* @returns a fresh array; mutating it does not affect the registry.
|
||||
*/
|
||||
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#L550)
|
||||
|
||||
### ctx.agents.roots()
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* All live top-level agents in registration order. A top-level agent was
|
||||
* created without an owning agent context; durable session lineage does not
|
||||
* affect this runtime relation, so a resumed fork may still be a root.
|
||||
* @returns a fresh array; mutating it does not affect the registry.
|
||||
*/
|
||||
roots(): Agent[]
|
||||
```
|
||||
|
||||
All live top-level agents in registration order. A top-level agent was created without an owning agent context; durable session lineage does not affect this runtime relation, so a resumed fork may still be a root.
|
||||
|
||||
**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#L560)
|
||||
@@ -1,41 +0,0 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
|
||||
# ctx.approval
|
||||
|
||||
`ApprovalService` — provided by `@deepseek-ai/dsh-user-approval`.
|
||||
|
||||
Approval service that applies session policy before answerers and logs every ask/outcome pair to the requesting session. It exposes deterministic policy changes to the model through prompt and pre-step notices.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/user-approval/src/index.ts#L229)
|
||||
|
||||
### ctx.approval.request(req)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Ask the composed answerers to decide one readonly same-process request.
|
||||
* The service borrows the request, agent, session, and live signal directly.
|
||||
* The request requires an open turn because the audit pair must be enclosed
|
||||
* by the durable log's commit/replay boundary; an idle ask rejects before
|
||||
* appending anything. The answerer phase always produces an outcome: an
|
||||
* aborted signal yields `'cancelled'`, a missing or throwing answerer yields
|
||||
* `'unavailable'` (fail closed), and a rogue non-vocabulary return value is
|
||||
* normalized to `'unavailable'`. A failure that prevents either audit append
|
||||
* from committing still rejects because returning an unlogged decision would
|
||||
* violate the pair. Session contains post-commit observer failures, so an
|
||||
* authoritative append cannot reject the request or suppress its matching
|
||||
* audit event.
|
||||
* @param req - the pending decision (agent, tool identity, reason, signal).
|
||||
* @returns the closed outcome; `'allowed-once'` is the only grant.
|
||||
* @throws when no turn is open or either audit event fails before the session
|
||||
* append commit point.
|
||||
*/
|
||||
async request(req: ApprovalRequest): Promise<ApprovalOutcome>
|
||||
```
|
||||
|
||||
Ask the composed answerers to decide one readonly same-process request. The service borrows the request, agent, session, and live signal directly. The request requires an open turn because the audit pair must be enclosed by the durable log's commit/replay boundary; an idle ask rejects before appending anything. The answerer phase always produces an outcome: an aborted signal yields `'cancelled'`, a missing or throwing answerer yields `'unavailable'` (fail closed), and a rogue non-vocabulary return value is normalized to `'unavailable'`. A failure that prevents either audit append from committing still rejects because returning an unlogged decision would violate the pair. Session contains post-commit observer failures, so an authoritative append cannot reject the request or suppress its matching audit event.
|
||||
|
||||
- `req` — the pending decision (agent, tool identity, reason, signal).
|
||||
|
||||
**Returns** the closed outcome; `'allowed-once'` is the only grant.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/user-approval/src/index.ts#L313)
|
||||
@@ -1,64 +0,0 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
|
||||
# ctx.bashEnv
|
||||
|
||||
`BashEnvRegistry` — provided by `@deepseek-ai/dsh-tool-bash`.
|
||||
|
||||
Registry (`ctx.bashEnv`) for trusted, per-execution `DSH_*` variables. The namespace is rebuilt for every model bash call: ambient `DSH_*` values are discarded by the executor, then the registry's current snapshot is injected. Built-in shell facts remain owned by the registry itself while plugins can register additional, enumerable facts with effect-scoped disposal.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/tool-bash/src/index.ts#L103)
|
||||
|
||||
### ctx.bashEnv.register(contributor)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Register one environment contributor. Names and keys are unique; built-in
|
||||
* keys are reserved. Registration is disposed with the calling plugin fiber.
|
||||
* @param contributor - declared key ownership and per-execution resolver.
|
||||
* @returns the disposer that unregisters the contribution.
|
||||
*/
|
||||
register(contributor: BashEnvContributor): () => void
|
||||
```
|
||||
|
||||
Register one environment contributor. Names and keys are unique; built-in keys are reserved. Registration is disposed with the calling plugin fiber.
|
||||
|
||||
- `contributor` — declared key ownership and per-execution resolver.
|
||||
|
||||
**Returns** the disposer that unregisters the contribution.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/tool-bash/src/index.ts#L124)
|
||||
|
||||
### ctx.bashEnv.collect(execution)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Build the trusted `DSH_*` snapshot for one bash tool execution.
|
||||
* @param execution - the current tool execution.
|
||||
* @returns an immutable environment overlay containing built-ins and current contributions.
|
||||
*/
|
||||
collect(execution: ToolExecution): DshEnvironment
|
||||
```
|
||||
|
||||
Build the trusted `DSH_*` snapshot for one bash tool execution.
|
||||
|
||||
- `execution` — the current tool execution.
|
||||
|
||||
**Returns** an immutable environment overlay containing built-ins and current contributions.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/tool-bash/src/index.ts#L166)
|
||||
|
||||
### ctx.bashEnv.list()
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Enumerate plugin-contributed variables without executing their resolvers.
|
||||
* @returns declarations sorted by environment variable name.
|
||||
*/
|
||||
list(): BashEnvVariableInfo[]
|
||||
```
|
||||
|
||||
Enumerate plugin-contributed variables without executing their resolvers.
|
||||
|
||||
**Returns** declarations sorted by environment variable name.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/tool-bash/src/index.ts#L198)
|
||||
@@ -1,88 +0,0 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
|
||||
# ctx.bash
|
||||
|
||||
`BashExecutor` (abstract seam) — provided by `@deepseek-ai/dsh-bash`.
|
||||
|
||||
Abstract bash execution service. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.bash` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior).
|
||||
Implementations must honor these semantics:
|
||||
- run rejects only for infrastructure failures. Nonzero exits, timeout kills, and abort kills resolve with a BashRunResult.
|
||||
- start returns immediately; no timeout applies to background processes. `done` settles at process close and never rejects; spawn failures settle as `killed` with the error on stderr.
|
||||
- BashProcess.readOutput is incremental: consecutive reads never repeat output. Lossy reads report truncation and available spill files.
|
||||
- Disposal kills all running background processes and awaits their exit.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L48)
|
||||
|
||||
### ctx.bash.sandboxMode
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* The sandbox mode this executor applies by default, or `undefined` when it
|
||||
* does not sandbox commands.
|
||||
* @returns the configured default sandbox mode, when supported.
|
||||
*/
|
||||
get sandboxMode(): SandboxMode | undefined
|
||||
```
|
||||
|
||||
The sandbox mode this executor applies by default, or `undefined` when it does not sandbox commands.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L58)
|
||||
|
||||
### ctx.bash.resolve(request)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Apply implementation-owned defaults and caps to a request before execution.
|
||||
* @param 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}.
|
||||
*/
|
||||
abstract resolve(request: BashExecRequest): BashExecSpec
|
||||
```
|
||||
|
||||
Apply implementation-owned defaults and caps to a request before execution.
|
||||
|
||||
- `request` — the caller's request; omitted fields get this implementation's defaults, capped fields are clamped.
|
||||
|
||||
**Returns** the fully-specified spec to hand to `run`/`start`.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L68)
|
||||
|
||||
### ctx.bash.run(spec)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Run a command in the foreground; resolves when it finishes.
|
||||
* @param 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.
|
||||
*/
|
||||
abstract run(spec: BashExecSpec): Promise<BashRunResult>
|
||||
```
|
||||
|
||||
Run a command in the foreground; resolves when it finishes.
|
||||
|
||||
- `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.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L76)
|
||||
|
||||
### ctx.bash.start(spec)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Start a background process and return its handle immediately.
|
||||
* @param spec - a resolved spec from {@link resolve}, never a raw request.
|
||||
* @returns the live process handle (reads, kill, quiescence promise).
|
||||
*/
|
||||
abstract start(spec: BashExecSpec): BashProcess
|
||||
```
|
||||
|
||||
Start a background process and return its handle immediately.
|
||||
|
||||
- `spec` — a resolved spec from `resolve`, never a raw request.
|
||||
|
||||
**Returns** the live process handle (reads, kill, quiescence promise).
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L83)
|
||||
@@ -1,65 +0,0 @@
|
||||
<!-- 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`.
|
||||
|
||||
Registers one `ctx.codeRuntime` implementation. Program, budget, abort, and substrate failures resolve in CodeRunResult; only seam misuse rejects. Implementations bridge structured-cloneable bindings while treating programs as hostile peers, isolate runs from one another, and terminate and await in-flight runs during disposal.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/code-runtime/code-runtime/src/index.ts#L30)
|
||||
|
||||
### ctx.codeRuntime.language
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* The source language {@link 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'`.
|
||||
*/
|
||||
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#L38)
|
||||
|
||||
### ctx.codeRuntime.isolation
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* 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'`.
|
||||
*/
|
||||
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#L46)
|
||||
|
||||
### ctx.codeRuntime.run(request)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* 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).
|
||||
* @param 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).
|
||||
*/
|
||||
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#L61)
|
||||
@@ -1,71 +0,0 @@
|
||||
<!-- 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. Implementations own trigger policy, retention, and summarization, and may consume a separate measurement service. A successful run replaces the selected surface span with one summary node and prevents concurrent compaction of the same session. Load one implementation per context as `ctx.compact`.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact/src/index.ts#L40)
|
||||
|
||||
### ctx.compact.compactIfNeeded(agent, trigger, signal)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Consider automatic compaction for one explicit trigger. Pressure policy
|
||||
* uses the latest durable routed request, while context-overflow policy may
|
||||
* force a useful balanced reduction even below the normal threshold. Return
|
||||
* `null` when no safe range can be compacted. A single oversized retained
|
||||
* unit or request envelope cannot be repaired through surface compaction.
|
||||
*
|
||||
* @param agent - agent context owning the session surface and routing options.
|
||||
* @param trigger - normal pressure or provider-confirmed context overflow.
|
||||
* @param signal - cancellation signal; model-backed implementations must forward it.
|
||||
* @returns the compaction result, or `null` if no compaction was needed.
|
||||
*/
|
||||
abstract compactIfNeeded( agent: CompactAgentContext, trigger: CompactionTrigger, signal: AbortSignal, ): Promise<CompactionResult | null>
|
||||
```
|
||||
|
||||
Consider automatic compaction for one explicit trigger. Pressure policy uses the latest durable routed request, while context-overflow policy may force a useful balanced reduction even below the normal threshold. Return `null` when no safe range can be compacted. A single oversized retained unit or request envelope cannot be repaired through surface compaction.
|
||||
|
||||
- `agent` — agent context owning the session surface and routing options.
|
||||
- `trigger` — normal pressure or provider-confirmed context overflow.
|
||||
- `signal` — cancellation signal; model-backed implementations must forward it.
|
||||
|
||||
**Returns** the compaction result, or `null` if no compaction was needed.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact/src/index.ts#L57)
|
||||
|
||||
### ctx.compact.compactRegion(start, end, agent, signal?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Forcibly compact a range of surface nodes into a single summary node.
|
||||
* `start` and `end` name an inclusive span by surface position, not numeric seq
|
||||
* order; replacements can make visible seqs non-monotonic. Both edges must be
|
||||
* balanced so assistant tool calls remain paired with their results. A model-
|
||||
* backed implementation forwards cancellation and rejects active, missing,
|
||||
* reversed, or unbalanced ranges. The target session is `agent.session`.
|
||||
* Use {@link toolPairingBalancedBefore} and {@link toolPairingBalancedAfter}
|
||||
* for the edge checks.
|
||||
*
|
||||
* @param start - first surface seq, inclusive.
|
||||
* @param end - last surface seq, inclusive.
|
||||
* @param agent - context whose session is mutated and whose routing options guide summarization.
|
||||
* @param signal - optional cancellation; model-backed implementations must forward it.
|
||||
* @throws when compaction is active or the range is missing, reversed, or unbalanced.
|
||||
* @returns the appended event seqs, summary, replaced range, and token accounting.
|
||||
*/
|
||||
abstract compactRegion( 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` name an inclusive span by surface position, not numeric seq order; replacements can make visible seqs non-monotonic. Both edges must be balanced so assistant tool calls remain paired with their results. A model- backed implementation forwards cancellation and rejects active, missing, reversed, or unbalanced ranges. The target session is `agent.session`. Use toolPairingBalancedBefore and toolPairingBalancedAfter for the edge checks.
|
||||
|
||||
- `start` — first surface seq, inclusive.
|
||||
- `end` — last surface seq, inclusive.
|
||||
- `agent` — context whose session is mutated and whose routing options guide summarization.
|
||||
- `signal` — optional cancellation; model-backed implementations must forward it.
|
||||
|
||||
**Returns** the appended event seqs, summary, replaced range, and token accounting.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact/src/index.ts#L80)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,236 +0,0 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
|
||||
# ctx.fs
|
||||
|
||||
`FileSystem` (abstract seam) — provided by `@deepseek-ai/dsh-fs`.
|
||||
|
||||
Abstract filesystem provider. Targets must preserve identity across aliases; reads expose regular UTF-8 text or typed errors, listings are stable and content-free, and mutations are atomic. Optional guards add stale protection without changing the unguarded provider contract.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L81)
|
||||
|
||||
### ctx.fs.sandboxMode
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
/**
|
||||
* The sandbox mode this backend enforces on mutations BY DEFAULT, or
|
||||
* `undefined` when it does not confine at all — the capability fact the tool
|
||||
* layer reads to advertise the escalation fields honestly (mirrors
|
||||
* `BashExecutor.sandboxMode`). The base class and the bare local backend
|
||||
* report `undefined`; a sandboxing backend (`@deepseek-ai/dsh-fs-sandbox`)
|
||||
* overrides it with the deployment default. A session override may make the
|
||||
* effective mode narrower or wider, so strict escalation widening is checked
|
||||
* per call rather than encoded in this default-relative fact.
|
||||
* @returns the configured default mode of a sandboxing backend; `undefined`
|
||||
* for a backend that never confines.
|
||||
*/
|
||||
get sandboxMode(): SandboxMode | undefined
|
||||
```
|
||||
|
||||
/** The sandbox mode this backend enforces on mutations BY DEFAULT, or `undefined` when it does not confine at all — the capability fact the tool layer reads to advertise the escalation fields honestly (mirrors `BashExecutor.sandboxMode`). The base class and the bare local backend report `undefined`; a sandboxing backend (`@deepseek-ai/dsh-fs-sandbox`) overrides it with the deployment default. A session override may make the effective mode narrower or wider, so strict escalation widening is checked per call rather than encoded in this default-relative fact.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L99)
|
||||
|
||||
### ctx.fs.resolve(path, opts?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Resolve a model/plugin-supplied path into a stable {@link 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.
|
||||
*
|
||||
* @param path - the path to resolve; relative paths resolve against `opts.cwd`.
|
||||
* @param opts - optional cwd override and cancellation signal.
|
||||
* @returns the stable target; the same file yields the same `targetKey`.
|
||||
*/
|
||||
abstract resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<FsTarget>
|
||||
```
|
||||
|
||||
Resolve a model/plugin-supplied path into a stable FsTarget. May perform I/O (a remote/sandboxed backend may need a round-trip to map a path to a stable identity), hence async even though the local backend only normalizes + realpaths.
|
||||
|
||||
- `path` — the path to resolve; relative paths resolve against `opts.cwd`.
|
||||
- `opts` — optional cwd override and cancellation signal.
|
||||
|
||||
**Returns** the stable target; the same file yields the same `targetKey`.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L112)
|
||||
|
||||
### ctx.fs.stat(target, signal?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Return target metadata, or `undefined` when the target does not exist.
|
||||
* @param target - the resolved target to stat.
|
||||
* @param signal - aborts the metadata round-trip.
|
||||
* @returns metadata only, never content; undefined for an absent target.
|
||||
*/
|
||||
abstract stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined>
|
||||
```
|
||||
|
||||
Return target metadata, or `undefined` when the target does not exist.
|
||||
|
||||
- `target` — the resolved target to stat.
|
||||
- `signal` — aborts the metadata round-trip.
|
||||
|
||||
**Returns** metadata only, never content; undefined for an absent target.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L120)
|
||||
|
||||
### ctx.fs.lstat(path, opts?, signal?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Return path metadata without following the final path component when it is a
|
||||
* symbolic link. This is intentionally path-shaped, not target-shaped:
|
||||
* {@link resolve} follows symlinks to produce the stable identity used by
|
||||
* normal reads/writes, while `lstat` lets a consumer reject the path itself
|
||||
* before that follow happens.
|
||||
*
|
||||
* `opts.cwd` follows {@link resolve}'s cwd rules. `undefined` means the path is
|
||||
* absent.
|
||||
* @param path - the path to inspect; relative paths resolve against `opts.cwd`.
|
||||
* @param opts - `cwd` overrides the backend's default base for relative paths.
|
||||
* @param signal - aborts the metadata round-trip.
|
||||
* @returns metadata only, never content; undefined for an absent path.
|
||||
*/
|
||||
abstract lstat(path: string, opts?: { cwd?: string }, signal?: AbortSignal): Promise<FsPathInfo | undefined>
|
||||
```
|
||||
|
||||
Return path metadata without following the final path component when it is a symbolic link. This is intentionally path-shaped, not target-shaped: resolve follows symlinks to produce the stable identity used by normal reads/writes, while `lstat` lets a consumer reject the path itself before that follow happens.
|
||||
`opts.cwd` follows resolve's cwd rules. `undefined` means the path is absent.
|
||||
|
||||
- `path` — the path to inspect; relative paths resolve against `opts.cwd`.
|
||||
- `opts` — `cwd` overrides the backend's default base for relative paths.
|
||||
- `signal` — aborts the metadata round-trip.
|
||||
|
||||
**Returns** metadata only, never content; undefined for an absent path.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L136)
|
||||
|
||||
### ctx.fs.readText(target, signal?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Read the whole regular text file as a single decoded string.
|
||||
* @param target - the resolved target to read.
|
||||
* @param signal - aborts the read.
|
||||
* @returns the full decoded UTF-8 content.
|
||||
*/
|
||||
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#L144)
|
||||
|
||||
### ctx.fs.streamText(target, signal?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Stream the whole regular text file as decoded text chunks (same text
|
||||
* semantics as {@link readText}, for large files). The backend owns
|
||||
* cross-chunk UTF-8 decoding and binary rejection so the policy layer never
|
||||
* touches raw bytes.
|
||||
* @param target - the resolved target to read.
|
||||
* @param signal - aborts the stream, including between chunks.
|
||||
* @returns the chunk iterable, decoded and validated like {@link readText}.
|
||||
*/
|
||||
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 `readText`.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L155)
|
||||
|
||||
### ctx.fs.listDir(target, signal?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* List direct children of a directory in stable name order. Returns resolved
|
||||
* child targets plus cheap metadata only; never reads file contents.
|
||||
* @param target - the resolved directory target.
|
||||
* @param signal - aborts the listing.
|
||||
* @returns one entry per direct child, in stable name order.
|
||||
*/
|
||||
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#L164)
|
||||
|
||||
### ctx.fs.writeText(target, content, expected?, signal?, sandboxMode?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Atomically create or replace UTF-8 text. `expected` guards intent and
|
||||
* staleness; omission allows unconditional overwrite.
|
||||
* @param target - the resolved target to write.
|
||||
* @param content - the full new file content.
|
||||
* @param expected - the write intent guarding the write; omit for unconditional.
|
||||
* @param signal - aborts before the atomic rename takes effect.
|
||||
* @param sandboxMode - the per-call sandbox mode this write runs under; a
|
||||
* sandboxing backend fences the write by it, the bare backend ignores it.
|
||||
* Omit to leave the backend its own default.
|
||||
* @returns the outcome, including the version the write produced.
|
||||
*/
|
||||
abstract writeText( target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal, sandboxMode?: SandboxMode, ): Promise<FsWriteOutcome>
|
||||
```
|
||||
|
||||
Atomically create or replace UTF-8 text. `expected` guards intent and staleness; omission allows unconditional overwrite.
|
||||
|
||||
- `target` — the resolved target to write.
|
||||
- `content` — the full new file content.
|
||||
- `expected` — the write intent guarding the write; omit for unconditional.
|
||||
- `signal` — aborts before the atomic rename takes effect.
|
||||
- `sandboxMode` — the per-call sandbox mode this write runs under; a sandboxing backend fences the write by it, the bare backend ignores it. Omit to leave the backend its own default.
|
||||
|
||||
**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#L178)
|
||||
|
||||
### ctx.fs.editText(target, edit, expected?, signal?, sandboxMode?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Atomically edit literal text. When supplied, the version guard is checked
|
||||
* before matching so stale content reports `FS_STALE_VERSION`; omission edits
|
||||
* the current content without a freshness precondition.
|
||||
* @param target - the resolved target to edit.
|
||||
* @param edit - the literal search/replace request.
|
||||
* @param expected - the version guard; omit for an unconditional edit.
|
||||
* @param signal - aborts before the atomic rename takes effect.
|
||||
* @param sandboxMode - the per-call sandbox mode this edit runs under; a
|
||||
* sandboxing backend fences the edit by it, the bare backend ignores it.
|
||||
* Omit to leave the backend its own default.
|
||||
* @returns the outcome, including the version the edit produced.
|
||||
*/
|
||||
abstract editText( target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal, sandboxMode?: SandboxMode, ): Promise<FsEditOutcome>
|
||||
```
|
||||
|
||||
Atomically edit literal text. When supplied, the version guard is checked before matching so stale content reports `FS_STALE_VERSION`; omission edits the current content without a freshness precondition.
|
||||
|
||||
- `target` — the resolved target to edit.
|
||||
- `edit` — the literal search/replace request.
|
||||
- `expected` — the version guard; omit for an unconditional edit.
|
||||
- `signal` — aborts before the atomic rename takes effect.
|
||||
- `sandboxMode` — the per-call sandbox mode this edit runs under; a sandboxing backend fences the edit by it, the bare backend ignores it. Omit to leave the backend its own default.
|
||||
|
||||
**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#L199)
|
||||
@@ -1,94 +0,0 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
|
||||
# ctx.llm
|
||||
|
||||
`LlmService` — provided by `@deepseek-ai/dsh-llm`.
|
||||
|
||||
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#L97)
|
||||
|
||||
### ctx.llm.registerAdapter(providers, adapter)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Register an adapter for the given provider routes. Throws `LlmError` with code
|
||||
* `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing).
|
||||
* Disposed with the fiber.
|
||||
* @param providers - every provider route this adapter should serve.
|
||||
* @param adapter - the adapter that streams calls for those providers.
|
||||
* @returns the disposer that unregisters all of them.
|
||||
*/
|
||||
registerAdapter(providers: string[], adapter: LlmAdapter): () => void
|
||||
```
|
||||
|
||||
Register an adapter for the given provider routes. Throws `LlmError` with code `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing). Disposed with the fiber.
|
||||
|
||||
- `providers` — every provider route this adapter should serve.
|
||||
- `adapter` — the adapter that streams calls for those providers.
|
||||
|
||||
**Returns** the disposer that unregisters all of them.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L112)
|
||||
|
||||
### ctx.llm.listProviders()
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Describe provider routes with a registered adapter.
|
||||
* @returns detached provider metadata in registration order.
|
||||
*/
|
||||
listProviders(): LlmProviderInfo[]
|
||||
```
|
||||
|
||||
Describe provider routes with a registered adapter.
|
||||
|
||||
**Returns** detached provider metadata in registration order.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L143)
|
||||
|
||||
### ctx.llm.listModels(provider)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Discover models advertised by one registered provider. Catalog membership
|
||||
* is advisory and never changes routing or request validation.
|
||||
* @param provider - registered provider route to inspect.
|
||||
* @returns detached model metadata in adapter-preferred order.
|
||||
*/
|
||||
async listModels(provider: string): Promise<LlmModelInfo[]>
|
||||
```
|
||||
|
||||
Discover models advertised by one registered provider. Catalog membership is advisory and never changes routing or request validation.
|
||||
|
||||
- `provider` — registered provider route to inspect.
|
||||
|
||||
**Returns** detached model metadata in adapter-preferred order.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L153)
|
||||
|
||||
### ctx.llm.stream(options)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Stream one model call as raw chunks (token-level deltas). Throws
|
||||
* `LlmError` with code `NO_ADAPTER` if no adapter is registered for
|
||||
* `options.provider`. Replay state is retained only when the same adapter
|
||||
* instance owns its historical provider and the target provider. Final
|
||||
* adapter selection, dispatch, and iteration failures retain their original
|
||||
* Error identity and are tagged in a call-local scope for narrow agent-loop
|
||||
* request recovery; middleware and nested-call failures remain untagged for
|
||||
* the outer call.
|
||||
* @param options - the full request; `options.provider` selects the adapter.
|
||||
* @returns the chunk stream, possibly wrapped by `llm/stream` listeners.
|
||||
*/
|
||||
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.provider`. Replay state is retained only when the same adapter instance owns its historical provider and the target provider. Final adapter selection, dispatch, and iteration failures retain their original Error identity and are tagged in a call-local scope for narrow agent-loop request recovery; middleware and nested-call failures remain untagged for the outer call.
|
||||
|
||||
- `options` — the full request; `options.provider` selects the adapter.
|
||||
|
||||
**Returns** the chunk stream, possibly wrapped by `llm/stream` listeners.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L264)
|
||||
@@ -1,104 +0,0 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
|
||||
# ctx.permission
|
||||
|
||||
`PermissionService` — provided by `@deepseek-ai/dsh-permission`.
|
||||
|
||||
Owns the deployment's permission presets and their write path. Requires a confining `ctx.bash` executor and `ctx.approval`; unmatched knob values are reported as CUSTOM_PRESET, not an error.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/permission/src/index.ts#L97)
|
||||
|
||||
### ctx.permission.names
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* The advertised preset names, in the preset table's declaration order.
|
||||
* @returns every switchable preset name.
|
||||
*/
|
||||
get names(): readonly string[]
|
||||
```
|
||||
|
||||
The advertised preset names, in the preset table's declaration order.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/permission/src/index.ts#L137)
|
||||
|
||||
### ctx.permission.current(events)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Resolve the preset matching the effective knob values. A still-matching
|
||||
* last selection wins shared-bundle ties; otherwise the first table match
|
||||
* wins, or {@link CUSTOM_PRESET} when no entry matches.
|
||||
* @param events - the session's events in log order.
|
||||
* @returns the effective preset name, or `custom` when nothing matches.
|
||||
*/
|
||||
current(events: readonly SessionEvent[]): string
|
||||
```
|
||||
|
||||
Resolve the preset matching the effective knob values. A still-matching last selection wins shared-bundle ties; otherwise the first table match wins, or CUSTOM_PRESET when no entry matches.
|
||||
|
||||
- `events` — the session's events in log order.
|
||||
|
||||
**Returns** the effective preset name, or `custom` when nothing matches.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/permission/src/index.ts#L148)
|
||||
|
||||
### ctx.permission.resolve(name)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Resolve a preset's knob bundle.
|
||||
* @param name - the preset name to resolve.
|
||||
* @returns the configured bundle.
|
||||
* @throws when `name` is not in the table.
|
||||
*/
|
||||
resolve(name: string): PresetSpec
|
||||
```
|
||||
|
||||
Resolve a preset's knob bundle.
|
||||
|
||||
- `name` — the preset name to resolve.
|
||||
|
||||
**Returns** the configured bundle.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/permission/src/index.ts#L169)
|
||||
|
||||
### ctx.permission.optionOf(name)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Build the client option for a table entry or {@link CUSTOM_PRESET}. A
|
||||
* missing label falls back to the table key.
|
||||
* @param name - a table key, or `custom`.
|
||||
* @returns the option a client renders.
|
||||
* @throws when `name` is neither a table key nor `custom`.
|
||||
*/
|
||||
optionOf(name: string): PresetOption
|
||||
```
|
||||
|
||||
Build the client option for a table entry or CUSTOM_PRESET. A missing label falls back to the table key.
|
||||
|
||||
- `name` — a table key, or `custom`.
|
||||
|
||||
**Returns** the option a client renders.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/permission/src/index.ts#L184)
|
||||
|
||||
### ctx.permission.set(session, name)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Record a changed preset, then update each changed knob through its own
|
||||
* setter. Selecting the effective preset again appends nothing.
|
||||
* @param session - the session the switch belongs to.
|
||||
* @param name - the preset to switch to; unknown names throw.
|
||||
*/
|
||||
set(session: Session, name: string): void
|
||||
```
|
||||
|
||||
Record a changed preset, then update each changed knob through its own setter. Selecting the effective preset again appends nothing.
|
||||
|
||||
- `session` — the session the switch belongs to.
|
||||
- `name` — the preset to switch to; unknown names throw.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/permission/src/index.ts#L198)
|
||||
@@ -1,31 +0,0 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
|
||||
# ctx.sandboxPolicy
|
||||
|
||||
`SandboxPolicyService` — provided by `@deepseek-ai/dsh-sandbox-policy`.
|
||||
|
||||
The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode and workspace root; enforcing implementations read defaultMode and workspaceRoot, and the tool layers fold each session's `sandbox/mode` override with effectiveSandboxMode on top.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/sandbox/sandbox-policy/src/index.ts#L60)
|
||||
|
||||
### ctx.sandboxPolicy.defaultMode
|
||||
|
||||
```ts website-api
|
||||
/** The deployment default mode — the fallback beneath a session override. */
|
||||
readonly defaultMode: SandboxMode
|
||||
```
|
||||
|
||||
The deployment default mode — the fallback beneath a session override.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/sandbox/sandbox-policy/src/index.ts#L70)
|
||||
|
||||
### ctx.sandboxPolicy.workspaceRoot
|
||||
|
||||
```ts website-api
|
||||
/** The absolute `workspace-write` boundary root both families fence against. */
|
||||
readonly workspaceRoot: string
|
||||
```
|
||||
|
||||
The absolute `workspace-write` boundary root both families fence against.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/sandbox/sandbox-policy/src/index.ts#L72)
|
||||
@@ -1,35 +0,0 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
|
||||
# ctx.sandbox
|
||||
|
||||
`SandboxProvider` (abstract seam) — provided by `@deepseek-ai/dsh-sandbox`.
|
||||
|
||||
Abstract process-sandbox service. confine must return enforcing argv or fail closed at wrap or runner-execution time; silent unconfined passthrough is forbidden. Functional probes arbitrate multi-runner chains and may be skipped for a sole candidate, whose own refusal remains the fail-closed end.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/sandbox/sandbox/src/index.ts#L122)
|
||||
|
||||
### ctx.sandbox.confine(argv, policy)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Wrap `argv` so it executes confined under `policy` on this host; the
|
||||
* caller spawns the returned argv in place of its own.
|
||||
* @param argv - the exact argv the caller is about to spawn (program plus
|
||||
* arguments), NOT a shell string — a shell-shaped consumer passes
|
||||
* `['bash', '-c', command]`.
|
||||
* @param policy - the file-effect policy this execution runs under,
|
||||
* carried per call (see {@link SandboxPolicy}).
|
||||
* @returns the argv to spawn instead, plus the enforcement completeness
|
||||
* the selected backend achieves for it.
|
||||
*/
|
||||
abstract confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv
|
||||
```
|
||||
|
||||
Wrap `argv` so it executes confined under `policy` on this host; the caller spawns the returned argv in place of its own.
|
||||
|
||||
- `argv` — the exact argv the caller is about to spawn (program plus arguments), NOT a shell string — a shell-shaped consumer passes `['bash', '-c', command]`.
|
||||
- `policy` — the file-effect policy this execution runs under, carried per call (see `SandboxPolicy`).
|
||||
|
||||
**Returns** the argv to spawn instead, plus the enforcement completeness the selected backend achieves for it.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/sandbox/sandbox/src/index.ts#L138)
|
||||
@@ -1,109 +0,0 @@
|
||||
<!-- 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`.
|
||||
|
||||
Durable append-only session storage. Implementations preserve contiguous, losslessly JSON-serializable events; append resolves only after durability, and load balances a complete interrupted tail without rewriting committed events.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L42)
|
||||
|
||||
### ctx.sessionPersistence.locate(meta)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Resolve this backend's independent local artifact for a session without
|
||||
* reading, creating, flushing, or otherwise materializing it. Backends such
|
||||
* as SQLite that do not own one artifact per session return `undefined`.
|
||||
* @param meta - the immutable session header whose artifact is requested.
|
||||
* @returns the backend-specific absolute location, when one exists.
|
||||
*/
|
||||
abstract locate(meta: SessionHeader): SessionLocation | undefined
|
||||
```
|
||||
|
||||
Resolve this backend's independent local artifact for a session without reading, creating, flushing, or otherwise materializing it. Backends such as SQLite that do not own one artifact per session return `undefined`.
|
||||
|
||||
- `meta` — the immutable session header whose artifact is requested.
|
||||
|
||||
**Returns** the backend-specific absolute location, when one exists.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L54)
|
||||
|
||||
### ctx.sessionPersistence.create(meta)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Register a new session's metadata. A backend MAY defer the physical write
|
||||
* until the first {@link append} (lazy materialization), in which case a
|
||||
* created-but-never-appended session is absent from {@link list}
|
||||
* — abandoned sessions leave nothing behind.
|
||||
* @param meta - the immutable header (id, version, cwd, lineage) to record.
|
||||
*/
|
||||
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#L63)
|
||||
|
||||
### ctx.sessionPersistence.append(id, events)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* 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.
|
||||
* @param id - the session the batch belongs to.
|
||||
* @param events - the contiguous batch to persist, in seq order.
|
||||
*/
|
||||
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#L74)
|
||||
|
||||
### ctx.sessionPersistence.load(id)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Load a header and balanced contiguous log. A complete interrupted final
|
||||
* turn is preserved and durably closed with missing tool errors plus any open
|
||||
* step and turn boundaries; only a torn final record is discarded. Unknown
|
||||
* versions and corruption in the committed prefix reject.
|
||||
* @param id - the persisted session to reload.
|
||||
* @returns the header and a log ending on a balanced `turn/end`.
|
||||
*/
|
||||
abstract load(id: SessionId): Promise<{ meta: SessionHeader; events: SessionEvent[] }>
|
||||
```
|
||||
|
||||
Load a header and balanced contiguous log. A complete interrupted final turn is preserved and durably closed with missing tool errors plus any open step and turn boundaries; only a torn final record is discarded. Unknown versions and corruption in the committed prefix reject.
|
||||
|
||||
- `id` — the persisted session to reload.
|
||||
|
||||
**Returns** the header and a log ending on a balanced `turn/end`.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L84)
|
||||
|
||||
### ctx.sessionPersistence.list()
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Lightweight listing from metadata, without a full-log parse.
|
||||
* @returns one header per materialized session.
|
||||
*/
|
||||
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#L90)
|
||||
@@ -1,103 +0,0 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
|
||||
# ctx.sessionQuery
|
||||
|
||||
`SessionQueryService` — provided by `@deepseek-ai/dsh-session-query`.
|
||||
|
||||
Live-preferred logical-corpus exact-read and relationship-tracing service.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-query/session-query/src/index.ts#L38)
|
||||
|
||||
### ctx.sessionQuery.listSessions()
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* List the complete logical corpus using live-preferred records.
|
||||
* @returns deterministic newest-first cloned session records.
|
||||
*/
|
||||
listSessions(): Promise<SessionRecord[]>
|
||||
```
|
||||
|
||||
List the complete logical corpus using live-preferred records.
|
||||
|
||||
**Returns** deterministic newest-first cloned session records.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-query/session-query/src/index.ts#L63)
|
||||
|
||||
### ctx.sessionQuery.listEvents(sessionId)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* List lightweight raw-log event records for one logical session.
|
||||
* @param sessionId - live-preferred session id to read.
|
||||
* @returns event records in ascending seq order.
|
||||
*/
|
||||
async listEvents(sessionId: SessionId): Promise<SessionEventRecord[]>
|
||||
```
|
||||
|
||||
List lightweight raw-log event records for one logical session.
|
||||
|
||||
- `sessionId` — live-preferred session id to read.
|
||||
|
||||
**Returns** event records in ascending seq order.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-query/session-query/src/index.ts#L72)
|
||||
|
||||
### ctx.sessionQuery.traceSession(sessionId)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Trace known ancestry and descendants from one corpus observation.
|
||||
* @param sessionId - logical session id to trace.
|
||||
* @returns a complete lineage or an explicit unresolved parent boundary.
|
||||
* @throws when corpus resolution fails, the target is absent, or its known ancestry cycles.
|
||||
*/
|
||||
async traceSession(sessionId: SessionId): Promise<SessionLineageTrace>
|
||||
```
|
||||
|
||||
Trace known ancestry and descendants from one corpus observation.
|
||||
|
||||
- `sessionId` — logical session id to trace.
|
||||
|
||||
**Returns** a complete lineage or an explicit unresolved parent boundary.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-query/session-query/src/index.ts#L83)
|
||||
|
||||
### ctx.sessionQuery.traceEvent(request)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Trace one event's direct positional and provenance relationships.
|
||||
* @param request - target session id and event seq.
|
||||
* @returns direct links plus the target's positional replacement chain.
|
||||
* @throws when source resolution fails, the target is absent, or surface/provenance validation fails.
|
||||
*/
|
||||
async traceEvent(request: SessionEventTraceRequest): Promise<SessionEventTrace>
|
||||
```
|
||||
|
||||
Trace one event's direct positional and provenance relationships.
|
||||
|
||||
- `request` — target session id and event seq.
|
||||
|
||||
**Returns** direct links plus the target's positional replacement chain.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-query/session-query/src/index.ts#L94)
|
||||
|
||||
### ctx.sessionQuery.readEvent(request)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Read one full event plus a bounded raw-log context window.
|
||||
* @param request - target session/seq and context sizes.
|
||||
* @returns cloned target and neighboring events.
|
||||
*/
|
||||
async readEvent(request: SessionEventReadRequest): Promise<SessionEventWindow>
|
||||
```
|
||||
|
||||
Read one full event plus a bounded raw-log context window.
|
||||
|
||||
- `request` — target session/seq and context sizes.
|
||||
|
||||
**Returns** cloned target and neighboring events.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-query/session-query/src/index.ts#L104)
|
||||
@@ -1,223 +0,0 @@
|
||||
<!-- 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#L549)
|
||||
|
||||
### ctx.sessions.create(id?, options?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* 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 {@link 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 the store attachment ends), do NOT use this
|
||||
* — fold the session lifecycle into the agent's own effect via
|
||||
* {@link prepare} + {@link enter} + {@link announce} (see
|
||||
* `dsh-agent-loop`'s creation transaction).
|
||||
*
|
||||
* @param id - the session id; omitted, the store mints `session-<n>`.
|
||||
* @param options - seed events and/or creation metadata for the header.
|
||||
* @returns the live session, already entered and announced.
|
||||
* @throws if a session with `id` already exists, metadata is not a plain
|
||||
* lossless-JSON record with valid scalar fields, or `meta.cwd` is a
|
||||
* non-absolute path (storage backends key directories off it).
|
||||
*/
|
||||
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 the store attachment ends), do NOT use this — fold the session lifecycle into the agent's own effect via prepare + enter + announce (see `dsh-agent-loop`'s creation transaction).
|
||||
|
||||
- `id` — the session id; omitted, the store mints `session-<n>`.
|
||||
- `options` — seed events and/or creation metadata for the header.
|
||||
|
||||
**Returns** the live session, already entered and announced.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L578)
|
||||
|
||||
### ctx.sessions.prepare(id?, options?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Build a session WITHOUT entering it into the store — validate the id/cwd and
|
||||
* construct the {@link Session} (with its immutable {@link SessionHeader}).
|
||||
* Pairs with {@link enter} + {@link announce}: a caller that owns a composite
|
||||
* `ctx.effect` (the agent factory) folds the session lifecycle into that ONE
|
||||
* effect so a fiber unload tears the session + agent down as a single ORDERED
|
||||
* chain rather than as racing sibling effects — which would remove the publication hooks
|
||||
* before the loop's closing `session/flush`, dropping the closing events.
|
||||
*
|
||||
* @param id - the session id; omitted, the store mints `session-<n>`.
|
||||
* @param options - seed events and/or creation metadata for the header.
|
||||
* @returns the constructed session, NOT yet in the store.
|
||||
* @throws if a session with `id` already exists, metadata is not a plain
|
||||
* lossless-JSON record with valid scalar fields, or `meta.cwd` is a
|
||||
* non-absolute path.
|
||||
*/
|
||||
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 remove the publication hooks before the loop's closing `session/flush`, dropping the closing events.
|
||||
|
||||
- `id` — the session id; omitted, the store mints `session-<n>`.
|
||||
- `options` — seed events and/or creation metadata for the header.
|
||||
|
||||
**Returns** the constructed session, NOT yet in the store.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L607)
|
||||
|
||||
### ctx.sessions.enter(session)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Enter a {@link prepare}d session into the store: install the module-private
|
||||
* append publication hooks and add it to the store. Returns the DETACH
|
||||
* disposer (hooks + store removal). Does NOT emit `session/created` —
|
||||
* the caller yields this disposer inside its effect and THEN calls
|
||||
* {@link 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 {@link create} convenience and the agent factory call
|
||||
* the two back-to-back so they never trip this, but the public seam cannot
|
||||
* assume that.
|
||||
*
|
||||
* @param session - a {@link prepare}d session not yet in the store.
|
||||
* @returns the detach disposer (publication hooks + store removal). When called from
|
||||
* a synchronous `session/created` listener, removal and disposal wait until
|
||||
* that creation dispatch unwinds.
|
||||
* @throws if a session with this id is already in the store.
|
||||
*/
|
||||
enter(session: Session): () => void
|
||||
```
|
||||
|
||||
Enter a prepared session into the store: install the module-private append publication hooks and add it to the store. Returns the DETACH disposer (hooks + store removal). Does NOT emit `session/created` — the caller yields this disposer inside its effect and THEN calls announce, so a throwing `session/created` listener rolls the attach back instead of leaking it.
|
||||
Re-checks the id for a duplicate: `prepare` and `enter` are public cross-package primitives and a caller may interleave arbitrary work (or another create) between them, so a stale prepared session must NOT overwrite a live store entry of the same id — its detach disposer would later delete the REAL session. The create convenience and the agent factory call the two back-to-back so they never trip this, but the public seam cannot assume that.
|
||||
|
||||
- `session` — a `prepare`d session not yet in the store.
|
||||
|
||||
**Returns** the detach disposer (publication hooks + store removal). When called from a synchronous `session/created` listener, removal and disposal wait until that creation dispatch unwinds.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L651)
|
||||
|
||||
### ctx.sessions.announce(session)
|
||||
|
||||
```ts website-api
|
||||
/** Emit `session/created` exactly once for an {@link enter}ed session (with
|
||||
* the carrier {@link enter} captured). Separate from {@link enter} so the
|
||||
* caller can yield the detach disposer first (rollback safety — see
|
||||
* {@link enter}).
|
||||
* @param session - the entered session to announce to listeners.
|
||||
* @throws if the session is not live or its announcement already began,
|
||||
* including a reentrant call from a creation listener. */
|
||||
announce(session: Session): void
|
||||
```
|
||||
|
||||
Emit `session/created` exactly once for an entered session (with the carrier enter captured). Separate from enter so the caller can yield the detach disposer first (rollback safety — see enter).
|
||||
|
||||
- `session` — the entered session to announce to listeners.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L706)
|
||||
|
||||
### ctx.sessions.flush(session)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Dispatch the awaited `session/flush` durability checkpoint for `session`,
|
||||
* with the carrier captured at {@link enter}. THE flush entry point: the
|
||||
* store owns the carrier, so callers (the loop's turn-end checkpoint, idle
|
||||
* injection, teardown drains) must come through here rather than dispatch a
|
||||
* raw `ctx.parallel('session/flush', …)` — one owner, one spelling, and the
|
||||
* scoped-dispatch invariant can pin it.
|
||||
* @param session - the session whose buffered events must reach durable storage.
|
||||
* @returns resolves when every flush listener has settled; after all settle,
|
||||
* rejects with the first registered listener failure if any listener failed.
|
||||
*/
|
||||
async flush(session: Session): Promise<void>
|
||||
```
|
||||
|
||||
Dispatch the awaited `session/flush` durability checkpoint for `session`, with the carrier captured at enter. THE flush entry point: the store owns the carrier, so callers (the loop's turn-end checkpoint, idle injection, teardown drains) must come through here rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner, one spelling, and the scoped-dispatch invariant can pin it.
|
||||
|
||||
- `session` — the session whose buffered events must reach durable storage.
|
||||
|
||||
**Returns** resolves when every flush listener has settled; after all settle, rejects with the first registered listener failure if any listener failed.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L758)
|
||||
|
||||
### ctx.sessions.get(id)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Look up a live session.
|
||||
* @param id - the session id to look up.
|
||||
* @returns the session, or undefined when no live session has that id.
|
||||
*/
|
||||
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#L790)
|
||||
|
||||
### ctx.sessions.list()
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* All live sessions, in creation order.
|
||||
* @returns a fresh array; mutating it does not affect the store.
|
||||
*/
|
||||
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#L798)
|
||||
|
||||
### ctx.sessions.fork(source, boundary?, childSessionId?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* 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`.
|
||||
*
|
||||
* @param source - Live source session object or id.
|
||||
* @param 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.
|
||||
* @param childSessionId - Optional child session id; omitted delegates to
|
||||
* `SessionStore`'s id policy.
|
||||
* @returns The created live child session.
|
||||
*/
|
||||
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#L815)
|
||||
@@ -1,96 +0,0 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
|
||||
# ctx.skills
|
||||
|
||||
`SkillService` — provided by `@deepseek-ai/dsh-skill`.
|
||||
|
||||
Registry of skill providers. It merges provider catalogs with stable first-wins duplicate handling, exposes sorted model-visible summaries, and loads full skill bodies on demand.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/skill/skill/src/index.ts#L141)
|
||||
|
||||
### ctx.skills.registerProvider(provider)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Register a borrowed same-process provider synchronously during plugin apply. Duplicate and
|
||||
* reserved names throw; remote initialization belongs in `list()`. Fiber disposal unregisters
|
||||
* the provider and invalidates catalog caches.
|
||||
* @param provider - the provider to register by `provider.name`.
|
||||
* @returns the exact Cordis effect disposer that unregisters this provider;
|
||||
* composite effects may yield it directly to preserve teardown ordering.
|
||||
*/
|
||||
registerProvider(provider: SkillProvider): () => void
|
||||
```
|
||||
|
||||
Register a borrowed same-process provider synchronously during plugin apply. Duplicate and reserved names throw; remote initialization belongs in `list()`. Fiber disposal unregisters the provider and invalidates catalog caches.
|
||||
|
||||
- `provider` — the provider to register by `provider.name`.
|
||||
|
||||
**Returns** the exact Cordis effect disposer that unregisters this provider; composite effects may yield it directly to preserve teardown ordering.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/skill/skill/src/index.ts#L168)
|
||||
|
||||
### ctx.skills.register(skill)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Register a borrowed readonly runtime skill. Project entries outrank runtime entries, which
|
||||
* outrank user entries. Same-name runtime entries are first-wins; a duplicate logs a warning and
|
||||
* receives a no-op disposer so it cannot remove the winner.
|
||||
* @param skill - the complete skill definition to expose for discovery.
|
||||
* @returns the exact Cordis effect disposer, preserving composite teardown order and invalidating caches.
|
||||
*/
|
||||
register(skill: SkillRegistration): () => void
|
||||
```
|
||||
|
||||
Register a borrowed readonly runtime skill. Project entries outrank runtime entries, which outrank user entries. Same-name runtime entries are first-wins; a duplicate logs a warning and receives a no-op disposer so it cannot remove the winner.
|
||||
|
||||
- `skill` — the complete skill definition to expose for discovery.
|
||||
|
||||
**Returns** the exact Cordis effect disposer, preserving composite teardown order and invalidating caches.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/skill/skill/src/index.ts#L199)
|
||||
|
||||
### ctx.skills.list(options?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* List model-invocable skill summaries for a workspace. Lookup options and
|
||||
* provider candidates are readonly same-process values borrowed throughout
|
||||
* discovery.
|
||||
* @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery.
|
||||
* @returns sorted summaries, excluding skills disabled for model invocation.
|
||||
*/
|
||||
async list(options: SkillLookupOptions = {}): Promise<SkillSummary[]>
|
||||
```
|
||||
|
||||
List model-invocable skill summaries for a workspace. Lookup options and provider candidates are readonly same-process values borrowed throughout discovery.
|
||||
|
||||
- `options` — lookup options; `cwd` selects project roots and `signal` cancels discovery.
|
||||
|
||||
**Returns** sorted summaries, excluding skills disabled for model invocation.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/skill/skill/src/index.ts#L230)
|
||||
|
||||
### ctx.skills.get(name, options?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Load and validate the winning candidate, passing its opaque discovery locator back to the
|
||||
* provider. Cancellation is rechecked after selection, including cache hits, and raced against
|
||||
* loading so an uncooperative provider cannot hang the caller.
|
||||
* @param name - kebab-case skill name.
|
||||
* @param options - lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work.
|
||||
* @returns the full skill, including body content, or `undefined`.
|
||||
*/
|
||||
async get(name: string, options: SkillLookupOptions = {}): Promise<SkillDefinition | undefined>
|
||||
```
|
||||
|
||||
Load and validate the winning candidate, passing its opaque discovery locator back to the provider. Cancellation is rechecked after selection, including cache hits, and raced against loading so an uncooperative provider cannot hang the caller.
|
||||
|
||||
- `name` — kebab-case skill name.
|
||||
- `options` — lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work.
|
||||
|
||||
**Returns** the full skill, including body content, or `undefined`.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/skill/skill/src/index.ts#L246)
|
||||
@@ -1,32 +0,0 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
|
||||
# ctx.spillStore
|
||||
|
||||
`SpillStore` (abstract seam) — provided by `@deepseek-ai/dsh-spill`.
|
||||
|
||||
Abstract spill storage service. Subclass, implement saveText, and load the subclass as a plugin — it registers as `ctx.spillStore` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior).
|
||||
Semantics every implementation must honor:
|
||||
- saveText persists the FULL `content` verbatim and returns an opaque locator, exact byte length, and model-facing retrieval guidance.
|
||||
- Storage is scoped by the request's SaveTextSpill.owner session; the backend chooses a private (not world-readable) location and a collision-free name derived from — never equal to — the caller's `suggestedName`.
|
||||
- `saveText` REJECTS on a real storage failure (permissions, ENOSPC, backend unavailable); the caller decides how to degrade (the spill policy treats a rejection as best-effort and keeps the inline result).
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/spill/spill/src/index.ts#L45)
|
||||
|
||||
### ctx.spillStore.saveText(input)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Persist `input.content` to a session-scoped spill artifact.
|
||||
* @param input - the owner, provenance, suggested name, and full text to save.
|
||||
* @returns the saved artifact's {@link SpillRef}; rejects on a storage failure.
|
||||
*/
|
||||
abstract saveText(input: SaveTextSpill): Promise<SpillRef>
|
||||
```
|
||||
|
||||
Persist `input.content` to a session-scoped spill artifact.
|
||||
|
||||
- `input` — the owner, provenance, suggested name, and full text to save.
|
||||
|
||||
**Returns** the saved artifact's `SpillRef`; rejects on a storage failure.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/spill/spill/src/index.ts#L55)
|
||||
@@ -1,89 +0,0 @@
|
||||
<!-- 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`.
|
||||
|
||||
Named provider registry and capability-checked start surface.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L153)
|
||||
|
||||
### ctx.subagents.registerProvider(provider)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Register a provider under its name. Registration is effect-scoped and HMR
|
||||
* safe; removing a provider blocks new starts but does not revoke runs that
|
||||
* were already returned to their holders.
|
||||
* @param provider - the trusted provider implementation.
|
||||
* @returns the exact Cordis effect disposer.
|
||||
*/
|
||||
registerProvider(provider: SubagentProvider): () => void
|
||||
```
|
||||
|
||||
Register a provider under its name. Registration is effect-scoped and HMR safe; removing a provider blocks new starts but does not revoke runs that were already returned to their holders.
|
||||
|
||||
- `provider` — the trusted provider implementation.
|
||||
|
||||
**Returns** the exact Cordis effect disposer.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L167)
|
||||
|
||||
### ctx.subagents.getProvider(name)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Look up a provider by name.
|
||||
* @param name - the provider name.
|
||||
* @returns the provider, or undefined when absent.
|
||||
*/
|
||||
getProvider(name: string): SubagentProvider | undefined
|
||||
```
|
||||
|
||||
Look up a provider by name.
|
||||
|
||||
- `name` — the provider name.
|
||||
|
||||
**Returns** the provider, or undefined when absent.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L190)
|
||||
|
||||
### ctx.subagents.list()
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* List registered provider names in insertion order.
|
||||
* @returns the registered names.
|
||||
*/
|
||||
list(): string[]
|
||||
```
|
||||
|
||||
List registered provider names in insertion order.
|
||||
|
||||
**Returns** the registered names.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L198)
|
||||
|
||||
### ctx.subagents.start(name, request)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Establish a ready child on the named provider. Capability and semantic
|
||||
* checks run before delegation. Provider ownership lasts until its promise
|
||||
* fulfills; a rejection therefore has no run for the caller to dispose and
|
||||
* emits no run lifecycle events.
|
||||
* @param name - the provider to use.
|
||||
* @param request - child prompt, parent, signal, and optional capabilities.
|
||||
* @returns the ready holder-owned run.
|
||||
*/
|
||||
async start(name: string, request: SubagentStartRequest): Promise<SubagentRun>
|
||||
```
|
||||
|
||||
Establish a ready child on the named provider. Capability and semantic checks run before delegation. Provider ownership lasts until its promise fulfills; a rejection therefore has no run for the caller to dispose and emits no run lifecycle events.
|
||||
|
||||
- `name` — the provider to use.
|
||||
- `request` — child prompt, parent, signal, and optional capabilities.
|
||||
|
||||
**Returns** the ready holder-owned run.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L211)
|
||||
@@ -1,96 +0,0 @@
|
||||
<!-- 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 for the prompt inputs assembled before each model step.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L209)
|
||||
|
||||
### ctx.systemPrompt.section(section)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Register an ordered prompt section in the calling context's scope. A scoped
|
||||
* section shadows a global section with the same name; duplicates within one
|
||||
* layer and non-finite orders throw. Registration and disposal emit
|
||||
* `system-prompt/change`.
|
||||
* @param section - the section to register.
|
||||
* @returns the exact Cordis effect disposer.
|
||||
*/
|
||||
section(section: PromptSection): () => void
|
||||
```
|
||||
|
||||
Register an ordered prompt section in the calling context's scope. A scoped section shadows a global section with the same name; duplicates within one layer and non-finite orders throw. Registration and disposal emit `system-prompt/change`.
|
||||
|
||||
- `section` — the section to register.
|
||||
|
||||
**Returns** the exact Cordis effect disposer.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L250)
|
||||
|
||||
### ctx.systemPrompt.tools(provider)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Register a tool-schema provider in the calling context's scope. Global and
|
||||
* matching scoped providers both contribute; returning the reserved
|
||||
* {@link TOOL_ORDER_REST} name makes assembly fail.
|
||||
* @param provider - evaluated for each assembly with its context.
|
||||
* @returns the exact Cordis effect disposer.
|
||||
*/
|
||||
tools(provider: (context: AssembleContext) => ToolProviderResult): () => void
|
||||
```
|
||||
|
||||
Register a tool-schema provider in the calling context's scope. Global and matching scoped providers both contribute; returning the reserved TOOL_ORDER_REST name makes assembly fail.
|
||||
|
||||
- `provider` — evaluated for each assembly with its context.
|
||||
|
||||
**Returns** the exact Cordis effect disposer.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L291)
|
||||
|
||||
### ctx.systemPrompt.variable(name, provider)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Register a prompt variable in the calling context's scope. Scoped values
|
||||
* shadow globals; invalid or duplicate names throw. A provider may return
|
||||
* `undefined`, but rendering a section that references that value then fails.
|
||||
* @param name - the `[a-z][a-z0-9_]*` reference name.
|
||||
* @param provider - evaluated for each assembly.
|
||||
* @returns the exact Cordis effect disposer.
|
||||
*/
|
||||
variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void
|
||||
```
|
||||
|
||||
Register a prompt variable in the calling context's scope. Scoped values shadow globals; invalid or duplicate names throw. A provider may return `undefined`, but rendering a section that references that value then fails.
|
||||
|
||||
- `name` — the `[a-z][a-z0-9_]*` reference name.
|
||||
- `provider` — evaluated for each assembly.
|
||||
|
||||
**Returns** the exact Cordis effect disposer.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L325)
|
||||
|
||||
### ctx.systemPrompt.assemble(context?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Assemble global and scoped providers, detach tool parameters, apply
|
||||
* canonical ordering, then run the assembly waterfall. Scoped sections and
|
||||
* variables shadow globals; the returned waterfall value is authoritative.
|
||||
* @param context - the optional scope and plugin-defined assembly fields.
|
||||
* @returns the authoritative post-waterfall assembly.
|
||||
*/
|
||||
async assemble(context: AssembleContext = {}): Promise<PromptAssembly>
|
||||
```
|
||||
|
||||
Assemble global and scoped providers, detach tool parameters, apply canonical ordering, then run the assembly waterfall. Scoped sections and variables shadow globals; the returned waterfall value is authoritative.
|
||||
|
||||
- `context` — the optional scope and plugin-defined assembly fields.
|
||||
|
||||
**Returns** the authoritative post-waterfall assembly.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L365)
|
||||
@@ -1,191 +0,0 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
|
||||
# ctx.tasks
|
||||
|
||||
`TaskService` — provided by `@deepseek-ai/dsh-tasks`.
|
||||
|
||||
The `tasks` service: the runtime-global background task registry. See the module doc for the ownership, isolation, and lifecycle contracts.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L76)
|
||||
|
||||
### ctx.tasks.start(spec)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Preflight access, validation, and owner cleanup before starting and
|
||||
* atomically registering work. A throwing starter leaves nothing registered;
|
||||
* after it returns, registration cannot fail. Settlement records the outcome,
|
||||
* notifies listeners, and releases waiters.
|
||||
* @param spec - task identity, owner, and synchronous starter.
|
||||
* @returns the registry-issued `<kind>-N` id.
|
||||
*/
|
||||
start(spec: TaskStart): TaskId
|
||||
```
|
||||
|
||||
Preflight access, validation, and owner cleanup before starting and atomically registering work. A throwing starter leaves nothing registered; after it returns, registration cannot fail. Settlement records the outcome, notifies listeners, and releases waiters.
|
||||
|
||||
- `spec` — task identity, owner, and synchronous starter.
|
||||
|
||||
**Returns** the registry-issued `<kind>-N` id.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L101)
|
||||
|
||||
### ctx.tasks.list(caller?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* List caller-owned and unowned tasks in registration order without exposing
|
||||
* another session's labels.
|
||||
* @param caller - reading agent; a non-agent caller sees only unowned tasks.
|
||||
* @returns fresh snapshots.
|
||||
*/
|
||||
list(caller?: Agent): TaskSnapshot[]
|
||||
```
|
||||
|
||||
List caller-owned and unowned tasks in registration order without exposing another session's labels.
|
||||
|
||||
- `caller` — reading agent; a non-agent caller sees only unowned tasks.
|
||||
|
||||
**Returns** fresh snapshots.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L153)
|
||||
|
||||
### ctx.tasks.get(id, caller?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Return a non-consuming snapshot without changing its read cursor or notice
|
||||
* state. Throws for an unknown or foreign task.
|
||||
* @param id - task to look up.
|
||||
* @param caller - reading agent checked against the owner.
|
||||
* @returns a fresh snapshot.
|
||||
*/
|
||||
get(id: TaskId, caller?: Agent): TaskSnapshot
|
||||
```
|
||||
|
||||
Return a non-consuming snapshot without changing its read cursor or notice state. Throws for an unknown or foreign task.
|
||||
|
||||
- `id` — task to look up.
|
||||
- `caller` — reading agent checked against the owner.
|
||||
|
||||
**Returns** a fresh snapshot.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L167)
|
||||
|
||||
### ctx.tasks.read(id, caller?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Read the next stream delta, or the idempotent final output after settlement.
|
||||
* A terminal read marks the task reported. Throws for an unknown or foreign
|
||||
* task.
|
||||
* @param id - task to read.
|
||||
* @param caller - reading agent checked against the owner.
|
||||
* @returns output text and the post-read snapshot.
|
||||
*/
|
||||
read(id: TaskId, caller?: Agent): TaskRead
|
||||
```
|
||||
|
||||
Read the next stream delta, or the idempotent final output after settlement. A terminal read marks the task reported. Throws for an unknown or foreign task.
|
||||
|
||||
- `id` — task to read.
|
||||
- `caller` — reading agent checked against the owner.
|
||||
|
||||
**Returns** output text and the post-read snapshot.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L181)
|
||||
|
||||
### ctx.tasks.kill(id, caller?, reason?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Request cancellation, then mark the task stopping and reported. A producer
|
||||
* throw propagates without changing task state. Throws for an unknown or
|
||||
* foreign task.
|
||||
* @param id - task to cancel.
|
||||
* @param caller - killing agent checked against the owner.
|
||||
* @param reason - logged reason forwarded to the producer.
|
||||
* @returns `requested` for live work, otherwise `already-finished`.
|
||||
*/
|
||||
kill(id: TaskId, caller?: Agent, reason?: string): 'requested' | 'already-finished'
|
||||
```
|
||||
|
||||
Request cancellation, then mark the task stopping and reported. A producer throw propagates without changing task state. Throws for an unknown or foreign task.
|
||||
|
||||
- `id` — task to cancel.
|
||||
- `caller` — killing agent checked against the owner.
|
||||
- `reason` — logged reason forwarded to the producer.
|
||||
|
||||
**Returns** `requested` for live work, otherwise `already-finished`.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L200)
|
||||
|
||||
### ctx.tasks.wait(id, timeoutMs, caller?, signal?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Wait for settlement or timeout without cancelling the task. Caller abort
|
||||
* rejects only while the task is live; after settlement it returns the
|
||||
* terminal snapshot so a notice suppressed for this waiter is still delivered.
|
||||
* Timed-out and aborted waits detach their resolvers. Throws for invalid,
|
||||
* unknown, or foreign input.
|
||||
* @param id - task to wait for.
|
||||
* @param timeoutMs - positive finite wait bound in milliseconds.
|
||||
* @param caller - waiting agent checked against the owner.
|
||||
* @param signal - optional cancellation of the wait itself.
|
||||
* @returns snapshot at settlement or timeout.
|
||||
*/
|
||||
async wait(id: TaskId, timeoutMs: number, caller?: Agent, signal?: AbortSignal): Promise<TaskSnapshot>
|
||||
```
|
||||
|
||||
Wait for settlement or timeout without cancelling the task. Caller abort rejects only while the task is live; after settlement it returns the terminal snapshot so a notice suppressed for this waiter is still delivered. Timed-out and aborted waits detach their resolvers. Throws for invalid, unknown, or foreign input.
|
||||
|
||||
- `id` — task to wait for.
|
||||
- `timeoutMs` — positive finite wait bound in milliseconds.
|
||||
- `caller` — waiting agent checked against the owner.
|
||||
- `signal` — optional cancellation of the wait itself.
|
||||
|
||||
**Returns** snapshot at settlement or timeout.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L226)
|
||||
|
||||
### ctx.tasks.onTaskDone(listener)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Register an effect-scoped completion listener. Each listener is contained;
|
||||
* returned promises are observed but not awaited. No listener runs after
|
||||
* service disposal.
|
||||
* @param listener - receives each terminal snapshot and its exact owner.
|
||||
* @returns disposer that unregisters the listener.
|
||||
*/
|
||||
onTaskDone(listener: TaskDoneListener): () => void
|
||||
```
|
||||
|
||||
Register an effect-scoped completion listener. Each listener is contained; returned promises are observed but not awaited. No listener runs after service disposal.
|
||||
|
||||
- `listener` — receives each terminal snapshot and its exact owner.
|
||||
|
||||
**Returns** disposer that unregisters the listener.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L283)
|
||||
|
||||
### ctx.tasks.attachSurface(name)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Attach an effect-scoped surface that can read and stop tasks. {@link start}
|
||||
* refuses work while none is attached.
|
||||
* @param name - diagnostic label; duplicate names remain independent.
|
||||
* @returns disposer that detaches this surface.
|
||||
*/
|
||||
attachSurface(name: string): () => void
|
||||
```
|
||||
|
||||
Attach an effect-scoped surface that can read and stop tasks. start refuses work while none is attached.
|
||||
|
||||
- `name` — diagnostic label; duplicate names remain independent.
|
||||
|
||||
**Returns** disposer that detaches this surface.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L297)
|
||||
@@ -1,72 +0,0 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
|
||||
# ctx.tokenMeter
|
||||
|
||||
`TokenMeterService` — provided by `@deepseek-ai/dsh-token-meter`.
|
||||
|
||||
Replay owner for one service-wide estimator and isolated per-session folds.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/token-meter/src/index.ts#L106)
|
||||
|
||||
### ctx.tokenMeter.contextWindow
|
||||
|
||||
```ts website-api
|
||||
/** Provider context-window capacity used by pressure consumers. */
|
||||
readonly contextWindow: number
|
||||
```
|
||||
|
||||
Provider context-window capacity used by pressure consumers.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/token-meter/src/index.ts#L112)
|
||||
|
||||
### ctx.tokenMeter.measure(session, requestHeader?)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Measure current request pressure and surface through the durable tail.
|
||||
*
|
||||
* Provider usage is reused only when the latest successful call's canonical
|
||||
* request envelope matches `requestHeader` and its total is no lower than
|
||||
* that call's full heuristic anchor; otherwise the complete envelope and
|
||||
* surface are heuristically repriced.
|
||||
*
|
||||
* `requestHeader` affects request pressure only; surface fields always
|
||||
* describe the current session surface. Every call clones those positional
|
||||
* nodes, so measurement is O(surface).
|
||||
*
|
||||
* @param session - session to replay through its current durable tail.
|
||||
* @param requestHeader - optional effective request envelope replacing the latest logged header.
|
||||
* @returns a detached deeply immutable pressure and surface measurement.
|
||||
*/
|
||||
measure(session: Session, requestHeader?: EpochHeader): TokenMeasurement
|
||||
```
|
||||
|
||||
Measure current request pressure and surface through the durable tail.
|
||||
Provider usage is reused only when the latest successful call's canonical request envelope matches `requestHeader` and its total is no lower than that call's full heuristic anchor; otherwise the complete envelope and surface are heuristically repriced.
|
||||
`requestHeader` affects request pressure only; surface fields always describe the current session surface. Every call clones those positional nodes, so measurement is O(surface).
|
||||
|
||||
- `session` — session to replay through its current durable tail.
|
||||
- `requestHeader` — optional effective request envelope replacing the latest logged header.
|
||||
|
||||
**Returns** a detached deeply immutable pressure and surface measurement.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/token-meter/src/index.ts#L143)
|
||||
|
||||
### ctx.tokenMeter.estimateMessage(message)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Heuristically price one model-visible message.
|
||||
* @param message - message to price without mutation.
|
||||
* @returns content and role-framing tokens under the fixed service heuristic.
|
||||
*/
|
||||
estimateMessage(message: Message): number
|
||||
```
|
||||
|
||||
Heuristically price one model-visible message.
|
||||
|
||||
- `message` — message to price without mutation.
|
||||
|
||||
**Returns** content and role-framing tokens under the fixed service heuristic.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/token-meter/src/index.ts#L181)
|
||||
@@ -1,83 +0,0 @@
|
||||
<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->
|
||||
|
||||
# ctx.toolResultPrune
|
||||
|
||||
`ToolResultPruneService` — provided by `@deepseek-ai/dsh-compact-tool-result-prune`.
|
||||
|
||||
Deterministic head/middle/tail pruning for current tool-result surface nodes.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact-tool-result-prune/src/index.ts#L39)
|
||||
|
||||
### ctx.toolResultPrune.config
|
||||
|
||||
```ts website-api
|
||||
/** Resolved and immutable character budgets. */
|
||||
readonly config: ResolvedConfig
|
||||
```
|
||||
|
||||
Resolved and immutable character budgets.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact-tool-result-prune/src/index.ts#L47)
|
||||
|
||||
### ctx.toolResultPrune.measureContent(blocks)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Measure text content in Unicode code points; non-text blocks cost zero.
|
||||
* @param blocks - tool-result content to measure.
|
||||
* @returns total Unicode code points across text blocks.
|
||||
*/
|
||||
measureContent(blocks: readonly ContentBlock[]): number
|
||||
```
|
||||
|
||||
Measure text content in Unicode code points; non-text blocks cost zero.
|
||||
|
||||
- `blocks` — tool-result content to measure.
|
||||
|
||||
**Returns** total Unicode code points across text blocks.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact-tool-result-prune/src/index.ts#L59)
|
||||
|
||||
### ctx.toolResultPrune.pruneContent(blocks)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Replace an over-budget text middle while retaining rich-block order.
|
||||
* Text slicing is by Unicode code point, not UTF-16 code unit, so a retained
|
||||
* boundary cannot split a surrogate pair. Grapheme clusters may still split.
|
||||
* @param blocks - original tool-result content.
|
||||
* @returns pruned content, or `null` when the text is within budget.
|
||||
*/
|
||||
pruneContent(blocks: readonly ContentBlock[]): ContentBlock[] | null
|
||||
```
|
||||
|
||||
Replace an over-budget text middle while retaining rich-block order. Text slicing is by Unicode code point, not UTF-16 code unit, so a retained boundary cannot split a surrogate pair. Grapheme clusters may still split.
|
||||
|
||||
- `blocks` — original tool-result content.
|
||||
|
||||
**Returns** pruned content, or `null` when the text is within budget.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact-tool-result-prune/src/index.ts#L74)
|
||||
|
||||
### ctx.toolResultPrune.pruneSession(session)
|
||||
|
||||
```ts website-api
|
||||
/**
|
||||
* Prune every over-budget tool result from one stable current-surface snapshot.
|
||||
* Each replacement preserves the complete event data except for `content`,
|
||||
* and points at the shadowed node for durable provenance and replay.
|
||||
* @param session - session whose current surface is rewritten.
|
||||
* @returns landed replacements and aggregate Unicode-code-point savings.
|
||||
* @throws when the session rejects a replacement; replacements committed
|
||||
* earlier in the pass remain durable.
|
||||
*/
|
||||
pruneSession(session: Session): PruneResult
|
||||
```
|
||||
|
||||
Prune every over-budget tool result from one stable current-surface snapshot. Each replacement preserves the complete event data except for `content`, and points at the shadowed node for durable provenance and replay.
|
||||
|
||||
- `session` — session whose current surface is rewritten.
|
||||
|
||||
**Returns** landed replacements and aggregate Unicode-code-point savings.
|
||||
|
||||
[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact-tool-result-prune/src/index.ts#L124)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user