mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-18 05:05:28 +00:00
refactor(agent-core-v2): strip comments from agent-core-v2, kap-server, and transcript (#3010)
Some checks are pending
CI / build (push) Waiting to run
CI / test (1) (push) Waiting to run
CI / test (2) (push) Waiting to run
CI / test (3) (push) Waiting to run
CI / test (4) (push) Waiting to run
CI / test (5) (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-vscode-legacy (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Some checks are pending
CI / build (push) Waiting to run
CI / test (1) (push) Waiting to run
CI / test (2) (push) Waiting to run
CI / test (3) (push) Waiting to run
CI / test (4) (push) Waiting to run
CI / test (5) (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-vscode-legacy (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
This commit is contained in:
parent
a7dc1ea284
commit
1ab19190e9
1213 changed files with 395 additions and 22907 deletions
|
|
@ -33,7 +33,7 @@ End-to-end procedures that span the stages. Reach for these before reading the s
|
|||
|
||||
## Stages
|
||||
|
||||
- [Stage 1 — Orient](orient.md): the DI black box (identity / dependencies / lifetime), the four `LifecycleScope` tiers and visibility, and the file-header comment convention. Read before touching business code.
|
||||
- [Stage 1 — Orient](orient.md): the DI black box (identity / dependencies / lifetime), the four `LifecycleScope` tiers and visibility, and the no-comment convention. Read before touching business code.
|
||||
- [Stage 2 — Design a service](design.md): pick a scope, split a domain across scopes, choose a calling style (direct call vs event vs hook), and direct dependencies. Decide *where things live and who knows whom* before coding.
|
||||
- Topic: [Domain boundaries vs Scope](domain-boundaries.md) — keep `session` / `agent` / `turn` from becoming god objects; data-ownership test and their split conclusions.
|
||||
- Topic: [Persistence layering](persistence.md) — the three-layer `Store → Storage → backend` model, naming Stores by access pattern, and which layer business code should depend on.
|
||||
|
|
|
|||
|
|
@ -66,45 +66,12 @@ There is no domain-layer numbering — a domain may import any other domain, gui
|
|||
- v2 never imports v1 (`@moonshot-ai/agent-core` or any subpath).
|
||||
- The kosong subtree (`src/kosong/{contract,protocol,provider,model}`) keeps its strict internal order (`contract ← protocol ← provider/model`), purity bans (no SDKs in `contract`/`protocol`), and the `provider/bases` registration boundary.
|
||||
|
||||
## File-header comment convention
|
||||
## Comment convention
|
||||
|
||||
`packages/agent-core-v2/AGENTS.md` mandates a header-only comment style:
|
||||
|
||||
- **Header only.** Comments live solely in the top-of-file `/** */` block — never beside functions, methods, or statements. The code is the source of truth for *how*; the header states *what the module exposes and the responsibility it owns*.
|
||||
- **Identity line first.** Start with `` `<domain>` domain — <one-line role>. `` Keep an existing `(cross-cutting)` label as-is. Write the role as a responsibility ("drives the turn lifecycle"), not a symbol list.
|
||||
- **Scope is in the filename.** `workspace*.ts` = Workspace, `session*.ts` = Session, `agent*.ts` = Agent, no prefix = App (see service-authoring.md). State the same scope in the header so the two never drift.
|
||||
- **Interface files** (`<name>.ts`) state the public contract + scope: which `IXxx` they define and what it is for.
|
||||
- **Impl files** (`<name>Service.ts`) add collaborators + scope: list every imported cross-domain collaborator as a role ("persists records through `records`"); read scope from `registerScopedService(LifecycleScope.X, …)`.
|
||||
- **Contribution files** (`<targetDomain>.ts` / `<what>.contrib.ts`) state what they register into the target domain (e.g. "registers the `log` config section into `config`").
|
||||
- **Pure-function / `.types` / `.errors` files** state the responsibility only — they own no scoped state, so no scope line.
|
||||
|
||||
Impl file example (`sessionMetadataService.ts`):
|
||||
|
||||
```ts
|
||||
/**
|
||||
* `sessionMetadata` domain — `ISessionMetadata` implementation.
|
||||
*
|
||||
* Persists the session metadata document (`state.json`) through the `storage`
|
||||
* access-pattern store (`IAtomicDocumentStore`), rooted at the `metaScope`
|
||||
* namespace from `sessionContext`. Loads the existing document on
|
||||
* construction (creating it on first run), and logs through `log`. Bound at
|
||||
* Session scope.
|
||||
*/
|
||||
```
|
||||
|
||||
Contribution file example (`config.ts` inside `log/`):
|
||||
|
||||
```ts
|
||||
/**
|
||||
* `log` domain — registers the `log` config section into `config`.
|
||||
*
|
||||
* Owns the `log` section schema and its env overlay; imported for the
|
||||
* registration side effect. Bound at App scope.
|
||||
*/
|
||||
```
|
||||
`packages/agent-core-v2/AGENTS.md` bans comments: no file headers, no section banners, no statement-level narration — the code is the source of truth. The only exception is JSDoc attached to exported symbols, which flows into the generated `.d.ts` and the consumers' IDE hover. Tooling directives (`eslint-disable`, `@ts-expect-error`, …) are banned too: fix the underlying lint/type problem instead, and put negative type-safety cases in compiler-asserted fixtures. Scope is carried by the filename: `workspace*.ts` = Workspace, `session*.ts` = Session, `agent*.ts` = Agent, no prefix = App (see service-authoring.md).
|
||||
|
||||
## Red lines (this stage)
|
||||
|
||||
- Import via the `#/...` alias (mapped to `src/`); never reach into another domain's internals by relative path.
|
||||
- Short-lived may inject long-lived; never the reverse.
|
||||
- File-header comments describe role and scope only; never narrate implementation beside statements.
|
||||
- No comments — not file headers, not beside statements; exported-symbol JSDoc is the only exception.
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ registerScopedService(
|
|||
Conventions:
|
||||
|
||||
- **Name** the domain `<domain>Legacy` and the interface with the scope prefix, `I<Scope><Domain>LegacyService` (e.g. `prompt` / `IAgentPromptService`), per service-authoring.md.
|
||||
- **Header comment** must say it is an `edge adapter` and name both the v1 contract it implements and the native v2 Service it leaves untouched (see `prompt.ts`).
|
||||
- **Role is carried by the name** — `<domain>Legacy` marks it as an `edge adapter`; the v1 contract it implements and the native v2 Service it leaves untouched stay evident from its delegation targets (see `prompt.ts`).
|
||||
- **Scope** = the lifetime of the *legacy* state it holds (the `prompt` queue is per-agent → `LifecycleScope.Agent`). Apply [orient.md](orient.md) / [design.md](design.md) normally — a LegacyService is not exempt from scope rules.
|
||||
- **Delegate, do not duplicate** business logic. The LegacyService translates the v1 contract into native-Service calls and translates results back; the real work stays in the native Service.
|
||||
- **Contract types come from the v1 wire schema homes** (the owning v2 domain contract or `kap-server/src/protocol`), so the interface cannot drift from the wire shape.
|
||||
|
|
@ -236,7 +236,7 @@ Before submitting a server-align change:
|
|||
- [ ] Request and response schemas come from their owning home (the `agent-core-v2` domain contract or `packages/kap-server/src/protocol`); no inline re-declaration in server-v2.
|
||||
- [ ] Existing schema fields are unchanged in name, type, and semantics; only optional fields added (if any).
|
||||
- [ ] Native v2 Service left clean; v1-only behavior isolated in a `<domain>Legacy` / `I<Domain>LegacyService` edge adapter when the semantics diverge.
|
||||
- [ ] LegacyService registered with the correct `LifecycleScope` and a header comment naming it an edge adapter + the native Service it preserves.
|
||||
- [ ] LegacyService registered with the correct `LifecycleScope` and named as the `<domain>Legacy` edge adapter preserving the native Service.
|
||||
- [ ] Domain error codes registered in `agent-core-v2`; wire codes registered in `packages/kap-server/src/protocol`; route maps them in `sendMappedError`, matching v1's status codes and idempotent envelopes.
|
||||
- [ ] Route resolves the scope from the URL by `accessor.get(IX)`; no cached scope; finishes before disposal.
|
||||
- [ ] Tests assert the wire envelope + protocol shape; wire-shape guards added/updated where the route mirrors v1.
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ One folder per domain, **camelCase**: `session/`, `sessionActivity/`, `contextMe
|
|||
```
|
||||
|
||||
- **Strictly one service per file.** An interface file holds exactly one injectable interface and exactly one `createDecorator(...)`; an impl file holds exactly one service implementation class and exactly one `registerScopedService(...)`. No exceptions for "tightly-coupled" groups: even same-scope collaborators each get their own `<name>.ts` + `<name>Service.ts` pair.
|
||||
- **Scope is in the filename.** `workspace*.ts` = Workspace, `session*.ts` = Session, `agent*.ts` = Agent, no scope prefix = App (see [Naming](#naming)). The header comment restates the same scope.
|
||||
- **Scope is in the filename.** `workspace*.ts` = Workspace, `session*.ts` = Session, `agent*.ts` = Agent, no scope prefix = App (see [Naming](#naming)).
|
||||
- A domain therefore has as many impl files as it has services (e.g. `logService.ts` for the App `ILogService`, `sessionLogService.ts` for the Session `ISessionLogService`). See [Multi-Service domains](#multi-service-domains).
|
||||
|
||||
The package entry `src/index.ts` imports and `export *`s every domain's leaf files precisely (one line per leaf), so importing the package still runs every `registerScopedService(...)` side effect — exactly as the old per-domain barrels did.
|
||||
|
|
@ -293,11 +293,10 @@ Importing the package therefore fires every `register*` side effect, exactly as
|
|||
|
||||
- Load the impl file too — its top-level `registerScopedService(...)` only runs when the module is imported.
|
||||
- `export *` helper modules only if they are part of the domain's public surface.
|
||||
- Each leaf's file-header comment still names the domain, scope, and (for impls) the `register*` binding it owns.
|
||||
|
||||
## Comments
|
||||
|
||||
- **File-header comment is mandatory** and the only place comments live (orient.md). State the identity line, the role, collaborators (impls), and scope.
|
||||
- **No comments** (orient.md): no file headers, no statement-level narration; the only exception is JSDoc attached to exported symbols.
|
||||
- **Methods and fields carry no comments by default.** Well-named identifiers and types say *what*; the code is the source of truth for *how*.
|
||||
- Write an inline comment only when the *why* is non-obvious (a hidden constraint, a subtle invariant, a workaround). One short line.
|
||||
- For unimplemented stubs, throw `NotImplementedError('feature')` rather than `throw new Error('TODO: …')` (errors.md).
|
||||
|
|
@ -351,4 +350,4 @@ import './greet/greetService';
|
|||
- Never `new` a `@IService`-carrying Service — except inside an explicit factory method, which is not a DI request.
|
||||
- Events: typed per-Service event → `Event<T>`/`Emitter` from `'#/_base/event'`; cross-domain broadcast → `IEventService` from `'#/event'`.
|
||||
- `src/index.ts` must import/export every leaf file (including the impl) so each `register*` side effect runs.
|
||||
- File-header comment only; methods/fields carry no comments by default; stubs throw `NotImplementedError`.
|
||||
- No comments by default (orient.md); stubs throw `NotImplementedError`.
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Walk the stages you touched and confirm:
|
|||
- **Design** — scope follows state identity; no `Map<sessionId, …>` at `App`; dependency arrows do not make a foundational layer know an upstream one; no cycle was routed around.
|
||||
- **Implement** — no `new` on `@IService`-carrying classes; `@IX` on constructor params only (service params after static params); interface + impl carry `_serviceBrand`; decorator names unique; coded errors only; flags for unreleased behavior.
|
||||
- **Test** — SUT resolved by interface; stubs under `test/`; scope tests re-register after `_clearScopedRegistryForTests()`; teardown through one `DisposableStore`.
|
||||
- **Files** — header comments describe role + scope only; registration runs from the impl file's top level; the new domain is exported from `src/index.ts`.
|
||||
- **Files** — no comments (exported-symbol JSDoc excepted); registration runs from the impl file's top level; the new domain is exported from `src/index.ts`.
|
||||
|
||||
Then re-read the [global red lines](SKILL.md#global-red-lines) once — they catch most cross-stage mistakes in a single scan.
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
|
|||
|
||||
## General Coding Rules
|
||||
|
||||
- `packages/agent-core-v2`, `packages/kap-server`, and `packages/transcript` are comment-free zones: no line/block comments; the exceptions are JSDoc attached to exported symbols and load-bearing lint-suppression directives (`oxlint-disable` / `eslint-disable`), while other tooling directives (`@ts-expect-error`, …) stay banned. Enforced by `scripts/check-no-comments.mjs`, which runs as part of `pnpm lint`.
|
||||
- For optional object properties, pass `undefined` directly instead of using conditional spread.
|
||||
- YES: `{ user }`
|
||||
- NO: `{ ...(user ? { user } : undefined) }`
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
"vis": "pnpm -C apps/vis run dev",
|
||||
"dev:docs": "pnpm -C docs install --ignore-workspace && pnpm -C docs run dev",
|
||||
"typecheck": "pnpm run build:packages && pnpm -r --filter './packages/*' run typecheck && pnpm --filter @moonshot-ai/kimi-code run typecheck && pnpm --filter kimi-code run typecheck && pnpm --filter @moonshot-ai/vis-server run typecheck && pnpm --filter @moonshot-ai/vis-web run typecheck",
|
||||
"lint": "oxlint --type-aware",
|
||||
"lint": "node scripts/check-no-comments.mjs && oxlint --type-aware",
|
||||
"lint:fix": "pnpm run lint --fix",
|
||||
"lint:pkg": "pnpm --filter @moonshot-ai/kimi-code exec publint && npm_config_cache=${TMPDIR:-/tmp}/kimi-code-npm-cache pnpm --filter @moonshot-ai/kimi-code exec attw --pack . --profile node16",
|
||||
"sherif": "sherif -i @agentclientprotocol/sdk",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Four `LifecycleScope` tiers — `App` / `Workspace` / `Session` / `Agent` (strin
|
|||
|
||||
The DI kernel (`src/_base/di/`) owns the unit layer on top of the scoped registry:
|
||||
|
||||
- `service.ts` — `Service`: the unit base class (extends `Disposable`). Capabilities live on `this` (`provide` / `effect` / `on` / `get` / `ref`, plus `name` / `state` / `config`). Two-phase construction: inside the ctor `provide`/`on`/`effect` buffer (writes only — `get`/`ref` throw, dependencies are constructor parameters); the kernel binds the runtime after `Reflect.construct` and flushes in writing order; a manually `new`ed instance throws on every capability call. Services whose own members collide with the `Service` vocabulary keep `extends Disposable` with a NOTE comment — still full DI units (cascade/ledger do not require `Service`).
|
||||
- `service.ts` — `Service`: the unit base class (extends `Disposable`). Capabilities live on `this` (`provide` / `effect` / `on` / `get` / `ref`, plus `name` / `state` / `config`). Two-phase construction: inside the ctor `provide`/`on`/`effect` buffer (writes only — `get`/`ref` throw, dependencies are constructor parameters); the kernel binds the runtime after `Reflect.construct` and flushes in writing order; a manually `new`ed instance throws on every capability call. Services whose own members collide with the `Service` vocabulary keep `extends Disposable` — still full DI units (cascade/ledger do not require `Service`).
|
||||
- `fiber.ts` — the `Fiber` capability interface (not a DI token), `FiberHandle` (thenable / `state` / `uid` / `update` / `dispose`), `ServiceRecipe` (class / arrow function / `{apply}`), the `FiberState` five-state machine, and `ScopeUnits(kind)` — the materialization collection token, one per scope kind.
|
||||
- `collection.ts` — `collection<T>(name)` contribution tokens. Contribute with `this.provide(token, value)`; a fold declares the token as a constructor parameter and receives a `CollectionView<T>` (`items` / `records` / incremental `onDidChange`). Records are visible to the provider's ancestors and descendants (never sibling subtrees); provider death withdraws. Collection edges enter the graph for introspection but never join a cascade contagion set.
|
||||
- `scopeUnits.ts` — the kernel fold: every scope-creation point (`createScopedChildHandle` / `Scope.createApp` / `Scope.createChild`) runs `watchScopeUnits(container, kind)` before eager activation, materializing each visible `ScopeUnits(kind)` record's recipe as a unit inside the new scope (disposal hangs on the record provider's book — provider death tears the materialized units down across the tree). `ScopeOptions.configureContainer` runs at the same point (the session seed adapters use it).
|
||||
|
|
@ -35,25 +35,8 @@ Domain-slice scenarios that used to live in `examples/<name>.example.ts` are now
|
|||
|
||||
## Comment conventions
|
||||
|
||||
- **Header only, external role only.** Comments live solely in the top-of-file `/** */` block — never beside functions, methods, or statements. Say what the module exposes and the responsibility it owns; the code is the source of truth for how it works, so do not narrate implementation steps, enumerate every export, or note porting / skeleton status.
|
||||
- **Identity line first.** Start with `` `<domain>` domain — <one-line role>. `` Keep an existing `(cross-cutting)` label as-is. Write the role as a responsibility ("drives the turn lifecycle"), not a symbol list ("turn driver + context + loop runner").
|
||||
- **Impl files add collaborators + scope; contract files add the public contract + scope.** For impls, list every imported cross-domain collaborator as a role ("persists records through `records`") — declared dependencies count even if not yet wired in this WIP port; infrastructure imports (`_base/**`) are not collaborators. Read scope from `registerScopedService(LifecycleScope.X, …)`.
|
||||
|
||||
### Examples
|
||||
|
||||
Impl (`src/session/sessionMetadata/sessionMetadataService.ts`):
|
||||
|
||||
```ts
|
||||
/**
|
||||
* `sessionMetadata` domain — `ISessionMetadata` implementation.
|
||||
*
|
||||
* Persists the session metadata document (`state.json`) through the `storage`
|
||||
* access-pattern store (`IAtomicDocumentStore`), rooted at the `metaScope`
|
||||
* namespace from `sessionContext`. Loads the existing document on
|
||||
* construction (creating it on first run), and logs through `log`. Bound at
|
||||
* Session scope.
|
||||
*/
|
||||
```
|
||||
- **No comments.** The code is the source of truth; do not write file headers, section banners, or implementation narration. The one exception is JSDoc attached to exported symbols (it flows into the generated `.d.ts` and the consumers' IDE hover); keep it focused on the public contract.
|
||||
- **Lint-suppression directives are the tooling exception.** `oxlint-disable` / `eslint-disable` comments are allowed where they suppress an active rule for a deliberate pattern (e.g. the Event2 class+payload-interface merging idiom). `@ts-expect-error`, `@ts-ignore`, and `ts-nocheck` stay banned — fix the underlying type problem instead; negative type-safety cases go into compiler-asserted fixtures.
|
||||
|
||||
## Telemetry
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ keep their static registrations; the service and the two tools go through the Fe
|
|||
|
||||
## Adding a new feature
|
||||
|
||||
1. `src/features/<name>/` — domain files follow the usual conventions (header comments,
|
||||
1. `src/features/<name>/` — domain files follow the usual conventions (no comments,
|
||||
one service per file pair, `.md?raw` assets move with the feature).
|
||||
2. `<name>Feature.ts` — the Feature subclass + `registerFeature(...)`.
|
||||
3. `src/index.ts` — precise leaf imports/exports; no barrel.
|
||||
|
|
|
|||
|
|
@ -1,26 +1,3 @@
|
|||
/**
|
||||
* Generates `docs/config-manifest.toml` — the single place to see every config
|
||||
* section registered via `registerConfigSection(...)` plus every effective
|
||||
* overlay registered via `registerConfigOverlay(...)`.
|
||||
*
|
||||
* Two passes:
|
||||
* 1. Static scan of `src/**` maps each registered section domain (and each
|
||||
* overlay) to the source file that registers it — the "owner".
|
||||
* 2. Runtime pass imports `src/index.ts` ("import = register") and drains the
|
||||
* module-level contributions, capturing defaults, env bindings, and the
|
||||
* registered hooks exactly as the running process sees them.
|
||||
*
|
||||
* The output is TOML in the on-disk shape (snake_case keys): one `[table]` per
|
||||
* section, uncommented assignments for registered defaults, and commented
|
||||
* `# field: type` lines for the remaining schema fields.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm --filter @moonshot-ai/agent-core-v2 gen:config-manifest # write the file
|
||||
* pnpm --filter @moonshot-ai/agent-core-v2 gen:config-manifest --check # freshness check (CI-style)
|
||||
*
|
||||
* Freshness is also enforced by `test/app/config/configManifest.test.ts`.
|
||||
*/
|
||||
|
||||
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { join, relative } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
|
@ -44,10 +21,6 @@ const PKG = join(import.meta.dirname, '..');
|
|||
const SRC = join(PKG, 'src');
|
||||
export const MANIFEST_PATH = join(PKG, 'docs', 'config-manifest.toml');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Static pass — domain/overlay → owner file
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function walk(dir: string, out: string[] = []): string[] {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const p = join(dir, entry);
|
||||
|
|
@ -62,7 +35,6 @@ function constStringValue(source: string, ident: string): string | undefined {
|
|||
return re.exec(source)?.[1];
|
||||
}
|
||||
|
||||
/** domain key → owner file (relative to the package root). */
|
||||
function scanSectionOwners(): Map<string, string> {
|
||||
const owners = new Map<string, string>();
|
||||
for (const file of walk(SRC)) {
|
||||
|
|
@ -77,12 +49,9 @@ function scanSectionOwners(): Map<string, string> {
|
|||
return owners;
|
||||
}
|
||||
|
||||
/** overlay variable name → owner file (relative to the package root). */
|
||||
function scanOverlayOwners(): Map<string, string> {
|
||||
const owners = new Map<string, string>();
|
||||
for (const file of walk(SRC)) {
|
||||
// Skip the collector module itself — its `registerConfigOverlay(overlay)`
|
||||
// function signature is not a registration.
|
||||
if (file.endsWith('configOverlayContributions.ts')) continue;
|
||||
const source = readFileSync(file, 'utf-8');
|
||||
if (!source.includes('registerConfigOverlay(')) continue;
|
||||
|
|
@ -94,11 +63,6 @@ function scanOverlayOwners(): Map<string, string> {
|
|||
return owners;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TOML-like rendering helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Serialize a small JSON value as an inline TOML value. */
|
||||
function toTomlValue(value: unknown): string {
|
||||
if (typeof value === 'string') return JSON.stringify(value);
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
||||
|
|
@ -116,7 +80,6 @@ interface EnvRow {
|
|||
readonly detail: string;
|
||||
}
|
||||
|
||||
/** Property access shape of an `EnvBinding` object (avoids index-signature access). */
|
||||
interface EnvBindingFields {
|
||||
readonly env?: unknown;
|
||||
readonly deprecatedEnv?: unknown;
|
||||
|
|
@ -148,11 +111,6 @@ function snakePath(field: string): string {
|
|||
|
||||
const RULE = `# ${'#'.repeat(74)}`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Section rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** `# field: type (default: x)` comment lines for an object schema's properties. */
|
||||
function renderFieldComments(
|
||||
properties: Record<string, unknown>,
|
||||
root: JsonSchema,
|
||||
|
|
@ -165,8 +123,6 @@ function renderFieldComments(
|
|||
const propDefault = asJsonSchema(resolved)?.default;
|
||||
const defNote = propDefault !== undefined ? ` (default: ${JSON.stringify(propDefault)})` : '';
|
||||
lines.push(`${indent}# ${camelToSnake(name)}: ${describeType(resolved)}${defNote}`);
|
||||
// Expand nested object fields one level at a time (depth-capped so a
|
||||
// recursive $ref cannot loop).
|
||||
const subProps = asJsonSchema(resolved)?.properties;
|
||||
if (depth < 3 && isRecord(subProps) && Object.keys(subProps).length > 0) {
|
||||
lines.push(...renderFieldComments(subProps, root, `${indent} `, depth + 1));
|
||||
|
|
@ -181,7 +137,6 @@ function renderBody(section: ConfigSectionContribution): string[] {
|
|||
const jsonSchema = schema === undefined ? undefined : toJsonSchema(schema);
|
||||
|
||||
if (jsonSchema === undefined) {
|
||||
// No schema (passthrough) or a schema that JSON Schema cannot represent.
|
||||
if (isRecord(options.defaultValue)) {
|
||||
return [
|
||||
`[${key}]`,
|
||||
|
|
@ -197,7 +152,6 @@ function renderBody(section: ConfigSectionContribution): string[] {
|
|||
return [`[${key}]`, `# (${schema === undefined ? 'no schema — passthrough' : 'schema uses transforms; see the owner file'})`];
|
||||
}
|
||||
|
||||
// Object with named fields.
|
||||
if (isRecord(jsonSchema.properties) && Object.keys(jsonSchema.properties).length > 0) {
|
||||
const defaults = isRecord(options.defaultValue) ? options.defaultValue : {};
|
||||
const lines = [`[${key}]`];
|
||||
|
|
@ -207,8 +161,6 @@ function renderBody(section: ConfigSectionContribution): string[] {
|
|||
lines.push(`${fieldKey} = ${truncate(toTomlValue(defaults[name]))}`);
|
||||
continue;
|
||||
}
|
||||
// A nested object field is an on-disk sub-table (`[section.field]`) —
|
||||
// render its own fields instead of a flat `field: object` comment.
|
||||
const resolved = resolveRef(prop, jsonSchema);
|
||||
const subProps = asJsonSchema(resolved)?.properties;
|
||||
if (isRecord(subProps) && Object.keys(subProps).length > 0) {
|
||||
|
|
@ -217,7 +169,6 @@ function renderBody(section: ConfigSectionContribution): string[] {
|
|||
lines.push(...renderFieldComments(subProps, jsonSchema, ' '));
|
||||
continue;
|
||||
}
|
||||
// An array-of-objects field carries its element fields inline.
|
||||
const itemProps = asJsonSchema(
|
||||
resolveRef(asJsonSchema(resolved)?.items, jsonSchema),
|
||||
)?.properties;
|
||||
|
|
@ -231,7 +182,6 @@ function renderBody(section: ConfigSectionContribution): string[] {
|
|||
return lines;
|
||||
}
|
||||
|
||||
// Record section — one sub-table per entry.
|
||||
if (jsonSchema.additionalProperties !== undefined) {
|
||||
const valueSchema = resolveRef(jsonSchema.additionalProperties, jsonSchema);
|
||||
const valueProps = asJsonSchema(valueSchema)?.properties;
|
||||
|
|
@ -247,10 +197,6 @@ function renderBody(section: ConfigSectionContribution): string[] {
|
|||
return lines;
|
||||
}
|
||||
|
||||
// Array-of-tables section — one `[[section]]` entry per element. There is
|
||||
// no `[section]` parent table in TOML, so the whole shape stays commented;
|
||||
// emitting a bare `[${key}]` header would parse as a plain table, which
|
||||
// array sections (e.g. `hooks`) reject on load.
|
||||
if (jsonSchema.type === 'array') {
|
||||
const itemProps = asJsonSchema(resolveRef(jsonSchema.items, jsonSchema))?.properties;
|
||||
if (isRecord(itemProps) && Object.keys(itemProps).length > 0) {
|
||||
|
|
@ -262,7 +208,6 @@ function renderBody(section: ConfigSectionContribution): string[] {
|
|||
}
|
||||
}
|
||||
|
||||
// Scalar / array section — a plain top-level key.
|
||||
if (options.defaultValue !== undefined) {
|
||||
return [`${key} = ${truncate(toTomlValue(options.defaultValue))}`];
|
||||
}
|
||||
|
|
@ -302,12 +247,7 @@ function renderSection(section: ConfigSectionContribution, owner: string | undef
|
|||
return lines;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Manifest rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function buildConfigManifest(): Promise<string> {
|
||||
// "import = register": loading the package root fills the contribution bags.
|
||||
await import('../src/index.ts');
|
||||
const sections = getConfigSectionContributions().toSorted((a, b) =>
|
||||
a.domain.localeCompare(b.domain),
|
||||
|
|
@ -345,10 +285,6 @@ export async function buildConfigManifest(): Promise<string> {
|
|||
return out.join('\n');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const check = process.argv.includes('--check');
|
||||
const manifest = await buildConfigManifest();
|
||||
|
|
|
|||
|
|
@ -1,39 +1,3 @@
|
|||
/**
|
||||
* Generates `docs/state-manifest.d.ts` — the single place to see every state
|
||||
* key registered into the four scoped state services (App-scope
|
||||
* `IAppStateService`, Workspace-scope `IWorkspaceStateService`, Session-scope
|
||||
* `ISessionStateService`, Agent-scope `IAgentStateService`).
|
||||
*
|
||||
* Pure static pass (state keys are registered inside DI scope constructors, so
|
||||
* there is no process-level registry to drain the way `gen-wire-manifest`
|
||||
* does):
|
||||
* 1. A ts-morph scan of `src/{app,workspace,session,agent,features}/**`
|
||||
* collects every top-level `defineState('name', ...)` key constant.
|
||||
* 2. Every `.register(key)` call site resolves its argument back to a key
|
||||
* constant (following imports); the key joins the scope of the
|
||||
* registering file (`src/app/**` → App, `src/workspace/**` → Workspace,
|
||||
* `src/session/**` → Session, `src/agent/**` → Agent). Files under
|
||||
* `src/features/**` register into whichever scope their services are
|
||||
* materialized in, so the scope is resolved from the register-call
|
||||
* receiver's type (`IAgentStateService` → Agent, …).
|
||||
* A key that is defined but never registered is excluded.
|
||||
*
|
||||
* The output is a self-contained `.d.ts`: each key's value type is the
|
||||
* compile-time `StateKey<T>` parameter, expanded fully inline through the type
|
||||
* checker — no imports and no helper declarations. Every named type is marked
|
||||
* at its expansion site with an inline `TypeName — source/file.ts` comment;
|
||||
* recursion stops with a `TypeName — recursive` marker on an `unknown`. Generic
|
||||
* instantiations are expanded structurally and classes render as their public
|
||||
* instance shape; only lib globals (`Map`/`Set`/…) and a few noted external
|
||||
* ambient types keep their names.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm --filter @moonshot-ai/agent-core-v2 gen:state-manifest # write the file
|
||||
* pnpm --filter @moonshot-ai/agent-core-v2 gen:state-manifest --check # freshness check (CI-style)
|
||||
*
|
||||
* Freshness is also enforced by `test/state/stateManifest.test.ts`.
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join, relative } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
|
@ -59,7 +23,6 @@ const REPO_ROOT = join(PKG, '..', '..');
|
|||
const SRC = join(PKG, 'src');
|
||||
export const MANIFEST_PATH = join(PKG, 'docs', 'state-manifest.d.ts');
|
||||
|
||||
/** src first-level directory → manifest section. */
|
||||
const SCOPES = [
|
||||
{
|
||||
dir: 'app',
|
||||
|
|
@ -92,11 +55,9 @@ type ScopeDir = (typeof SCOPES)[number]['dir'];
|
|||
interface KeyDef {
|
||||
readonly constName: string;
|
||||
readonly keyName: string;
|
||||
/** Absolute path of the file defining the key constant. */
|
||||
readonly file: string;
|
||||
readonly exported: boolean;
|
||||
readonly declaration: VariableDeclaration;
|
||||
/** Present when the key chains `.replayable(...)` — the key is materialized into the Agent-scope state service. */
|
||||
readonly replayable?: {
|
||||
readonly durable: boolean;
|
||||
readonly undoable: boolean;
|
||||
|
|
@ -111,7 +72,6 @@ interface Registration {
|
|||
|
||||
interface StateManifestModel {
|
||||
readonly registrations: readonly Registration[];
|
||||
/** Keys defined under the scope dirs but never registered (dead candidates). */
|
||||
readonly unregistered: readonly KeyDef[];
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +84,6 @@ function isFeaturesFile(file: string): boolean {
|
|||
return relative(SRC, file).split(/[\\/]/)[0] === 'features';
|
||||
}
|
||||
|
||||
/** Feature files register into the scope of their materialized services — resolve it from the register-call receiver's state-service type. */
|
||||
const FEATURES_RECEIVER_SCOPE: Readonly<Record<string, ScopeDir>> = {
|
||||
IAppStateService: 'app',
|
||||
IWorkspaceStateService: 'workspace',
|
||||
|
|
@ -132,7 +91,6 @@ const FEATURES_RECEIVER_SCOPE: Readonly<Record<string, ScopeDir>> = {
|
|||
IAgentStateService: 'agent',
|
||||
};
|
||||
|
||||
/** Resolve the scope from the contributeState-call receiver's state-service type. */
|
||||
function receiverScope(
|
||||
expression: PropertyAccessExpression,
|
||||
checker: TypeChecker,
|
||||
|
|
@ -157,36 +115,23 @@ function featuresRegisterScope(
|
|||
return scope;
|
||||
}
|
||||
|
||||
/** Package-root-relative posix path (used in index/comment columns). */
|
||||
function srcRelative(file: string): string {
|
||||
return relative(PKG, file).split('\\').join('/');
|
||||
}
|
||||
|
||||
/** Repo-root-relative posix path (used in type-name comments). */
|
||||
function repoRelative(file: string): string {
|
||||
return relative(REPO_ROOT, file).split('\\').join('/');
|
||||
}
|
||||
|
||||
/** Quote a property key only when it is not a plain identifier. */
|
||||
function tsFieldKey(key: string): string {
|
||||
return /^[$A-Z_a-z][$\w]*$/.test(key) ? key : JSON.stringify(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* The checker names a `unique symbol` key `__@<declName>@<globalSymbolId>` —
|
||||
* the numeric id is a compilation-global counter that shifts with unrelated
|
||||
* edits, so the manifest renders the stable `__@<declName>` form instead.
|
||||
*/
|
||||
function stableSymbolKey(key: string): string {
|
||||
const match = /^__@(.+)@\d+$/.exec(key);
|
||||
return match === null ? key : `__@${match[1]}`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Static pass — key constants and their register call sites
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Pass 1 — every top-level `defineState('name', ...)` constant under the scope dirs. */
|
||||
function collectKeyDefs(project: Project): Map<VariableDeclaration, KeyDef> {
|
||||
const defs = new Map<VariableDeclaration, KeyDef>();
|
||||
for (const sf of project.getSourceFiles()) {
|
||||
|
|
@ -212,11 +157,6 @@ function collectKeyDefs(project: Project): Map<VariableDeclaration, KeyDef> {
|
|||
return defs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk a `defineState('name', ...)` / `.replayable({...})` / `.undoable(...)` /
|
||||
* `.on(Event, fold)` call chain down to the `defineState` call; returns the key
|
||||
* name plus the replayable metadata when the chain promotes the key.
|
||||
*/
|
||||
function parseDefineStateChain(
|
||||
initializer: CallExpression,
|
||||
): { keyName: string; replayable?: KeyDef['replayable'] } | undefined {
|
||||
|
|
@ -260,7 +200,6 @@ function parseDefineStateChain(
|
|||
}
|
||||
}
|
||||
|
||||
/** Resolve a `.register(...)` argument back to its `defineState` constant. */
|
||||
function resolveKeyDef(
|
||||
identifier: Identifier,
|
||||
defs: ReadonlyMap<VariableDeclaration, KeyDef>,
|
||||
|
|
@ -275,7 +214,6 @@ function resolveKeyDef(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/** Pass 2 — every `.register(key)` call site whose argument is a state key. */
|
||||
function collectRegistrations(
|
||||
project: Project,
|
||||
defs: ReadonlyMap<VariableDeclaration, KeyDef>,
|
||||
|
|
@ -331,36 +269,19 @@ function createProject(): Project {
|
|||
return project;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Type expansion — render every key's value type fully inline.
|
||||
//
|
||||
// Every named type declared in the repo is expanded at the use site and marked
|
||||
// with a `/* TypeName — source/file.ts */` comment; a recursion point stops
|
||||
// with a `/* TypeName — recursive (...) */ unknown` marker. Types from lib
|
||||
// (`Map`, `Set`, `Date`, …) or node_modules are ambient and keep their names
|
||||
// (type arguments are still rendered recursively). Generic instantiations and
|
||||
// anonymous shapes are expanded structurally from their apparent members, so
|
||||
// the checker always hands us substituted, concrete member types.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const NO_TRUNCATION = ts.TypeFormatFlags.NoTruncation;
|
||||
|
||||
class TypeRenderer {
|
||||
private readonly checker: ts.TypeChecker;
|
||||
/** Cycle guard for anonymous / generic-instantiation structural expansion. */
|
||||
private readonly expanding = new Set<ts.Type>();
|
||||
/** Named types currently being expanded along this path (recursion guard). */
|
||||
private readonly expandingNamed: ts.Symbol[] = [];
|
||||
/** Ambient names kept as-is whose declaration lives outside the TS lib. */
|
||||
readonly externals = new Set<string>();
|
||||
/** Degradations worth reporting (cycle fallbacks). */
|
||||
readonly warnings = new Set<string>();
|
||||
|
||||
constructor(private readonly project: Project) {
|
||||
this.checker = project.getTypeChecker().compilerObject;
|
||||
}
|
||||
|
||||
/** Render the value type `T` of a key's `StateKey<T>`. */
|
||||
renderKeyType(def: KeyDef): string {
|
||||
const valueType = def.declaration.getType().getTypeArguments()[0];
|
||||
if (valueType === undefined) {
|
||||
|
|
@ -371,8 +292,6 @@ class TypeRenderer {
|
|||
return this.renderType(valueType, def.declaration, 0);
|
||||
}
|
||||
|
||||
// -- core dispatch --------------------------------------------------------
|
||||
|
||||
private renderType(
|
||||
type: MorphType,
|
||||
location: Node,
|
||||
|
|
@ -381,12 +300,10 @@ class TypeRenderer {
|
|||
): string {
|
||||
if (depth > 40) return this.fallback(type, location, 'depth cap');
|
||||
|
||||
// A single enum-literal type (e.g. `FaultKind.A`) — value + enum comment.
|
||||
if ((type.getFlags() & ts.TypeFlags.EnumLiteral) !== 0) {
|
||||
return this.renderEnumLiteral(type);
|
||||
}
|
||||
|
||||
// The boolean union (`false | true`) collapses to `boolean`.
|
||||
if (type.isUnion() && (type.getFlags() & ts.TypeFlags.Boolean) !== 0) return 'boolean';
|
||||
|
||||
if (type.isUnion()) {
|
||||
|
|
@ -431,11 +348,6 @@ class TypeRenderer {
|
|||
return this.fallback(type, location, 'unhandled type kind');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render union members: a `false | true` pair anywhere collapses to
|
||||
* `boolean`, `null`/`undefined` sort last, duplicates removed, and parens
|
||||
* are only added when the union actually has multiple members.
|
||||
*/
|
||||
private renderUnionMembers(
|
||||
members: readonly MorphType[],
|
||||
location: Node,
|
||||
|
|
@ -488,10 +400,8 @@ class TypeRenderer {
|
|||
);
|
||||
}
|
||||
|
||||
/** typeToString is only safe on leaf types (never emits `import(...)`). */
|
||||
private leafText(type: MorphType): string {
|
||||
const text = this.checker.typeToString(type.compilerType, undefined, NO_TRUNCATION);
|
||||
// Normalize double-quoted string literals to the repo's single-quote style.
|
||||
if (text.length >= 2 && text.startsWith('"') && text.endsWith('"')) {
|
||||
const value = JSON.parse(text) as string;
|
||||
return value.includes("'") ? JSON.stringify(value) : `'${value}'`;
|
||||
|
|
@ -509,9 +419,6 @@ class TypeRenderer {
|
|||
return text;
|
||||
}
|
||||
|
||||
// -- enums ----------------------------------------------------------------
|
||||
|
||||
/** The literal value of an enum-literal type, quoted TS-style. */
|
||||
private enumLiteralValue(type: MorphType): string {
|
||||
const value = (type.compilerType as ts.LiteralType).value;
|
||||
if (typeof value === 'string') {
|
||||
|
|
@ -521,7 +428,6 @@ class TypeRenderer {
|
|||
return this.leafText(type);
|
||||
}
|
||||
|
||||
/** The enum declaration backing an enum-literal type, if any. */
|
||||
private enumDeclOf(type: MorphType): Node | undefined {
|
||||
const memberDecl = type.getSymbol()?.getDeclarations()[0];
|
||||
if (memberDecl === undefined || !Node.isEnumMember(memberDecl)) return undefined;
|
||||
|
|
@ -540,7 +446,6 @@ class TypeRenderer {
|
|||
return text;
|
||||
}
|
||||
|
||||
/** Collapse a union covering every member of one enum: comment + values. */
|
||||
private tryRenderEnumUnion(type: MorphType): string | undefined {
|
||||
const members = type.getUnionTypes();
|
||||
if (members.length === 0) return undefined;
|
||||
|
|
@ -560,13 +465,6 @@ class TypeRenderer {
|
|||
return `/* ${sym.getName()} — ${repoRelative(enumDecl.getSourceFile().getFilePath())} */ ${values.join(' | ')}`;
|
||||
}
|
||||
|
||||
// -- named-type annotation --------------------------------------------------
|
||||
|
||||
/**
|
||||
* Where do the symbol's declarations live: repo ('named' — expand inline
|
||||
* with a name comment), lib/node_modules ('ambient' — keep the name), or
|
||||
* mixed/anonymous ('inline' — expand without a comment).
|
||||
*/
|
||||
private classify(sym: MorphSymbol): 'named' | 'ambient' | 'inline' {
|
||||
const decls = sym.getDeclarations();
|
||||
if (decls.length === 0) return 'inline';
|
||||
|
|
@ -599,10 +497,6 @@ class TypeRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `Name — origin` comment prefixed to the expansion. A named type already
|
||||
* on the expansion path stops with a recursion marker instead.
|
||||
*/
|
||||
private renderNamed(sym: MorphSymbol, expand: () => string): string {
|
||||
const decl = sym.getDeclarations()[0];
|
||||
const origin =
|
||||
|
|
@ -621,14 +515,6 @@ class TypeRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a type through the alias it was referenced with, when that alias is
|
||||
* worth keeping: a repo-declared non-generic alias expands inline under its
|
||||
* name comment; a lib or node_modules alias (`Readonly`, `Record`,
|
||||
* `Partial`, …) is referenced as `Name<args>` with recursive arguments.
|
||||
* `skipSymbol` suppresses the alias's own annotation while its right-hand
|
||||
* side is being rendered (the alias type still carries itself as aliasSymbol).
|
||||
*/
|
||||
private tryRenderAlias(
|
||||
type: MorphType,
|
||||
location: Node,
|
||||
|
|
@ -655,8 +541,6 @@ class TypeRenderer {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
// -- object types -----------------------------------------------------------
|
||||
|
||||
private renderObjectType(
|
||||
type: MorphType,
|
||||
location: Node,
|
||||
|
|
@ -667,8 +551,6 @@ class TypeRenderer {
|
|||
if (alias !== undefined) return alias;
|
||||
const sym = type.getSymbol();
|
||||
const typeArgs = type.getTypeArguments();
|
||||
// `__type`/`__object` are checker names for anonymous shapes — they are
|
||||
// never real symbols, so skip the named-type paths and expand structurally.
|
||||
const anonymous = sym === undefined || /^__(type|object)$/.test(sym.getName());
|
||||
if (!anonymous && sym.compilerSymbol !== skipSymbol) {
|
||||
const kind = this.classify(sym);
|
||||
|
|
@ -686,9 +568,7 @@ class TypeRenderer {
|
|||
return this.renderStructural(type, location, depth);
|
||||
}
|
||||
|
||||
/** Structural rendering from the type's apparent members (braced or arrow). */
|
||||
private renderStructural(type: MorphType, location: Node, depth: number): string {
|
||||
// Cycle guard for self-referential instantiations expanded inline.
|
||||
if (this.expanding.has(type.compilerType)) {
|
||||
return this.fallback(type, location, 'cycle expanding');
|
||||
}
|
||||
|
|
@ -719,7 +599,6 @@ class TypeRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
/** Member lines of an object type, each indented by two spaces. */
|
||||
private renderObjectBody(
|
||||
type: MorphType,
|
||||
location: Node,
|
||||
|
|
@ -736,7 +615,6 @@ class TypeRenderer {
|
|||
const at = decl ?? location;
|
||||
const propType = prop.getTypeAtLocation(at);
|
||||
const optional = (prop.getFlags() & ts.SymbolFlags.Optional) !== 0;
|
||||
// An optional prop's `| undefined` is redundant with the `?` — drop it.
|
||||
const rendered =
|
||||
optional && propType.isUnion()
|
||||
? this.renderUnionMembers(
|
||||
|
|
@ -818,10 +696,6 @@ class TypeRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Manifest rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function renderManifest(
|
||||
model: StateManifestModel,
|
||||
project: Project,
|
||||
|
|
@ -837,7 +711,6 @@ function renderManifest(
|
|||
);
|
||||
}
|
||||
|
||||
// Snapshot interfaces — rendering these fills the external-name registry.
|
||||
const sections: string[] = [];
|
||||
for (const scope of SCOPES) {
|
||||
const regs = byScope.get(scope.dir) ?? [];
|
||||
|
|
@ -969,10 +842,6 @@ export function buildStateManifest(): string {
|
|||
return buildAll().manifest;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function main(): void {
|
||||
const check = process.argv.includes('--check');
|
||||
const { model, manifest, warnings } = buildAll();
|
||||
|
|
|
|||
|
|
@ -1,28 +1,3 @@
|
|||
/**
|
||||
* Generates `docs/wire-manifest.d.ts` — the single place to see every durable
|
||||
* wire record type declared as an `Event2` subclass (`static type` +
|
||||
* `static durable = true` + `static schema`).
|
||||
*
|
||||
* Two passes:
|
||||
* 1. Static scan of `src/**` maps each durable event type to the source file
|
||||
* that declares it — the "owner" — and collects the migration chain from
|
||||
* `src/wire/migration/v*.ts`.
|
||||
* 2. Runtime pass imports `src/index.ts` plus every event/state module found
|
||||
* in the static pass ("import = register") and drains `EVENT2_REGISTRY`
|
||||
* (type → class → schema) and `REPLAYABLE_STATE_KEYS` (folding states, blob
|
||||
* codecs) exactly as the running process sees them.
|
||||
*
|
||||
* The output is a `.d.ts` — one payload declaration per record type, with a
|
||||
* `WirePayloadMap` from record type to declaration — using real TypeScript
|
||||
* type syntax for the sketches.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm --filter @moonshot-ai/agent-core-v2 gen:wire-manifest # write the file
|
||||
* pnpm --filter @moonshot-ai/agent-core-v2 gen:wire-manifest --check # freshness check (CI-style)
|
||||
*
|
||||
* Freshness is also enforced by `test/wire/wireManifest.test.ts`.
|
||||
*/
|
||||
|
||||
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
|
@ -43,10 +18,6 @@ const PKG = join(import.meta.dirname, '..');
|
|||
const SRC = join(PKG, 'src');
|
||||
export const MANIFEST_PATH = join(PKG, 'docs', 'wire-manifest.d.ts');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Static pass — durable event type → owner file; migration chain
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function walk(dir: string, out: string[] = []): string[] {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const p = join(dir, entry);
|
||||
|
|
@ -60,14 +31,6 @@ const TYPE_DECL_RE = /static\s+override\s+readonly\s+type\s*=\s*'([^']+)'/g;
|
|||
const DURABLE_DECL_RE = /static\s+override\s+readonly\s+durable\s*=\s*true/;
|
||||
const CLASS_DECL_RE = /class\s+(\w+)\s+extends\s+Event2/g;
|
||||
|
||||
/**
|
||||
* event type → owner file (relative to the package root), the files worth
|
||||
* importing for the runtime pass, the types statically declared durable
|
||||
* (the class window between one `type` declaration and the next carries
|
||||
* `durable = true`), and the event class name → type map (the class window
|
||||
* between one `class … extends Event2` declaration and the next carries one
|
||||
* `static type`).
|
||||
*/
|
||||
function scanEventDeclarations(): {
|
||||
owners: Map<string, string>;
|
||||
importFiles: string[];
|
||||
|
|
@ -104,7 +67,6 @@ function scanEventDeclarations(): {
|
|||
return { owners, importFiles, durableTypes, classTypes };
|
||||
}
|
||||
|
||||
/** `1.0 -> 1.1 -> ...` chain read from the `src/wire/migration/v*.ts` files. */
|
||||
function scanMigrationChain(): string {
|
||||
const dir = join(SRC, 'wire', 'migration');
|
||||
const pairs: { source: string; target: string }[] = [];
|
||||
|
|
@ -122,10 +84,6 @@ function scanMigrationChain(): string {
|
|||
return chain.join(' -> ');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Static pass — replayable state keys and their fold targets
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ReplayableStateScan {
|
||||
readonly keyName: string;
|
||||
readonly constName: string;
|
||||
|
|
@ -138,7 +96,6 @@ const ON_FOLD_RE = /\.on\(\s*([A-Za-z_$][\w$]*)/g;
|
|||
const KEY_ON_RE = /\b([A-Za-z_$][\w$]*)\.on\(\s*([A-Za-z_$][\w$]*)/g;
|
||||
const PROTOCOL_EVENT_RE = /(?:appendMessage|applyCompaction|clear|undo):\s*([A-Za-z_$][\w$]*)/g;
|
||||
|
||||
/** Balanced-paren read of the argument list starting at the `(` after a chain method. */
|
||||
function readCallArguments(text: string, parenIndex: number): string {
|
||||
let depth = 0;
|
||||
for (let i = parenIndex; i < text.length; i++) {
|
||||
|
|
@ -161,11 +118,6 @@ function readCallArguments(text: string, parenIndex: number): string {
|
|||
return text.slice(parenIndex + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* `readExpression` variant for `defineState(...)` chains: the fold bodies are
|
||||
* arbitrary code, where `<` / `>` are comparison operators as often as generic
|
||||
* brackets — only `()` `{}` `[]` delimit the statement reliably.
|
||||
*/
|
||||
function readChain(source: string, start: number): string {
|
||||
let depth = 0;
|
||||
const n = source.length;
|
||||
|
|
@ -214,8 +166,6 @@ function scanReplayableStates(): ReplayableStateScan[] {
|
|||
byConst.set(constName, scan);
|
||||
}
|
||||
}
|
||||
// A key's fold vocabulary may grow outside its defining chain
|
||||
// (`otherKey.on(Event, …)` in a feature module).
|
||||
for (const file of walk(SRC)) {
|
||||
const source = readFileSync(file, 'utf-8');
|
||||
if (!source.includes('.on(')) continue;
|
||||
|
|
@ -229,7 +179,6 @@ function scanReplayableStates(): ReplayableStateScan[] {
|
|||
return states;
|
||||
}
|
||||
|
||||
/** The four undoable-protocol event types, resolved through the class-name map. */
|
||||
function scanUndoableProtocolTypes(classTypes: ReadonlyMap<string, string>): string[] {
|
||||
for (const file of walk(SRC)) {
|
||||
const source = readFileSync(file, 'utf-8');
|
||||
|
|
@ -252,23 +201,12 @@ function scanUndoableProtocolTypes(classTypes: ReadonlyMap<string, string>): str
|
|||
throw new Error('[gen-wire-manifest] registerUndoableProtocol call not found under src/');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Payload sketch
|
||||
//
|
||||
// A Sketch is a small tree: strings are one-line type annotations, dicts are
|
||||
// object shapes, and a one-element array marks an array-of shape. The d.ts
|
||||
// renderer below turns the tree into real TypeScript syntax.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type SketchDict = { [key: string]: Sketch };
|
||||
type Sketch = string | SketchDict | [Sketch];
|
||||
|
||||
/** First key of a dict produced by expanding a named type. */
|
||||
const TYPE_KEY = '_type';
|
||||
/** Marker key rendered as a `// …` comment when a field list is capped. */
|
||||
const MORE_KEY = '…';
|
||||
|
||||
/** Compact one-line rendering of a Sketch (used inside unions/intersections). */
|
||||
function stringifySketch(sketch: Sketch): string {
|
||||
if (typeof sketch === 'string') return sketch;
|
||||
if (Array.isArray(sketch)) {
|
||||
|
|
@ -280,7 +218,6 @@ function stringifySketch(sketch: Sketch): string {
|
|||
.join(', ')} }`;
|
||||
}
|
||||
|
||||
/** Build a Sketch tree from a zod JSON-schema projection. */
|
||||
function sketchFromJsonSchema(schema: unknown, root: JsonSchema, depth: number): Sketch {
|
||||
const resolved = resolveRef(schema, root);
|
||||
const s = asJsonSchema(resolved);
|
||||
|
|
@ -301,7 +238,6 @@ function sketchFromJsonSchema(schema: unknown, root: JsonSchema, depth: number):
|
|||
return describeType(resolved, tsQuote);
|
||||
}
|
||||
|
||||
/** Build the payload Sketch tree for one op (all three data paths converge). */
|
||||
function buildPayloadSketch(
|
||||
schema: unknown,
|
||||
staticSketch?: string | Map<string, Sketch>,
|
||||
|
|
@ -322,7 +258,6 @@ function buildPayloadSketch(
|
|||
}
|
||||
return dict;
|
||||
}
|
||||
// An empty object schema (`z.object({})`) is a payload-less record.
|
||||
if (
|
||||
jsonSchema.type === 'object' &&
|
||||
(jsonSchema.additionalProperties === undefined || jsonSchema.additionalProperties === false)
|
||||
|
|
@ -332,10 +267,6 @@ function buildPayloadSketch(
|
|||
return describeType(jsonSchema, tsQuote);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// d.ts rendering — Sketch tree → TypeScript declarations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function pascalCase(name: string): string {
|
||||
return name
|
||||
.split(/[^A-Za-z0-9]+/)
|
||||
|
|
@ -348,11 +279,6 @@ function tsFieldKey(key: string): string {
|
|||
return /^[$A-Z_a-z][$\w]*$/.test(key) ? key : JSON.stringify(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a one-line sketch annotation into a valid TS type expression.
|
||||
* Returns the type plus an optional doc note (the expanded type's name, or a
|
||||
* hoisted shared spread that cannot be expressed inline).
|
||||
*/
|
||||
function sketchStringToTs(text: string): { type: string; doc?: string } {
|
||||
let t = text.trim();
|
||||
const docs: string[] = [];
|
||||
|
|
@ -361,7 +287,6 @@ function sketchStringToTs(text: string): { type: string; doc?: string } {
|
|||
docs.push(named[1]);
|
||||
t = named[2].trim();
|
||||
}
|
||||
// A hoisted shared spread (`...base & A | B`) becomes a doc note + variants.
|
||||
const spread = /^((?:\.\.\.[$\w]+(?: \+ )?)+) & ([\s\S]+)$/.exec(t);
|
||||
if (spread?.[1] !== undefined && spread[2] !== undefined) {
|
||||
docs.push(`shared base: ${spread[1]}`);
|
||||
|
|
@ -373,10 +298,6 @@ function sketchStringToTs(text: string): { type: string; doc?: string } {
|
|||
return { type: t, doc: docs.length > 0 ? docs.join(' · ') : undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a Sketch as TS type-expression lines. The first line continues after
|
||||
* the field's `key: `; subsequent lines carry `indent`.
|
||||
*/
|
||||
function renderTsType(sketch: Sketch, indent: string): { doc?: string; lines: string[] } {
|
||||
if (typeof sketch === 'string') {
|
||||
const { type, doc } = sketchStringToTs(sketch);
|
||||
|
|
@ -401,7 +322,7 @@ function emitTsDict(lines: string[], dict: SketchDict, indent: string): void {
|
|||
lines.push(`${indent}// …`);
|
||||
continue;
|
||||
}
|
||||
if (key === TYPE_KEY) continue; // surfaces as the field's doc comment
|
||||
if (key === TYPE_KEY) continue;
|
||||
if (key.startsWith('...')) {
|
||||
lines.push(`${indent}// spread: ${key}`);
|
||||
continue;
|
||||
|
|
@ -418,7 +339,6 @@ function emitTsDict(lines: string[], dict: SketchDict, indent: string): void {
|
|||
}
|
||||
}
|
||||
|
||||
/** One record type's payload declaration (`interface` for objects, `type` otherwise). */
|
||||
function renderPayloadDecl(
|
||||
entry: { type: string },
|
||||
owner: string | undefined,
|
||||
|
|
@ -436,7 +356,6 @@ function renderPayloadDecl(
|
|||
if (typeof sketch === 'string') {
|
||||
const { type, doc } = sketchStringToTs(sketch);
|
||||
if (type.startsWith('(')) {
|
||||
// Unrepresentable schema note — keep the declaration parseable.
|
||||
header.push(` * ${type.slice(1, -1)}`);
|
||||
header.push(' */');
|
||||
return [...header, `interface ${name} {\n ${nameField}\n}`, ''];
|
||||
|
|
@ -470,12 +389,6 @@ function renderPayloadDecl(
|
|||
return lines;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Static payload fallback — sketch fields from source when the zod schema
|
||||
// cannot be projected to JSON Schema (payloads using `z.custom<T>()`)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Find the index of the closer matching the opener at `start` (quotes-aware). */
|
||||
function matchDelimiter(source: string, start: number, open: string, close: string): number {
|
||||
let depth = 0;
|
||||
for (let i = start; i < source.length; i++) {
|
||||
|
|
@ -509,7 +422,6 @@ function matchDelimiter(source: string, start: number, open: string, close: stri
|
|||
return -1;
|
||||
}
|
||||
|
||||
/** Split `body` into top-level parts on any of `separators` (quotes/nesting-aware). */
|
||||
function splitTopLevel(body: string, separators: readonly string[] = [',']): string[] {
|
||||
const parts: string[] = [];
|
||||
let depth = 0;
|
||||
|
|
@ -537,7 +449,6 @@ function splitTopLevel(body: string, separators: readonly string[] = [',']): str
|
|||
return parts.filter((p) => p !== '');
|
||||
}
|
||||
|
||||
/** Split an object literal's body into top-level `key: expr` fields. */
|
||||
function splitObjectFields(body: string): Map<string, string> {
|
||||
const fields = new Map<string, string>();
|
||||
for (const part of splitTopLevel(body)) {
|
||||
|
|
@ -552,13 +463,11 @@ function splitObjectFields(body: string): Map<string, string> {
|
|||
return fields;
|
||||
}
|
||||
|
||||
/** Extract the body of the first balanced `{...}` in `text` starting at `braceIndex`. */
|
||||
function objectBody(text: string, braceIndex: number): string | undefined {
|
||||
const end = matchDelimiter(text, braceIndex, '{', '}');
|
||||
return end === -1 ? undefined : text.slice(braceIndex + 1, end);
|
||||
}
|
||||
|
||||
/** Read one expression from `start` up to the top-level `;` that ends the statement. */
|
||||
function readExpression(source: string, start: number): string {
|
||||
let depth = 0;
|
||||
const n = source.length;
|
||||
|
|
@ -580,7 +489,6 @@ function readExpression(source: string, start: number): string {
|
|||
return source.slice(start);
|
||||
}
|
||||
|
||||
/** Quote a string literal TS-style (single quotes) so sketches need no JSON escapes. */
|
||||
function tsQuote(raw: string): string {
|
||||
return raw.includes("'") ? JSON.stringify(raw) : `'${raw}'`;
|
||||
}
|
||||
|
|
@ -589,15 +497,12 @@ function escapeRegExp(raw: string): string {
|
|||
return raw.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
/** Resolve a `schema:` expression to an object-literal body, following local consts. */
|
||||
function resolveSchemaLiteral(expr: string, source: string, depth = 0): string | undefined {
|
||||
if (depth > 2) return undefined;
|
||||
// z.object({ ... }) / z.strictObject({ ... }) — inline literal.
|
||||
const inline = /^z\.\w*[oO]bject\s*\(/.exec(expr);
|
||||
if (inline !== null) {
|
||||
const rest = expr.slice(inline[0].length).trimStart();
|
||||
if (rest.startsWith('{')) return objectBody(rest, 0);
|
||||
// z.object(SHAPE_CONST) — look up the local shape const.
|
||||
const shapeName = /^([$\w]+)/.exec(rest)?.[1];
|
||||
if (shapeName !== undefined) {
|
||||
const constRe = new RegExp(`const\\s+${shapeName}\\s*(?::[^=;]+)?=\\s*\\{`);
|
||||
|
|
@ -606,7 +511,6 @@ function resolveSchemaLiteral(expr: string, source: string, depth = 0): string |
|
|||
}
|
||||
return undefined;
|
||||
}
|
||||
// schema: SOME_CONST — follow `const X = z.object(...)` in the same file.
|
||||
const ident = /^([$\w]+)$/.exec(expr.trim())?.[1];
|
||||
if (ident !== undefined) {
|
||||
const constRe = new RegExp(`const\\s+${ident}\\s*(?::[^=;]+)?=\\s*`);
|
||||
|
|
@ -619,13 +523,6 @@ function resolveSchemaLiteral(expr: string, source: string, depth = 0): string |
|
|||
return undefined;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TS type summarizer — expand `z.custom<T>()` type names into readable sketches
|
||||
// by resolving the alias (or interface) across local definitions, imports, and
|
||||
// re-exports. Discriminated unions collapse to `union on type: "a" | "b"`.
|
||||
// Resolution work is bounded by a per-expansion step budget.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface Budget {
|
||||
remaining: number;
|
||||
}
|
||||
|
|
@ -654,7 +551,6 @@ interface TsField {
|
|||
readonly optional: boolean;
|
||||
}
|
||||
|
||||
/** Split a TS object type literal body into fields (separators: `;` / `,`). */
|
||||
function splitTsTypeFields(body: string): Map<string, TsField> {
|
||||
const fields = new Map<string, TsField>();
|
||||
for (const part of splitTopLevel(body, [';', ','])) {
|
||||
|
|
@ -697,7 +593,6 @@ function renderTsFields(
|
|||
return dict;
|
||||
}
|
||||
|
||||
/** Find a local `type X = ...` / `interface X {...}` definition's RHS text. */
|
||||
function findTsTypeDef(name: string, file: string): string | undefined {
|
||||
const source = readCached(file);
|
||||
const typeRe = new RegExp(`(?:export\\s+)?type\\s+${name}(?:<[^>;=]*>)?\\s*=\\s*`);
|
||||
|
|
@ -712,7 +607,6 @@ function findTsTypeDef(name: string, file: string): string | undefined {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/** Find the module specifier a name is imported (or named-re-exported) from. */
|
||||
function findImportSource(file: string, name: string): string | undefined {
|
||||
const source = readCached(file);
|
||||
const re = /(?:import|export)\s+(?:type\s+)?\{([^}]+)\}\s*from\s*'([^']+)'/g;
|
||||
|
|
@ -744,8 +638,6 @@ function summarizeTsUnion(
|
|||
charBudget: number,
|
||||
depth: number,
|
||||
): string {
|
||||
// Resolve member idents one level so alias unions (ContextMessage = A | B | C)
|
||||
// still expose their object shapes.
|
||||
const resolved = members.map((m) => {
|
||||
const t = m.trim();
|
||||
if (/^[$\w]+$/.test(t)) {
|
||||
|
|
@ -757,7 +649,6 @@ function summarizeTsUnion(
|
|||
const bodies = resolved.map((m) => (m.trim().startsWith('{') ? objectBody(m.trim(), 0) : undefined));
|
||||
if (bodies.length > 0 && bodies.every((b) => b !== undefined)) {
|
||||
const fieldMaps = bodies.map((b) => splitTsTypeFields(b!));
|
||||
// Discriminated union: one field is a string literal in every member.
|
||||
for (const [name, info] of fieldMaps[0]!) {
|
||||
if (
|
||||
/^'[^']*'$/.test(info.type) &&
|
||||
|
|
@ -767,7 +658,6 @@ function summarizeTsUnion(
|
|||
return truncate(`union on ${name}: ${values.join(' | ')}`, charBudget);
|
||||
}
|
||||
}
|
||||
// Unions stay one-line strings; object members use the compact renderer.
|
||||
return truncate(
|
||||
fieldMaps
|
||||
.map((fm) => stringifySketch(renderTsFields(fm, file, budget, charBudget, depth + 1)))
|
||||
|
|
@ -810,7 +700,6 @@ function summarizeTsTypeExpr(
|
|||
if (intersections.length > 1) {
|
||||
if (!spend(budget)) return truncate(text, 80);
|
||||
const sides = intersections.map((m) => summarizeTsTypeExpr(m, file, budget, charBudget, depth + 1));
|
||||
// An intersection of object shapes merges into one dictionary.
|
||||
if (sides.every((side) => typeof side !== 'string' && !Array.isArray(side))) {
|
||||
return Object.assign({}, ...sides) as SketchDict;
|
||||
}
|
||||
|
|
@ -830,7 +719,6 @@ function summarizeTsTypeExpr(
|
|||
return truncate(text, 80);
|
||||
}
|
||||
|
||||
/** Resolve a type name to a readable summary across aliases, imports, re-exports. */
|
||||
function summarizeTsType(name: string, fromFile: string, budget: Budget): Sketch | undefined {
|
||||
if (!spend(budget)) return undefined;
|
||||
const def = findTsTypeDef(name, fromFile);
|
||||
|
|
@ -849,16 +737,8 @@ function summarizeTsType(name: string, fromFile: string, budget: Budget): Sketch
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a zod field expression as a Sketch, in the same notation the
|
||||
* JSON-Schema path produces (`string`, `'a' | 'b'`, `Foo[]`). `z.custom<T>()`
|
||||
* and bare type idents expand through the TS type summarizer — object shapes
|
||||
* become nested dicts (keyed with the type name under `_type`), everything
|
||||
* else stays a one-line string.
|
||||
*/
|
||||
function friendlyZodExpr(expr: string, ownerFile: string, depth = 0): Sketch {
|
||||
let text = expr.replaceAll(/\s+/g, ' ').trim();
|
||||
// Strip trailing modifiers the sketch does not mark.
|
||||
let stripped = true;
|
||||
while (stripped) {
|
||||
stripped = false;
|
||||
|
|
@ -874,8 +754,6 @@ function friendlyZodExpr(expr: string, ownerFile: string, depth = 0): Sketch {
|
|||
const custom = /^z\.custom<(.+)>\(\)$/.exec(text);
|
||||
if (custom?.[1] !== undefined) {
|
||||
const typeName = custom[1].trim();
|
||||
// Expand the TS type only at the top levels — nested fields keep the bare
|
||||
// type name so long union member sketches stay readable.
|
||||
if (depth > 1) return typeName;
|
||||
const summary = summarizeTsType(typeName, ownerFile, TS_BUDGET());
|
||||
if (summary === undefined) return typeName;
|
||||
|
|
@ -949,14 +827,12 @@ function friendlyZodExpr(expr: string, ownerFile: string, depth = 0): Sketch {
|
|||
return truncate(text, 80);
|
||||
}
|
||||
|
||||
/** Sketch a `z.union([...])` body (one-line string); object members get field sketches. */
|
||||
function friendlyZodUnion(body: string, ownerFile: string, depth: number): string {
|
||||
const members = splitTopLevel(body.trim().replace(/^\[/, '').replace(/\]$/, ''));
|
||||
const source = readCached(ownerFile);
|
||||
const bodies = members.map((m) => resolveSchemaLiteral(m, source));
|
||||
if (members.length > 0 && bodies.every((b) => b !== undefined)) {
|
||||
const fieldMaps = bodies.map((b) => splitObjectFields(b));
|
||||
// Hoist spreads shared by every member (`...base & { … } | { … }`).
|
||||
const spreadSets = fieldMaps.map((fm) => [...fm.keys()].filter((k) => fm.get(k) === ''));
|
||||
const commonSpreads = (spreadSets[0] ?? []).filter((s) =>
|
||||
spreadSets.every((set) => set.includes(s)),
|
||||
|
|
@ -979,12 +855,6 @@ function friendlyZodUnion(body: string, ownerFile: string, depth: number): strin
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort payload sketch from the owner source for schemas that use
|
||||
* `z.custom` (not representable as JSON Schema). Returns a field map for
|
||||
* object payloads, a type string for whole-payload custom schemas, or
|
||||
* `undefined` when the source shape is not recognized.
|
||||
*/
|
||||
function sketchPayloadFromSource(
|
||||
ownerFile: string,
|
||||
type: string,
|
||||
|
|
@ -1025,15 +895,8 @@ function sketchPayloadFromSource(
|
|||
return sketch;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Manifest rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function buildWireManifest(): Promise<string> {
|
||||
const { owners, importFiles, durableTypes, classTypes } = scanEventDeclarations();
|
||||
// "import = register": loading the package root plus every event/state module
|
||||
// found in the static pass fills EVENT2_REGISTRY, even for
|
||||
// modules index.ts does not load.
|
||||
await import('../src/index.ts');
|
||||
for (const file of importFiles) {
|
||||
await import(relative(join(PKG, 'scripts'), file));
|
||||
|
|
@ -1045,7 +908,6 @@ export async function buildWireManifest(): Promise<string> {
|
|||
const entries = [...EVENT2_REGISTRY.values()].toSorted((a, b) => a.type.localeCompare(b.type));
|
||||
const migrationChain = scanMigrationChain();
|
||||
|
||||
// type → folding states / blob codec owners, scanned from the defineState chains.
|
||||
const folding = new Map<string, { states: string[]; blobs: string[] }>();
|
||||
const protocolTypes = scanUndoableProtocolTypes(classTypes);
|
||||
for (const state of scanReplayableStates()) {
|
||||
|
|
@ -1138,7 +1000,6 @@ export async function buildWireManifest(): Promise<string> {
|
|||
declNames.push([entry.type, `${pascalCase(entry.type)}Payload`]);
|
||||
}
|
||||
|
||||
// Record type → payload declaration map.
|
||||
out.push('/** Record type → payload sketch. */');
|
||||
out.push('interface WirePayloadMap {');
|
||||
for (const [type, declName] of declNames) {
|
||||
|
|
@ -1149,10 +1010,6 @@ export async function buildWireManifest(): Promise<string> {
|
|||
return out.join('\n');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const check = process.argv.includes('--check');
|
||||
const manifest = await buildWireManifest();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,3 @@
|
|||
/**
|
||||
* Shared JSON-schema helpers for the manifest generators
|
||||
* (`gen-config-manifest.mts`, `gen-wire-manifest.mts`).
|
||||
*
|
||||
* Both generators drain runtime registries that carry zod schemas and render
|
||||
* field/type sketches from their JSON Schema projection.
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
|
|
@ -76,9 +68,6 @@ export function describeType(
|
|||
}
|
||||
if (s.type === 'array') return `${describeType(s.items, quoteString)}[]`;
|
||||
if (s.type === 'object') {
|
||||
// Named sub-tables (zod objects emit `additionalProperties: false`) are
|
||||
// rendered by the caller; only a schema-valued additionalProperties marks
|
||||
// a true record.
|
||||
if (isRecord(s.properties)) return 'object';
|
||||
if (isRecord(s.additionalProperties)) {
|
||||
return `record<string, ${describeType(s.additionalProperties, quoteString)}>`;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,3 @@
|
|||
/**
|
||||
* `_base.asyncEventQueue` — push-based async iterable.
|
||||
*
|
||||
* Bridges a callback-driven producer (e.g. a streaming LLM's `onMessagePart`)
|
||||
* to an async-generator consumer. Values pushed while there is a pending
|
||||
* `next()` waiter are delivered immediately; otherwise they buffer in-order.
|
||||
* `end()` signals normal termination; `fail(err)` terminates with an error
|
||||
* that is thrown at the next `next()` (once the buffered values have been
|
||||
* drained). Idempotent — repeated `end`/`fail`/`push` after termination are
|
||||
* no-ops.
|
||||
*
|
||||
* Layer L0 substrate.
|
||||
*/
|
||||
|
||||
export class AsyncEventQueue<T> implements AsyncIterable<T>, AsyncIterator<T> {
|
||||
private readonly values: T[] = [];
|
||||
private readonly waiters: Array<{
|
||||
|
|
|
|||
|
|
@ -1,19 +1,3 @@
|
|||
/**
|
||||
* `_base/contribution` domain — generic source-keyed contribution
|
||||
* registry.
|
||||
*
|
||||
* The storage half of the Contribution / Registry / Catalog extension-point
|
||||
* pattern: a *contribution* is a plain data structure offered by an outer
|
||||
* contributor (a loader, a plugin, a code module); the *registry* stores at
|
||||
* most one contribution per `sourceId` — re-registering the same `sourceId`
|
||||
* replaces the previous entry, which is the only dedup this layer performs.
|
||||
* Content-level dedup (e.g. by item name), ordering, and merge rules are the
|
||||
* Catalog's projection job, never the registry's. `register` returns a handle
|
||||
* whose `dispose` unregisters — but only the entry it registered, so a stale
|
||||
* handle can never evict a newer re-registration. Every mutation fires
|
||||
* `onDidChange` with the affected `sourceId` so catalogs can re-project.
|
||||
*/
|
||||
|
||||
import { Disposable, type IDisposable } from '../di/lifecycle';
|
||||
import { Emitter, type Event } from '../event';
|
||||
|
||||
|
|
@ -27,7 +11,6 @@ export interface RegisterContributionOptions {
|
|||
readonly priority?: number;
|
||||
}
|
||||
|
||||
// NOTE: stays Disposable — its own 'get' collides with the Fiber
|
||||
export class ContributionRegistry<T> extends Disposable {
|
||||
private readonly registrations = new Map<string, ContributionRegistration<T>>();
|
||||
private readonly onDidChangeEmitter = this._register(new Emitter<string>());
|
||||
|
|
|
|||
|
|
@ -1,33 +1,3 @@
|
|||
/**
|
||||
* `di` domain — cascade engine + wait scheduler (L2), one per container, with
|
||||
* tree-wide orchestration (D9: cascades propagate along instance edges across
|
||||
* scopes).
|
||||
*
|
||||
* The dependency graph, request queue, in-flight set, and settle waiters are
|
||||
* shared by the whole scope tree (`CascadeTree`, owned by the root). Every
|
||||
* change (provide / unprovide / update) runs as a single transaction
|
||||
* orchestrated by the engine of the scope where the change was submitted:
|
||||
* ① compute the contagion set from the tree-global graph;
|
||||
* ② broadcast WillCascade to the orchestrator's abort hook (bounded wait,
|
||||
* then forced; failures are best-effort, never a veto);
|
||||
* ③ tear the contagion set down in global reverse topological order, serially
|
||||
* (each scope's engine executes its own units; Active → Unloading →
|
||||
* Pending, or removed for an unprovided token; a descendant scope that dies
|
||||
* mid-transaction is skipped idempotently);
|
||||
* ④ apply the change in its own scope (a replace never passes through the
|
||||
* waiting area);
|
||||
* ⑤ recheck the waiting area across scopes and rebuild satisfied units in
|
||||
* global topological order;
|
||||
* ⑥ append the transaction to the orchestrator's history ring.
|
||||
*
|
||||
* Requests serialize through the tree queue; requests queued together merge
|
||||
* their contagion sets (deduped by scope+token) into one transaction. This is
|
||||
* one transaction across the tree but not a distributed transaction: a single
|
||||
* orchestrator, a deterministic order, local execution per scope. Like the
|
||||
* Ledger, the engine has a sync fast path: with no async abort wait and no
|
||||
* async disposers, a transaction completes within the tick.
|
||||
*/
|
||||
|
||||
import { onUnexpectedError } from '../errors/unexpectedError';
|
||||
import { Emitter, type Event } from '../event';
|
||||
import { isPromiseLike } from '../lifecycle/disposer';
|
||||
|
|
@ -48,7 +18,6 @@ export type UnitActivation = 'eager' | 'ondemand';
|
|||
|
||||
export interface CascadeChange {
|
||||
readonly action: CascadeAction;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
readonly token: ServiceIdentifier<any>;
|
||||
readonly descriptor?: SyncDescriptor<unknown>;
|
||||
readonly instance?: unknown;
|
||||
|
|
@ -96,37 +65,25 @@ export interface CascadeEngineOptions {
|
|||
}
|
||||
|
||||
export interface CascadeHost {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
isRegistered(token: ServiceIdentifier<any>): boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ownerScopeOf(token: ServiceIdentifier<any>): object | undefined;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
isMaterialized(token: ServiceIdentifier<any>): boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
materialize(token: ServiceIdentifier<any>): unknown;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
retire(token: ServiceIdentifier<any>): void | Promise<void>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
applyProvide(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
token: ServiceIdentifier<any>,
|
||||
descriptor: SyncDescriptor<unknown>,
|
||||
config: unknown,
|
||||
): number;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
applyProvideInstance(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
token: ServiceIdentifier<any>,
|
||||
instance: unknown,
|
||||
config: unknown,
|
||||
): number;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
applyUnprovide(token: ServiceIdentifier<any>): void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
recipeOf(token: ServiceIdentifier<any>): SyncDescriptor<unknown> | undefined;
|
||||
dependenciesOf(
|
||||
recipe: SyncDescriptor<unknown>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): Array<ServiceIdentifier<any>>;
|
||||
}
|
||||
|
||||
|
|
@ -225,14 +182,11 @@ export class CascadeTree {
|
|||
|
||||
export class CascadeEngine {
|
||||
private readonly _units = new Map<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ServiceIdentifier<any>,
|
||||
UnitRecord
|
||||
>();
|
||||
private readonly _pendingIndex = new Map<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ServiceIdentifier<any>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
Set<ServiceIdentifier<any>>
|
||||
>();
|
||||
private readonly _history: CascadeHistoryEntry[] = [];
|
||||
|
|
@ -256,28 +210,23 @@ export class CascadeEngine {
|
|||
this._options = { ...this._options, ...options };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
stateOf(token: ServiceIdentifier<any>): UnitState | undefined {
|
||||
return this._units.get(token)?.state;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
activationOf(token: ServiceIdentifier<any>): UnitActivation | undefined {
|
||||
return this._units.get(token)?.activation;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
materializable(token: ServiceIdentifier<any>): boolean {
|
||||
return this._host.recipeOf(token) !== undefined;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
failureOf(token: ServiceIdentifier<any>): unknown {
|
||||
const unit = this._units.get(token);
|
||||
return unit?.state === 'Failed' ? unit.error : undefined;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
isInFlight(token: ServiceIdentifier<any>): boolean {
|
||||
const owner = this._host.ownerScopeOf(token) ?? this._scope;
|
||||
return this._tree.inFlightHas({ scope: owner, token });
|
||||
|
|
@ -335,7 +284,6 @@ export class CascadeEngine {
|
|||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
update(token: ServiceIdentifier<any>, reason?: string): Promise<void> {
|
||||
return this.submit({
|
||||
action: 'update',
|
||||
|
|
@ -356,7 +304,6 @@ export class CascadeEngine {
|
|||
}
|
||||
|
||||
resolveWhenAvailable<T>(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
token: ServiceIdentifier<any>,
|
||||
timeoutMs?: number,
|
||||
): Promise<T> {
|
||||
|
|
@ -387,7 +334,6 @@ export class CascadeEngine {
|
|||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
observedMaterialization(token: ServiceIdentifier<any>): void {
|
||||
const unit = this._units.get(token);
|
||||
if (unit !== undefined && unit.state === 'Pending') {
|
||||
|
|
@ -415,9 +361,7 @@ export class CascadeEngine {
|
|||
this._onDidCascade.dispose();
|
||||
}
|
||||
|
||||
|
||||
_teardownForCascade(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
token: ServiceIdentifier<any>,
|
||||
tornDown: string[],
|
||||
parkAsPending: boolean,
|
||||
|
|
@ -467,7 +411,6 @@ export class CascadeEngine {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private _pump(): void {
|
||||
if (this._tree.running) {
|
||||
return;
|
||||
|
|
@ -505,7 +448,6 @@ export class CascadeEngine {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private _transact(batch: QueuedRequest[]): void | Promise<void> {
|
||||
const changes = mergeBatch(batch);
|
||||
const started = this._options.now?.() ?? Date.now();
|
||||
|
|
@ -656,8 +598,6 @@ export class CascadeEngine {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private _unitFor(token: ServiceIdentifier<any>): UnitRecord {
|
||||
let unit = this._units.get(token);
|
||||
if (unit === undefined) {
|
||||
|
|
@ -669,7 +609,6 @@ export class CascadeEngine {
|
|||
}
|
||||
|
||||
private _setUnitState(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
token: ServiceIdentifier<any>,
|
||||
unit: UnitRecord,
|
||||
state: UnitState,
|
||||
|
|
@ -687,7 +626,6 @@ export class CascadeEngine {
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private _markPending(
|
||||
token: ServiceIdentifier<any>,
|
||||
activation?: UnitActivation,
|
||||
|
|
@ -706,7 +644,6 @@ export class CascadeEngine {
|
|||
private _recheckPending(rebuilt: string[], failed: string[]): void {
|
||||
for (;;) {
|
||||
this._pendingIndex.clear();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const satisfied: ServiceIdentifier<any>[] = [];
|
||||
for (const [token, unit] of this._units) {
|
||||
if (unit.state !== 'Pending') continue;
|
||||
|
|
@ -747,7 +684,6 @@ export class CascadeEngine {
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private _activate(token: ServiceIdentifier<any>, rebuilt: string[], failed: string[]): void {
|
||||
const unit = this._unitFor(token);
|
||||
this._setUnitState(token, unit, 'Activating', undefined);
|
||||
|
|
@ -762,7 +698,6 @@ export class CascadeEngine {
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private _missingDeps(token: ServiceIdentifier<any>): Array<ServiceIdentifier<any>> {
|
||||
const recipe = this._host.recipeOf(token);
|
||||
if (recipe === undefined) {
|
||||
|
|
@ -773,7 +708,6 @@ export class CascadeEngine {
|
|||
.filter((dep) => !this._isAvailable(dep));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private _isAvailable(dep: ServiceIdentifier<any>): boolean {
|
||||
if (!this._host.isRegistered(dep)) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,30 +1,8 @@
|
|||
/**
|
||||
* `di` domain — collection tokens, live views, and the tree-global record
|
||||
* store (L3, D12).
|
||||
*
|
||||
* A contribution point is a `collection<T>(name)` token; contributing is
|
||||
* `this.provide(token, value)` — no registry API. Records physically live
|
||||
* under the provider's scope and are visible to the provider's ancestors AND
|
||||
* descendants (never to sibling subtrees): capabilities flow upward, and a
|
||||
* fold at any tier also sees what its own subtree contributed. Every record
|
||||
* carries the provider unit's name and scope path so folds can group/filter
|
||||
* by source. Record lifetime hangs on the provider's book — provider death
|
||||
* withdraws the record (and scope death tears the provider's book).
|
||||
*
|
||||
* A fold service declares the token as a constructor parameter and receives
|
||||
* a `CollectionView<T>`: `items`/`records` are computed live, `onDidChange`
|
||||
* delivers incremental `{added, removed}` payloads. Collection edges are
|
||||
* recorded in the persistent graph for introspection but never join a
|
||||
* cascade contagion set — a fold refolds incrementally instead of being
|
||||
* rebuilt.
|
||||
*/
|
||||
|
||||
import { Emitter, type Event } from '../event';
|
||||
import type { Ledger } from '../lifecycle/ledger';
|
||||
import { storeCustomDependency, type ServiceIdentifier } from './instantiation';
|
||||
|
||||
export interface CollectionToken<T> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(target: any, key: string | symbol | undefined, index: number): void;
|
||||
|
||||
readonly name: string;
|
||||
|
|
@ -50,7 +28,6 @@ export function collection<T>(
|
|||
return existing as CollectionToken<T>;
|
||||
}
|
||||
const token = function collectionDecorator(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
target: any,
|
||||
_key: string | symbol | undefined,
|
||||
index: number,
|
||||
|
|
@ -58,7 +35,6 @@ export function collection<T>(
|
|||
if (arguments.length !== 3) {
|
||||
throw new Error('@CollectionToken-decorator can only be used to decorate a parameter');
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
storeCustomDependency(token as unknown as ServiceIdentifier<any>, 'collection', target, index);
|
||||
} as unknown as CollectionToken<T>;
|
||||
Object.defineProperty(token, 'toString', {
|
||||
|
|
|
|||
|
|
@ -1,23 +1,7 @@
|
|||
/**
|
||||
* `di` domain — persistent dependency graph (L2 substrate), tree-global.
|
||||
*
|
||||
* One graph is shared by every container of a scope tree. Edges are recorded
|
||||
* when a service's constructor dependencies are resolved and removed when the
|
||||
* consumer is torn down, so the graph always mirrors the live containers.
|
||||
* Both ends of an edge are scope-tagged: a consumer in a child scope may bind
|
||||
* a token owned by an ancestor scope (child → parent only — a parent can never
|
||||
* resolve a child's token, so cross-tree cycles are impossible by
|
||||
* construction). Instance edges bind a consumer to its dependency's
|
||||
* generation (the dependency changes → the consumer is torn down and rebuilt,
|
||||
* across scopes); collection edges (Phase 3) are recorded for introspection
|
||||
* but never join a cascade contagion set.
|
||||
*/
|
||||
|
||||
import type { ServiceIdentifier } from './instantiation';
|
||||
|
||||
export interface ScopedToken {
|
||||
readonly scope: object;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
readonly token: ServiceIdentifier<any>;
|
||||
}
|
||||
|
||||
|
|
@ -31,7 +15,6 @@ export interface DependencyEdge {
|
|||
|
||||
export class PairIndex<V> {
|
||||
private readonly _map = new Map<object, Map<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ServiceIdentifier<any>,
|
||||
V
|
||||
>>();
|
||||
|
|
@ -82,7 +65,6 @@ export class DependencyGraph {
|
|||
addInstance(
|
||||
instance: object,
|
||||
scope: object,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
token: ServiceIdentifier<any>,
|
||||
): void {
|
||||
const ref: ScopedToken = { scope, token };
|
||||
|
|
|
|||
|
|
@ -1,15 +1,8 @@
|
|||
/**
|
||||
* `di` domain — `SyncDescriptor` packaging a constructor and its static arguments.
|
||||
*/
|
||||
|
||||
export class SyncDescriptor<T> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public readonly ctor: any;
|
||||
|
||||
constructor(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ctor: new (...args: any[]) => T,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public readonly staticArguments: ReadonlyArray<any> = [],
|
||||
) {
|
||||
this.ctor = ctor;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,14 @@
|
|||
/**
|
||||
* `di` domain — `CyclicDependencyError` raised on DI dependency cycles.
|
||||
*/
|
||||
|
||||
import type { Graph } from './graph';
|
||||
|
||||
export class CyclicDependencyError extends Error {
|
||||
readonly path: ReadonlyArray<string>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructor(pathOrGraph: ReadonlyArray<string> | Graph<any>) {
|
||||
if (Array.isArray(pathOrGraph)) {
|
||||
const path = pathOrGraph as ReadonlyArray<string>;
|
||||
super(`Cyclic DI dependency detected: ${path.join(' → ')}`);
|
||||
this.path = path;
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const graph = pathOrGraph as Graph<any>;
|
||||
const cycle = graph.findCycleSlow();
|
||||
const detail = cycle ?? `UNABLE to detect cycle, dumping graph:\n${graph.toString()}`;
|
||||
|
|
|
|||
|
|
@ -1,34 +1,3 @@
|
|||
/**
|
||||
* `di` domain — the L3 unit layer: the `Fiber` capability contract, unit
|
||||
* recipes, and the construction protocol that binds them to a container.
|
||||
*
|
||||
* A unit recipe comes in three shapes — a class extending `Service`
|
||||
* (`service.ts`), a function `(fiber, config) => cleanup`, or an object with
|
||||
* `apply(fiber, config)` — carrying optional statics (`name` / `inject` /
|
||||
* `Config`; `Config` is a standard-schema that must validate
|
||||
* synchronously). A materialized unit receives a `Fiber` facade exposing the
|
||||
* five capabilities: `provide` (token-bound units, anonymous sub-units, and
|
||||
* collection records), `effect` (ledger-anchored side effects), `on` (event
|
||||
* subscriptions), `get` (declared-dependency resolution) and `ref` (live
|
||||
* references). Every capability returns a `FiberHandle` — a thenable that
|
||||
* settles once the unit is active, and carries `update` / `dispose`.
|
||||
*
|
||||
* `FiberRuntime` never touches the container directly: it delegates to a
|
||||
* `FiberHost` (implemented by the instantiation service) and anchors every
|
||||
* teardown into the unit's `Ledger`, so provider death withdraws everything
|
||||
* the unit provided. `get` is restricted to the recipe's declared
|
||||
* dependencies (constructor parameters for class recipes, the `inject`
|
||||
* static for function/object recipes).
|
||||
*
|
||||
* The construction protocol bridges class recipes and the container: the
|
||||
* container pushes a `ConstructionFrame`, the `Service` base buffers
|
||||
* capability calls made inside the constructor as `BufferedOp`s (answered
|
||||
* with `PendingFiberHandle`s), and `bindServiceUnit` flushes the buffer
|
||||
* against the freshly bound runtime once construction finishes — 构造期只写
|
||||
* 不读. `ScopeUnits(kind)` mints the per-scope-kind materialization
|
||||
* collection token folded by `scopeUnits.ts`.
|
||||
*/
|
||||
|
||||
import type { IDisposable } from './lifecycle';
|
||||
import type { Emitter } from '../event';
|
||||
import { isPromiseLike, type EffectBody } from '../lifecycle/disposer';
|
||||
|
|
@ -64,29 +33,23 @@ export interface ConfigSchema {
|
|||
|
||||
export interface RecipeStatics {
|
||||
readonly name?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
readonly inject?: readonly ServiceIdentifier<any>[];
|
||||
readonly Config?: ConfigSchema;
|
||||
}
|
||||
|
||||
export type ServiceClassRecipe =
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(new (...args: any[]) => unknown) & RecipeStatics;
|
||||
|
||||
export type ServiceFunctionRecipe = ((
|
||||
fiber: Fiber,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
config?: any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) => any) &
|
||||
RecipeStatics;
|
||||
|
||||
export type ServiceObjectRecipe = {
|
||||
apply(
|
||||
fiber: Fiber,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
config?: any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): any;
|
||||
} & RecipeStatics;
|
||||
|
||||
|
|
@ -116,7 +79,6 @@ export interface Fiber {
|
|||
|
||||
effect(body: EffectBody, label?: string): FiberHandle;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
on(event: string | Emitter<any>, handler: (e: any) => void): FiberHandle;
|
||||
|
||||
get<T>(id: ServiceIdentifier<T>): T;
|
||||
|
|
@ -146,10 +108,8 @@ export class ServiceRecipeError extends Error {
|
|||
}
|
||||
|
||||
export interface ConstructionFrame {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
readonly ctor: new (...args: any[]) => any;
|
||||
readonly config: unknown;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
readonly token: ServiceIdentifier<any> | undefined;
|
||||
readonly host: FiberHost;
|
||||
}
|
||||
|
|
@ -170,7 +130,6 @@ export function currentConstruction(): ConstructionFrame | undefined {
|
|||
|
||||
export const SERVICE_MARK = Symbol('serviceUnit');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function isServiceRecipe(ctor: any): ctor is ServiceClassRecipe {
|
||||
return typeof ctor === 'function' && ctor.prototype?.[SERVICE_MARK] === true;
|
||||
}
|
||||
|
|
@ -201,15 +160,12 @@ export interface FiberHost {
|
|||
},
|
||||
): TokenProvideCore;
|
||||
provideTokenInstance<T>(id: ServiceIdentifier<T>, instance: T): TokenProvideCore;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
tokenState(id: ServiceIdentifier<any>): string | undefined;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
updateToken(id: ServiceIdentifier<any>, config: unknown, hasConfig: boolean): Promise<void>;
|
||||
resolveTokenWhenAvailable<T>(id: ServiceIdentifier<T>): Promise<T>;
|
||||
resolveInstance<T>(id: ServiceIdentifier<T>): T;
|
||||
materializedInstance<T>(id: ServiceIdentifier<T>): T | undefined;
|
||||
liveRef<T>(id: ServiceIdentifier<T>): LiveRef<T>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
recordInstanceEdge(node: object | undefined, id: ServiceIdentifier<any>): void;
|
||||
collectionView<T>(token: CollectionToken<T>): CollectionView<T>;
|
||||
addCollectionRecord<T>(
|
||||
|
|
@ -218,7 +174,6 @@ export interface FiberHost {
|
|||
providerBook: Ledger,
|
||||
value: T,
|
||||
): () => void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructService<T>(ctor: new (...args: any[]) => T, config: unknown): T;
|
||||
}
|
||||
|
||||
|
|
@ -231,7 +186,6 @@ export interface TokenProvideCore {
|
|||
export type FiberEventResolver = (
|
||||
host: FiberHost,
|
||||
event: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
handler: (e: any) => void,
|
||||
) => IDisposable;
|
||||
|
||||
|
|
@ -246,7 +200,6 @@ export function bindServiceUnit(instance: UnitInternals & IDisposable, frame: Co
|
|||
if (buffer === null) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const ctor = (instance as any).constructor as ServiceClassRecipe;
|
||||
const runtime = new FiberRuntime(
|
||||
frame.host,
|
||||
|
|
@ -316,9 +269,7 @@ export class FiberRuntime implements Fiber {
|
|||
private readonly _book: Ledger,
|
||||
readonly name: string,
|
||||
readonly config: unknown,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private readonly _token: ServiceIdentifier<any> | undefined,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private readonly _declared: ReadonlySet<ServiceIdentifier<any>>,
|
||||
private readonly _edgeNode: object | undefined,
|
||||
) {}
|
||||
|
|
@ -339,9 +290,7 @@ export class FiberRuntime implements Fiber {
|
|||
provide(recipe: ServiceRecipe, opts?: FiberProvideOptions): FiberHandle;
|
||||
provide<T>(token: CollectionToken<T>, value: T): FiberHandle;
|
||||
provide(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
first: ServiceIdentifier<any> | ServiceRecipe | CollectionToken<any>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
second?: any,
|
||||
third?: FiberProvideOptions,
|
||||
): FiberHandle {
|
||||
|
|
@ -384,7 +333,6 @@ export class FiberRuntime implements Fiber {
|
|||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
on(event: string | Emitter<any>, handler: (e: any) => void): FiberHandle {
|
||||
let subscription: IDisposable;
|
||||
if (typeof event === 'string') {
|
||||
|
|
@ -443,7 +391,6 @@ export class FiberRuntime implements Fiber {
|
|||
const config = validateConfig(recipe.Config, opts?.config, name);
|
||||
const core = this._host.provideToken(
|
||||
id,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
new SyncDescriptor<T>(recipe as new (...args: any[]) => T),
|
||||
{
|
||||
activation: opts?.activation === ScopeActivation.OnDemand ? 'ondemand' : 'eager',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
/**
|
||||
* `di` domain — directed `Graph` with cycle detection for DI instantiation.
|
||||
*/
|
||||
|
||||
export class Node<T> {
|
||||
readonly incoming = new Map<string, Node<T>>();
|
||||
readonly outgoing = new Map<string, Node<T>>();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
/**
|
||||
* `di` domain — service identifiers, `createDecorator`, and the `IInstantiationService` contract.
|
||||
*/
|
||||
|
||||
import type { SyncDescriptor, SyncDescriptor0 } from './descriptors';
|
||||
import type { CascadeEngine } from './cascadeEngine';
|
||||
import type { Event } from '../event';
|
||||
|
|
@ -10,15 +6,12 @@ import type { ServiceCollection } from './serviceCollection';
|
|||
|
||||
export type DependencyKind = 'instance' | 'collection' | 'ref';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace _util {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const serviceIds = new Map<string, ServiceIdentifier<any>>();
|
||||
export const DI_TARGET = '$di$target';
|
||||
export const DI_DEPENDENCIES = '$di$dependencies';
|
||||
|
||||
export interface ServiceDependency {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
readonly id: ServiceIdentifier<any>;
|
||||
readonly index: number;
|
||||
readonly kind: DependencyKind;
|
||||
|
|
@ -38,11 +31,8 @@ export namespace _util {
|
|||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
export interface DI_TARGET_OBJ extends Function {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
[DI_TARGET]: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[DI_DEPENDENCIES]: { id: ServiceIdentifier<any>; index: number; kind: DependencyKind }[];
|
||||
}
|
||||
}
|
||||
|
|
@ -53,14 +43,12 @@ export interface IConstructorSignature<T, Args extends any[] = []> {
|
|||
new <Services extends BrandedService[]>(...args: [...Args, ...Services]): T;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type GetLeadingNonServiceArgs<TArgs extends any[]> =
|
||||
TArgs extends [] ? []
|
||||
: TArgs extends [...infer TFirst, BrandedService] ? GetLeadingNonServiceArgs<TFirst>
|
||||
: TArgs;
|
||||
|
||||
export interface ServiceIdentifier<T> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(target: any, key: string | symbol | undefined, index: number): void;
|
||||
|
||||
readonly type: T;
|
||||
|
|
@ -69,9 +57,7 @@ export interface ServiceIdentifier<T> {
|
|||
}
|
||||
|
||||
function storeServiceDependency(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
id: ServiceIdentifier<any>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
target: Function,
|
||||
index: number,
|
||||
kind: DependencyKind = 'instance',
|
||||
|
|
@ -86,10 +72,8 @@ function storeServiceDependency(
|
|||
}
|
||||
|
||||
export function storeCustomDependency(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
id: ServiceIdentifier<any>,
|
||||
kind: DependencyKind,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
target: any,
|
||||
index: number,
|
||||
): void {
|
||||
|
|
@ -103,7 +87,6 @@ export function createDecorator<T>(name: string): ServiceIdentifier<T> {
|
|||
}
|
||||
|
||||
const id = function serviceDecorator(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
target: any,
|
||||
_key: string | symbol | undefined,
|
||||
index: number,
|
||||
|
|
@ -158,7 +141,6 @@ export interface LiveRef<T> {
|
|||
export function ref<T>(
|
||||
id: ServiceIdentifier<T>,
|
||||
): (target: object, key: string | symbol | undefined, index: number) => void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return function refDecorator(target: any, _key: string | symbol | undefined, index: number): void {
|
||||
if (arguments.length !== 3) {
|
||||
throw new Error('@ref-decorator can only be used to decorate a parameter');
|
||||
|
|
@ -195,11 +177,9 @@ export interface IInstantiationService {
|
|||
fn: (accessor: ServicesAccessor, ...args: TS) => R,
|
||||
...args: TS
|
||||
): R;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
createInstance<T>(descriptor: SyncDescriptor0<T>): T;
|
||||
createInstance<
|
||||
Ctor extends new (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
...args: any[]
|
||||
) => unknown,
|
||||
R extends InstanceType<Ctor>,
|
||||
|
|
@ -222,14 +202,10 @@ export const IInstantiationService: ServiceIdentifier<IInstantiationService> =
|
|||
createDecorator<IInstantiationService>('instantiationService');
|
||||
|
||||
export interface ServiceCollectionLike {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
set<T>(id: ServiceIdentifier<T>, instanceOrDescriptor: any): unknown;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
get<T>(id: ServiceIdentifier<T>): any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
has(id: ServiceIdentifier<any>): boolean;
|
||||
forEach(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
callback: (id: ServiceIdentifier<any>, value: any) => void,
|
||||
): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
/**
|
||||
* `di` domain — `InstantiationService` container (instantiation, child scopes, cycle detection).
|
||||
*/
|
||||
|
||||
import { SyncDescriptor } from './descriptors';
|
||||
import { CascadeEngine, CascadeTree, type CascadeChange, type CascadeHost } from './cascadeEngine';
|
||||
import {
|
||||
|
|
@ -41,7 +37,6 @@ import { Ledger, type LedgerEntry } from '../lifecycle/ledger';
|
|||
import type { Disposer } from '../lifecycle/disposer';
|
||||
import { ServiceCollection } from './serviceCollection';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const enum TraceType {
|
||||
None = 0,
|
||||
Creation = 1,
|
||||
|
|
@ -58,7 +53,6 @@ export class Trace {
|
|||
override branch() { return this; }
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
static traceInvocation(_enableTracing: boolean, fn: any): Trace {
|
||||
return !_enableTracing
|
||||
? Trace._None
|
||||
|
|
@ -68,14 +62,12 @@ export class Trace {
|
|||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
static traceCreation(_enableTracing: boolean, ctor: any): Trace {
|
||||
return !_enableTracing ? Trace._None : new Trace(TraceType.Creation, ctor.name);
|
||||
}
|
||||
|
||||
private static _totals: number = 0;
|
||||
private readonly _start: number = Date.now();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private readonly _dep: [ServiceIdentifier<any>, boolean, Trace?][] = [];
|
||||
|
||||
private constructor(
|
||||
|
|
@ -83,7 +75,6 @@ export class Trace {
|
|||
readonly name: string | null
|
||||
) { }
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
branch(id: ServiceIdentifier<any>, first: boolean): Trace {
|
||||
const child = new Trace(TraceType.Branch, id.toString());
|
||||
this._dep.push([id, first, child]);
|
||||
|
|
@ -149,7 +140,6 @@ export class InstantiationService implements IInstantiationService {
|
|||
private readonly _instanceEntries = new Map<unknown, LedgerEntry>();
|
||||
|
||||
private readonly _provideEntries = new Map<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ServiceIdentifier<any>,
|
||||
{ readonly entry: LedgerEntry; readonly core: TokenProvideCore }
|
||||
>();
|
||||
|
|
@ -158,17 +148,14 @@ export class InstantiationService implements IInstantiationService {
|
|||
|
||||
protected readonly _children = new Set<InstantiationService>();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private readonly _inProgress: ServiceIdentifier<any>[] = [];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private readonly _activeInstantiations = new Set<ServiceIdentifier<any>>();
|
||||
|
||||
private readonly _collectionStore: CollectionStore;
|
||||
|
||||
private readonly _collectionViews = new Map<
|
||||
CollectionToken<unknown>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
CollectionViewImpl<any>
|
||||
>();
|
||||
|
||||
|
|
@ -240,7 +227,6 @@ export class InstantiationService implements IInstantiationService {
|
|||
return (this._parent?.cascadeDepth ?? -1) + 1;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private _ownerOf(id: ServiceIdentifier<any>): InstantiationService | undefined {
|
||||
if (this._services.has(id)) {
|
||||
return this;
|
||||
|
|
@ -431,7 +417,6 @@ export class InstantiationService implements IInstantiationService {
|
|||
void this._unprovideCore(id);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private _releaseProvideEntry(id: ServiceIdentifier<any>): void {
|
||||
const prev = this._provideEntries.get(id);
|
||||
if (prev !== undefined) {
|
||||
|
|
@ -508,7 +493,6 @@ export class InstantiationService implements IInstantiationService {
|
|||
provideToken: (id, descriptor, options) => this._provideCore(id, descriptor, options),
|
||||
provideTokenInstance: <T>(id: ServiceIdentifier<T>, instance: T) =>
|
||||
this._provideCore(id, instance, undefined),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
tokenState: (id: ServiceIdentifier<any>) => {
|
||||
const owner = this._ownerOf(id) ?? this;
|
||||
return owner.cascade.stateOf(id);
|
||||
|
|
@ -530,7 +514,6 @@ export class InstantiationService implements IInstantiationService {
|
|||
materializedInstance: <T>(id: ServiceIdentifier<T>): T | undefined =>
|
||||
this._materializedInstanceOf(id),
|
||||
liveRef: <T>(id: ServiceIdentifier<T>): LiveRef<T> => this._liveRef(id),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
recordInstanceEdge: (node: object | undefined, id: ServiceIdentifier<any>) => {
|
||||
if (node === undefined) {
|
||||
return;
|
||||
|
|
@ -556,7 +539,6 @@ export class InstantiationService implements IInstantiationService {
|
|||
providerBook,
|
||||
value,
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructService: <T>(ctor: new (...args: any[]) => T, config: unknown): T => {
|
||||
return this._createInstance(ctor, [], Trace.traceCreation(this._enableTracing, ctor), {
|
||||
config,
|
||||
|
|
@ -622,14 +604,10 @@ export class InstantiationService implements IInstantiationService {
|
|||
return labels.join('/');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
createInstance<T>(descriptor: SyncDescriptor<T>, ...rest: any[]): T;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
createInstance<T>(ctor: new (...args: any[]) => T, ...rest: any[]): T;
|
||||
createInstance<T>(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ctorOrDescriptor: SyncDescriptor<T> | (new (...args: any[]) => T),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
...rest: any[]
|
||||
): T {
|
||||
this._assertNotDisposed();
|
||||
|
|
@ -698,9 +676,7 @@ export class InstantiationService implements IInstantiationService {
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private _createInstance<T>(ctor: any, args: unknown[], _trace: Trace, unit?: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
id?: ServiceIdentifier<any>;
|
||||
config?: unknown;
|
||||
}): T {
|
||||
|
|
@ -730,7 +706,6 @@ export class InstantiationService implements IInstantiationService {
|
|||
serviceDependencies.length > 0 ? serviceDependencies[0]!.index : args.length;
|
||||
|
||||
if (args.length !== firstServiceArgPos) {
|
||||
// eslint-disable-next-line no-console
|
||||
globalThis.console.trace(
|
||||
`[createInstance] First service dependency of ${(ctor as { name?: string }).name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`,
|
||||
);
|
||||
|
|
@ -755,7 +730,6 @@ export class InstantiationService implements IInstantiationService {
|
|||
pushConstructionFrame(frame);
|
||||
let instance: T;
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
instance = Reflect.construct<unknown[], T>(ctor as new (...args: any[]) => T, finalArgs);
|
||||
} finally {
|
||||
popConstructionFrame();
|
||||
|
|
@ -814,7 +788,6 @@ export class InstantiationService implements IInstantiationService {
|
|||
desc: SyncDescriptor<T>,
|
||||
_trace: Trace,
|
||||
): T {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type Triple = { id: ServiceIdentifier<any>; desc: SyncDescriptor<any>; _trace: Trace };
|
||||
const graph = new Graph<Triple>(data => data.id.toString());
|
||||
|
||||
|
|
@ -887,7 +860,6 @@ export class InstantiationService implements IInstantiationService {
|
|||
|
||||
private _createServiceInstanceWithOwner<T>(
|
||||
id: ServiceIdentifier<T>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ctor: any,
|
||||
args: ReadonlyArray<unknown> = [],
|
||||
_trace: Trace,
|
||||
|
|
@ -908,7 +880,6 @@ export class InstantiationService implements IInstantiationService {
|
|||
|
||||
private _createServiceInstance<T>(
|
||||
id: ServiceIdentifier<T>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ctor: any,
|
||||
args: ReadonlyArray<unknown> = [],
|
||||
_trace: Trace,
|
||||
|
|
@ -977,7 +948,6 @@ export class InstantiationService implements IInstantiationService {
|
|||
|
||||
private _getServiceInstanceOrDescriptor<T>(
|
||||
id: ServiceIdentifier<T>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): T | SyncDescriptor<T> | undefined {
|
||||
const instanceOrDesc = this._services.get(id);
|
||||
if (instanceOrDesc === undefined && this._parent) {
|
||||
|
|
@ -988,7 +958,6 @@ export class InstantiationService implements IInstantiationService {
|
|||
|
||||
private _throwIfStrict(msg: string, printWarning: boolean): void {
|
||||
if (printWarning) {
|
||||
// eslint-disable-next-line no-console
|
||||
globalThis.console.warn(msg);
|
||||
}
|
||||
if (this._strict) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
/**
|
||||
* `di` domain — disposable lifecycle primitives (`Disposable`, `DisposableStore`, `IDisposable`).
|
||||
*/
|
||||
|
||||
import { onUnexpectedError } from '../errors/unexpectedError';
|
||||
import { Ledger, type LedgerEntry } from '../lifecycle/ledger';
|
||||
|
||||
|
|
@ -340,7 +336,6 @@ export abstract class Disposable implements IDisposable {
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace Disposable {
|
||||
export const None: IDisposable = Object.freeze({
|
||||
dispose(): void {},
|
||||
|
|
@ -560,7 +555,6 @@ export class DisposableMap<K, V extends IDisposable = IDisposable>
|
|||
|
||||
set(key: K, value: V, skipDisposeOnOverwrite = false): void {
|
||||
if (this._isDisposed) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
new Error(
|
||||
'Trying to add a disposable to a DisposableMap that has already been disposed of. The added object will be leaked!',
|
||||
|
|
@ -643,7 +637,6 @@ export class DisposableSet<V extends IDisposable = IDisposable>
|
|||
|
||||
add(value: V): void {
|
||||
if (this._isDisposed) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
new Error(
|
||||
'Trying to add a disposable to a DisposableSet that has already been disposed of. The added object will be leaked!',
|
||||
|
|
|
|||
|
|
@ -1,14 +1,3 @@
|
|||
/**
|
||||
* `di` domain — DI Scope tree (`Scope`) and scoped service registry.
|
||||
*
|
||||
* Scoped services are resolved when their scope is created by default;
|
||||
* registrations that defer construction until first resolution use `OnDemand`.
|
||||
*
|
||||
* The kernel only knows the scope tree and the `ScopeKind` partial order.
|
||||
* The tier set is a business concept: the host bootstrap declares it through
|
||||
* `setScopeTopology` (see `src/app/scopes.ts`).
|
||||
*/
|
||||
|
||||
import { BugIndicatingError } from '../errors/errors';
|
||||
import { SyncDescriptor } from './descriptors';
|
||||
import { ScopeActivation, type ProvideAllEntry } from './instantiation';
|
||||
|
|
@ -54,7 +43,6 @@ const _scopedRegistry: ScopedEntry[] = [];
|
|||
export function registerScopedService<T>(
|
||||
scope: ScopeKind,
|
||||
id: ServiceIdentifier<T>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ctor: new (...args: any[]) => T,
|
||||
activation: ScopeActivation = ScopeActivation.OnScopeCreated,
|
||||
domain: string = 'unknown',
|
||||
|
|
@ -78,7 +66,6 @@ export function _clearScopedRegistryForTests(): void {
|
|||
}
|
||||
|
||||
export type ScopeSeed = ReadonlyArray<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
readonly [ServiceIdentifier<any>, unknown]
|
||||
>;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,3 @@
|
|||
/**
|
||||
* `di` domain — the kernel-side `ScopeUnits(kind)` fold (L3, D11/G2).
|
||||
*
|
||||
* `ScopeUnits(kind)` is the materialization collection token the kernel mints
|
||||
* per scope kind. When a scope of that kind is created, this fold watches the
|
||||
* new scope's live view of the token and materializes every record's recipe
|
||||
* as a unit INSIDE that scope (cross-scope materialization): a feature
|
||||
* contributed once at App scope becomes one live unit per Session/Agent
|
||||
* scope, automatically.
|
||||
*
|
||||
* Lifetime rules (per §5.6):
|
||||
* - the materialized unit's disposal hangs on the RECORD PROVIDER's book —
|
||||
* disposing the provider retracts the record and tears the materialized
|
||||
* units down across the tree (连坐);
|
||||
* - a target scope's natural death tears its materialized units down with it
|
||||
* (the fold ledger is anchored into the scope's container ledger); both
|
||||
* anchors are idempotent, so a provider dying mid-teardown is a no-op;
|
||||
* - records visible at creation are materialized immediately; the view's
|
||||
* incremental changes reconcile the set by record identity.
|
||||
*
|
||||
* A materialized unit's own `this.provide(...)` registrations are ordinary
|
||||
* token provides in the target scope — they join the graph and cascades as
|
||||
* usual. The materialized unit itself carries no token identity, so its own
|
||||
* constructor dependencies do not independently join cascades (feature
|
||||
* recipes are dependency-free assemblies by convention, per the Plan
|
||||
* sample); its provided tokens fully participate.
|
||||
*/
|
||||
|
||||
import { onUnexpectedError } from '../errors/unexpectedError';
|
||||
import type { IDisposable } from './lifecycle';
|
||||
import { Ledger } from '../lifecycle/ledger';
|
||||
|
|
@ -118,7 +90,6 @@ export function watchScopeUnits(container: InstantiationService, kind: ScopeKind
|
|||
materialize(record);
|
||||
}
|
||||
}
|
||||
// Snapshot: `retract()` deletes its own entry from `materialized`.
|
||||
for (const [id, retract] of Array.from(materialized)) {
|
||||
if (!seen.has(id)) {
|
||||
retract();
|
||||
|
|
|
|||
|
|
@ -1,25 +1,3 @@
|
|||
/**
|
||||
* `di` domain — the `Service` base class for L3 unit recipes.
|
||||
*
|
||||
* Extending `Service` turns a class into a unit recipe with the five `Fiber`
|
||||
* capabilities (`this.provide` / `effect` / `on` / `get` / `ref`). The class
|
||||
* follows the two-phase construction protocol: inside the constructor — when
|
||||
* the container builds the instance under a matching `ConstructionFrame` —
|
||||
* capability calls do not run immediately; they are buffered as
|
||||
* `BufferedOp`s and answered with `PendingFiberHandle`s, then flushed
|
||||
* against the real `FiberRuntime` by `bindServiceUnit` right after
|
||||
* construction (`fiber.ts`). Reads (`get` / `ref`) are forbidden during this
|
||||
* phase — declare dependencies as constructor parameters instead (构造期只写
|
||||
* 不读). A `Service` created by manual `new` never gets a bound runtime, and
|
||||
* its capability calls throw `FiberProtocolError`.
|
||||
*
|
||||
* The `SERVICE_MARK` prototype marker (set below) lets the container
|
||||
* recognize `Service`-derived class recipes and drive them through this
|
||||
* protocol; services whose members collide with the `Service` vocabulary
|
||||
* keep `extends Disposable` and use the function/object recipe forms
|
||||
* instead.
|
||||
*/
|
||||
|
||||
import type { Emitter } from '../event';
|
||||
import type { EffectBody } from '../lifecycle/disposer';
|
||||
import type { Ledger } from '../lifecycle/ledger';
|
||||
|
|
@ -56,7 +34,6 @@ export abstract class Service extends Disposable implements Fiber, UnitInternals
|
|||
const frame = currentConstruction();
|
||||
if (
|
||||
frame !== undefined &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
frame.ctor === (new.target as unknown as new (...args: any[]) => any)
|
||||
) {
|
||||
this.__unitBuffer = [];
|
||||
|
|
@ -65,7 +42,6 @@ export abstract class Service extends Disposable implements Fiber, UnitInternals
|
|||
this.__unitBuffer = null;
|
||||
this.config = undefined;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.name = (this.constructor as any).name || 'anonymous';
|
||||
}
|
||||
|
||||
|
|
@ -78,9 +54,7 @@ export abstract class Service extends Disposable implements Fiber, UnitInternals
|
|||
provide(recipe: ServiceRecipe, opts?: FiberProvideOptions): FiberHandle;
|
||||
provide<T>(token: CollectionToken<T>, value: T): FiberHandle;
|
||||
provide(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
first: any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
second?: any,
|
||||
third?: FiberProvideOptions,
|
||||
): FiberHandle {
|
||||
|
|
@ -105,7 +79,6 @@ export abstract class Service extends Disposable implements Fiber, UnitInternals
|
|||
return this._runtime().effect(body, label);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
on(event: string | Emitter<any>, handler: (e: any) => void): FiberHandle {
|
||||
const label = typeof event === 'string' ? `on:${event}` : 'on:emitter';
|
||||
if (this.__unitBuffer !== null) {
|
||||
|
|
@ -154,7 +127,6 @@ export abstract class Service extends Disposable implements Fiber, UnitInternals
|
|||
return this.__unitRuntime;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private _pendingName(first: any): string {
|
||||
if (typeof first === 'function') {
|
||||
return (first as RecipeStatics).name ?? String(first);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
/**
|
||||
* `di` domain — `ServiceCollection`: the dynamic registry (L1).
|
||||
*
|
||||
* Maps a service id to its recipe (`SyncDescriptor`) or materialized instance.
|
||||
* Every write stamps the entry with a container-monotonic `uid` (a generation
|
||||
* marker used for introspection and history — it plays no role in change
|
||||
* detection) and fires the token's availability event with `{ oldUid, newUid }`.
|
||||
*/
|
||||
|
||||
import { Emitter } from '../event';
|
||||
import { SyncDescriptor } from './descriptors';
|
||||
import type { ServiceIdentifier } from './instantiation';
|
||||
|
|
@ -25,17 +16,14 @@ export interface AvailabilityChange {
|
|||
}
|
||||
|
||||
export class ServiceCollection {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private readonly _entries = new Map<ServiceIdentifier<any>, ServiceCollectionEntry<any>>();
|
||||
private readonly _emitters = new Map<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ServiceIdentifier<any>,
|
||||
Emitter<AvailabilityChange>
|
||||
>();
|
||||
private _nextUid = 0;
|
||||
|
||||
constructor(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
...entries: ReadonlyArray<readonly [ServiceIdentifier<any>, unknown]>
|
||||
) {
|
||||
for (const [id, value] of entries) {
|
||||
|
|
@ -102,17 +90,14 @@ export class ServiceCollection {
|
|||
return prev.value as T | SyncDescriptor<T> | undefined;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
entry(id: ServiceIdentifier<any>): ServiceCollectionEntry | undefined {
|
||||
return this._entries.get(id);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
uidOf(id: ServiceIdentifier<any>): number | undefined {
|
||||
return this._entries.get(id)?.uid;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
configOf(id: ServiceIdentifier<any>): unknown {
|
||||
return this._entries.get(id)?.config;
|
||||
}
|
||||
|
|
@ -124,7 +109,6 @@ export class ServiceCollection {
|
|||
return this._emitterFor(id).event(listener);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
has(id: ServiceIdentifier<any>): boolean {
|
||||
return this._entries.has(id);
|
||||
}
|
||||
|
|
@ -135,7 +119,6 @@ export class ServiceCollection {
|
|||
|
||||
forEach(
|
||||
callback: (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
id: ServiceIdentifier<any>,
|
||||
value: unknown,
|
||||
) => void,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
/**
|
||||
* `di` domain — scoped test host and service-stub helpers for DI domain tests.
|
||||
*/
|
||||
|
||||
export {
|
||||
createServices,
|
||||
TestInstantiationService,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
/**
|
||||
* `di` domain — `TestInstantiationService` and scoped test-container helpers.
|
||||
*/
|
||||
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
import { SyncDescriptor, type SyncDescriptor0 } from './descriptors';
|
||||
|
|
@ -14,12 +10,10 @@ import { InstantiationService, Trace } from './instantiationService';
|
|||
import { DisposableStore, dispose, isDisposable, toDisposable, type IDisposable } from './lifecycle';
|
||||
import { ServiceCollection } from './serviceCollection';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type AnyConstructor<T = unknown> = new (...args: any[]) => T;
|
||||
|
||||
interface IServiceMock<T> {
|
||||
id: ServiceIdentifier<T>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
service?: any;
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +73,6 @@ export class TestInstantiationService extends InstantiationService implements ID
|
|||
...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>
|
||||
): R;
|
||||
public override createInstance(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ctorOrDescriptor: any,
|
||||
...rest: unknown[]
|
||||
): unknown {
|
||||
|
|
@ -116,10 +109,8 @@ export class TestInstantiationService extends InstantiationService implements ID
|
|||
): V extends Function ? sinon.SinonSpy : sinon.SinonStub;
|
||||
public stub<T>(
|
||||
id: ServiceIdentifier<T>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
arg2: any,
|
||||
arg3?: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
arg4?: any,
|
||||
): T | SyncDescriptor<T> | sinon.SinonStub | sinon.SinonSpy {
|
||||
if (arg2 instanceof SyncDescriptor && typeof arg3 !== 'string') {
|
||||
|
|
@ -156,31 +147,24 @@ export class TestInstantiationService extends InstantiationService implements ID
|
|||
public stubPromise<T>(
|
||||
id?: ServiceIdentifier<T>,
|
||||
fnProperty?: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
value?: any,
|
||||
): T | sinon.SinonStub;
|
||||
public stubPromise<T, V>(
|
||||
id?: ServiceIdentifier<T>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ctor?: any,
|
||||
fnProperty?: string,
|
||||
value?: V,
|
||||
): V extends Function ? sinon.SinonSpy : sinon.SinonStub;
|
||||
public stubPromise<T, V>(
|
||||
id?: ServiceIdentifier<T>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
obj?: any,
|
||||
fnProperty?: string,
|
||||
value?: V,
|
||||
): V extends Function ? sinon.SinonSpy : sinon.SinonStub;
|
||||
public stubPromise(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
arg1?: any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
arg2?: any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
arg3?: any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
arg4?: any,
|
||||
): unknown {
|
||||
arg3 = typeof arg2 === 'string' ? Promise.resolve(arg3) : arg3;
|
||||
|
|
@ -195,9 +179,7 @@ export class TestInstantiationService extends InstantiationService implements ID
|
|||
}
|
||||
|
||||
private _create<T>(serviceMock: IServiceMock<T>, options: SinonOptions, reset?: boolean): T;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private _create<T>(ctor: any, options: SinonOptions): T | sinon.SinonMock;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private _create(arg1: any, options: SinonOptions, reset: boolean = false): any {
|
||||
if (this._isServiceMock(arg1)) {
|
||||
const service = this._getOrCreateService(arg1, options, reset);
|
||||
|
|
@ -238,7 +220,6 @@ export class TestInstantiationService extends InstantiationService implements ID
|
|||
return service as T;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private _createStub(arg: any): any {
|
||||
if (arg instanceof SyncDescriptor) {
|
||||
return sinon.createStubInstance(arg.ctor);
|
||||
|
|
@ -252,7 +233,6 @@ export class TestInstantiationService extends InstantiationService implements ID
|
|||
return Object.create(null);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private _createReplacement(value: any): sinon.SinonStub | sinon.SinonSpy {
|
||||
if (typeof value === 'function') {
|
||||
return isSinonSpyLike(value) ? value : sinon.spy(value);
|
||||
|
|
@ -260,12 +240,10 @@ export class TestInstantiationService extends InstantiationService implements ID
|
|||
return value ? sinon.stub().returns(value) : sinon.stub();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private _hasSinonOption(service: any, key: keyof SinonOptions): boolean {
|
||||
return Boolean(service?.sinonOptions?.[key]);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private _isServiceMock(arg: any): arg is IServiceMock<unknown> {
|
||||
return typeof arg === 'object' && arg !== null && 'id' in arg;
|
||||
}
|
||||
|
|
@ -292,7 +270,6 @@ interface SinonOptions {
|
|||
}
|
||||
|
||||
export interface ServiceRegistration {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
define<T>(id: ServiceIdentifier<T>, ctor: new (...args: any[]) => T): void;
|
||||
defineInstance<T>(id: ServiceIdentifier<T>, instance: T): void;
|
||||
definePartialInstance<T>(id: ServiceIdentifier<T>, instance: Partial<T>): void;
|
||||
|
|
@ -311,7 +288,6 @@ export function createServices(
|
|||
options: CreateServicesOptions = {},
|
||||
): TestInstantiationService {
|
||||
const serviceCollection = new ServiceCollection();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const instanceIds = new Set<ServiceIdentifier<any>>();
|
||||
|
||||
const register = <T>(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
/**
|
||||
* `di` domain — `LinkedList` with O(1) push/removal for parked event listeners.
|
||||
*/
|
||||
|
||||
class Node<E> {
|
||||
static readonly Undefined = new Node<unknown>(undefined);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
/**
|
||||
* `errors` domain (cross-cutting) — error-code contract, runtime registry, and
|
||||
* metadata backing serialization.
|
||||
*
|
||||
* Owns the `ErrorDomain` contract every business domain uses to contribute its
|
||||
* codes, the registry (`registerErrorDomain` / `errorInfo` / `isErrorCode`),
|
||||
* and the domain-independent core codes (`internal`, `not_implemented`).
|
||||
*/
|
||||
|
||||
export interface ErrorInfo {
|
||||
readonly title: string;
|
||||
readonly retryable: boolean;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
/**
|
||||
* Render thrown values as human-readable lines for logs and CLI output.
|
||||
*/
|
||||
|
||||
import { isCodedError } from './serialize';
|
||||
|
||||
export function toErrorMessage(error: unknown, verbose = false): string {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,3 @@
|
|||
/**
|
||||
* Base error classes shared by every domain — `Error2` and related
|
||||
* control-flow errors.
|
||||
*/
|
||||
|
||||
import { CoreErrors } from './codes';
|
||||
import type { ErrorCode } from '#/errors';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,3 @@
|
|||
/**
|
||||
* `errors` domain (cross-cutting) — wire serialization of thrown values.
|
||||
*
|
||||
* Converts between thrown values and the portable `ErrorPayload` that crosses
|
||||
* process / language boundaries, recursively through the `cause` chain. Knows
|
||||
* only coded errors and the core codes: business-domain translation (e.g.
|
||||
* provider API errors) happens at the owning domain's boundary before errors
|
||||
* reach this layer, so `_base/errors` never imports a business domain.
|
||||
*/
|
||||
|
||||
import { CoreErrors, errorInfo, isErrorCode } from './codes';
|
||||
import type { ErrorCode } from '#/errors';
|
||||
import { Error2 } from './errors';
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
/**
|
||||
* Unexpected-error reporting hook (`onUnexpectedError`) — surfaces exceptions
|
||||
* thrown by listener callbacks.
|
||||
*/
|
||||
|
||||
export type UnexpectedErrorHandler = (err: unknown) => void;
|
||||
|
||||
const defaultHandler: UnexpectedErrorHandler = (err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[unexpected]', err);
|
||||
};
|
||||
|
||||
|
|
@ -24,7 +18,6 @@ export function onUnexpectedError(err: unknown): void {
|
|||
try {
|
||||
currentHandler(err);
|
||||
} catch (handlerErr) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[unexpected] handler threw', handlerErr, 'while reporting', err);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,3 @@
|
|||
/**
|
||||
* `event` domain — `Event` / `Emitter` primitives, the async
|
||||
* `AsyncEmitter` / `IWaitUntil` participation primitive (for interceptable
|
||||
* `onWill` events whose listeners register work via `waitUntil`), the
|
||||
* `handleVetos` helper (for `onBefore*` veto events whose listeners answer
|
||||
* with `veto(value, id)`), and event combinators (`once` / `map` / `filter`
|
||||
* / `any`). `Emitter` accepts an optional debug name that its
|
||||
* `EventSubscription` carries as an `on:<name>` ledger label, so event
|
||||
* subscriptions stay identifiable in unit-book introspection.
|
||||
*/
|
||||
|
||||
import { onUnexpectedError, safelyCallListener } from './errors/unexpectedError';
|
||||
import {
|
||||
Disposable,
|
||||
|
|
@ -207,7 +196,6 @@ export function handleVetos(
|
|||
return Promise.allSettled(promises).then(() => lazyValue);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace Event {
|
||||
export const None: Event<unknown> = () => Disposable.None;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
/**
|
||||
* `_base/execEnv` — `BufferedReadable` stream helper.
|
||||
*
|
||||
* A `Readable` wrapper that preserves source backpressure while still allowing
|
||||
* consumers to read buffered output after the source has ended. Used by process
|
||||
* spawners so `wait()`-then-read on small/medium outputs works without draining
|
||||
* unboundedly. Kept as a pure helper with no DI dependencies.
|
||||
*/
|
||||
|
||||
import { Readable } from 'node:stream';
|
||||
|
||||
export class BufferedReadable extends Readable {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,3 @@
|
|||
/**
|
||||
* `_base/execEnv` — Python-compatible text decoding with `errors` handling.
|
||||
*
|
||||
* Reads text with the same `strict`/`replace`/`ignore` semantics Python's
|
||||
* `open(..., errors=)` provides. Kept as a pure helper with no DI
|
||||
* dependencies.
|
||||
*/
|
||||
|
||||
export type TextDecodeErrors = 'strict' | 'replace' | 'ignore';
|
||||
|
||||
function isUtf8Continuation(byte: number): boolean {
|
||||
|
|
@ -135,7 +127,6 @@ export function decodeTextWithErrors(
|
|||
ignoreBOM: boolean = false,
|
||||
): string {
|
||||
let webLabel: string | undefined;
|
||||
// eslint-disable-next-line typescript-eslint/switch-exhaustiveness-check
|
||||
switch (encoding) {
|
||||
case 'utf-8':
|
||||
case 'utf8':
|
||||
|
|
|
|||
|
|
@ -1,22 +1,3 @@
|
|||
/**
|
||||
* `_base/execEnv` — OS / shell probe.
|
||||
*
|
||||
* Detects the host operating system, architecture, kernel release, and a
|
||||
* usable POSIX shell path. The result is a pure function of injected probes
|
||||
* (`platform` / `arch` / `release` / `env` / `isFile` / `execFileText`) so the
|
||||
* same suite runs identically on any host OS. `probeHostEnvironmentFromNode()`
|
||||
* bundles the Node defaults for production callers and memoises the promise.
|
||||
*
|
||||
* On Windows the probe expects bash from Git for Windows or MSYS2. If no
|
||||
* shell can be located the function throws `ProbeShellNotFoundError`, a
|
||||
* distinct type carrying the checked paths (`checked`) with an install hint
|
||||
* in its message, so the DI boundary can tell a missing shell apart from
|
||||
* other probe errors and translate it into a coded error. Set
|
||||
* `KIMI_SHELL_PATH` to override.
|
||||
*
|
||||
* Kept as a pure helper with no DI dependencies.
|
||||
*/
|
||||
|
||||
import { execFile as nodeExecFile } from 'node:child_process';
|
||||
import { constants as fsConstants } from 'node:fs';
|
||||
import { access } from 'node:fs/promises';
|
||||
|
|
|
|||
|
|
@ -1,15 +1,3 @@
|
|||
/**
|
||||
* `_base/execEnv` — glob-pattern-to-regex conversion.
|
||||
*
|
||||
* Pure function. Mirrors Python pathlib semantics: includes dotfiles,
|
||||
* case-sensitive by default.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert a single glob pattern segment (e.g. `"*.txt"`, `"file?.log"`) into
|
||||
* a RegExp. `*` matches any run of non-`/` characters; `?` matches any single
|
||||
* non-`/` character; `[abc]` matches one of a set (leading `!` negates).
|
||||
*/
|
||||
export function globPatternToRegex(pattern: string, caseSensitive: boolean): RegExp {
|
||||
let regex = '^';
|
||||
for (let i = 0; i < pattern.length; i++) {
|
||||
|
|
|
|||
|
|
@ -1,26 +1,3 @@
|
|||
/**
|
||||
* `_base/execEnv` — login-shell PATH probe.
|
||||
*
|
||||
* Enriches `process.env.PATH` with entries from the user's login shell. When
|
||||
* kimi-code is launched from a context that skipped the user's shell profile
|
||||
* (GUI launchers, non-login parent shells), `process.env.PATH` misses entries
|
||||
* like `/opt/homebrew/bin`, so commands spawned by the Bash tool can't find
|
||||
* tools the user has in their interactive shell (e.g. `gh`). We run the user's
|
||||
* login shell once (`$SHELL -l -c /usr/bin/env`), extract its PATH, and append
|
||||
* the entries the current PATH lacks. Existing entries keep their order and
|
||||
* priority; failures (no resolvable shell, hung or broken profile) silently
|
||||
* leave PATH untouched.
|
||||
*
|
||||
* launchd/daemon launches can leave `$SHELL` unset or blank, so the probe falls
|
||||
* back to the OS account's login shell from the user database before giving up.
|
||||
*
|
||||
* The probe is a pure function of injected deps so the suite runs identically
|
||||
* on any host. Windows is skipped: the problem is specific to POSIX
|
||||
* login-shell profiles.
|
||||
*
|
||||
* Kept as a pure helper with no DI dependencies.
|
||||
*/
|
||||
|
||||
import { userInfo } from 'node:os';
|
||||
|
||||
import { execFileText } from './environmentProbe';
|
||||
|
|
|
|||
|
|
@ -1,13 +1,3 @@
|
|||
/**
|
||||
* `_base.lifecycle` — disposer types shared by the Ledger.
|
||||
*
|
||||
* A `Disposer` undoes one registered side effect. Disposers are dual-track
|
||||
* (sync / async), mirroring ES explicit resource management: a Ledger whose
|
||||
* entries are all synchronous tears down within a single tick; any async
|
||||
* entry suspends the teardown promise until it settles.
|
||||
*/
|
||||
|
||||
/** Why the ledger is being torn down; threaded through to every disposer. */
|
||||
export type TeardownReason = 'scope-close' | 'cascade' | 'unload';
|
||||
|
||||
export type Disposer = (reason: TeardownReason) => void | Promise<void>;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
/**
|
||||
* `_base.lifecycle` — Ledger errors.
|
||||
*/
|
||||
|
||||
export class LedgerDisposedError extends Error {
|
||||
constructor(
|
||||
readonly ledgerLabel: string,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,3 @@
|
|||
/**
|
||||
* `_base.lifecycle` — `Ledger`: an ordered book of rollbackable registrations.
|
||||
*
|
||||
* A Ledger records entries (disposers, effects, child ledgers) in registration
|
||||
* order and tears them down in strict reverse order, awaiting each entry
|
||||
* serially — never in parallel. Rollback is uninterruptible: a failing entry
|
||||
* is logged (with its label) and teardown continues. Registering into a
|
||||
* disposing/disposed ledger throws immediately.
|
||||
*
|
||||
* The Ledger knows nothing about DI; scopes and containers build on top of it.
|
||||
*/
|
||||
|
||||
import { onUnexpectedError } from '../errors/unexpectedError';
|
||||
import {
|
||||
isAsyncIterable,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,3 @@
|
|||
/**
|
||||
* `_base.lifecycle` — in-memory lifecycle transitions with guarded async transactions.
|
||||
*
|
||||
* Provides a domain-independent state holder that enters a transition state before
|
||||
* asynchronous work begins and coordinates explicit commit, rollback, cleanup, and
|
||||
* compensation actions. It has no persistence, event, DI, or scope dependencies.
|
||||
*/
|
||||
|
||||
export type LifecycleTransitionErrorReason =
|
||||
| 'invalid_state'
|
||||
| 'transition_conflict'
|
||||
|
|
|
|||
|
|
@ -1,15 +1,3 @@
|
|||
/**
|
||||
* `_base/log` — plain (non-DI) log sinks.
|
||||
*
|
||||
* Owns the `RotatingFileWriter` (size-rotated, async-serial, sync-flush on
|
||||
* exit) and the `ILogWriter` implementations built on top of it (`FileLogWriter`),
|
||||
* plus the in-memory and console sinks used by tests and debugging. All classes
|
||||
* here are plain: constructed with an explicit options object, no `@IService`
|
||||
* deps, never registered with the container — a `*LogService` creates and owns
|
||||
* them. Uses `node:fs` rather than `kaos` because rotation needs atomic rename
|
||||
* and synchronous append.
|
||||
*/
|
||||
|
||||
import { appendFileSync, mkdirSync } from 'node:fs';
|
||||
import { mkdir, open, rename, stat, unlink } from 'node:fs/promises';
|
||||
import { dirname } from 'pathe';
|
||||
|
|
@ -282,19 +270,15 @@ export class ConsoleLogWriter implements ILogWriter {
|
|||
const { text } = formatEntry(entry, { ansi: process.stderr.isTTY === true });
|
||||
switch (entry.level) {
|
||||
case 'error':
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(text);
|
||||
break;
|
||||
case 'warn':
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(text);
|
||||
break;
|
||||
case 'debug':
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(text);
|
||||
break;
|
||||
default:
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(text);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
/**
|
||||
* `log` domain — logfmt entry formatter.
|
||||
*
|
||||
* Renders a `LogEntry` as a single logfmt line (`ISO LEVEL msg k=v ...`),
|
||||
* redacts secret-shaped keys and raw secret patterns, truncates oversized
|
||||
* fields, optionally colorizes the level with ANSI, and indents error stacks.
|
||||
* Pure — no I/O, no DI.
|
||||
*/
|
||||
|
||||
import type { LogContext, LogEntry, LogEntryError } from './log';
|
||||
|
||||
export const MSG_MAX_CHARS = 200;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,3 @@
|
|||
/**
|
||||
* `_base/log` — structured logging contract.
|
||||
*
|
||||
* Defines the public logging model shared by every scope: the `LogEntry` /
|
||||
* `LogLevel` types, the `ILogger` / `ILogService` facade used by other domains
|
||||
* to emit leveled entries, and the plain `ILogWriter` sink shape. There is a
|
||||
* single `ILogService` DI token; each scope binds its own `*LogService`
|
||||
* implementation to it, so consumers just inject `@ILogService` and the scope
|
||||
* decides where entries land. `ILogWriter` is a plain (non-DI) interface — sinks
|
||||
* are created by the `*LogService` implementations, not registered.
|
||||
*/
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
||||
export type LogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug';
|
||||
|
|
|
|||
|
|
@ -1,11 +1,3 @@
|
|||
/**
|
||||
* `log` domain — runtime logging configuration.
|
||||
*
|
||||
* Builds the `LoggingConfig` from `KIMI_LOG_*` environment variables plus
|
||||
* defaults, resolves the global and per-session log paths, and exposes the
|
||||
* `ILogOptions` seed used to inject the resolved config into a App scope.
|
||||
*/
|
||||
|
||||
import { join } from 'pathe';
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
|
|
|||
|
|
@ -1,14 +1,3 @@
|
|||
/**
|
||||
* `_base/log` — `BoundLogger` base and the App-scope `ILogService`.
|
||||
*
|
||||
* `BoundLogger` filters entries by level, extracts the payload into ctx/error,
|
||||
* merges bound context, and writes to a plain `ILogWriter`. It extends
|
||||
* `Service` so scope implementations can flush synchronously when their
|
||||
* scope is disposed. `AppLogService` is the App-scope binding of the single
|
||||
* `ILogService` token: it owns the global rotating file sink and reads its
|
||||
* level from `ILogOptions`.
|
||||
*/
|
||||
|
||||
import { Service } from '#/_base/di/service';
|
||||
import { LifecycleScope } from '#/app/scopes';
|
||||
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
|
||||
|
|
|
|||
|
|
@ -1,39 +1,3 @@
|
|||
/**
|
||||
* `state` domain — scope-agnostic keyed state container primitives.
|
||||
*
|
||||
* Owns the typed `StateKey<T>` descriptor (manufactured by `defineState` in
|
||||
* the top-level `state` domain), the `IStateRegistry` base interface shared
|
||||
* by the per-scope state services, and the `StateRegistry` implementation
|
||||
* backing them: a `Map`-backed store
|
||||
* where keys are declared
|
||||
* up front (`register`), read and replaced (`get` / `set`), and observed
|
||||
* (`onDidChange(key)` per key, `onDidChangeAny` globally). Two exports serve
|
||||
* debugging: `entries()` returns the live key/value references for in-process
|
||||
* readers, and `snapshot()` returns a JSON-safe deep copy for RPC / inspector
|
||||
* export: Maps become plain objects or entry arrays, Sets become arrays,
|
||||
* functions are dropped, circular references become `'(circular)'`, and
|
||||
* instances with a custom prototype (service references, tools, Promises)
|
||||
* collapse to a `'(ClassName)'` marker — plain data is recursed, resource
|
||||
* graphs are not, so a value that reaches into the DI object graph cannot
|
||||
* fan the copy out until the heap is exhausted. A key flagged
|
||||
* `snapshotExcluded` (replayable event-sourced state, whose authoritative
|
||||
* copy is the wire journal) is skipped by `snapshot()` so the debug export
|
||||
* never deep-copies it. Misuse (duplicate registration, reading or writing an
|
||||
* unregistered key) is a caller bug and raises `BugIndicatingError`.
|
||||
*
|
||||
* Cascading inspection: each scope's state service keeps a reference to the
|
||||
* parent scope's registry (`inspectParent`, assigned from the injected
|
||||
* parent-tier state service; App is the root) and declares its tier name
|
||||
* (`inspectScope`). `inspect()` folds that chain into a `StateInspection`
|
||||
* tree — this scope's `snapshot()` plus the ancestors' — so one RPC call
|
||||
* from any scope tier exports the whole App → … → current-scope state path.
|
||||
*
|
||||
* Values are stored as-is — the container does not freeze or clone, so
|
||||
* replacing the whole value via `set` is the recommended update style;
|
||||
* mutating a held `Map` / `Set` in place bypasses change notification.
|
||||
* Persistence and replay are out of scope here. Scope-agnostic.
|
||||
*/
|
||||
|
||||
import { Disposable, type IDisposable, toDisposable } from '../di/lifecycle';
|
||||
import { BugIndicatingError } from '../errors/errors';
|
||||
import { Emitter, type Event } from '../event';
|
||||
|
|
@ -67,7 +31,6 @@ export interface IStateRegistry {
|
|||
inspect(): StateInspection;
|
||||
}
|
||||
|
||||
// NOTE: stays Disposable — its own 'get' collides with the Fiber
|
||||
export class StateRegistry extends Disposable implements IStateRegistry {
|
||||
private readonly values = new Map<string, unknown>();
|
||||
private readonly registrations = new Map<string, object>();
|
||||
|
|
|
|||
|
|
@ -1,26 +1,3 @@
|
|||
/**
|
||||
* `_base` text helpers — UTF text encoding detection and decoding.
|
||||
*
|
||||
* Detection algorithm derived from VS Code
|
||||
* `src/vs/workbench/services/textfile/common/encoding.ts`
|
||||
* (MIT License, Copyright (c) Microsoft Corporation): BOM sniffing plus a
|
||||
* zero-byte parity heuristic that recognizes BOM-less UTF-16 LE/BE, so text
|
||||
* files saved as UTF-16 (e.g. Windows Notepad `.txt`) can be transcoded to
|
||||
* UTF-8 instead of being refused as binary.
|
||||
*
|
||||
* The parity heuristic deliberately deviates from VS Code in one way: VS
|
||||
* Code requires *every* byte pair to conform (a single CJK character, whose
|
||||
* UTF-16 unit carries no zero byte, falsifies the pattern and the file is
|
||||
* deemed binary). Here, zero bytes must instead appear at least twice and at
|
||||
* exactly one parity — odd indices mean UTF-16 LE (`0xAA 0x00`), even
|
||||
* indices mean UTF-16 BE (`0x00 0xAA`) — which tolerates mixed Latin/CJK
|
||||
* content while still rejecting real binaries (zeros at both parities, or
|
||||
* an isolated zero byte). Legacy 8-bit encodings (GBK, Big5, Shift-JIS, …)
|
||||
* are never guessed — a wrong silent guess is worse than a clear refusal.
|
||||
*
|
||||
* Pure functions over bytes; no io happens here.
|
||||
*/
|
||||
|
||||
export type UtfTextEncoding = 'utf-8' | 'utf-16le' | 'utf-16be';
|
||||
|
||||
export interface TextClassification {
|
||||
|
|
@ -46,12 +23,6 @@ export interface TextEncodingDetection {
|
|||
/** Number of leading bytes inspected for the zero-byte heuristic. */
|
||||
export const ENCODING_DETECTION_SAMPLE_BYTES = 512;
|
||||
|
||||
/**
|
||||
* Minimum zero bytes (at a single parity) before the BOM-less UTF-16
|
||||
* heuristic commits. One isolated zero byte is too ambiguous — a short
|
||||
* binary blob like `"plain prefix" + 00 01` would otherwise masquerade as
|
||||
* UTF-16 BE.
|
||||
*/
|
||||
const MIN_ZERO_BYTES_FOR_UTF16 = 2;
|
||||
|
||||
const UTF16BE_BOM = [0xfe, 0xff] as const;
|
||||
|
|
@ -59,7 +30,6 @@ const UTF16LE_BOM = [0xff, 0xfe] as const;
|
|||
const UTF8_BOM = [0xef, 0xbb, 0xbf] as const;
|
||||
|
||||
function sniffTextEncoding(sample: Uint8Array): TextEncodingDetection {
|
||||
// Always trust a BOM first.
|
||||
if (sample.length >= 2) {
|
||||
const b0 = sample[0]!;
|
||||
const b1 = sample[1]!;
|
||||
|
|
@ -74,10 +44,6 @@ function sniffTextEncoding(sample: Uint8Array): TextEncodingDetection {
|
|||
}
|
||||
}
|
||||
|
||||
// BOM-less UTF-16: zero bytes cluster at one parity — odd indices for LE
|
||||
// (`0xAA 0x00`), even for BE (`0x00 0xAA`). CJK units carry no zero byte,
|
||||
// so only the *placement* of zeros is checked, not their density. Zeros
|
||||
// at both parities, or fewer than the ambiguity threshold, mean binary.
|
||||
let zerosAtOdd = 0;
|
||||
let zerosAtEven = 0;
|
||||
const limit = Math.min(sample.length, ENCODING_DETECTION_SAMPLE_BYTES);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
/**
|
||||
* `_base` text helpers — Markdown frontmatter parsing.
|
||||
*
|
||||
* Splits a Markdown document into its YAML frontmatter block and body. Pure
|
||||
* text processing with no IO and no domain knowledge. A document without a
|
||||
* leading `---` fence parses as all body with `data: null`; an unterminated
|
||||
* fence is a `FrontmatterError`.
|
||||
*/
|
||||
|
||||
import { load as loadYaml } from 'js-yaml';
|
||||
|
||||
export class FrontmatterError extends Error {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/**
|
||||
* `_base` text helpers — model-text line-ending normalization.
|
||||
*
|
||||
* Normalizes CRLF → LF for display and re-materializes CRLF on write, so the
|
||||
* model sees a consistent view while the on-disk bytes stay faithful.
|
||||
*/
|
||||
|
||||
export type LineEndingStyle = 'lf' | 'crlf' | 'mixed';
|
||||
|
||||
export interface ModelTextView {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,3 @@
|
|||
/**
|
||||
* Abort-signal helpers — user-cancellation errors, abortable promises, signal
|
||||
* linking, and deadline abort signals.
|
||||
*/
|
||||
|
||||
export function abortError(message = 'Aborted'): Error {
|
||||
const error = new Error(message);
|
||||
error.name = 'AbortError';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
/**
|
||||
* `_base` utility — canonical JSON argument serialization for stable tool-call keys.
|
||||
*/
|
||||
|
||||
export function canonicalTelemetryArgs(args: unknown): string {
|
||||
const json = JSON.stringify(sortJsonValue(args));
|
||||
return json ?? String(args);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
/**
|
||||
* Parse environment-variable string values into typed primitives.
|
||||
*/
|
||||
|
||||
const TRUE_BOOLEAN_ENV_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
||||
const FALSE_BOOLEAN_ENV_VALUES = new Set(['0', 'false', 'no', 'off']);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,3 @@
|
|||
/**
|
||||
* File content metadata helpers — binary detection, line counting, etag, and
|
||||
* extension-based mime / language guessing.
|
||||
*
|
||||
* Pure functions over bytes, text, and stat-like shapes; no io happens here.
|
||||
* Binary detection samples the leading `FS_BINARY_SAMPLE_BYTES` of a file and
|
||||
* flags it as binary when the non-printable fraction exceeds
|
||||
* `FS_BINARY_NONPRINTABLE_FRACTION`; etags are built from any stat-like shape
|
||||
* carrying `size` / `mtimeMs` / `ino` (`FileMetaStat`).
|
||||
*/
|
||||
|
||||
import { extname } from 'node:path';
|
||||
|
||||
import { classifyTextSample } from '#/_base/text/encoding';
|
||||
|
|
|
|||
|
|
@ -1,8 +1,3 @@
|
|||
/**
|
||||
* Low-level durable file-write primitives — atomic writes plus file and
|
||||
* directory fsync helpers.
|
||||
*/
|
||||
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { closeSync, fsyncSync, openSync } from 'node:fs';
|
||||
import * as nodeFs from 'node:fs';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
/**
|
||||
* Hero-name slug generator for readable, memorable identifiers.
|
||||
*/
|
||||
|
||||
import { randomInt } from 'node:crypto';
|
||||
|
||||
export const HERO_NAMES = [
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
/**
|
||||
* `_base/utils/paths` (cross-cutting) — pure path predicates and directory
|
||||
* walks.
|
||||
*
|
||||
* Constrains filesystem watches to selected subtrees and scanner-visible
|
||||
* entries, and walks host directory chains with platform-native path
|
||||
* semantics so drive-letter / UNC roots keep their host form.
|
||||
*/
|
||||
|
||||
import nodePath from 'node:path';
|
||||
|
||||
function normalizeSlashes(p: string): string {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,3 @@
|
|||
/**
|
||||
* Timeout outcome promise — resolves with a fixed value after a delay.
|
||||
*
|
||||
* The timer goes through `setClampedTimeout`, so huge ("effectively
|
||||
* unbounded") timeouts still mean a long wait instead of overflowing into an
|
||||
* immediate fire.
|
||||
*/
|
||||
|
||||
import { setClampedTimeout } from './timer';
|
||||
|
||||
const NEVER = new Promise<never>(() => {});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,3 @@
|
|||
/**
|
||||
* Resolve and install proxy configuration for outbound `fetch` and spawned
|
||||
* child processes (HTTP/HTTPS and SOCKS, honoring `NO_PROXY`).
|
||||
*/
|
||||
|
||||
import {
|
||||
Agent,
|
||||
buildConnector,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,3 @@
|
|||
/**
|
||||
* Shared prompt-template renderer (`renderPrompt`).
|
||||
*
|
||||
* A single `${var}` substitution pass: every variable present in `vars` is
|
||||
* replaced with its string value, unknown or non-string placeholders stay
|
||||
* verbatim, and a bare `$` is never special. There is no conditional or loop
|
||||
* syntax by design — call sites compose optional sections in code and pass
|
||||
* them as pre-rendered blocks. This keeps user-facing templates (agent files,
|
||||
* `SYSTEM.md`) safe to write: a literal `${...}` inside prose or a code
|
||||
* snippet can never crash rendering.
|
||||
*/
|
||||
|
||||
const PROMPT_VARIABLE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
||||
|
||||
export function renderPrompt(template: string, vars: Record<string, unknown>): string {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/**
|
||||
* `_base` retry helpers — exponential and server-directed backoff, abortable
|
||||
* sleeps, and error-field extraction. The default budget is 10 attempts per
|
||||
* step: the 500ms ×2 ramp capped at 32s waits out multi-minute provider
|
||||
* overload (sustained 429s) before a turn fails.
|
||||
*/
|
||||
|
||||
import { abortable } from '#/_base/utils/abort';
|
||||
|
||||
export const DEFAULT_MAX_RETRY_ATTEMPTS = 10;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,3 @@
|
|||
/**
|
||||
* Repeating timer primitive — a disposable `setInterval` wrapper.
|
||||
*
|
||||
* `IntervalTimer` owns a single `setInterval` handle: `cancelAndSet` (re)starts
|
||||
* the loop (cancelling any previous handle first), `cancel` stops it, and
|
||||
* `dispose` guarantees the handle is cleared — so it can be `_register`-ed on a
|
||||
* `Disposable` owner and cleaned up for free. One instance is reused across
|
||||
* start/stop cycles instead of juggling raw `ReturnType<typeof setInterval>`
|
||||
* values. Mirrors VS Code's `IntervalTimer`.
|
||||
*
|
||||
* `setClampedTimeout` is a `setTimeout` whose delay is clamped to
|
||||
* `MAX_TIMER_DELAY_MS`, the largest delay the host timer accepts: beyond it
|
||||
* the delay overflows into an immediate (~1ms) fire, so huge ("effectively
|
||||
* unbounded") timeouts would fire at once instead of waiting. Callers that
|
||||
* outlive the clamp (~24.8 days) re-arm.
|
||||
*/
|
||||
|
||||
import type { IDisposable } from '#/_base/di/lifecycle';
|
||||
|
||||
export const MAX_TIMER_DELAY_MS = 0x7fffffff;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,3 @@
|
|||
/**
|
||||
* Compile-time type equality.
|
||||
*
|
||||
* Used to pin a hand-written type to the zod schema that re-derives it: a
|
||||
* drift in either direction (added / removed field, changed field type,
|
||||
* optionality flip) fails typecheck.
|
||||
*
|
||||
* `Equal` compares by mutual assignability through a contravariant
|
||||
* function-type trick, so it is stricter than a one-way `A extends B`
|
||||
* check. Both sides are flattened first (a homomorphic mapped type), so a
|
||||
* schema-side intersection (e.g. the `{...} & { [k: string]: unknown }`
|
||||
* that a passthrough object infers to) compares equal to the equivalent
|
||||
* hand-written object type instead of failing on type-node shape. The
|
||||
* comparison cannot see `readonly` modifiers (an inherent TS limitation),
|
||||
* so hand-written types should match zod's mutable inference exactly.
|
||||
*/
|
||||
|
||||
type Flatten<T> = { [K in keyof T]: T[K] } & {};
|
||||
|
||||
export type Equal<A, B> =
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
/**
|
||||
* Promise-aware utility types for function and method signatures.
|
||||
*/
|
||||
|
||||
export type Promisify<T> = [T] extends [Promise<any>] ? T : Promise<T>;
|
||||
export type PromisifyMethods<T> = {
|
||||
[K in keyof T]: T[K] extends (...args: infer Args) => infer Return
|
||||
|
|
|
|||
|
|
@ -1,15 +1,3 @@
|
|||
/**
|
||||
* Working-directory identity helpers.
|
||||
*
|
||||
* `slugifyWorkDirName` turns a directory name into a safe, bounded token;
|
||||
* `encodeWorkDirKey` derives the stable, opaque `workspaceId` for a working
|
||||
* directory (`wd_<slug>_<hash>`). The `workspaceId` is the backend-neutral
|
||||
* identity used to group sessions and to key the workspace registry; backends
|
||||
* never expose the raw working-directory path. `workspaceRootKey` is the
|
||||
* comparison-only companion: it answers "is this the same directory?" without
|
||||
* changing the id that was already minted for it.
|
||||
*/
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
const MAX_WORKDIR_SLUG_LENGTH = 40;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
/**
|
||||
* XML escaping helpers for content, attribute values, and tag delimiters.
|
||||
*/
|
||||
|
||||
export function escapeXml(input: string): string {
|
||||
return input
|
||||
.replaceAll('&', '&')
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
/**
|
||||
* agent-core-v2 version helper — exposes the package version to integrations.
|
||||
*/
|
||||
|
||||
export function getCoreVersion(): string {
|
||||
return '0.0.0';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,4 @@
|
|||
/**
|
||||
* `activityView` domain — the agent's one-way activity projection.
|
||||
*
|
||||
* Defines `IAgentActivityView`: a per-agent, read-only, event-folded read
|
||||
* model of "what this agent is doing" — the current turn with its live
|
||||
* phase/stream/step/retry/pending-approval/tool-call detail and the latest
|
||||
* turn outcome, published on the agent's event bus as
|
||||
* `agent.activity.updated`. The view OWNS NO authoritative state: every fact
|
||||
* is folded from the agent's own event bus (loop turn/step/delta/tool/retry,
|
||||
* permission approval, task, and full-compaction events) and seeded once from
|
||||
* the owning services; it can be discarded and rebuilt at any time. Bound at
|
||||
* Agent scope — one instance per agent, dying with it.
|
||||
*/
|
||||
|
||||
/* oxlint-disable typescript-eslint/no-unsafe-declaration-merging, eslint-plugin-import/namespace -- Event2 class+payload-interface declaration merging is the sanctioned event-declaration idiom. */
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
import type { PromptOrigin } from '#/agent/contextMemory/types';
|
||||
import type { TurnEndReason } from '#/agent/loop/turnEvents';
|
||||
|
|
|
|||
|
|
@ -1,23 +1,3 @@
|
|||
/**
|
||||
* `activityView` domain — `IAgentActivityView` implementation.
|
||||
*
|
||||
* A pure fold of the agent's own event bus: turn boundaries drive the turn
|
||||
* slice (active → detail updates → ended → `lastTurn`), step/delta/tool/retry
|
||||
* events drive the live phase/stream/retry detail, permission approval events
|
||||
* drive the pending-approval list, while task and full-compaction events drive
|
||||
* the background-work slice. The view seeds once from `IAgentLoopService`,
|
||||
* `IAgentTaskService`, and `IAgentFullCompactionService`, and recovers the
|
||||
* last turn's outcome from the durable `turnKey` state through `state`
|
||||
* (`IEventDispatcher`), so a cold-resumed agent still reports how its
|
||||
* previous turn ended (reads, never writes). Otherwise the view holds only
|
||||
* derived state, so it can be discarded and rebuilt at any time. The mutable
|
||||
* view state (`lifecycle`, `turn`, `lastTurn`, `background`, `current`) is
|
||||
* registered into `agentState` (`IAgentStateService`) and read/written
|
||||
* through it; the event-bus subscription handles stay mechanism held by the
|
||||
* `Disposable` base, and `MutableTurn`'s in-place-mutated Maps stay instance
|
||||
* fields of that per-turn class. Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
import { LifecycleScope } from '#/app/scopes';
|
||||
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
|
||||
|
|
@ -91,7 +71,6 @@ export const activityViewCurrentKey = defineState<AgentActivityState>('activityV
|
|||
background: [],
|
||||
}));
|
||||
|
||||
// NOTE: stays Disposable — its own 'state' collides with the Fiber
|
||||
export class AgentActivityView extends Disposable implements IAgentActivityView {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,3 @@
|
|||
/**
|
||||
* `agentsMdReminder` domain — AGENTS.md discovery-reminder contract.
|
||||
*
|
||||
* Defines the `IAgentAgentsMdReminderService`, the seed side of the domain:
|
||||
* `profile` reports the AGENTS.md paths it injected into the system prompt
|
||||
* (on every profile apply, with the agent's effective cwd), and `sessionInit`
|
||||
* re-seeds after `/init` regenerates the file, so the reminder hook can tell
|
||||
* "already injected" apart from newly discovered instruction files. Bound at
|
||||
* Agent scope.
|
||||
*/
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
||||
export interface IAgentAgentsMdReminderService {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,3 @@
|
|||
/**
|
||||
* `agentsMdReminder` domain — `IAgentAgentsMdReminderService`
|
||||
* implementation.
|
||||
*
|
||||
* Discovers AGENTS.md files reached through `toolExecutor` and the tool path
|
||||
* policy, parsing Bash targets through `bashParser` and probing through the os
|
||||
* services. Restores prompt provenance through the `profile` state on the
|
||||
* event dispatcher, resolves
|
||||
* roots through `sessionContext` and `bootstrap`, stores discovery state in
|
||||
* `agentState`, appends through `systemReminder`, and reports through
|
||||
* `telemetry`. Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { basename, dirname, isAbsolute, join, normalize } from 'pathe';
|
||||
|
||||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
|
|
|
|||
|
|
@ -1,26 +1,3 @@
|
|||
/**
|
||||
* `agentsMdReminder` domain — Bash-command directory extraction.
|
||||
*
|
||||
* Statically extracts the directories a Bash tool call is going to inspect,
|
||||
* walking the `bashParser` syntax tree: the literal operands of
|
||||
* directory-listing commands (`ls` / `tree` / `find` / `dir` / `exa` / `eza` /
|
||||
* `lsd`), with literal `cd` commands rebasing relative resolution as they
|
||||
* appear (`cd packages && ls kap-server`) and a genuinely operand-less
|
||||
* listing command listing the current base (one whose operands all failed
|
||||
* resolution is skipped instead). Only top-level simple commands are read —
|
||||
* anything not statically resolvable (expansions, command
|
||||
* substitution, glob characters (quoted or not), `~`, quoting mixes, compound
|
||||
* constructs, `cd -`, a `cd` inside a pipeline, or a listing command invoked
|
||||
* through a path prefix like `./ls` whose semantics are unknown) is skipped,
|
||||
* and a `cd` whose operand cannot be resolved poisons relative resolution
|
||||
* (never guesses a base) until an absolute `cd` re-anchors. Flags are dropped
|
||||
* together with the arguments of the known argument-taking options
|
||||
* (`ls --sort size`), and `find` collects leading paths past its no-argument
|
||||
* global options (`find -L packages`) before stopping at the expression.
|
||||
* A missed directory is recovered by the later Read/Edit/Write
|
||||
* probes; a wrong one is not, so skipping always wins over guessing.
|
||||
*/
|
||||
|
||||
import { isAbsolute, join, normalize } from 'pathe';
|
||||
|
||||
import type { BashSyntaxNode } from '#/app/bashParser/bashParser';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/**
|
||||
* `blob` domain — `IAgentBlobService` contract.
|
||||
*
|
||||
* Offloads large inline media payloads to content-addressed blob storage and
|
||||
* loads them back on read. Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import type { ContentPart } from '#/kosong/contract/message';
|
||||
|
||||
import { createDecorator } from "#/_base/di/instantiation";
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
/**
|
||||
* `blob` domain — `IAgentBlobService` implementation.
|
||||
*
|
||||
* Offloads large inline media payloads into content-addressed blobs and
|
||||
* loads them back on read; persists bytes through `IBlobStore` under the
|
||||
* agent's `scope('blobs')` root, matching the v1 `<agentDir>/blobs/<sha256>`
|
||||
* layout. Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import type { ContentPart } from '#/kosong/contract/message';
|
||||
import { LifecycleScope } from '#/app/scopes';
|
||||
|
|
|
|||
|
|
@ -1,16 +1,3 @@
|
|||
/**
|
||||
* `blob` domain — byte-bounded LRU cache.
|
||||
*
|
||||
* A small, dependency-free cache whose capacity is measured in **bytes** rather
|
||||
* than entries. Hits refresh an entry to most-recently-used; inserts evict the
|
||||
* least-recently-used entries until the payload fits. A single payload larger
|
||||
* than `maxBytes` is never cached.
|
||||
*
|
||||
* Module-private helper; not part of the package surface. Owned as a value
|
||||
* (not a DI service) so each agent keeps its own cache. Promote to a shared
|
||||
* util only when a second caller appears.
|
||||
*/
|
||||
|
||||
export class ByteLruCache {
|
||||
private readonly map = new Map<string, Buffer>();
|
||||
private currentBytes = 0;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
/**
|
||||
* `command` domain — the `IAgentCommandService` contract.
|
||||
*
|
||||
* The agent-scope registry over the `CommandContribution` collection: lists
|
||||
* the contributed executable commands (name-level dedup, last record wins,
|
||||
* `source` = provider unit name) and runs one by name with an args string.
|
||||
* Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import type { Event } from '#/_base/event';
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,3 @@
|
|||
/**
|
||||
* `command` domain — `IAgentCommandService` implementation.
|
||||
*
|
||||
* The fold over the `CommandContribution` collection (`command`): `list()`
|
||||
* dedupes the live records by name (a later record shadows an earlier one of
|
||||
* the same name), and `run` invokes the contribution's callback inside an
|
||||
* `invokeFunction` so its `ctx.get` resolves through the agent container.
|
||||
* Unknown names fail with a coded `REQUEST_INVALID` error. Bound at Agent
|
||||
* scope; constructed on demand — nothing pushes to a command registry, every
|
||||
* consumer pulls.
|
||||
*/
|
||||
|
||||
import { Emitter, type Event } from '#/_base/event';
|
||||
import { type CollectionRecord, type CollectionView } from '#/_base/di/collection';
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,3 @@
|
|||
/**
|
||||
* `command` domain — the `CommandContribution` collection token and payload.
|
||||
*
|
||||
* An executable command a Feature (or any unit) contributes into the
|
||||
* agent-scope registry (`IAgentCommandService`) — unlike plugin commands,
|
||||
* which are prompt templates, a contributed command runs engine-side with DI
|
||||
* access. `run` receives a `CommandRunContext` whose `get` resolves services
|
||||
* from the target agent's container; the records carry the provider unit's
|
||||
* name as `source`, and a record is withdrawn when its provider dies. No
|
||||
* scoped state — pure payload + token.
|
||||
*/
|
||||
|
||||
import { collection } from '#/_base/di/collection';
|
||||
import type { ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,3 @@
|
|||
/**
|
||||
* `contextInjector` domain — `IAgentContextInjectorService` implementation.
|
||||
*
|
||||
* Reconciles registered model-context providers against `contextMemory` at the
|
||||
* head of every loop step (before the step's request is built), so every LLM
|
||||
* request sees the freshest injections. A compaction splice re-arms the
|
||||
* new-turn flag for the next step. `reconcileWhenIdle` lets out-of-loop
|
||||
* callers (SDK RPC surfaces) refresh one provider immediately while the loop
|
||||
* is quiet. Writes reminders through `systemReminder` and reports provider
|
||||
* failures through `log`. Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { toDisposable, type IDisposable } from "#/_base/di/lifecycle";
|
||||
import { Service } from "#/_base/di/service";
|
||||
import { LifecycleScope } from '#/app/scopes';
|
||||
|
|
@ -96,9 +84,6 @@ export class AgentContextInjectorService extends Service implements IAgentContex
|
|||
const rearmed = this.takeCompactionRearm();
|
||||
await this.inject(ctx.firstStepOfTurn || rearmed);
|
||||
await next();
|
||||
// Compaction can run inside a later handler of this same chain
|
||||
// (full-compaction's beforeStep). Its splice always drops injection
|
||||
// messages, so re-reconcile here — still before the step's request.
|
||||
if (this.takeCompactionRearm()) {
|
||||
await this.inject(true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,3 @@
|
|||
/**
|
||||
* `contextMemory` domain helper — builds the bounded context window produced
|
||||
* by compaction and exposes the shared user-message selection rules used by
|
||||
* live execution and replay.
|
||||
*
|
||||
* Estimates token sizes through `kosong`'s contract heuristics (injectable as
|
||||
* `TokenEstimate`) and wraps elision notes through `systemReminder`.
|
||||
* Scope-agnostic.
|
||||
*/
|
||||
|
||||
import { estimateTokens, estimateTokensForMessage, estimateTokensForMessages } from '#/kosong/contract/tokens';
|
||||
import type { ContentPart } from '#/kosong/contract/message';
|
||||
import { wrapSystemReminder } from '#/agent/systemReminder/systemReminder';
|
||||
|
|
@ -127,7 +117,6 @@ export function buildContextCompactionShape(
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
export function buildCompactionSummaryText(summary: string): string {
|
||||
const suffix = summary.trim();
|
||||
return `${COMPACTION_SUMMARY_PREFIX}\n${suffix.length > 0 ? suffix : '(no summary available)'}`;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,4 @@
|
|||
/**
|
||||
* `contextMemory` domain — the durable `context.*` Event2 classes and the
|
||||
* observable `context.spliced` fact.
|
||||
*
|
||||
* The five durable classes are the wire-protocol 1.4 record vocabulary for
|
||||
* the per-agent conversation history; their `serialize()` output is the
|
||||
* on-disk record (flat payload, epoch-ms `time`), so v1- and v2-written
|
||||
* sessions reduce identically and replay stays silent. `ContextSpliced` is
|
||||
* the live-only observable counterpart broadcast after every splice-shaped
|
||||
* mutation (`clear` / `applyCompaction` / `undo` / verified cross-model
|
||||
* trailing removal). Scope-agnostic.
|
||||
*/
|
||||
|
||||
/* oxlint-disable typescript-eslint/no-unsafe-declaration-merging, eslint-plugin-import/namespace -- Event2 class+payload-interface declaration merging is the sanctioned event-declaration idiom. */
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Event2 } from '#/app/event/event2';
|
||||
|
|
|
|||
|
|
@ -1,15 +1,3 @@
|
|||
/**
|
||||
* `contextMemory` domain — `IAgentContextMemoryService` implementation.
|
||||
*
|
||||
* Owns per-agent conversation history through the event dispatcher, maintains
|
||||
* measurements with `tokenCounting`. Every
|
||||
* splice-shaped mutation (`clear` / `applyCompaction` / `undo`, plus verified
|
||||
* cross-model trailing removal) publishes `context.spliced` from the live path
|
||||
* only — replay rebuilds silently — and truncates the measured-anchor ledger
|
||||
* when a cut crosses an anchor, letting `tokenCounting` restore the surviving
|
||||
* prefix's REAL size from the remaining anchors. Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
import { LifecycleScope } from '#/app/scopes';
|
||||
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
|
||||
|
|
@ -46,7 +34,6 @@ import {
|
|||
import type { LoopRecordedEvent } from './loopEventFold';
|
||||
import type { ContextMessage } from './types';
|
||||
|
||||
// NOTE: stays Disposable — its own 'get' collides with the Fiber
|
||||
export class AgentContextMemoryService extends Disposable implements IAgentContextMemoryService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
|
|
@ -165,9 +152,6 @@ export class AgentContextMemoryService extends Disposable implements IAgentConte
|
|||
private dispatchCutEvents(cutIndex: number): void {
|
||||
const model = this.agentState.get(tokenCountingKey);
|
||||
if (!model.anchors.some((anchor) => anchor.length > cutIndex)) return;
|
||||
// The display tokens are the post-cut size computed from the CURRENT
|
||||
// ledger — anchors at or below the cut are identical before and after
|
||||
// the truncation, so the pre-dispatch read is exact.
|
||||
void this.dispatcher.dispatch(
|
||||
new TokenCountingTruncated({
|
||||
length: cutIndex,
|
||||
|
|
|
|||
|
|
@ -1,39 +1,3 @@
|
|||
/**
|
||||
* `contextMemory` domain — the conversation-history state (`contextMemoryKey`)
|
||||
* and its folds over the durable `context.*` events (`ContextAppendMessage` /
|
||||
* `ContextAppendLoopEvent` / `ContextClear` / `ContextApplyCompaction` /
|
||||
* `ContextUndo`), plus the undo-cut and compaction-record helpers.
|
||||
*
|
||||
* Declares the history as `ContextMessage[]` (initial `[]`); every fold runs
|
||||
* on the immer draft and either mutates it or returns a replacement, so a
|
||||
* no-op keeps the same reference (immer returns the base state untouched).
|
||||
* The live write path emits the v1 vocabulary: non-loop appends (user
|
||||
* prompts, injections, hook/task notices) go on the wire as
|
||||
* `context.append_message` (persisted without local ids — the on-disk record
|
||||
* matches v1's field set), while the agent loop streams each turn as
|
||||
* `context.append_loop_event` records — the same on-disk shape the v1 loop
|
||||
* writes — folded into assistant / tool messages both at live dispatch time
|
||||
* and on replay, so v1- and v2-written sessions reduce identically.
|
||||
* Swarm-mode announcements are owned by the `swarm` domain's
|
||||
* context-injection provider; the trailing enter-reminder pop on
|
||||
* `swarm_mode.exit` is registered by the swarm feature onto `contextMemoryKey`
|
||||
* (see `popSwarmModeReminder`).
|
||||
*
|
||||
* `context.undo` counts conversation ticks with the single `isUndoAnchor`
|
||||
* predicate — the same definition the checkpoint
|
||||
* protocol pushes with, so anchor counting and checkpoint pushing can never
|
||||
* drift apart.
|
||||
*
|
||||
* Blob handling is declared as a `StateBlobCodec` on `contextMemoryKey.replayable.blobs`:
|
||||
* - `dehydrate(record, transform)`: at dispatch time, traverses message content
|
||||
* in `context.append_message` and `context.append_loop_event` records,
|
||||
* passing each `ContentPart[]` through `transform` to offload oversized data
|
||||
* URIs.
|
||||
* - `rehydrate(state, transform)`: after replay, traverses the surviving final
|
||||
* state and loads `blobref:` URLs back to inline data — skipping I/O for
|
||||
* data that was compacted away during the session.
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ErrorCodes, Error2 } from '#/errors';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/**
|
||||
* `contextMemory` domain — rebuilds display history from the wire journal.
|
||||
*
|
||||
* Supplies transcript consumers with full pre-compaction history and folded
|
||||
* context length while preserving undo/clear semantics. Scope-agnostic.
|
||||
*/
|
||||
|
||||
import { type ContentPart, type ToolCall } from '#/kosong/contract/message';
|
||||
import type { WireRecord } from '#/wire/record';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,3 @@
|
|||
/**
|
||||
* `contextMemory` domain — shared conversation clock and the undoable
|
||||
* protocol registration.
|
||||
*
|
||||
* Defines the undo anchor vocabulary and registers the undoable protocol
|
||||
* consumed by the state domain's `.undoable()` expansion: the four protocol
|
||||
* events (`context.append_message` / `context.apply_compaction` /
|
||||
* `context.clear` / `context.undo`), the single `isUndoAnchor` tick
|
||||
* predicate, and the undo-count guard. A state key whose value must follow
|
||||
* conversation undo chains `.undoable()` — never hand-rolling the
|
||||
* checkpoint/clear/rollback folds — so undo anchors push a checkpoint,
|
||||
* compaction/clear drop the markers, and `context.undo` rolls back through
|
||||
* inverse patches (or through the key's custom `onUndo`). Scope-agnostic.
|
||||
*/
|
||||
|
||||
import { registerUndoableProtocol } from '#/state/state';
|
||||
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/**
|
||||
* `contextMemory` domain — Agent-scoped post-undo reconciliation registry.
|
||||
*
|
||||
* Hosts state-repair participants for the undo coordinator. Bound at Agent
|
||||
* scope.
|
||||
*/
|
||||
|
||||
import { createDecorator } from '#/_base/di/instantiation';
|
||||
import { toDisposable, type IDisposable } from '#/_base/di/lifecycle';
|
||||
import { Service } from '#/_base/di/service';
|
||||
|
|
|
|||
|
|
@ -1,44 +1,3 @@
|
|||
/**
|
||||
* `contextMemory` loop-event fold — reduction of `context.append_loop_event`
|
||||
* records into folded `ContextMessage`s.
|
||||
*
|
||||
* The agent loop streams a turn as `context.append_loop_event` records
|
||||
* (`step.begin` / `content.part` / `tool.call` / `tool.result` / `step.end`)
|
||||
* and never writes a folded assistant message, keeping the on-disk shape
|
||||
* byte-compatible with v1. This fold turns them into assistant / tool
|
||||
* messages — at live dispatch time and again when `WireService.restore`
|
||||
* restores an Agent. Without it, restore would skip those records (no Op is
|
||||
* registered for the type) and the restored `contextMemoryKey` — and every
|
||||
* consumer built on it — would show only the user prompts.
|
||||
*
|
||||
* Semantics mirror the v1 fold exactly:
|
||||
* - `step.begin` → open an assistant message (`partial: true`); first settle
|
||||
* the step left open by a failed attempt
|
||||
* - `content.part`→ append to the open assistant's content
|
||||
* - `tool.call` → append to the open assistant's `toolCalls`, mark pending
|
||||
* - `tool.result` → push a `tool` message (with the v1 output
|
||||
* wrapping), clear its pending id
|
||||
* - `step.end` → settle the assistant
|
||||
* "Settle" closes any tool exchange left open (interrupted result messages),
|
||||
* then drops the partial assistant when nothing sendable was recorded (no
|
||||
* tool calls; every content part vacuous — an output-free assistant only
|
||||
* trips provider message validation) and seals it (`partial: undefined`)
|
||||
* when it carries output. v1 never produced
|
||||
* `step.begin` without `step.end` (its retries stayed inside one request), so
|
||||
* the drop/seal rule is the v2 extension that makes loop-level retries — a
|
||||
* retried attempt is its own `step.begin` — replay to the same history the
|
||||
* live loop folded.
|
||||
* A `context.append_message` reduced while a tool exchange is still open is
|
||||
* deferred and flushed once the exchange closes, so strict-provider
|
||||
* assistant↔tool adjacency is preserved.
|
||||
*
|
||||
* The fold is stateful across records within one replay. State is carried in a
|
||||
* `WeakMap` keyed by each committed state array (immer drafts resolve to
|
||||
* their `original`), so the public `getState(ContextModel)` view stays a
|
||||
* plain `ContextMessage[]` and concurrent replays of different agent scopes
|
||||
* never share fold state.
|
||||
*/
|
||||
|
||||
import { isDraft, original } from 'immer';
|
||||
|
||||
import type { FinishReason } from '#/kosong/contract/provider';
|
||||
|
|
@ -114,7 +73,6 @@ interface FoldCtx {
|
|||
const foldCtxMap = new WeakMap<object, FoldCtx>();
|
||||
|
||||
function ctxOf(state: readonly ContextMessage[]): FoldCtx {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const key = (isDraft(state) ? original(state as any) : state) as object;
|
||||
let ctx = foldCtxMap.get(key);
|
||||
if (ctx === undefined) {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,3 @@
|
|||
/**
|
||||
* `contextMemory` message id helpers.
|
||||
*
|
||||
* Local message ids (`msg_<ulid>`) are process-lifetime identifiers only —
|
||||
* they are NOT persisted: the on-disk `context.append_message` record carries
|
||||
* exactly v1's field set, and public message ids are derived from the
|
||||
* transcript index (by the server layer's `ContextMessage → wire Message`
|
||||
* projection), which stays stable across live reads and resume.
|
||||
* `newMessageId` remains for callers that need an opaque per-process id.
|
||||
* Provider-assigned ids live on the separate `providerMessageId` field and
|
||||
* never collide with this namespace.
|
||||
*/
|
||||
|
||||
import { ulid } from 'ulid';
|
||||
|
||||
export function newMessageId(): string {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
/**
|
||||
* `contextMemory` domain helper — projects stored tool result facts into
|
||||
* model-visible content.
|
||||
*
|
||||
* Tool messages keep the raw tool output plus structured status fields in
|
||||
* context. The LLM projection is the only boundary that turns those facts into
|
||||
* system status text or appends model-only notes.
|
||||
*/
|
||||
|
||||
import type { ContentPart } from '#/kosong/contract/message';
|
||||
|
||||
const TOOL_ERROR_STATUS = '<system>ERROR: Tool execution failed.</system>';
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
/**
|
||||
* `contextMemory` vacuous-content predicate — shared test for content parts
|
||||
* that carry nothing the provider wire can represent. Vacuous means an empty
|
||||
* or whitespace-only text block, or an empty thinking block with no provider
|
||||
* signature; a signed thinking block (`encrypted`) is never vacuous —
|
||||
* reasoning providers require it back verbatim — and media parts always
|
||||
* carry content.
|
||||
*/
|
||||
|
||||
import type { ContentPart } from '#/kosong/contract/message';
|
||||
|
||||
export function isVacuousContentPart(part: ContentPart): boolean {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,3 @@
|
|||
/**
|
||||
* `contextProjector` domain — Agent-scope context projection contract.
|
||||
*
|
||||
* Defines wire-safe history projections and an opaque snapshot of the media
|
||||
* identities that a provider rejected, allowing later steps to strip only
|
||||
* that content while preserving newly generated recovery media.
|
||||
*
|
||||
* Projection variability is expressed as data: a `ProjectionPolicy` —
|
||||
* `structure: 'strict'` adds the structural repairs strict providers need
|
||||
* (duplicate tool calls dropped, consecutive assistants merged, leading
|
||||
* non-user messages dropped); `media` selects the provider-rejection
|
||||
* fallback (`'degraded'` replaces all but the most recent media with text
|
||||
* markers after an HTTP 413; `{ strip }` replaces exactly the snapshotted
|
||||
* media identities after a rejected-format or still-too-large resend).
|
||||
*/
|
||||
|
||||
import { createDecorator } from '#/_base/di/instantiation';
|
||||
import type { Message } from '#/kosong/contract/message';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,3 @@
|
|||
/**
|
||||
* `contextProjector` domain — `IAgentContextProjectorService` implementation.
|
||||
*
|
||||
* Projects stored context history into the wire messages sent to the model,
|
||||
* applies the read-side media fallbacks selected by `policy.media`, and
|
||||
* surfaces every repair the projection had to apply: the repairs are
|
||||
* summarized once per distinct signature into a single deduped warning
|
||||
* (through `log`) plus a `context_projection_repaired` telemetry event
|
||||
* (through `telemetry`), so a silently-mangled history always leaves a
|
||||
* trace. The mutable repair-dedup signature (`lastRepairSignature`) is
|
||||
* registered into `agentState` (`IAgentStateService`) and read/written
|
||||
* through it. Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { LifecycleScope } from '#/app/scopes';
|
||||
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
|
||||
import { ILogService } from '#/_base/log/log';
|
||||
|
|
|
|||
|
|
@ -1,15 +1,3 @@
|
|||
/**
|
||||
* `contextProjector` domain — read-side media fallbacks for the two
|
||||
* deterministic provider rejections.
|
||||
*
|
||||
* The degraded projection replaces all but the most recent media parts with
|
||||
* text markers after an HTTP 413 body-size rejection; the strip projection
|
||||
* replaces exactly the snapshotted media identities after a rejected-format
|
||||
* or still-too-large resend, so a newly generated recovery image stays
|
||||
* visible on later steps. Both rewrite only the projected wire messages —
|
||||
* the stored history keeps its media.
|
||||
*/
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
import type { ContentPart, Message } from '#/kosong/contract/message';
|
||||
|
|
|
|||
|
|
@ -1,23 +1,3 @@
|
|||
/**
|
||||
* `contextProjector` domain — rebuilds stored context history into
|
||||
* provider-valid wire messages and reports every repair through an anomaly
|
||||
* sink.
|
||||
*
|
||||
* The default projection pairs tool calls with their results (a displaced
|
||||
* result returns to its call, an orphan is dropped, a call left open is
|
||||
* closed with a synthetic interrupted result), renders stored tool-result
|
||||
* facts for the model, drops blank text and wholly-vacuous messages, skips
|
||||
* partial messages, and merges consecutive user prompts. The strict
|
||||
* projection adds the repairs strict providers need: duplicate tool calls
|
||||
* dropped, consecutive assistants merged, leading non-user messages dropped.
|
||||
*
|
||||
* A history slice without any assistant message is a sizing slice (used to
|
||||
* size tool results): tool messages project like any other message instead
|
||||
* of pairing into exchanges. A synthesized close counts as `trailing` — an
|
||||
* expected in-flight close rather than a defect — exactly when no non-tool,
|
||||
* non-partial message follows the owning message in the slice.
|
||||
*/
|
||||
|
||||
import { ErrorCodes, Error2 } from '#/errors';
|
||||
import { renderToolResultForModel } from '#/agent/contextMemory/toolResultRender';
|
||||
import type { ContextMessage } from '#/agent/contextMemory/types';
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue