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

This commit is contained in:
Haozhe 2026-08-18 00:30:49 +08:00 committed by GitHub
parent a7dc1ea284
commit 1ab19190e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1213 changed files with 395 additions and 22907 deletions

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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`.

View file

@ -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.

View file

@ -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) }`

View file

@ -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",

View file

@ -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

View file

@ -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.

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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)}>`;

View file

@ -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<{

View file

@ -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>());

View file

@ -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;

View file

@ -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', {

View file

@ -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 };

View file

@ -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;

View file

@ -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()}`;

View file

@ -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',

View file

@ -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>>();

View file

@ -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;
}

View file

@ -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) {

View file

@ -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!',

View file

@ -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]
>;

View file

@ -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();

View file

@ -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);

View file

@ -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,

View file

@ -1,7 +1,3 @@
/**
* `di` domain scoped test host and service-stub helpers for DI domain tests.
*/
export {
createServices,
TestInstantiationService,

View file

@ -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>(

View file

@ -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);

View file

@ -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;

View file

@ -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 {

View file

@ -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';

View file

@ -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';

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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 {

View file

@ -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':

View file

@ -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';

View file

@ -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++) {

View file

@ -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';

View file

@ -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>;

View file

@ -1,7 +1,3 @@
/**
* `_base.lifecycle` Ledger errors.
*/
export class LedgerDisposedError extends Error {
constructor(
readonly ledgerLabel: string,

View file

@ -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,

View file

@ -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'

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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>();

View file

@ -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);

View file

@ -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 {

View file

@ -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 {

View file

@ -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';

View file

@ -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);

View file

@ -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']);

View file

@ -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';

View file

@ -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';

View file

@ -1,7 +1,3 @@
/**
* Hero-name slug generator for readable, memorable identifiers.
*/
import { randomInt } from 'node:crypto';
export const HERO_NAMES = [

View file

@ -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 {

View file

@ -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>(() => {});

View file

@ -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,

View file

@ -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 {

View file

@ -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;

View file

@ -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;

View file

@ -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> =

View file

@ -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

View file

@ -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;

View file

@ -1,7 +1,3 @@
/**
* XML escaping helpers for content, attribute values, and tag delimiters.
*/
export function escapeXml(input: string): string {
return input
.replaceAll('&', '&amp;')

View file

@ -1,7 +1,3 @@
/**
* agent-core-v2 version helper exposes the package version to integrations.
*/
export function getCoreVersion(): string {
return '0.0.0';
}

View file

@ -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';

View file

@ -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;

View file

@ -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 {

View file

@ -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';

View file

@ -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';

View file

@ -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";

View file

@ -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';

View file

@ -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;

View file

@ -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';

View file

@ -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 {

View file

@ -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';

View file

@ -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);
}

View file

@ -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)'}`;

View file

@ -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';

View file

@ -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,

View file

@ -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';

View file

@ -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';

View file

@ -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 {

View file

@ -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';

View file

@ -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
* assistanttool 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) {

View file

@ -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 {

View file

@ -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>';

View file

@ -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 {

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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