diff --git a/.agents/skills/agent-core-dev/SKILL.md b/.agents/skills/agent-core-dev/SKILL.md index a7ec68c259..c4b60edbb5 100644 --- a/.agents/skills/agent-core-dev/SKILL.md +++ b/.agents/skills/agent-core-dev/SKILL.md @@ -60,7 +60,7 @@ Invariants that hold across every stage. Each is expanded in the stage file note 2. `@IX` decorates constructor parameters only; parameter order depends on construction (static-first for `createInstance`, `@IX`-first for scoped services). (service-authoring.md) 3. Both interface and impl carry `_serviceBrand`; the `createDecorator` name is globally unique. (implement.md) 4. Parent scope never depends on child scope — short-lived may inject long-lived, never the reverse. (orient.md) -5. No cyclic dependencies — refactor (extract a third Service / use an event / re-scope); do not break the cycle with `Delayed`. (design.md, implement.md) +5. No cyclic dependencies — refactor (extract a third Service / use an event / re-scope); activation timing does not break dependency cycles. (design.md, implement.md) 6. `ServicesAccessor` is valid only during `invokeFunction` — never stash it for async use. (implement.md) 7. Scope follows state identity — no `Map` at `App` to fake per-session state. (design.md) 8. Foundational layers never know upstream ones; business code never depends on the edge layer (`gateway`/`rpc`). (design.md) diff --git a/.agents/skills/agent-core-dev/align.md b/.agents/skills/agent-core-dev/align.md index 50c1c4936f..82e70fa3be 100644 --- a/.agents/skills/agent-core-dev/align.md +++ b/.agents/skills/agent-core-dev/align.md @@ -12,8 +12,8 @@ v1 is a **VSCode-style singleton container**: services self-register with `regis | Concern | v1 (`agent-core`) | v2 (`agent-core-v2`) | |---|---|---| -| Registration | `registerSingleton(IX, X, InstantiationType.Delayed)` | `registerScopedService(LifecycleScope.X, IX, X, InstantiationType.Delayed, 'domain')` | -| DI import | `from '../../di'` | `from '#/_base/di/scope'` / `'#/_base/di/instantiation'` / `'#/_base/di/extensions'` / `'#/_base/di/lifecycle'` | +| Registration | `registerSingleton(IX, X, InstantiationType.Delayed)` | `registerScopedService(LifecycleScope.X, IX, X, ScopeActivation.OnDemand, 'domain')` | +| DI import | `from '../../di'` | `from '#/_base/di/scope'` / `'#/_base/di/instantiation'` / `'#/_base/di/lifecycle'` | | Lifetime | implicit singleton-per-container | explicit `LifecycleScope` (App/Session/Agent) — see orient.md | | Domain granularity | coarse (`session`, `tool`, `loop`) | fine, split by scope + responsibility | | Test import | `from '@moonshot-ai/agent-core/di/test'` | `from '#/_base/di/test'` | @@ -157,9 +157,8 @@ import { InstantiationType, registerSingleton } from '../../di'; registerSingleton(IXxxService, XxxService, InstantiationType.Delayed); // v2 -import { InstantiationType } from '#/_base/di/extensions'; -import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; -registerScopedService(LifecycleScope.Session, IXxxService, XxxService, InstantiationType.Delayed, 'xxx'); +import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; +registerScopedService(LifecycleScope.Session, IXxxService, XxxService, ScopeActivation.OnDemand, 'xxx'); ``` **Imports:** @@ -232,4 +231,4 @@ Before submitting a port: - Decide scope from state identity before writing v2 code; the scope is fixed at registration. - Verify the domain mapping against current v2 `src/`; the table here is a starting point, not authority. - One Service owns state at exactly one lifetime; split global-view + per-instance into registry + per-instance. -- A dependency cycle introduced by the port means a v1 import is now backwards — refactor, do not route around it with `Delayed`. +- A dependency cycle introduced by the port means a v1 import is now backwards — refactor it; activation timing cannot break the cycle. diff --git a/.agents/skills/agent-core-dev/implement.md b/.agents/skills/agent-core-dev/implement.md index 77a845773f..f8df820ed4 100644 --- a/.agents/skills/agent-core-dev/implement.md +++ b/.agents/skills/agent-core-dev/implement.md @@ -5,7 +5,7 @@ Write the contract leaf, implementation leaf (with its registration), and the pa ## Standard recipe for a new `IXxxService` 1. **Contract leaf** — `src//.ts`: interface (with `_serviceBrand`) + `createDecorator` identity. -2. **Impl leaf** — `src//Service.ts`: class with `@IX` constructor deps; top-level `registerScopedService(scope, IX, Impl, type, '')`. +2. **Impl leaf** — `src//Service.ts`: class with `@IX` constructor deps; top-level `registerScopedService(scope, IX, Impl, activation, '')`. The fourth argument is activation; the fifth is the domain. 3. **Entry** — `src/index.ts`: load each leaf precisely — `export * from './/';` for the contract and `import './/Service';` for the impl (importing the impl runs the registration). **No `src//index.ts` barrel.** 4. **Tests** — see test.md. @@ -31,8 +31,7 @@ export const IGreeter: ServiceIdentifier = createDecorator(' ```ts // greet/greetService.ts -import { InstantiationType } from '#/_base/di/extensions'; -import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; +import { LifecycleScope, registerScopedService, ScopeActivation } from '#/_base/di/scope'; import { IGreeter } from './greet'; export class Greeter implements IGreeter { @@ -41,11 +40,11 @@ export class Greeter implements IGreeter { } registerScopedService( - LifecycleScope.App, // lifetime: process-wide - IGreeter, // identity - Greeter, // implementation - InstantiationType.Eager, // when to construct: immediately - 'greet', // domain name (for diagnostics) + LifecycleScope.App, // lifetime: process-wide + IGreeter, // identity + Greeter, // implementation + ScopeActivation.OnScopeCreated, // construct when the App scope is created + 'greet', // domain name (for diagnostics) ); ``` @@ -95,10 +94,16 @@ const meta = accessor.get(ISessionMetadata); // type is ISessionMetadata ## §3 Scoped registration (not global) -Swap the `scope` argument to bind to a different tier: +Swap the `scope` argument to bind to a different tier. Use `ScopeActivation.OnDemand` when the service should be constructed only on its first `get()`: ```ts -registerScopedService(LifecycleScope.Session, ISessionMetadata, SessionMetadata, InstantiationType.Delayed, 'sessionMetadata'); +registerScopedService( + LifecycleScope.Session, + ISessionMetadata, + SessionMetadata, + ScopeActivation.OnDemand, + 'sessionMetadata', +); ``` Remember the visibility rule from orient.md: a service may inject services from its own scope or any ancestor; never from a descendant. @@ -124,19 +129,44 @@ export class WSBroadcastService extends Disposable implements IWSBroadcastServic - The container calls `dispose()` automatically when the service is torn down; child resources release in turn. - Disposal order is deterministic (orient.md): child scopes first, then reverse construction order within a scope. -## §5 Eager vs delayed instantiation +## §5 Scope activation + +`ScopeActivation` is the only construction-timing choice for scoped services: ```ts -// Eager: constructed when the scope is created -registerScopedService(LifecycleScope.App, ILogService, LogService, InstantiationType.Eager, 'log'); +export enum ScopeActivation { + OnScopeCreated = 0, + OnDemand = 1, +} +``` -// Delayed: constructed on first get -registerScopedService(LifecycleScope.App, IScopeRegistry, ScopeRegistry, InstantiationType.Delayed, 'gateway'); +```ts +// Default: construct the real instance while the App scope is created. +registerScopedService( + LifecycleScope.App, + ILogService, + LogService, + ScopeActivation.OnScopeCreated, + 'log', +); + +// Construct the real instance on the first get(IScopeRegistry). +registerScopedService( + LifecycleScope.App, + IScopeRegistry, + ScopeRegistry, + ScopeActivation.OnDemand, + 'gateway', +); ``` -A `Delayed` service returns a **Proxy** that constructs the real instance on first property access. Listeners registered on its `onDid…` / `onWill…` events before construction are not lost — the container records them and replays the subscriptions once the instance exists. +`ScopeActivation.OnScopeCreated` is the default fourth argument. Scope creation constructs every registration using this mode, after constructing its dependencies. If any constructor fails, scope creation fails. Use it for ordinary services and for constructor side effects that must exist when the scope becomes ready. + +`ScopeActivation.OnDemand` stores the descriptor without constructing the service. The first `get()` constructs and caches the real instance directly; later `get()` calls return that same instance. Use it only when construction should wait until the service is actually requested. + +Both modes use the same dependency graph and reject cycles with `CyclicDependencyError`. -> Rule of thumb: `Eager` for dependency-free, frequently-used, or "early side effect" services (e.g. `ILogService`); default to `Delayed` otherwise. +The complete registration signature is `registerScopedService(scope, id, ctor, activation = ScopeActivation.OnScopeCreated, domain?)`: activation is the fourth argument and domain is the fifth. ## §6 Using a service inside a plain function (`invokeFunction`) @@ -227,9 +257,9 @@ v2's stance: **the dependency graph must be acyclic.** 2. **Decouple with an event.** If A only needs to know about a change in B, have B emit via `IEventService` and A subscribe, rather than A holding a reference to B. 3. **Re-partition scope.** One of them may belong at a different tier — moving it makes the cycle disappear. -### Delayed as a cycle-breaker (legacy escape hatch — forbidden) +### Activation does not break cycles -A legacy mechanism lets a `Delayed` edge turn a "soft cycle" into a non-synchronous Proxy. **Do not use it to bypass cyclic dependencies** — it exists for historical compatibility, not to paper over your design. On `CyclicDependencyError`, refactor per the above. +Both `ScopeActivation.OnScopeCreated` and `ScopeActivation.OnDemand` construct through the same synchronous dependency graph. Changing activation cannot make a cycle valid. On `CyclicDependencyError`, refactor per the above. ## Interface cheat sheet @@ -237,7 +267,7 @@ A legacy mechanism lets a `Delayed` edge turn a "soft cycle" into a non-synchron |---|---|---| | `createDecorator(name)` → `ServiceIdentifier` | §1 | identity (runtime key + compile-time type + param decorator) | | `@IService` | §2, §7 | declare a dependency on a constructor param | -| `registerScopedService(scope, id, ctor, type, domain)` | §1, §3, §5 | bind an impl to a lifetime tier | +| `registerScopedService(scope, id, ctor, activation, domain)` | §1, §3, §5 | bind an impl to a lifetime tier and construction time | | `ServicesAccessor.get(IX)` | §2, §6 | resolve an instance by interface | | `IInstantiationService.invokeFunction(fn, …)` | §6, §8 | obtain a temporary accessor inside a function | | `IInstantiationService.createInstance(ctor, …args)` | §7 | build a non-singleton object with deps injected | @@ -245,6 +275,7 @@ A legacy mechanism lets a `Delayed` edge turn a "soft cycle" into a non-synchron | `getScopedServiceDescriptors(scope)` | §8 | retrieve all descriptors registered at a tier | | `Disposable` / `DisposableStore` / `IDisposable` | §4 | resource management and disposal | | `Scope` / `LifecycleScope` | §3, §8 | the lifetime tree | +| `ScopeActivation` | §3, §5 | choose scope-created or first-`get()` construction | | `SyncDescriptor` | (tests / low-level) | package a constructor + static args into a pending descriptor | > Legacy export (not used in v2, just recognize it): `refineServiceDecorator` is a VS Code leftover DI helper. v2 src/test has zero references; always use `registerScopedService`. @@ -255,4 +286,4 @@ A legacy mechanism lets a `Delayed` edge turn a "soft cycle" into a non-synchron - `@IX` decorates constructor params only; parameter order depends on construction (static-first for `createInstance`, `@IX`-first for scoped services — see service-authoring.md). - Both interface and impl carry `_serviceBrand`; the `createDecorator` name is globally unique. - `ServicesAccessor` is valid only during `invokeFunction` — never stash it for async use. -- No cyclic dependencies — refactor (extract / event / re-scope); do not break the cycle with `Delayed`. +- No cyclic dependencies — refactor (extract / event / re-scope); activation does not change cycle detection. diff --git a/.agents/skills/agent-core-dev/server-align.md b/.agents/skills/agent-core-dev/server-align.md index f7976b7c9e..7e1ab7b99a 100644 --- a/.agents/skills/agent-core-dev/server-align.md +++ b/.agents/skills/agent-core-dev/server-align.md @@ -109,6 +109,8 @@ export const IAgentPromptService: ServiceIdentifier = ```ts // promptService.ts — impl delegates to the native v2 Service +import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; + constructor(@IAgentPromptService private readonly prompt: IAgentPromptService /*, ... */) {} // submit() builds v2-native input, calls the native Service, projects the result // back into the protocol PromptSubmitResult. @@ -117,7 +119,7 @@ registerScopedService( LifecycleScope.Agent, // scope = the lifetime of the legacy state IAgentPromptService, AgentPromptLegacyService, - InstantiationType.Delayed, + ScopeActivation.OnDemand, 'prompt', ); ``` diff --git a/.agents/skills/agent-core-dev/service-authoring.md b/.agents/skills/agent-core-dev/service-authoring.md index d05e1fc0aa..316ccf05cb 100644 --- a/.agents/skills/agent-core-dev/service-authoring.md +++ b/.agents/skills/agent-core-dev/service-authoring.md @@ -137,8 +137,7 @@ Holds the concrete class(es) and the top-level registration. A typical impl: * … collaborators as roles ("logs through `log`") … Bound at App scope. */ -import { InstantiationType } from '#/_base/di/extensions'; -import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; +import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { ILogService } from '#/log'; import { type Greeting, IGreeter } from './greet'; @@ -154,12 +153,12 @@ export class Greeter implements IGreeter { } } -registerScopedService(LifecycleScope.App, IGreeter, Greeter, InstantiationType.Eager, 'greet'); +registerScopedService(LifecycleScope.App, IGreeter, Greeter, ScopeActivation.OnScopeCreated, 'greet'); ``` What belongs here: -- **Imports** — `InstantiationType` from `'#/_base/di/extensions'`; `LifecycleScope` + `registerScopedService` from `'#/_base/di/scope'`; collaborators via the `#/` alias; the contract's types + decorator via a relative `./` import. +- **Imports** — `LifecycleScope` + `ScopeActivation` + `registerScopedService` from `'#/_base/di/scope'`; collaborators via the `#/` alias; the contract's types + decorator via a relative `./` import. - **Class** — `XxxService implements IXxxService`, with `declare readonly _serviceBrand: undefined`. - **Helper classes / functions** used only by this impl (e.g. a built-in writer, an `extractError` helper) — co-located in the same file. - **Top-level `registerScopedService(...)`** — one per Service the file owns; importing the impl file runs the registration. @@ -203,6 +202,17 @@ A scoped Service may expose a factory method that returns a **new** instance of - `readonly` public fields only for immutable exposed state; prefer a getter (`get level()`) when the value can change. - Keep state minimal — a Service owns only the state that matches its scope's identity (design.md §2). Anything else belongs in a different Service. +### Runtime state goes into the per-scope state container + +Session/Agent-scope Services register their runtime state into the scope's state container (`ISessionStateService` / `IAgentStateService`, both over `_base`'s `StateRegistry`) instead of holding it in bare instance fields, so per-scope state lives in one observable place (`snapshot()` / `onDidChange`) and dies with the scope. Reference: `session/interaction/interactionService.ts`. + +- Declare keys in the domain file and export them: `export const interactionPendingKey = defineState>('interaction.pending', () => new Map())` — `.` naming, factory initializers. +- Inject `@ISessionStateService private readonly states` (or the Agent token) and `this.states.register(key)` per key at the top of the constructor. +- Replace the field with accessors: a getter for collections only mutated in place (`this.foo.add(...)` keeps working — the container stores references, never clones); add a setter routed through `states.set` for reassigned scalars. Call sites stay unchanged. +- Values must be plain data: scalars, arrays, and literal objects/Maps/Sets built from them. Never register class instances, resource handles (disposables, abort controllers, Promise locks), or objects holding service references — the regression precedent: one registry key whose class instances reached the whole DI graph deep-copied to hundreds of MB on `snapshot()` and OOM-killed the server. This means registries whose entries carry resources (the tool registry, the task map, prompt queues) stay as instance fields alongside Emitters, hook slots, disposable slots, waiter arrays, caches, and queue instances. +- `snapshot()` additionally recurses plain data only: values with a custom prototype collapse to a `'(ClassName)'` marker — a `_base`-level backstop, not a license to register resource-bearing values. +- Durable, replayable state does NOT belong here — it stays on wire Models. The container is memory-only. + ## Events v2 has two distinct event mechanisms. Pick by audience: @@ -234,7 +244,7 @@ Conventions: - Back the public `Event` with a private `Emitter`, registered with `this._register(...)` so it disposes with the Service. - Naming: `onDid…` for "happened" (past tense, after the fact); `onWill…` for "about to happen" (may allow `waitUntil` participation / veto — see `AsyncEmitter` / `IWaitUntil` in `'#/_base/event'`). -- The Delayed-instantiation Proxy preserves early `onDid…` / `onWill…` subscriptions (implement.md §5). +- A service must be constructed before consumers can subscribe to its events. Use the default `OnScopeCreated` activation when subscriptions must be available as soon as the scope is ready. ### `IEventService` — global pub-sub bus @@ -307,8 +317,7 @@ export const IGreeter: ServiceIdentifier = createDecorator(' ```ts // greet/greetService.ts -import { InstantiationType } from '#/_base/di/extensions'; -import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; +import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { type Greeting, IGreeter } from './greet'; export class Greeter implements IGreeter { @@ -316,7 +325,7 @@ export class Greeter implements IGreeter { hello(): Greeting { return { message: 'hi' }; } } -registerScopedService(LifecycleScope.App, IGreeter, Greeter, InstantiationType.Eager, 'greet'); +registerScopedService(LifecycleScope.App, IGreeter, Greeter, ScopeActivation.OnScopeCreated, 'greet'); ``` ```ts diff --git a/.agents/skills/agent-core-dev/test.md b/.agents/skills/agent-core-dev/test.md index 37f9319840..d70945fb91 100644 --- a/.agents/skills/agent-core-dev/test.md +++ b/.agents/skills/agent-core-dev/test.md @@ -79,9 +79,9 @@ Reach for this only when *which layer a service lives in* is itself the thing be ```ts import { beforeEach, describe, expect, it } from 'vitest'; -import { InstantiationType } from '#/_base/di/extensions'; import { LifecycleScope, + ScopeActivation, _clearScopedRegistryForTests, registerScopedService, } from '#/_base/di/scope'; @@ -94,7 +94,7 @@ describe('XxxService (scoped)', () => { LifecycleScope.Agent, IXxxService, XxxService, - InstantiationType.Delayed, + ScopeActivation.OnDemand, 'xxx', ); }); diff --git a/.changeset/agent-scope-state-container.md b/.changeset/agent-scope-state-container.md new file mode 100644 index 0000000000..dff36e468d --- /dev/null +++ b/.changeset/agent-scope-state-container.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Hold per-agent runtime state of the experimental engine in the agent-scope state container, so it is observable in one place and disposed with the agent; state snapshots collapse class instances to name markers so resource graphs cannot exhaust memory during export. diff --git a/.changeset/eager-scope-instantiation.md b/.changeset/eager-scope-instantiation.md new file mode 100644 index 0000000000..bef351871d --- /dev/null +++ b/.changeset/eager-scope-instantiation.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Instantiate every registered service eagerly at scope creation on the experimental engine, following the dependency graph automatically, and drop the hand-maintained lists that resolved side-effect services one by one at startup. diff --git a/.changeset/session-scope-state-container.md b/.changeset/session-scope-state-container.md new file mode 100644 index 0000000000..de22c0e6de --- /dev/null +++ b/.changeset/session-scope-state-container.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Hold per-session runtime state of the experimental engine in the session-scope state container, so it is observable in one place and disposed with the session. diff --git a/AGENTS.md b/AGENTS.md index 02c44a9b1e..31b25124b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo - `apps/kimi-code`: the CLI / TUI application. It consumes core capabilities through `@moonshot-ai/kimi-code-sdk` and must not depend directly on `@moonshot-ai/agent-core`. When writing or modifying its terminal UI, use the `write-tui` skill (`.agents/skills/write-tui/SKILL.md`). - `apps/kimi-web`: the browser web UI, a peer to the TUI. Vue 3 + Vite + vue-i18n; talks to the server over REST + WebSocket under `/api/v1`. It must not depend on `@moonshot-ai/agent-core` (wire types are re-implemented locally). Debug against the two engines via the root `pnpm dev:v1` / `pnpm dev:v2` backend scripts — the dev Sidebar shows the active backend and switches it at runtime. See `apps/kimi-web/AGENTS.md`. - `apps/vis`, `apps/vis/server`, `apps/vis/web`: visual debugging tools for sessions and replays. -- `apps/kimi-inspect`: web inspector for the kap-server `/api/v1/debug` RPC surface — workspace/session browser, per-session chat, and Service panels (data + trigger buttons) for the Session and Agent scopes. A left icon rail (`src/components/NavRail.tsx`) switches top-level views: the Chat workspace, the Model Catalog (`src/components/ModelCatalogView.tsx` — every Provider with its Models and the default marker, via `IModelCatalog` / `IModelService` channel proxies), and App Services (`src/components/AppServicesView.tsx` — the app-scope Service reflection, full width; session/agent scopes stay in the Chat view's right `Inspector`, whose agent tab also carries a Plan lookup card — `PlanCard` in `src/components/Inspector.tsx` — querying `GET /sessions/{id}/transcript/plan` (one tool_call_id, or every plan of the agent) via `src/transcript/api.ts`'s `fetchTranscriptPlan`). Expanding a Model opens the model inspector inside that view: provider/model config layers plus the resolved runtime view with per-value provenance (config / override / builtin / env / synthesized), served on demand by `IModelCatalog.inspect` — the same resolution pass the runtime's `get` serves, traced via `ResolutionTraceCollector` and assembled by `kosong/model/inspection.ts`. Built on its own old-klient-style channel layer (`src/channel/`: the VS Code `ProxyChannel` model — service-bound `IChannel`, HTTP `ProxyChannel` for calls routed to `/api/v1/debug`), typed by `agent-core-v2` Service interfaces; `GET /api/v1/debug/channels` loads the whole wire protocol 1:1 (every scoped Service, no whitelist). There is no Service-event push channel: panels fetch/refresh on demand (`Sidebar` polls react-query on a 15 s interval), and a connection failure shows a blocking "Debug surface unavailable" screen instead of falling back anywhere. Session-level coarse status is the one exception: `src/activity/` holds a second `/api/v1/ws` client (`GlobalEventsWs`) that subscribes to nothing and consumes the server-pushed global facts — `event.session.work_changed` updates a per-session activity map (`SessionActivityHub` + subscribe/version store, seeded on connect/reconnect from `GET /api/v1/sessions`), while `event.session.created` / `session.meta.updated` invalidate the `['sessions']` query; the `Sidebar` session rows render `running` / `approval` / `question` / `failed` badges from it via `useSessionActivities`. The Vite dev server proxies `/api` to a running kap-server (`KIMI_SERVER_URL`, default `http://127.0.0.1:58627`) and exposes `GET /__inspect/servers` (`vite/serverDiscovery.ts`), which scans the local kap-server instance registry (`~/.kimi-code/server/instances` + legacy `lock`) and the home token so the app can zero-config auto-connect and switch servers from the header dropdown at runtime. The per-session chat (`src/components/ChatView.tsx`) renders turn-granularly from the **transcript** surface instead of context memory: full state is read from `GET /api/v1/sessions/{id}/transcript` (initial load = newest page, refreshes re-read from the tail backwards), older history auto-pages with `before_turn` via an IntersectionObserver sentinel at the top of the scroll view, and each timeline item is wrapped in `content-visibility: auto` + `contain-intrinsic-size` so the browser virtualizes off-screen rendering natively (no windowing library); `/api/v1/ws` is an incremental channel (`transcript.ops`, grade `block` — the cheapest grade that still carries whole-state frame upserts, dropping per-token `append` frames; `transcript.reset` is ignored by the store, surfaced only to the audit recorder via the optional `onReset` handler). The channel tracks the op-batch watermark: a dedicated `subscribe_v2` control frame carries the per-agent grades and the `transcript_since` cursor, a seq gap / reconnect / `resync_required` / append gap triggers a point-to-point catch-up (`fetchTranscriptOps` → `GET .../transcript/ops?since_seq=`), and any legacy/incomplete answer falls back to the full REST refresh. Convergence reuses `@moonshot-ai/transcript`'s L2 reducer (`src/transcript/`: REST/WS clients + store; the data model and reducer come from the package, nothing is re-implemented locally). The Transcript audit panel (`src/components/audit/`, always docked right of the chat) replays how the visible store was built: an `AuditTrail` (`src/audit/`) records every step — each REST page (request + replace/prepend), every WS frame (`transcript.ops` live/buffered/flushed/catchup, `transcript.reset`), loss signals, and prompt/cancel actions — with the resulting immutable `AgentState` per entry; the panel offers a draggable timeline plus a Diff tab (structural diff vs the previous entry: added/modified/removed colored, long strings tail-truncated, all fields kept), a full State view, and the raw Event payload. +- `apps/kimi-inspect`: web inspector for the kap-server `/api/v1/debug` RPC surface — workspace/session browser, per-session chat, and Service panels (data + trigger buttons) for the Session and Agent scopes. A left icon rail (`src/components/NavRail.tsx`) switches top-level views: the Chat workspace, the Model Catalog (`src/components/ModelCatalogView.tsx` — every Provider with its Models and the default marker, via `IModelCatalog` / `IModelService` channel proxies), and App Services (`src/components/AppServicesView.tsx` — the app-scope Service reflection, full width; the Agent scope stays in the Chat view's right dock (`src/components/RightPanel.tsx`) across two tabs: the `Agent` tab (`Inspector`: agent switcher + a Plan lookup card — `PlanCard` in `src/components/Inspector.tsx` — querying `GET /sessions/{id}/transcript/plan` (one tool_call_id, or every plan of the agent) via `src/transcript/api.ts`'s `fetchTranscriptPlan` — plus the agent Service panels) and the `State` tab (every key an Agent Service registered into the agent-state container, polled live via `IAgentStateService.snapshot()` — the same live diff-tree view as the session State tab, sharing `StateCard` from `src/components/StateCard.tsx`), while the Session scope has its own column right next to the session-list sidebar (`src/components/SessionPane.tsx`) with two tabs: Services (the pending-interactions card — `src/components/InteractionsCard.tsx` — plus the session Service panels) and State (every key a Session Service registered into the session-state container, read on demand via `ISessionStateService.snapshot()`)). Expanding a Model opens the model inspector inside that view: provider/model config layers plus the resolved runtime view with per-value provenance (config / override / builtin / env / synthesized), served on demand by `IModelCatalog.inspect` — the same resolution pass the runtime's `get` serves, traced via `ResolutionTraceCollector` and assembled by `kosong/model/inspection.ts`. Built on its own old-klient-style channel layer (`src/channel/`: the VS Code `ProxyChannel` model — service-bound `IChannel`, HTTP `ProxyChannel` for calls routed to `/api/v1/debug`), typed by `agent-core-v2` Service interfaces; `GET /api/v1/debug/channels` loads the whole wire protocol 1:1 (every scoped Service, no whitelist). There is no Service-event push channel: panels fetch/refresh on demand (`Sidebar` polls react-query on a 15 s interval), and a connection failure shows a blocking "Debug surface unavailable" screen instead of falling back anywhere. Session-level coarse status is the one exception: `src/activity/` holds a second `/api/v1/ws` client (`GlobalEventsWs`) that subscribes to nothing and consumes the server-pushed global facts — `event.session.work_changed` updates a per-session activity map (`SessionActivityHub` + subscribe/version store, seeded on connect/reconnect from `GET /api/v1/sessions`), while `event.session.created` / `session.meta.updated` invalidate the `['sessions']` query; the `Sidebar` session rows render `running` / `approval` / `question` / `failed` badges from it via `useSessionActivities`. The Vite dev server proxies `/api` to a running kap-server (`KIMI_SERVER_URL`, default `http://127.0.0.1:58627`) and exposes `GET /__inspect/servers` (`vite/serverDiscovery.ts`), which scans the local kap-server instance registry (`~/.kimi-code/server/instances` + legacy `lock`) and the home token so the app can zero-config auto-connect and switch servers from the header dropdown at runtime. The per-session chat (`src/components/ChatView.tsx`) renders turn-granularly from the **transcript** surface instead of context memory: full state is read from `GET /api/v1/sessions/{id}/transcript` (initial load = newest page, refreshes re-read from the tail backwards), older history auto-pages with `before_turn` via an IntersectionObserver sentinel at the top of the scroll view, and each timeline item is wrapped in `content-visibility: auto` + `contain-intrinsic-size` so the browser virtualizes off-screen rendering natively (no windowing library); `/api/v1/ws` is an incremental channel (`transcript.ops`, grade `block` — the cheapest grade that still carries whole-state frame upserts, dropping per-token `append` frames; `transcript.reset` is ignored by the store, surfaced only to the audit recorder via the optional `onReset` handler). The channel tracks the op-batch watermark: a dedicated `subscribe_v2` control frame carries the per-agent grades and the `transcript_since` cursor, a seq gap / reconnect / `resync_required` / append gap triggers a point-to-point catch-up (`fetchTranscriptOps` → `GET .../transcript/ops?since_seq=`), and any legacy/incomplete answer falls back to the full REST refresh. Convergence reuses `@moonshot-ai/transcript`'s L2 reducer (`src/transcript/`: REST/WS clients + store; the data model and reducer come from the package, nothing is re-implemented locally). The Transcript audit panel (`src/components/audit/`, the `Audit` tab of the chat view's right dock — `src/components/RightPanel.tsx`, fed the trail by `ChatView`'s `onTrailChange`) replays how the visible store was built: an `AuditTrail` (`src/audit/`) records every step — each REST page (request + replace/prepend), every WS frame (`transcript.ops` live/buffered/flushed/catchup, `transcript.reset`), loss signals, and prompt/cancel actions — with the resulting immutable `AgentState` per entry; the panel offers a draggable timeline plus a Diff tab (structural diff vs the previous entry: added/modified/removed colored, long strings tail-truncated, all fields kept), a full State view, and the raw Event payload. - `packages/agent-core`: the unified agent engine, including Agent, Session, profile, skills, tools, plan, permission, background, records, the in-process DI service layer (`src/services/`), and other core capabilities. - `packages/node-sdk`: the public TypeScript SDK and harness. - `packages/kosong`: the LLM / provider abstraction layer. diff --git a/apps/kimi-code/src/tui/components/messages/tool-call.ts b/apps/kimi-code/src/tui/components/messages/tool-call.ts index 5bf2689bc5..52e8b480c4 100644 --- a/apps/kimi-code/src/tui/components/messages/tool-call.ts +++ b/apps/kimi-code/src/tui/components/messages/tool-call.ts @@ -197,9 +197,8 @@ const PLAN_SAVED_TO_RE = /\nPlan saved to: ([^\n]+)\n/; /** * Parses the ExitPlanMode result content string to recover the approval outcome * and optional plan path. Core-side templates live in - * `packages/agent-core-v2/src/agent/plan/tools/exit-plan-mode.ts` (auto-approved - * path) and `.../permissionPolicy/policies/exit-plan-mode-review-ask.ts` - * (user-reviewed path): + * `packages/agent-core/src/tools/builtin/planning/exit-plan-mode.ts` and + * `.../agent/permission/policies/exit-plan-mode-review-ask.ts`: * - Approved output starts with 'Exited plan mode.' and selected options * are reported as 'Selected approach: