Skip to content

Commit 7799bd7

Browse files
authored
feat(agent-core-v2): add per-scope keyed state container (#2192)
* refactor(agent-core-v2): instantiate all registered services eagerly at scope creation - add instantiateAll to scope creation: every service registered for a scope tier is constructed when the scope is created, following the static dependency graph; a failing constructor fails scope creation - drop the hand-maintained eager-resolution lists: igniteEagerServices in AgentLifecycleService, the force-instantiated session services in SessionLifecycleService, and the kosong config bridge get in bootstrap - update DI docs and agent-core-dev skill guidance for the new semantics - adjust affected tests (registry hygiene, timer draining, listener ordering) and add scope-tree coverage for eager instantiation * feat(agent-core-v2): add per-scope keyed state container (state domain) - add _base StateRegistry: typed StateKey/defineState descriptors with register/get/set, per-key onDidChange and global onDidChangeAny events, and BugIndicatingError on duplicate or unregistered key access - bind thin per-scope services at each tier: IStateService (App), ISessionStateService (Session), IAgentStateService (Agent), so scoped plain-data state lives in one observable container that dies with the scope - register the state domain in the layer check script and export the new services from the package index - add StateRegistry unit tests and scoped resolution tests * refactor(agent-core-v2): move session-scope service state into ISessionStateService - add StateRegistry.snapshot() with JSON-safe serialization for Map/Set/Date/circular values - migrate plain-data fields of 13 session-scope services (cron, interaction, sessionActivity, agentProfileCatalog, fs, fsWatch, log, metadata, skillCatalog, toolPolicy, workspaceCommand, workspaceContext) to defineState keys registered in sessionState - register SessionStateService in affected tests and add test/state/stubs.ts helper * feat(kimi-inspect): rework chat layout with session pane and tabbed right dock - add SessionPane column next to the sidebar: Services tab (pending interactions + session Service panels) and State tab polling ISessionStateService.snapshot() every second, rendered as a live diff tree - merge the transcript audit panel and the agent inspector into one RightPanel with Audit/Agent tabs that keep panel state across switches - extract InteractionsCard from Inspector into its own component - collapse multiline strings in StateTree into a compact hover-preview button with a viewport-clamped fixed popup - test: cover StateRegistry.snapshot() conversions (Map/Set/circular); pass a session state service to SessionInteractionService in kap-server test fakes - update the AGENTS.md project map for the new layout * refactor(agent-core-v2): move agent-scope service mutable state into agentState Register each Agent-scope service's mutable fields into the agent-state container (IAgentStateService) via defineState keys, and access them through get/set accessors backed by states.get/set. Promise locks, disposables, and other mechanism-only fields stay plain instance fields. - define per-service state keys (e.g. activityView.*, goal.*, toolDedupe.*) and register them in each service constructor - replace direct field reads/writes with states-backed accessors across the touched services - update the affected unit tests for the new IAgentStateService dependency * fix(agent-core-v2): collapse class instances in state snapshots - StateRegistry.snapshot() now stops at custom-prototype boundaries and emits '(ClassName)' markers, so resource graphs reachable from registered values (e.g. tool instances holding service references) can no longer exhaust the heap during export; plain data keeps recursing - add agent-scope state test covering the full assembled agent scope - kimi-inspect: extract the session State card into a shared StateCard and add an agent State tab in RightPanel polling IAgentStateService.snapshot() - document the per-scope state container pattern in the agent-core-dev skill * refactor(agent-core-v2): keep resource-holding state out of the state container - move fields holding live resources (tool entries, managed tasks, turn jobs, prompt records, MCP registrations, profile callbacks) back to plain private fields so the agent state service only holds snapshot-safe plain data - add gen:state-manifest script that statically collects defineState keys and their register call sites into docs/state-manifest.d.ts - add state manifest freshness test and update agent-state docs * chore: add changeset for session-scope state container * docs(agent-core-v2): move eager-instantiation notes into the scope.ts header * fix(kimi-inspect): suppress stale state tree while switching state owner
1 parent c497af6 commit 7799bd7

119 files changed

Lines changed: 5231 additions & 637 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/agent-core-dev/implement.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,16 +127,20 @@ export class WSBroadcastService extends Disposable implements IWSBroadcastServic
127127
## §5 Eager vs delayed instantiation
128128

129129
```ts
130-
// Eager: constructed when the scope is created
130+
// Eager (default): constructed when the scope is created — registration alone
131+
// (via module import) is enough, nobody has to `get()` it first
131132
registerScopedService(LifecycleScope.App, ILogService, LogService, InstantiationType.Eager, 'log');
132133

133-
// Delayed: constructed on first get
134+
// Delayed: scope creation materializes only a Proxy; the real constructor
135+
// still defers to first property access
134136
registerScopedService(LifecycleScope.App, IScopeRegistry, ScopeRegistry, InstantiationType.Delayed, 'gateway');
135137
```
136138

139+
When a scope (App / Session / Agent) is created, the container instantiates **every** service registered for that tier: the dependency graph is statically known from the `@IX` constructor metadata, so construction automatically follows dependency order, and cycles still throw `CyclicDependencyError`. A failing constructor fails the whole scope creation.
140+
137141
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.
138142

139-
> Rule of thumb: `Eager` for dependency-free, frequently-used, or "early side effect" services (e.g. `ILogService`); default to `Delayed` otherwise.
143+
> Rule of thumb: keep the default `Eager` for everything; mark `Delayed` only when construction is genuinely expensive and you want it deferred (a legacy escape hatch — only a handful of services use it).
140144
141145
## §6 Using a service inside a plain function (`invokeFunction`)
142146

.agents/skills/agent-core-dev/service-authoring.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,17 @@ A scoped Service may expose a factory method that returns a **new** instance of
203203
- `readonly` public fields only for immutable exposed state; prefer a getter (`get level()`) when the value can change.
204204
- 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.
205205

206+
### Runtime state goes into the per-scope state container
207+
208+
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`.
209+
210+
- Declare keys in the domain file and export them: `export const interactionPendingKey = defineState<Map<string, Pending>>('interaction.pending', () => new Map())``<domain>.<field>` naming, factory initializers.
211+
- Inject `@ISessionStateService private readonly states` (or the Agent token) and `this.states.register(key)` per key at the top of the constructor.
212+
- 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.
213+
- 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.
214+
- `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.
215+
- Durable, replayable state does NOT belong here — it stays on wire Models. The container is memory-only.
216+
206217
## Events
207218

208219
v2 has two distinct event mechanisms. Pick by audience:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@moonshot-ai/kimi-code": patch
3+
---
4+
5+
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.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@moonshot-ai/kimi-code": patch
3+
---
4+
5+
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.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@moonshot-ai/kimi-code": patch
3+
---
4+
5+
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.

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
1717
- `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`).
1818
- `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`.
1919
- `apps/vis`, `apps/vis/server`, `apps/vis/web`: visual debugging tools for sessions and replays.
20-
- `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.
20+
- `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.
2121
- `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.
2222
- `packages/node-sdk`: the public TypeScript SDK and harness.
2323
- `packages/kosong`: the LLM / provider abstraction layer.

apps/kimi-inspect/src/App.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,26 @@
33
* push anymore: the v2 socket (`/api/v2/ws`) that fed the core/session/agent
44
* event streams was removed server-side, so Service panels and the pending
55
* interactions card fetch on demand and the sidebar polls.
6-
* Layout: header / icon rail / view. The `chat` view is the classic trio
7-
* (left sidebar with workspaces + sessions, chat, inspector); the `models`
8-
* view is the full-width model catalog; the `services` view is the
9-
* full-width app-scope Service reflection (`AppServicesView`).
6+
* Layout: header / icon rail / view. The `chat` view is a strip of the
7+
* left sidebar (workspaces + sessions), the session pane (session Services
8+
* / State tabs), the chat column, and the right dock (`RightPanel`) merging
9+
* the transcript audit and the agent inspector under Audit / Agent tabs;
10+
* the `models` view is the full-width model catalog; the `services` view is
11+
* the full-width app-scope Service reflection (`AppServicesView`).
1012
*/
1113

1214
import { useEffect, useState } from 'react';
1315

1416
import { ISessionLifecycleService } from '@moonshot-ai/agent-core-v2/app/sessionLifecycle/sessionLifecycle';
1517

18+
import type { AuditTrail } from './audit/trail';
1619
import { AppServicesView } from './components/AppServicesView';
1720
import { ChatView } from './components/ChatView';
18-
import { Inspector } from './components/Inspector';
1921
import { ModelCatalogView } from './components/ModelCatalogView';
2022
import { NavRail, type AppView } from './components/NavRail';
23+
import { RightPanel } from './components/RightPanel';
2124
import { ServerSwitcher } from './components/ServerSwitcher';
25+
import { SessionPane } from './components/SessionPane';
2226
import { Sidebar } from './components/Sidebar';
2327
import { useConnection } from './connection';
2428
import { errorMessage } from './ui';
@@ -30,6 +34,8 @@ export function App() {
3034
const [view, setView] = useState<AppView>('chat');
3135
const [ready, setReady] = useState(false);
3236
const [resumeError, setResumeError] = useState<unknown>(null);
37+
/** Audit trail of the chat view's transcript channel, rendered in the right dock. */
38+
const [trail, setTrail] = useState<AuditTrail | null>(null);
3339

3440
// Resume (materialize) the session on the server when it is selected, so
3541
// session / agent scoped Services become reachable.
@@ -86,18 +92,25 @@ export function App() {
8692
) : (
8793
<>
8894
<Sidebar activeSessionId={sessionId} onSelectSession={setSessionId} />
95+
<SessionPane sessionId={sessionId} ready={ready} />
8996
{resumeError !== null ? (
9097
<div className="flex flex-1 items-center justify-center p-6 text-center text-[12px] text-red-400">
9198
Failed to open session: {errorMessage(resumeError)}
9299
</div>
93100
) : (
94-
<ChatView sessionId={sessionId} agentId={agentId} ready={ready} />
101+
<ChatView
102+
sessionId={sessionId}
103+
agentId={agentId}
104+
ready={ready}
105+
onTrailChange={setTrail}
106+
/>
95107
)}
96-
<Inspector
108+
<RightPanel
97109
sessionId={sessionId}
98110
agentId={agentId}
99111
onAgentChange={setAgentId}
100112
ready={ready}
113+
trail={trail}
101114
/>
102115
</>
103116
)}

0 commit comments

Comments
 (0)