From cf3e93591fb5daf2b4413303df65f7a4329ca7c5 Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Wed, 29 Jul 2026 20:23:33 +0800 Subject: [PATCH] refactor(agent-core-v2): clean up workspace-domain leftovers and docs - Drop dead code: v2 mergeCallerMcpServers, the transitional ISessionContext.additionalDirs field, an unreachable guard - Fix stale domain references in comments; correct test names - Give the fs-watch refresh test a realistic wait budget under load - Document the four-scope model and workspace domain in AGENTS.md, agent-core-v2 docs, and the agent-core-dev skill --- .agents/skills/agent-core-dev/align.md | 4 +- .agents/skills/agent-core-dev/design.md | 60 +++++++++++-------- .../agent-core-dev/domain-boundaries.md | 2 +- .../skills/agent-core-dev/edge-exposure.md | 16 ++--- .agents/skills/agent-core-dev/implement.md | 2 +- .agents/skills/agent-core-dev/orient.md | 2 +- .../agent-core-dev/service-authoring.md | 6 +- AGENTS.md | 3 +- packages/agent-core-v2/AGENTS.md | 4 ++ packages/agent-core-v2/docs/di.md | 20 ++++--- .../agent-core-v2/docs/rw-model-design.md | 6 +- packages/agent-core-v2/docs/service-design.md | 6 +- .../scripts/check-domain-layers.mjs | 2 +- .../agent-core-v2/src/_base/utils/fileMeta.ts | 2 +- .../src/_base/utils/isoDateTime.ts | 2 +- .../src/agent/mcp/session-config.ts | 15 ----- .../src/agent/plan/configSection.ts | 2 +- .../agent-core-v2/src/agent/plan/planOps.ts | 2 +- .../agent-core-v2/src/app/gateway/gateway.ts | 5 +- .../hostFolderBrowser/hostFolderBrowser.ts | 4 +- .../src/app/sessionIndex/sessionIndex.ts | 2 +- .../src/app/sessionLegacy/sessionLegacy.ts | 4 +- .../src/os/interface/hostEnvironment.ts | 3 +- .../src/os/interface/hostFsWatch.ts | 2 +- .../session/agentLifecycle/agentLifecycle.ts | 7 ++- .../src/session/agentLifecycle/mainAgent.ts | 2 +- packages/agent-core-v2/src/session/errors.ts | 2 +- .../session/sessionContext/sessionContext.ts | 19 ++---- .../workspaceDirs/workspaceDirsService.ts | 4 +- .../workspaceHandlerService.ts | 5 +- .../test/agent/mcp/session-config.test.ts | 58 ------------------ .../workspace/workspaceFs/fsService.test.ts | 4 +- .../test/workspace/workspaceResources.test.ts | 6 +- packages/kap-server/src/protocol/rest-fs.ts | 5 +- packages/kap-server/src/routes/fs.ts | 2 +- packages/kap-server/src/routes/workspaceFs.ts | 2 +- .../src/transports/memory/dispatcher.ts | 2 +- 37 files changed, 119 insertions(+), 175 deletions(-) delete mode 100644 packages/agent-core-v2/test/agent/mcp/session-config.test.ts diff --git a/.agents/skills/agent-core-dev/align.md b/.agents/skills/agent-core-dev/align.md index 82e70fa3b..ae3d2a8b6 100644 --- a/.agents/skills/agent-core-dev/align.md +++ b/.agents/skills/agent-core-dev/align.md @@ -14,7 +14,7 @@ v1 is a **VSCode-style singleton container**: services self-register with `regis |---|---|---| | 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 | +| Lifetime | implicit singleton-per-container | explicit `LifecycleScope` (App/Workspace/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'` | | Resolve SUT in tests | `ix.createInstance(Impl)` (common) | `ix.get(IX)` by interface — see test.md | @@ -63,7 +63,7 @@ Worked example — v1 `ISessionService` (one class, ~600 lines) holds: - this session's metadata → **per-session** unit → v2 `sessionMetaStore` (`ISessionMetaStore`, Session); - this session's activity / status → **per-session** unit → v2 `sessionActivity`; - this session's context projection → **per-session** unit → v2 `sessionContext`; -- child-agent lifecycle driven by a session → **per-session** unit → v2 `agentLifecycle`; create/close/archive/fork of the session itself → **global** unit → v2 `sessionLifecycle` (App). +- child-agent lifecycle driven by a session → **per-session** unit → v2 `agentLifecycle`; create/close/archive/fork of the session itself → **per-workspace** unit → v2 `workspaceHandler` (Workspace, one per live workspace handler). A v1 class that maps cleanly to one v1 decorator often becomes **three to five** v2 Services. That is expected and correct — do not try to keep the v1 class shape. diff --git a/.agents/skills/agent-core-dev/design.md b/.agents/skills/agent-core-dev/design.md index c9b4c2f0c..a7778e7b9 100644 --- a/.agents/skills/agent-core-dev/design.md +++ b/.agents/skills/agent-core-dev/design.md @@ -20,6 +20,7 @@ A Service = a bundle of **state** + a set of **behaviors**, bound to a **lifetim | Scope | State identity (keyed by) | Lifetime | |---|---|---| | `App` | none (single global instance) | the process | +| `Workspace` | `workspaceId` | one workspace handler (materialized once per workspace, never closed — dies with the process) | | `Session` | `sessionId` | one session | | `Agent` | `agentId` | one agent | @@ -33,6 +34,7 @@ A Service = a bundle of **state** + a set of **behaviors**, bound to a **lifetim **Q2. What is the identity of that state?** - one global instance → **`App`** +- one per workspace (shared by every session of that workspace) → **`Workspace`** - one per session → **`Session`** - one per agent → **`Agent`** - a mix (a global registry *and* per-instance state) → **split it** (see §3). @@ -70,7 +72,7 @@ The standard split is "global registry / factory" + "per-instance": | Tier | Role | Naming tends to | |---|---|---| | `App` | global registry / catalog / factory — knows "all of them" and how to create one | `XxxStore` / `XxxRegistry` / `XxxCatalog` | -| `Session` / `Agent` | one instance — only the state of "this one" | `XxxService` / `ISessionXxx` / `IAgentXxx` | +| `Workspace` / `Session` / `Agent` | one instance — only the state of "this one" | `XxxService` / `IWorkspaceXxx` / `ISessionXxx` / `IAgentXxx` | Canonical splits in the codebase: @@ -189,8 +191,9 @@ domain: `` (owning scope: ) │ └─ (accessor) @ ├─ exposes (interfaces I provide, by scope) │ ├─ App : -│ ├─ Session : -│ └─ Agent : +│ ├─ Workspace : +│ ├─ Session : +│ └─ Agent : └─ depends (what I inject) tag = calling style └─ @ direct/event/hook — ``` @@ -225,47 +228,54 @@ Read it as: - `──holds──►` = the ancestor owns a handle to the child scope (it stores the key, not the service). DI allows this. - `accessor.get(...)` = a **runtime borrow**, not a dependency edge. It must cross an `IScopeHandle`, run on demand, never be cached, and finish before the child scope is disposed. -Worked example — `sessionLifecycle`: +Worked example — `workspaceHandler`: ```text -domain: `sessionLifecycle` (owning scope: App) +domain: `workspaceHandler` (owning scope: Workspace) ├─ serves (who uses me) -│ ├─ (inject) — (none yet) +│ ├─ (inject) — (none) │ └─ (accessor) │ ├─ sessionLegacy @App(edge) — v1-compatible create/fork/archive/… │ └─ gateway / rpc @App(edge) — native v2 session lifecycle actions ├─ exposes (interfaces I provide, by scope) -│ ├─ App : ISessionLifecycleService — owns the live session scope tree +│ ├─ Workspace : IWorkspaceHandlerService — owns this workspace's live session scope tree │ ├─ Session : — — (per-session state lives in sessionMetadata / agentLifecycle / …) │ └─ Agent : — — (per-agent state lives in agentLifecycle) └─ depends (what I inject) - ├─ bootstrap @App direct — addresses session storage - ├─ hostEnvironment @App direct — gates scope creation on the probe - ├─ sessionIndex @App direct — persisted read model for cold resumes - ├─ storage @App direct — atomic docs + append logs - ├─ workspace @App direct — resolves a session's workspace - └─ event @App direct — broadcasts session-level facts (e.g. archived) + ├─ workspaceContext @Workspace seed — handler identity + persistence scope + ├─ bootstrap @App direct — addresses session storage + ├─ hostEnvironment @App direct — gates scope creation on the probe + ├─ sessionIndex @App direct — persisted read model for cold resumes + ├─ storage @App direct — atomic docs + append logs + ├─ workspaceDirs / workspaceSkillCatalog / workspaceMcp / … + │ @Workspace direct — the handler's shared resource services + └─ event @App direct — broadcasts session-level facts (e.g. archived) ``` -Cross-scope borrow for `sessionLifecycle`: +Cross-scope borrow for `workspaceHandler`: ```text App scope - SessionLifecycleService ──holds──┐ - GatewayService ───────────holds──┼──► IScopeHandle(sessionId) - │ - │ accessor.get(ISessionMetadata) … - │ └── resolve runs inside the Session scope - ▼ - Session scope (sessionId) - sessionMetadata / agentLifecycle / … ← per-session services live here + WorkspaceLifecycleService ──holds──► IScopeHandle(workspaceId) (one per live handler) + │ + │ accessor.get(IWorkspaceHandlerService) + │ └── resolve runs inside the Workspace scope + ▼ + Workspace scope (workspaceId) + WorkspaceHandlerService ──holds──► IScopeHandle(sessionId) + │ + │ accessor.get(ISessionMetadata) … + │ └── resolve runs inside the Session scope + ▼ + Session scope (sessionId) + sessionMetadata / agentLifecycle / … ← per-session services live here ``` How the three lenses shaped it: -- **Scope (§2)** → the live registry of session scopes is process-wide, so it is App-scoped; per-session data stays in Session-scoped services, reached through the handle's `accessor`. -- **Dependency direction (§5)** → `sessionLifecycle` is consumed by the edge via `accessor` borrows; it never imports the edge. Every downward arrow lands on a peer or a more foundational Service. -- **Extension points (§4)** → new per-session behavior plugs into the Session-scoped services (`sessionMetadata`, `agentLifecycle`, `sessionActivity`); new transports stay at the edge. Neither edits `sessionLifecycle`. +- **Scope (§2)** → the live registry of one workspace's session scopes is per-handler, so it is Workspace-scoped; the process-wide handler registry lives in the App-scoped `workspaceLifecycle`; per-session data stays in Session-scoped services, reached through the handle's `accessor`. +- **Dependency direction (§5)** → `workspaceHandler` is consumed by the edge via `accessor` borrows; it never imports the edge. Every downward arrow lands on a peer or a more foundational Service. +- **Extension points (§4)** → new per-session behavior plugs into the Session-scoped services (`sessionMetadata`, `agentLifecycle`, `sessionActivity`); new transports stay at the edge. Neither edits `workspaceHandler`. For a multi-scope split, the `exposes` block fills more than one scope — see the `records` pattern in §3. diff --git a/.agents/skills/agent-core-dev/domain-boundaries.md b/.agents/skills/agent-core-dev/domain-boundaries.md index c2f62cc9c..8f97847a9 100644 --- a/.agents/skills/agent-core-dev/domain-boundaries.md +++ b/.agents/skills/agent-core-dev/domain-boundaries.md @@ -82,7 +82,7 @@ The `session` domain owns only Session-level identity, metadata, lifecycle comma |---|---|---| | `sessionId`, `workspaceId`, `sessionDir`, `metaScope` | `sessionContext` | Seeded facts; no IO | | `SessionMeta` | `sessionMetadata` | Durable atomic document; entity-like | -| Open session scope registry | `sessionLifecycle` | App-scope live handles; not the persisted entity table | +| Open session scope registry | `workspaceHandler` | Workspace-scope live handles, one registry per workspace handler (the process-wide handler registry is `workspaceLifecycle`); not the persisted entity table | | Session commands such as `archive()` | `session` | Orchestrates metadata, agent teardown, and events | | Persisted session list / get / count | `sessionIndex` | Backend-neutral read model | | Running / idle / awaiting status | `sessionActivity` | Derived from interactions and active turns; owns no state | diff --git a/.agents/skills/agent-core-dev/edge-exposure.md b/.agents/skills/agent-core-dev/edge-exposure.md index 280b1320f..db03a8e79 100644 --- a/.agents/skills/agent-core-dev/edge-exposure.md +++ b/.agents/skills/agent-core-dev/edge-exposure.md @@ -6,10 +6,11 @@ The transport (`/api/v2` over HTTP + WS) lives in the **edge** layer (`gateway`/ ## 1. The edge model -Three scopes, three URL shapes, one dispatcher: +Four scopes, four URL shapes, one dispatcher: ```text GET|POST /api/v2/:sa Core +GET|POST /api/v2/workspace/:workspace_id/:sa Workspace GET|POST /api/v2/session/:session_id/:sa Session GET|POST /api/v2/session/:session_id/agent/:agent_id/:sa Agent ``` @@ -26,9 +27,10 @@ GET|POST /api/v2/session/:session_id/agent/:agent_id/:sa Agent ```ts // actionMap — the allowlist; hides internal domain names. const actionMap = { - core: { 'sessions:list': { service: ISessionIndex, method: 'list' }, ... }, - session: { 'session:read': { service: ISessionMetadata, method: 'read' }, ... }, - agent: { 'profile:getModel': { service: IProfileService, method: 'getModel' }, ... }, + core: { 'sessions:list': { service: ISessionIndex, method: 'list' }, ... }, + workspace: { 'skills:list': { service: IWorkspaceSkillCatalog, method: 'list' }, ... }, + session: { 'session:read': { service: ISessionMetadata, method: 'read' }, ... }, + agent: { 'profile:getModel': { service: IProfileService, method: 'getModel' }, ... }, }; ``` @@ -83,14 +85,14 @@ Read = `GET`, write = `POST`. `sid` = `session_id`, `aid` = `agent_id`. | `session` | `setArchived` | ISessionMetadata.setArchived | POST | | `session` | `status` | ISessionActivity.status | GET | | `session` | `isIdle` | ISessionActivity.isIdle | GET | -| `session` | `archive` | ISessionLifecycleService.archive | POST | +| `session` | `archive` | IWorkspaceHandlerService.archive | POST | | `approvals` | `listPending` | IApprovalService.listPending | GET | | `approvals` | `decide` | IApprovalService.decide | POST | | `questions` | `listPending` | IQuestionService.listPending | GET | | `questions` | `answer` | IQuestionService.answer | POST | | `interactions` | `listPending` | IInteractionService.listPending | GET | | `interactions` | `respond` | IInteractionService.respond | POST | -| `workspace` | `workDir` / `additionalDirs` / `resolve` | IWorkspaceContext.* | GET | +| `workspace` | `workDir` / `additionalDirs` / `resolve` | ISessionWorkspaceContext.* | GET | ### Agent (`/api/v2/session/:sid/agent/:aid/:resource:action`) @@ -126,7 +128,7 @@ These fail §2 and must be wrapped in a facade that takes ids and returns data: | Service | Why not direct | Facade shape | |---|---|---| -| ISessionLifecycleService | returns `IScopeHandle` | `sessions.create` / `fork` / `close` / `archive` → wire Session | +| IWorkspaceHandlerService | returns `IScopeHandle` | `sessions.create` / `fork` / `close` / `archive` → wire Session | | IAgentPromptService / IAgentTurnService | returns `Turn` handle | `prompts.submit` / `steer` / `abort` / `undo` | | ILLMRequester | `AsyncIterable` stream | stream over WS, not RPC | | ISubagentHost | `SubagentHandle` | `subagents.spawn` / `resume` → info | diff --git a/.agents/skills/agent-core-dev/implement.md b/.agents/skills/agent-core-dev/implement.md index f8df820ed..1f860ab90 100644 --- a/.agents/skills/agent-core-dev/implement.md +++ b/.agents/skills/agent-core-dev/implement.md @@ -246,7 +246,7 @@ If A needs B while being created and B needs A while being created, the containe ### Why cycles are disallowed -- Scope layering makes normal dependencies a DAG (Agent → Session → App, resolving upward); a cycle is almost always a design smell. +- Scope layering makes normal dependencies a DAG (Agent → Session → Workspace → App, resolving upward); a cycle is almost always a design smell. - "Making the cycle happen to work" turns construction order into an implicit contract — hard to debug. v2's stance: **the dependency graph must be acyclic.** diff --git a/.agents/skills/agent-core-dev/orient.md b/.agents/skills/agent-core-dev/orient.md index 38750394f..a16ccb28a 100644 --- a/.agents/skills/agent-core-dev/orient.md +++ b/.agents/skills/agent-core-dev/orient.md @@ -75,7 +75,7 @@ So a Session-scoped service is not "L1" — e.g. `session` is Session-scoped but - **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 (Ln) — . `` 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.** `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. +- **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** (`.ts`) state the public contract + scope: which `IXxx` they define and what it is for. - **Impl files** (`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** (`.ts` / `.contrib.ts`) state what they register into the target domain (e.g. "registers the `log` config section into `config`"). diff --git a/.agents/skills/agent-core-dev/service-authoring.md b/.agents/skills/agent-core-dev/service-authoring.md index 316ccf05c..5bd569a08 100644 --- a/.agents/skills/agent-core-dev/service-authoring.md +++ b/.agents/skills/agent-core-dev/service-authoring.md @@ -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 `.ts` + `Service.ts` pair. -- **Scope is in the filename.** `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)). The header comment restates the same scope. - 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. @@ -28,12 +28,12 @@ The package entry `src/index.ts` imports and `export *`s every domain's leaf fil | Artifact | Rule | Example | |---|---|---| -| Interface | `I` + scope prefix + PascalCase domain + role suffix. Scope prefix: `Session` / `Agent` / none (= App). Role suffix is usually `Service`. | `ISessionLogService`, `IAgentLoopService`, `ILogService` (App) | +| Interface | `I` + scope prefix + PascalCase domain + role suffix. Scope prefix: `Workspace` / `Session` / `Agent` / none (= App). Role suffix is usually `Service`. | `IWorkspaceDirs`, `ISessionLogService`, `IAgentLoopService`, `ILogService` (App) | | Class | the interface name minus the leading `I`, plus `Service` if it does not already end in `Service`; `implements` the interface | `SessionLogService implements ISessionLogService`, `AppendLogStoreService implements IAppendLogStore` | | Decorator string | lowerCamelCase of the interface name minus the leading `I`; **globally unique and stable** (it surfaces in `CyclicDependencyError.path` and "no service registered" errors) | `createDecorator('sessionLogService')` | | Model / non-service types | PascalCase, no `I` prefix | `SessionMeta`, `LogEntry`, `ConfigSection` | -The scope prefix makes a service's lifetime readable from its name. App services carry **no** prefix (App is the default, longest-lived tier); Session and Agent services always carry `Session` / `Agent`. The prefix applies to the interface, the class, and therefore the file names. +The scope prefix makes a service's lifetime readable from its name. App services carry **no** prefix (App is the default, longest-lived tier); Workspace, Session and Agent services always carry `Workspace` / `Session` / `Agent`. The prefix applies to the interface, the class, and therefore the file names. > Do **not** use the scope prefix to re-merge domains by lifetime. `IAgentEntityService`, `IAgentDataService`, and `ISessionEntityService` are still banned — the prefix marks lifetime, the rest of the name must still be the real owning domain (`IBackgroundTaskEntityService`, `ISessionMetadata`, `IPermissionRulesService`). See [domain-boundaries.md](domain-boundaries.md). diff --git a/AGENTS.md b/AGENTS.md index e6fe24046..6195671aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,13 +19,14 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo - `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 global message search (`src/components/SearchView.tsx` — cross-session full-text search over `POST /api/v1/search`, cursor-paged via a manual Load more; an exact-match checkbox maps to the API's `mode: 'literal'` substring search, which ignores sort and orders newest-first; a `live`/`index` badge on the results shows which server route served them (in-memory session transcript vs the persisted index)), 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 and carries an in-chat search bar (`src/components/ChatSearchBar.tsx`): it searches the current session via `POST /api/v1/search` with `container: { session_id }` (usually served by the live route, since selecting a session resumes it), and a result click funnels through the app shell's `openSearchHit` — the same agent-switch + `ChatJump` (page-back, scroll, flash) path the global search view uses; 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/agent-core-v2`: the DI × Scope agent engine (the v2 port behind kap-server). Four `LifecycleScope` tiers — `App` / `Workspace` / `Session` / `Agent` (`_base/di/scope.ts`). The `workspace/` domain owns one Workspace scope per materialized workspace handler: the App-scope `IWorkspaceLifecycleService` keeps the live handler registry (create-or-get + join, handlers never closed), and each handler's `IWorkspaceHandlerService` owns session create/resume/fork/close as its child scopes — there is no App-level session lifecycle facade, callers compose `ISessionIndex` → `handlerFor` → the handler. Workspace-scope services hold the handler-shared resources loaded once per handler and refreshed by fs watch: skills / agent profiles / AGENTS.md (`workspaceSkillCatalog` / `workspaceAgentProfileCatalog` / `workspaceInstructions`), one shared MCP connection set (`workspaceMcp`), fs / fs-watch / process runner / git (`workspaceFs` / `workspaceFsWatch` / `workspaceProcess` / `workspaceGit`), the additional-directory set (`workspaceDirs`, backed by `.kimi-code/local.toml`), and the os-level tool veto (`workspaceToolPolicy`). Session/Agent scopes consume these through session-domain seed contracts with change events (`session/mcp`, `session/workspaceInfo`, `session/sessionSkillCatalog` data, …) — Session/Agent never import the Workspace domain. See `packages/agent-core-v2/AGENTS.md` and use the `agent-core-dev` skill (`.agents/skills/agent-core-dev/SKILL.md`) when developing here. - `packages/node-sdk`: the public TypeScript SDK and harness. - `packages/kosong`: the LLM / provider abstraction layer. - `packages/kaos`: the execution environment and file/process abstractions. - `packages/oauth`: Kimi OAuth and managed auth utilities. - `packages/telemetry`: shared client-side telemetry infrastructure. - `packages/transcript`: the isomorphic transcript rendering data layer — agent-granular L1 store, idempotent L2 operations, `off/turn/block/delta` L3 subscription granularity, framework-free L4 view registry, and turn-cursor pagination. Pure TypeScript (browser-safe, no engine imports) and the sole owner of all transcript contract types (`src/contract/`); consumed by `packages/kap-server` (engine events → transcript, REST + WS surface; live stores backfill history from the persisted per-agent wire records — main on first attach, any agent on demand, cold sessions rebuild any agent — with 0-based turn ordinals matching the engine's). The cold rebuild is a two-level fold over `wire.jsonl` as the single source of truth: `history/groupTurns.ts` (context messages → turn tree) plus `history/foldFacts.ts` (non-context records → tasks, interactions, todos, goal/plan/swarm meta, and end-appended markers/taskrefs; interactions left pending at shutdown fold to `cancelled`). Plan content is a recorded fact too: each ExitPlanMode review submission offloads the document to `agents//plan//v.md` and persists a reference-only `plan.revision` record (`{id, version, path, sha256, bytes}`), which projects — live and cold — to a `plan.revision` marker and the `modes.plan` badge (`{reviewPath, version}`). It also owns the op-batch sequencing contract (`transcriptSeqSchema` in `contract/schema.ts`): a per-(session, agent) monotonic batch `seq` on `transcript.ops` / `transcript.reset` / the REST transcript response, the `transcript_since` subscription cursor, and the `GET .../transcript/ops` catch-up response shape — every field optional so pre-seq peers fall back to loss-signal-driven refreshes. Beyond the timeline, the model carries wire-equivalent detail: steps carry `usage` / `finishReason` / `timing` (LLM latencies) / `retry` / interrupt reason, turns carry `durationMs` / `error` / `usage`, tool frames carry the streamed `inputText` and the latest `progress`, tasks carry subagent `resultSummary` / `error` / `stateReason` / `usage`, `meta.agent` mirrors the agent status slices (model / usage / context / permission / phase), a global `prompts` entity (op `prompt.upsert`) tracks the prompt queue, and `hook.result` lands as a `'hook'` marker. These live-projected fields are NOT backfilled by the cold rebuild (known limitation). -- `packages/kap-server`: the Kimi Code server, backed by the DI × Scope agent engine (`@moonshot-ai/agent-core-v2`). Exposes sessions over REST + WebSocket (`/api/v1` + `/api/v1/ws`); bootstrapped from `src/start.ts` and consumed by `apps/kimi-code`. The RPC surface is `/api/v1/debug/*` — a reflection dispatcher over the ENTIRE scoped DI registry (every Service callable, no whitelist; `src/transport/registerDebugRoutes.ts` + `serviceDispatcherRoutes.ts`), mounted only with `--debug-endpoints` on a loopback bind and gated by the global bearer auth; repo dev scripts pass the flag. Its transcript surface implements the op-batch sequencing contract: `TranscriptService.dispatchOps` assigns every dispatched batch a per-agent consecutive `seq` and retains it in a bounded in-memory journal (`TRANSCRIPT_OPS_JOURNAL_CAPACITY`, dies with the live store); WS `transcript.ops`/`transcript.reset` payloads carry the seq/watermark, a `transcript_since` subscription cursor (carried, with the per-agent grades, by the `subscribe_v2` control frame — the only transcript subscription channel; its agent-grained counterpart `unsubscribe_v2` detaches listed agents' streams, or the whole session's when `agent_ids` is absent, letting the detached agents' legacy events flow again) replays journaled batches instead of a baseline reset when the journal covers it, and `GET /sessions/{id}/transcript/ops?since_seq=` serves point-to-point catch-up (`complete: false` = journal can't cover or session cold → caller falls back to a full refresh). Beside the paged route, `GET /sessions/{id}/transcript/plan?agent_id=[&tool_call_id=]` projects an agent's ExitPlanMode plan info (content / path / options / review outcome; `tool_call_id` narrows to one call, omitted lists every recoverable plan) from the first available fact — the linked approval interaction's persisted request display, the live tool frame's display, or the tool result output text. The baseline `transcript.reset` itself is items-empty (`TRANSCRIPT_RESET_TAIL_TURNS = 0`): it carries only global state + the watermark + `has_more_older`, because history always pages in over REST. When a WS connection subscribes to the transcript protocol (grade ≠ `off` for an agent), the broadcaster suppresses the transcript-projected `session_event` types for that connection × agent (`TRANSCRIPT_PROJECTED_EVENT_TYPES` + `suppressedByTranscript` in `sessionEventBroadcaster.ts`; cursor replay via `getBufferedSince` applies the same filter). Suppression is only a per-connection send view — the journal still records everything, and connections without transcript grades are unaffected. The session's work aggregate behind `event.session.work_changed` (`busy` / `main_turn_active` / `pending_interaction` / `last_turn_reason`) is owned by the core's `ISessionActivityView` (`sessionActivity` domain, Session scope): the broadcaster only schedules the wire emission around turn frames (`busy:false` lands after `turn.ended`), and `resolveSessionFacts` (`src/routes/sessions.ts`) reads the same view — never fold per-agent activity at the edge. Delivery split on `/api/v1/ws`: global events (`session.meta.updated` and the `event.session.*` / `event.workspace.*` / `event.config.*` families, including every activated session's `event.session.work_changed`) fan out to EVERY established connection — `WsConnectionV1` registers itself via `broadcaster.addGlobalTarget` on construction and unregisters on close — while session/agent-grained events only reach connections subscribed to that session (subject to `agent_filter` and the transcript suppression above); transcript frames are a separate channel governed by the per-agent grades alone and bypass `agent_filter` entirely. The global search surface is `POST /api/v1/search` (`src/search/` + `src/routes/search.ts`): a cross-session full-text search over user messages, assistant text, and session titles, backed by a single minidb database at `/search-index` (`IGlobalSearchService`, App scope — the write-lock holder is the indexer, other processes open read-only and catch up via WAL). It serves two modes: `terms` (the default — minidb's inverted text index over ASCII words + CJK uni/bigrams, no positions, term-level AND) and `literal` (substring-exact search: a hashed 2/3-gram index supplies candidates, every candidate's text is then confirmed with `includes`, so hits carry zero false positives; literal ignores `sort` and returns newest-first, and a candidate set truncated at `LITERAL_CANDIDATE_CAP` is flagged `incomplete: 'candidate_cap'`). When `container.session_id` is provided and that session is live in this process (`TranscriptService.forSessionLive` returns a store, wired via `setLiveTranscriptSource` in `start.ts`), BOTH modes instead scan the in-memory transcript store (turn prompts + assistant text frames, history established via `whenReady`/`ensureAgentHistory`) — no index involved; terms-mode live hits are scored Σ log(1+tf) (comparable only within a route, per the `GlobalSearchSource` contract), live-route errors never fall back to the index, and the response's `source: 'live' | 'index'` field (also mixed into the page-token fingerprint, so a mid-pagination route flip invalidates the old token) tells the caller which route served the page. +- `packages/kap-server`: the Kimi Code server, backed by the DI × Scope agent engine (`@moonshot-ai/agent-core-v2` — four scopes, App/Workspace/Session/Agent; session create/resume/fork routes compose `ISessionIndex` → `IWorkspaceLifecycleService.handlerFor` → the handler's `IWorkspaceHandlerService`, and the fs routes resolve session → handler → the Workspace-scope fs services). Exposes sessions over REST + WebSocket (`/api/v1` + `/api/v1/ws`); bootstrapped from `src/start.ts` and consumed by `apps/kimi-code`. The RPC surface is `/api/v1/debug/*` — a reflection dispatcher over the ENTIRE scoped DI registry (every Service callable, no whitelist, Workspace scope addressable alongside App/Session/Agent; `src/transport/registerDebugRoutes.ts` + `serviceDispatcherRoutes.ts`), mounted only with `--debug-endpoints` on a loopback bind and gated by the global bearer auth; repo dev scripts pass the flag. Its transcript surface implements the op-batch sequencing contract: `TranscriptService.dispatchOps` assigns every dispatched batch a per-agent consecutive `seq` and retains it in a bounded in-memory journal (`TRANSCRIPT_OPS_JOURNAL_CAPACITY`, dies with the live store); WS `transcript.ops`/`transcript.reset` payloads carry the seq/watermark, a `transcript_since` subscription cursor (carried, with the per-agent grades, by the `subscribe_v2` control frame — the only transcript subscription channel; its agent-grained counterpart `unsubscribe_v2` detaches listed agents' streams, or the whole session's when `agent_ids` is absent, letting the detached agents' legacy events flow again) replays journaled batches instead of a baseline reset when the journal covers it, and `GET /sessions/{id}/transcript/ops?since_seq=` serves point-to-point catch-up (`complete: false` = journal can't cover or session cold → caller falls back to a full refresh). Beside the paged route, `GET /sessions/{id}/transcript/plan?agent_id=[&tool_call_id=]` projects an agent's ExitPlanMode plan info (content / path / options / review outcome; `tool_call_id` narrows to one call, omitted lists every recoverable plan) from the first available fact — the linked approval interaction's persisted request display, the live tool frame's display, or the tool result output text. The baseline `transcript.reset` itself is items-empty (`TRANSCRIPT_RESET_TAIL_TURNS = 0`): it carries only global state + the watermark + `has_more_older`, because history always pages in over REST. When a WS connection subscribes to the transcript protocol (grade ≠ `off` for an agent), the broadcaster suppresses the transcript-projected `session_event` types for that connection × agent (`TRANSCRIPT_PROJECTED_EVENT_TYPES` + `suppressedByTranscript` in `sessionEventBroadcaster.ts`; cursor replay via `getBufferedSince` applies the same filter). Suppression is only a per-connection send view — the journal still records everything, and connections without transcript grades are unaffected. The session's work aggregate behind `event.session.work_changed` (`busy` / `main_turn_active` / `pending_interaction` / `last_turn_reason`) is owned by the core's `ISessionActivityView` (`sessionActivity` domain, Session scope): the broadcaster only schedules the wire emission around turn frames (`busy:false` lands after `turn.ended`), and `resolveSessionFacts` (`src/routes/sessions.ts`) reads the same view — never fold per-agent activity at the edge. Delivery split on `/api/v1/ws`: global events (`session.meta.updated` and the `event.session.*` / `event.workspace.*` / `event.config.*` families, including every activated session's `event.session.work_changed`) fan out to EVERY established connection — `WsConnectionV1` registers itself via `broadcaster.addGlobalTarget` on construction and unregisters on close — while session/agent-grained events only reach connections subscribed to that session (subject to `agent_filter` and the transcript suppression above); transcript frames are a separate channel governed by the per-agent grades alone and bypass `agent_filter` entirely. The global search surface is `POST /api/v1/search` (`src/search/` + `src/routes/search.ts`): a cross-session full-text search over user messages, assistant text, and session titles, backed by a single minidb database at `/search-index` (`IGlobalSearchService`, App scope — the write-lock holder is the indexer, other processes open read-only and catch up via WAL). It serves two modes: `terms` (the default — minidb's inverted text index over ASCII words + CJK uni/bigrams, no positions, term-level AND) and `literal` (substring-exact search: a hashed 2/3-gram index supplies candidates, every candidate's text is then confirmed with `includes`, so hits carry zero false positives; literal ignores `sort` and returns newest-first, and a candidate set truncated at `LITERAL_CANDIDATE_CAP` is flagged `incomplete: 'candidate_cap'`). When `container.session_id` is provided and that session is live in this process (`TranscriptService.forSessionLive` returns a store, wired via `setLiveTranscriptSource` in `start.ts`), BOTH modes instead scan the in-memory transcript store (turn prompts + assistant text frames, history established via `whenReady`/`ensureAgentHistory`) — no index involved; terms-mode live hits are scored Σ log(1+tf) (comparable only within a route, per the `GlobalSearchSource` contract), live-route errors never fall back to the index, and the response's `source: 'live' | 'index'` field (also mixed into the page-token fingerprint, so a mid-pagination route flip invalidates the old token) tells the caller which route served the page. - `packages/klient`: the client SDK — a contract-driven facade over agent-core-v2 with aggregated `global.*` / `session(id).*` / `agent(id).*` methods, zod validation on every call, and klient-level typed event forwarding. Transport is chosen once at creation via subpath entry (`@moonshot-ai/klient/ipc|memory`); both return the same `Klient`. The package also hosts the e2e suites: the legacy `/api/v1` live suites (`test/e2e/legacy/`) and the docker e2e runner (`pnpm --filter @moonshot-ai/klient docker:e2e`). See `packages/klient/AGENTS.md`. - `packages/server-e2e`: live e2e tests and scenarios against a running server (`KIMI_SERVER_URL`, default `http://127.0.0.1:58627`). See `packages/server-e2e/AGENTS.md`. - `packages/tree-sitter-bash`: a pure-TypeScript bash parser (no runtime deps, no wasm) that produces a syntax tree with tree-sitter-bash 0.25.0 named-node type names and UTF-16 code-unit offsets. `parse(source, { timeoutMs, maxNodes })` runs under a deterministic budget (default 50 ms / 50k nodes, plus per-chain recursion depth caps) and returns a discriminated `ParseResult` (`{ ok, rootNode, hasError }` or `{ ok: false, reason: 'aborted' }`) — callers must treat aborted/hasError trees as "cannot analyze" and degrade. Parser only, no safety judgments; consumers (e.g. Bash tool permission matching) live elsewhere. Known deviations from the reference are tracked in the package README's "Known differences" section, pinned by differential fixtures tested against the real `tree-sitter-bash` wasm (dev-only). diff --git a/packages/agent-core-v2/AGENTS.md b/packages/agent-core-v2/AGENTS.md index 8646b066a..cb1429e07 100644 --- a/packages/agent-core-v2/AGENTS.md +++ b/packages/agent-core-v2/AGENTS.md @@ -2,6 +2,10 @@ > New agent engine built on the DI Scope architecture — work-in-progress port of `packages/agent-core`. Design: `plan/PLAN.md`. Porting status: `GAP_ANALYSIS.md`. +## Scopes + +Four `LifecycleScope` tiers — `App` (0) / `Workspace` (1) / `Session` (2) / `Agent` (3) (`src/_base/di/scope.ts`). The `workspace/` domain owns the Workspace tier: the App-scope `workspaceLifecycle` holds the live handler registry (one handler per workspaceId, create-or-get + join, never closed), and each handler's `workspaceHandler` owns the session lifecycle (create/resume/fork/close) as its child scopes. Workspace-scope services (`workspaceSkillCatalog` / `workspaceAgentProfileCatalog` / `workspaceInstructions` / `workspaceMcp` / `workspaceDirs` / `workspaceFs` / `workspaceFsWatch` / `workspaceProcess` / `workspaceGit` / `workspaceToolPolicy`) hold the handler-shared resources — loaded once at handler materialization, then refreshed by fs watch — and sessions consume them through session-domain seed contracts with change events. Dependency red line: **Session/Agent never import the Workspace domain**; the App-level `ISessionLifecycleService` / `ISessionMcpService` / `ISessionFsService` are gone — compose `sessionIndex` → `workspaceLifecycle.handlerFor` → the handler instead. + ## Examples > The runnable examples have moved to the standalone `kimi-code-mini-bench` package at `../kimi-code-mini-bench`. They are wired to `agent-core-v2` through a pnpm `link:` dependency and run as a separate Vitest project. diff --git a/packages/agent-core-v2/docs/di.md b/packages/agent-core-v2/docs/di.md index e892be896..15be4b847 100644 --- a/packages/agent-core-v2/docs/di.md +++ b/packages/agent-core-v2/docs/di.md @@ -139,15 +139,16 @@ const meta = accessor.get(ISessionMetadata); // 类型是 ISessionMetadata > 你要做的:每个会话一份、或每个 agent 一份。参考 [`sessionMetadata`](../src/session/sessionMetadata/sessionMetadata.ts)、[`turn`](../src/turn/turn.ts)。 -这一步引入:**`LifecycleScope` 三层生命周期** 与 **父子 scope 的可见性**。 +这一步引入:**`LifecycleScope` 四层生命周期** 与 **父子 scope 的可见性**。 -### 3.1 三层,按寿命从长到短 +### 3.1 四层,按寿命从长到短 ```ts export enum LifecycleScope { - App = 0, // 进程级,全局一份 - Session = 1, // 一次会话 - Agent = 2, // 一个 agent + App = 0, // 进程级,全局一份 + Workspace = 1, // 一个工作区 handler(与 Session 一对多) + Session = 2, // 一次会话 + Agent = 3, // 一个 agent } ``` @@ -171,15 +172,16 @@ Scope 是一棵树,`kind` 必须沿父子方向**严格递增**: ``` App (0) - └── Session (1) - └── Agent (2) + └── Workspace (1) + └── Session (2) + └── Agent (3) ``` 解析服务时,容器先看自己这一层,没有就**递归问父 scope**。所以一条铁律: > **短寿命的服务可以注入长寿命的服务,反过来不行。** -- ✅ Agent 服务注入 Session / App 服务(往上找,找得到)。 +- ✅ Agent 服务注入 Session / Workspace / App 服务(往上找,找得到)。 - ❌ App 服务注入 Session 服务(App 创建时 Session 还不存在,且父不会往下找)。 这条规则由树的结构强制保证,不靠纪律维持。 @@ -350,7 +352,7 @@ A 创建中要 B,B 创建中又要 A——容器会抛 `CyclicDependencyError` ### 9.2 为什么不允许 -- scope 分层让正常依赖天然是 DAG(Agent → Session → App 向上找),一个环几乎总是设计味道。 +- scope 分层让正常依赖天然是 DAG(Agent → Session → Workspace → App 向上找),一个环几乎总是设计味道。 - 靠「让环刚好能跑」会把构造顺序变成隐式约定,难调试、难排错。 所以 v2 的立场是:**依赖图必须是无环的。** diff --git a/packages/agent-core-v2/docs/rw-model-design.md b/packages/agent-core-v2/docs/rw-model-design.md index ccb2a71ea..766946ee7 100644 --- a/packages/agent-core-v2/docs/rw-model-design.md +++ b/packages/agent-core-v2/docs/rw-model-design.md @@ -65,7 +65,7 @@ - W3 Session 域借 main agent 的 wire 写(todo/cron),main 缺失时**静默丢写** (`sessionTodoService.ts:99-100`),且要 `as never` 绕过类型。 - W4 fork 直接在 appendLogStore 层改写 wire log,绕过全部写模型 - (`sessionLifecycleService.ts:303-337`)。 + (`workspaceHandlerService.ts` 的 `fork` / `copyAgentWire`)。 - W5 restore 期 append 在 wireRecord 层被静默吞掉(`wireRecordService.ts:81`), 但 recordService 仍然 foldViews、仍然跑 facet——"进内存不进磁盘"完全隐式。 @@ -98,7 +98,7 @@ onChange 处理器若 append 会无检测地重入。 - L3 restore 正确性依赖三重隐式契约:DI 构造顺序 + hook 注册顺序 + "resumer 先于 hooks";`doResume` 需手动预热 contextMemory - (`sessionLifecycleService.ts:158-162`)。 + (现 `workspaceHandlerService.ts` 的 `doResume` / `materializeSession`)。 - L4 相位规则(restoring / postRestoring / live)在 append/signal/push/hook 四条通道上各不相同,没有一处集中定义。 @@ -189,7 +189,7 @@ 逻辑 seq 顺序,因此边缘 journal 的 seq 与核心逻辑 seq 单调一致。 - fork 保持现实现(复制 main 的 wire log);接口上表达为 `stream.forkInto(target)`,实现仍走 appendLogStore(W4 的接口层收口: - 唯一入口,不再散落在 sessionLifecycle 里手写)。 + 唯一入口,不再散落在 workspaceHandler 里手写)。 - App scope 一条逻辑流(config/model catalog/session 生命周期),取代 `IEventService`(V4)——App 流本就无持久化,纯接口替换。 - **Topic = 流上的类型化过滤视角**,不是独立机制。订阅方用 diff --git a/packages/agent-core-v2/docs/service-design.md b/packages/agent-core-v2/docs/service-design.md index e78aa848e..54bd30117 100644 --- a/packages/agent-core-v2/docs/service-design.md +++ b/packages/agent-core-v2/docs/service-design.md @@ -37,11 +37,12 @@ Every principle below derives from two root questions: **First principle: Scope = the identity + lifetime of the owned state.** -`App` / `Session` / `Agent` are three tiers of identity + lifetime: +`App` / `Workspace` / `Session` / `Agent` are four tiers of identity + lifetime: | Scope | State identity (keyed by) | Lifetime | |---|---|---| | `App` | none (single global instance) | the process | +| `Workspace` | `workspaceId` | one workspace handler (materialized once per workspace, never closed — dies with the process) | | `Session` | `sessionId` | one session | | `Agent` | `agentId` | one agent | @@ -55,6 +56,7 @@ Every principle below derives from two root questions: **Q2. What is the identity of that state?** - one global instance → **`App`** +- one per workspace (shared by every session of that workspace) → **`Workspace`** - one per session → **`Session`** - one per agent → **`Agent`** - a mix (a global registry *and* per-instance state) → **do not put it in one Service; @@ -108,7 +110,7 @@ job well. | Tier | Role | Naming tends to | |---|---|---| | `App` | **global registry / catalog / factory** — knows "all of them" and how to create one | `XxxStore` / `XxxRegistry` / `XxxCatalog` | -| `Session` / `Agent` | **one instance** — only the state of "this one" | `XxxService` / `ISessionXxx` / `IAgentXxx` | +| `Workspace` / `Session` / `Agent` | **one instance** — only the state of "this one" | `XxxService` / `IWorkspaceXxx` / `ISessionXxx` / `IAgentXxx` | This pattern recurs throughout the codebase and confirms the rule: diff --git a/packages/agent-core-v2/scripts/check-domain-layers.mjs b/packages/agent-core-v2/scripts/check-domain-layers.mjs index 21db7c83e..80309d6d9 100644 --- a/packages/agent-core-v2/scripts/check-domain-layers.mjs +++ b/packages/agent-core-v2/scripts/check-domain-layers.mjs @@ -228,7 +228,7 @@ const DOMAIN_LAYER = new Map([ // `activityView` is the Agent-scope read model folding the agent's own event // bus into the activity projection (`agent.activity.updated`); it owns no // authoritative state (turn mechanics live in `loop`, admission/drain in - // `sessionLifecycle`, background bookkeeping in `agentLifecycle`). + // `workspaceHandler`, background bookkeeping in `agentLifecycle`). ['activityView', 4], ['context', 4], ['message', 4], diff --git a/packages/agent-core-v2/src/_base/utils/fileMeta.ts b/packages/agent-core-v2/src/_base/utils/fileMeta.ts index f3419c5b3..19757d895 100644 --- a/packages/agent-core-v2/src/_base/utils/fileMeta.ts +++ b/packages/agent-core-v2/src/_base/utils/fileMeta.ts @@ -2,7 +2,7 @@ * File content metadata helpers — binary detection, line counting, etag, and * extension-based mime / language guessing. * - * Shared by the fs edge domains (`sessionFs`) and the kap-server fs routes so + * Shared by the fs edge domains (`workspaceFs`) and the kap-server fs routes so * every read-style surface classifies and labels file content the same way. * 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 diff --git a/packages/agent-core-v2/src/_base/utils/isoDateTime.ts b/packages/agent-core-v2/src/_base/utils/isoDateTime.ts index f5346c4d2..6b1d75a6a 100644 --- a/packages/agent-core-v2/src/_base/utils/isoDateTime.ts +++ b/packages/agent-core-v2/src/_base/utils/isoDateTime.ts @@ -6,7 +6,7 @@ const ISO_8601_REGEX = /** * Wire-schema primitive for ISO 8601 datetime strings: validates the shape and * normalizes to `Date#toISOString()` output. Shared by the edge DTO schemas - * (`sessionFs`, `file`, `terminal`, `auth`, …) that expose timestamps. + * (`workspaceFs`, `file`, `terminal`, `auth`, …) that expose timestamps. */ export const isoDateTimeSchema = z .string() diff --git a/packages/agent-core-v2/src/agent/mcp/session-config.ts b/packages/agent-core-v2/src/agent/mcp/session-config.ts index b8a7b1b21..4922ca371 100644 --- a/packages/agent-core-v2/src/agent/mcp/session-config.ts +++ b/packages/agent-core-v2/src/agent/mcp/session-config.ts @@ -21,18 +21,3 @@ export async function resolveSessionMcpConfig( if (Object.keys(servers).length === 0) return undefined; return { servers }; } - -export function mergeCallerMcpServers( - base: SessionMcpConfig | undefined, - callerServers: Readonly> | undefined, -): SessionMcpConfig | undefined { - if (callerServers === undefined || Object.keys(callerServers).length === 0) { - return base; - } - return { - servers: { - ...base?.servers, - ...callerServers, - }, - }; -} diff --git a/packages/agent-core-v2/src/agent/plan/configSection.ts b/packages/agent-core-v2/src/agent/plan/configSection.ts index 04b1cfce0..18c184455 100644 --- a/packages/agent-core-v2/src/agent/plan/configSection.ts +++ b/packages/agent-core-v2/src/agent/plan/configSection.ts @@ -4,7 +4,7 @@ * Top-level boolean preference (`default_plan_mode` on disk, v1-compatible): * when `true`, every freshly created session starts in plan mode. Resumed / * forked sessions restore plan state from wire records and ignore this. Read by - * `sessionLifecycle` at session creation; runtime plan state lives on the wire + * `workspaceHandler` at session creation; runtime plan state lives on the wire * `PlanModel`, not here. */ diff --git a/packages/agent-core-v2/src/agent/plan/planOps.ts b/packages/agent-core-v2/src/agent/plan/planOps.ts index 1781af018..31087362f 100644 --- a/packages/agent-core-v2/src/agent/plan/planOps.ts +++ b/packages/agent-core-v2/src/agent/plan/planOps.ts @@ -26,7 +26,7 @@ * `agent.status.updated` planMode slice — are NOT part of `apply`: they run * after `wire.dispatch` on the live path, and `wire.replay` rebuilds the * Model silently from the persisted `plan_mode.*` / `plan.revision` records - * (seeded by `sessionLifecycle`). The legacy `toReplay: plan_updated` + * (seeded by `workspaceHandler`). The legacy `toReplay: plan_updated` * projection is dropped (inert — nothing reads it). `plan.revision` carries * a `toEvent` so the live transcript projector can map it onto a marker plus * the plan badge; replay never emits it. Consumed by the Agent-scope diff --git a/packages/agent-core-v2/src/app/gateway/gateway.ts b/packages/agent-core-v2/src/app/gateway/gateway.ts index a4a9437fd..80dab0fbe 100644 --- a/packages/agent-core-v2/src/app/gateway/gateway.ts +++ b/packages/agent-core-v2/src/app/gateway/gateway.ts @@ -2,8 +2,9 @@ * `gateway` domain (L7) — REST/WS gateways. * * Defines the public contracts of the gateway layer: the `IRestGateway` / - * `IWSGateway` entry points. Session scope creation is owned by - * `sessionLifecycle`; the gateway resolves sessions through it. + * `IWSGateway` entry points. Session scope creation is owned by the workspace + * handler (`workspaceHandler`); the gateway resolves sessions through the live + * handler registry (`workspaceLifecycle`). * App-scoped — shared across the application. */ diff --git a/packages/agent-core-v2/src/app/hostFolderBrowser/hostFolderBrowser.ts b/packages/agent-core-v2/src/app/hostFolderBrowser/hostFolderBrowser.ts index 82bfd2263..1ec2fecec 100644 --- a/packages/agent-core-v2/src/app/hostFolderBrowser/hostFolderBrowser.ts +++ b/packages/agent-core-v2/src/app/hostFolderBrowser/hostFolderBrowser.ts @@ -3,8 +3,8 @@ * * Defines the `IHostFolderBrowser` used by the program side (TUI / server) to * let the user browse the real local filesystem when choosing a workspace - * folder. Distinct from the Session-side `sessionFs`, which is sandboxed and may - * be remote. App-scoped. + * folder. Distinct from the Workspace-side `workspaceFs`, which is sandboxed and + * may be remote. App-scoped. * * The wire shapes (`FsBrowseResponse` / `FsHomeResponse`) are defined here as * zod schemas so the `/api/v1` and `/api/v2` transports share one contract. diff --git a/packages/agent-core-v2/src/app/sessionIndex/sessionIndex.ts b/packages/agent-core-v2/src/app/sessionIndex/sessionIndex.ts index 2c196b6b9..743e85717 100644 --- a/packages/agent-core-v2/src/app/sessionIndex/sessionIndex.ts +++ b/packages/agent-core-v2/src/app/sessionIndex/sessionIndex.ts @@ -5,7 +5,7 @@ * query facade over the set of persisted sessions (open or closed). It * enumerates sessions and derives session identity (`workspaceId`), returning * data (`SessionSummary`) or counts — never filesystem paths or live handles. - * Writes (create / archive) live in `sessionLifecycle` / `session`; the index + * Writes (create / archive) live in `workspaceHandler` / `session`; the index * is a read model. Backends are deployment-specific (local filesystem today; * database / query store on a server). */ diff --git a/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacy.ts b/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacy.ts index 3886beff1..555969ded 100644 --- a/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacy.ts +++ b/packages/agent-core-v2/src/app/sessionLegacy/sessionLegacy.ts @@ -5,12 +5,12 @@ * metadata merge, and the cross-domain `agent_config` patch), * `GET /sessions/{id}/status` (`status`), and `GET /sessions/{id}/goal` * (`goal`) on top of the native v2 services - * (`ISessionLifecycleService`, `IAgentProfileService`, …). + * (`IWorkspaceHandlerService`, `IAgentProfileService`, …). * * The thin pass-through actions (`fork` / `compact` / `abort` / `archive`), the * `:undo` action, and the `/sessions/{id}/children` endpoints are deliberately * NOT wrapped here: the edge route calls the native services directly — - * `ISessionLifecycleService.fork` / `archive` / `createChild`, + * `IWorkspaceHandlerService.fork` / `archive` / `createChild`, * `IAgentFullCompactionService.begin`, `IAgentRPCService.cancel`, * `IAgentPromptService.undo`, and `ISessionIndex.list({ childOf })` — because * none of them carries v1-only projection worth centralizing beyond what the diff --git a/packages/agent-core-v2/src/os/interface/hostEnvironment.ts b/packages/agent-core-v2/src/os/interface/hostEnvironment.ts index c9856592b..ec024f33b 100644 --- a/packages/agent-core-v2/src/os/interface/hostEnvironment.ts +++ b/packages/agent-core-v2/src/os/interface/hostEnvironment.ts @@ -11,7 +11,8 @@ * * Async initialization: probing (`ready`) discovers the shell path — on * Windows this may run `git.exe --exec-path`. The composition root - * (`sessionLifecycle`) `await`s `ready` before creating any Session scope, so + * (`workspaceLifecycle` / `workspaceHandler`) `await`s `ready` before creating + * any Session scope, so * every Session/Agent-scope consumer reads the sync fields safely. * * App-scoped — one shared instance for the whole process. diff --git a/packages/agent-core-v2/src/os/interface/hostFsWatch.ts b/packages/agent-core-v2/src/os/interface/hostFsWatch.ts index 890ce98c6..02830826e 100644 --- a/packages/agent-core-v2/src/os/interface/hostFsWatch.ts +++ b/packages/agent-core-v2/src/os/interface/hostFsWatch.ts @@ -4,7 +4,7 @@ * Defines the `IHostFsWatchService`, a thin primitive over the host OS file * watcher. It reports raw create/modify/delete events under an absolute path * and knows nothing about sessions, connections, workspaces or wire frames. - * App-scoped — one shared instance. Higher layers (e.g. `sessionFsWatch`) + * App-scoped — one shared instance. Higher layers (e.g. `workspaceFsWatch`) * subscribe, confine events to a workspace, debounce/coalesce and re-expose * them as domain events. */ diff --git a/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycle.ts b/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycle.ts index 571f6ba29..45556adda 100644 --- a/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycle.ts +++ b/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycle.ts @@ -5,9 +5,10 @@ * registry (`get` / `list` / `remove`), and the lifecycle events — plus the * session-wide fan-outs only the live registry can reach * (`broadcastPermissionMode`). Driving turns on an agent — and the hook/event - * surface those runs announce — lives in the `subagent` domain; session-level - * MCP lives in the `sessionMcp` domain. Session-scoped — one instance per - * session. + * surface those runs announce — lives in the `subagent` domain; the shared MCP + * connection manager lives in the Workspace-scope `workspaceMcp` domain and + * reaches agents through the seeded `ISessionMcpHandle`. Session-scoped — one + * instance per session. * * Invariants: * - The registry is flat: agents have no nesting. There is no parent/child or diff --git a/packages/agent-core-v2/src/session/agentLifecycle/mainAgent.ts b/packages/agent-core-v2/src/session/agentLifecycle/mainAgent.ts index 4d81f6f56..17e4c5567 100644 --- a/packages/agent-core-v2/src/session/agentLifecycle/mainAgent.ts +++ b/packages/agent-core-v2/src/session/agentLifecycle/mainAgent.ts @@ -12,7 +12,7 @@ * and returns an already-created main agent as-is — so concurrent * bootstrappers always receive the same, fully-bootstrapped handle (activity * lane `idle`). Session services activated when their scope is created (cron, - * external hooks) are materialized by `sessionLifecycle.materializeSession`; + * external hooks) are materialized by `workspaceHandler.materializeSession`; * the default permission posture is * applied in `bindBootstrap`. * diff --git a/packages/agent-core-v2/src/session/errors.ts b/packages/agent-core-v2/src/session/errors.ts index 5413a89c5..d39a72fc1 100644 --- a/packages/agent-core-v2/src/session/errors.ts +++ b/packages/agent-core-v2/src/session/errors.ts @@ -1,6 +1,6 @@ /** * `session` domain error codes — shared across the session layer - * (`sessionLifecycle` / `sessionLegacy` / `messageLegacy`). + * (`workspaceHandler` / `sessionLegacy` / `messageLegacy`). */ import { registerErrorDomain, type ErrorDomain } from '#/_base/errors/codes'; diff --git a/packages/agent-core-v2/src/session/sessionContext/sessionContext.ts b/packages/agent-core-v2/src/session/sessionContext/sessionContext.ts index 89cbf1e53..2b6704029 100644 --- a/packages/agent-core-v2/src/session/sessionContext/sessionContext.ts +++ b/packages/agent-core-v2/src/session/sessionContext/sessionContext.ts @@ -3,16 +3,15 @@ * * Defines the `ISessionContext` carrying the session's identity, storage * addressing (`sessionId`, `workspaceId`, `sessionDir`, `metaScope`), the - * session's working directory (`cwd`) — frozen at session creation — the - * materialization-time snapshot of the handler's additional workspace - * directories (`additionalDirs`), and a `scope(subKey?)` + * session's working directory (`cwd`) — frozen at session creation — and a + * `scope(subKey?)` * helper that returns the session's persistence scope (or a child under it, * e.g. `scope('agents/main/cron')`). Seeded into the Session scope by * `workspaceHandler` when the session is created. * * `cwd` is the default root the `process` runner spawns in and the seed the - * `workspaceContext` derives its read-only `workDir` / `additionalDirs` from. - * Pure facts — no store, no IO. Session-scoped. + * `workspaceContext` derives its read-only `workDir` from. Pure facts — no + * store, no IO. Session-scoped. */ import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation'; @@ -26,14 +25,6 @@ export interface ISessionContext { readonly sessionDir: string; readonly metaScope: string; readonly cwd: string; - /** - * Extra directories beyond `cwd` the session may touch — a snapshot of the - * handler-shared set (`workspaceDirs`: project-local `.kimi-code/local.toml` - * ∪ caller-provided dirs) taken at materialization. Live updates reach - * consumers through the `ISessionWorkspaceInfo` seed; this field stays the - * creation-time snapshot. - */ - readonly additionalDirs?: readonly string[]; scope(subKey?: string): string; } @@ -50,7 +41,6 @@ export function makeSessionContext(input: { readonly sessionDir: string; readonly sessionScope: string; readonly cwd: string; - readonly additionalDirs?: readonly string[]; readonly metaScope?: string; }): ISessionContext { const { sessionScope } = input; @@ -61,7 +51,6 @@ export function makeSessionContext(input: { sessionDir: input.sessionDir, metaScope: input.metaScope ?? sessionScope, cwd: input.cwd, - additionalDirs: input.additionalDirs, scope: (subKey?: string): string => subKey === undefined || subKey === '' ? sessionScope : `${sessionScope}/${subKey}`, }; diff --git a/packages/agent-core-v2/src/workspace/workspaceDirs/workspaceDirsService.ts b/packages/agent-core-v2/src/workspace/workspaceDirs/workspaceDirsService.ts index ada0a110e..2207d31ab 100644 --- a/packages/agent-core-v2/src/workspace/workspaceDirs/workspaceDirsService.ts +++ b/packages/agent-core-v2/src/workspace/workspaceDirs/workspaceDirsService.ts @@ -154,9 +154,11 @@ export class WorkspaceDirsService extends Disposable implements IWorkspaceDirs { * Watch the project root recursively, pruned to the `local.toml` * candidate: watching the file directly never fires when its parent * `.kimi-code` directory does not exist yet either. + * + * Runs only after `ready` resolves, so `reloadFromDisk` has already + * populated `projectRoot` / `configPath`. */ private watchLocalToml(): void { - if (this.configPath === '') return; try { const handle = this.fsWatch.watch(this.projectRoot, { recursive: true, diff --git a/packages/agent-core-v2/src/workspace/workspaceHandler/workspaceHandlerService.ts b/packages/agent-core-v2/src/workspace/workspaceHandler/workspaceHandlerService.ts index fcce2b7c3..a4b6858b6 100644 --- a/packages/agent-core-v2/src/workspace/workspaceHandler/workspaceHandlerService.ts +++ b/packages/agent-core-v2/src/workspace/workspaceHandler/workspaceHandlerService.ts @@ -226,8 +226,8 @@ export class WorkspaceHandlerService extends Disposable implements IWorkspaceHan // across all sessions of this workspace, §6.1) — the workspace dirs // service owns the local.toml set and its watch; sessions read the // combined view through the `ISessionWorkspaceInfo` seed below. Await - // the initial local.toml load first so the ctx snapshot and the seed - // both start from the assembled set. + // the initial local.toml load first so the seed starts from the + // assembled set. await this.workspaceDirs.ready; await this.workspaceDirs.mergeAdditionalDirs(opts.workDir, opts.additionalDirs ?? []); const ctx: ISessionContext = { @@ -237,7 +237,6 @@ export class WorkspaceHandlerService extends Disposable implements IWorkspaceHan sessionDir, metaScope, cwd: opts.workDir, - additionalDirs: this.workspaceDirs.additionalDirs, scope: (subKey?: string): string => subKey === undefined || subKey === '' ? sessionScope : `${sessionScope}/${subKey}`, }; diff --git a/packages/agent-core-v2/test/agent/mcp/session-config.test.ts b/packages/agent-core-v2/test/agent/mcp/session-config.test.ts deleted file mode 100644 index 9ad619d41..000000000 --- a/packages/agent-core-v2/test/agent/mcp/session-config.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { mergeCallerMcpServers, type SessionMcpConfig } from '#/agent/mcp/session-config'; -import type { McpServerConfig } from '#/agent/mcp/config-schema'; - -const stdio = (command: string): McpServerConfig => ({ - transport: 'stdio', - command, -}); - -const http = (url: string): McpServerConfig => ({ - transport: 'http', - url, -}); - -describe('mergeCallerMcpServers', () => { - it('returns base unchanged when callerServers is undefined', () => { - const base: SessionMcpConfig = { servers: { fs: stdio('fs') } }; - expect(mergeCallerMcpServers(base, undefined)).toBe(base); - }); - - it('returns base unchanged when callerServers is empty', () => { - const base: SessionMcpConfig = { servers: { fs: stdio('fs') } }; - expect(mergeCallerMcpServers(base, {})).toBe(base); - }); - - it('returns undefined when both base and callerServers are absent', () => { - expect(mergeCallerMcpServers(undefined, undefined)).toBeUndefined(); - expect(mergeCallerMcpServers(undefined, {})).toBeUndefined(); - }); - - it('promotes a caller-only payload into a fresh SessionMcpConfig when base is undefined', () => { - const callerServers = { docs: http('https://mcp.example.com') }; - expect(mergeCallerMcpServers(undefined, callerServers)).toEqual({ - servers: { docs: http('https://mcp.example.com') }, - }); - }); - - it('layers caller on top of base with caller winning on key collision', () => { - const base: SessionMcpConfig = { - servers: { - shared: stdio('disk-version'), - diskOnly: stdio('disk-only'), - }, - }; - const callerServers = { - shared: stdio('caller-version'), - callerOnly: http('https://caller.example.com'), - }; - expect(mergeCallerMcpServers(base, callerServers)).toEqual({ - servers: { - shared: stdio('caller-version'), - diskOnly: stdio('disk-only'), - callerOnly: http('https://caller.example.com'), - }, - }); - }); -}); diff --git a/packages/agent-core-v2/test/workspace/workspaceFs/fsService.test.ts b/packages/agent-core-v2/test/workspace/workspaceFs/fsService.test.ts index 78261c787..a5bbbe96d 100644 --- a/packages/agent-core-v2/test/workspace/workspaceFs/fsService.test.ts +++ b/packages/agent-core-v2/test/workspace/workspaceFs/fsService.test.ts @@ -371,7 +371,7 @@ function makeSession( const emptyHandler: RunHandler = () => ({ stdout: '', exitCode: 0 }); describe('WorkspaceFsService.gitStatus', () => { - it('delegates to IGitService with the session cwd and a confined filter', async () => { + it('delegates to IWorkspaceGitService with the handler root and a confined filter', async () => { const calls: Array<{ cwd: string; filter: ReadonlySet | undefined }> = []; const git: IGitService = { _serviceBrand: undefined, @@ -413,7 +413,7 @@ describe('WorkspaceFsService.gitStatus', () => { }); describe('WorkspaceFsService.diff', () => { - it('delegates to IGitService with confined rel and abs paths', async () => { + it('delegates to IWorkspaceGitService with confined rel and abs paths', async () => { const calls: Array<{ cwd: string; rel: string; abs: string }> = []; const git: IGitService = { _serviceBrand: undefined, diff --git a/packages/agent-core-v2/test/workspace/workspaceResources.test.ts b/packages/agent-core-v2/test/workspace/workspaceResources.test.ts index fa0487932..0cbcdc2ef 100644 --- a/packages/agent-core-v2/test/workspace/workspaceResources.test.ts +++ b/packages/agent-core-v2/test/workspace/workspaceResources.test.ts @@ -416,7 +416,9 @@ describe('workspace resource sharing (handler chain)', () => { () => { expect(catalog.catalog.getSkill('watched-skill')?.description).toBe('from watch'); }, - { timeout: 10000, interval: 100 }, + // Real FSEvents delivery + the 200 ms source debounce + a real disk + // rescan: under high parallel load the 10 s budget flakes, so allow 30 s. + { timeout: 30000, interval: 100 }, ); - }, 20000); + }, 60000); }); diff --git a/packages/kap-server/src/protocol/rest-fs.ts b/packages/kap-server/src/protocol/rest-fs.ts index 113c4cbb3..adc47d545 100644 --- a/packages/kap-server/src/protocol/rest-fs.ts +++ b/packages/kap-server/src/protocol/rest-fs.ts @@ -1,7 +1,8 @@ /** * The `fs:open` / `fs:open_in` / `fs:reveal` request schemas — the only fs - * wire shapes the engine does not own (the `sessionFs` domain in agent-core-v2 - * holds the rest). Also home of `fsOpenInAppIdSchema`, referenced by the + * wire shapes the engine does not own (the `workspaceFs` domain in + * agent-core-v2 holds the rest). Also home of `fsOpenInAppIdSchema`, + * referenced by the * `/v1/meta` capabilities document. */ diff --git a/packages/kap-server/src/routes/fs.ts b/packages/kap-server/src/routes/fs.ts index 5e69cf9a8..d01a9cb54 100644 --- a/packages/kap-server/src/routes/fs.ts +++ b/packages/kap-server/src/routes/fs.ts @@ -537,7 +537,7 @@ function sendMappedError(reply: Reply, req: { id: string }, err: unknown): void case ErrorCodes.SESSION_NOT_FOUND: reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, requestId, err.stack)); return; - // hostFs errors that escaped the sessionFs layer keep their `os.fs.*` + // hostFs errors that escaped the workspaceFs layer keep their `os.fs.*` // code; map them onto the closest v1 wire code (ENOTDIR collapses into // path-not-found, matching `mapFsError`). case ErrorCodes.OS_FS_NOT_FOUND: diff --git a/packages/kap-server/src/routes/workspaceFs.ts b/packages/kap-server/src/routes/workspaceFs.ts index f10725c54..ff5902260 100644 --- a/packages/kap-server/src/routes/workspaceFs.ts +++ b/packages/kap-server/src/routes/workspaceFs.ts @@ -26,7 +26,7 @@ * All file handling lives here in the transport layer on top of the os * `IHostFileSystem` primitives — the engine deliberately has no "unconfined * read" domain Service. The mime / etag helpers are shared with the engine's - * `sessionFs` via `agent-core-v2/_base/utils/fileMeta` so both surfaces label + * `workspaceFs` via `agent-core-v2/_base/utils/fileMeta` so both surfaces label * content the same way. `IHostFileSystem` failures arrive as coded `os.fs.*` * errors and are mapped here: * diff --git a/packages/klient/src/transports/memory/dispatcher.ts b/packages/klient/src/transports/memory/dispatcher.ts index c654bb88b..11c2c3fb5 100644 --- a/packages/klient/src/transports/memory/dispatcher.ts +++ b/packages/klient/src/transports/memory/dispatcher.ts @@ -3,7 +3,7 @@ * against a live engine scope and mirrors kap-server's dispatcher semantics * (reflection call, non-function members are property reads, `main` agent * auto-materialized via `ensureMainAgent`). Scope routing walks - * `ISessionLifecycleService` / `IAgentLifecycleService` exactly like the + * `IWorkspaceLifecycleService` / `IAgentLifecycleService` exactly like the * server's `resolveScope`. Every argument, result, and event payload passes * through `wireClone` (a JSON round-trip), so consumers observe * byte-identical data no matter whether the call crossed a socket or stayed