Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
273067f
feat(agent-core-v2): insert Workspace lifecycle scope and remove muta…
sailist Jul 29, 2026
76d3beb
feat(agent-core-v2): add Workspace scope with handler-owned session l…
sailist Jul 29, 2026
091e127
feat(agent-core-v2): collect workspace resources into the handler scope
sailist Jul 29, 2026
ec75b11
feat(agent-core-v2): restore add-dir as a workspace-level capability
sailist Jul 29, 2026
af434d5
feat(agent-core-v2): collect os-level services into the workspace scope
sailist Jul 29, 2026
cee2559
refactor(agent-core-v2): clean up workspace-domain leftovers and docs
sailist Jul 29, 2026
6b4b944
test(node-sdk): wait for the initial MCP connect to settle in the par…
sailist Jul 29, 2026
83638ba
refactor(agent-core-v2): extract git work-tree discovery into the git…
sailist Jul 29, 2026
951e9d5
feat(kimi-inspect): add Workspace Services view
sailist Jul 29, 2026
da86e56
refactor(agent-core-v2): extract mcpCore and workspaceMcpConfig domains
sailist Jul 29, 2026
e9e49f7
refactor(agent-core-v2): remove the fault-injection experimental feature
sailist Jul 30, 2026
c556bc6
feat(agent-core-v2): gate project-level MCP config behind workspace t…
sailist Jul 30, 2026
d12c298
feat(kimi-inspect): replace the workspace picker with a directory bro…
sailist Jul 30, 2026
3456ec6
fix(agent-core-v2): resolve the effective cwd into the profile binding
sailist Jul 30, 2026
96a588c
refactor(agent-core-v2): introduce the contribution/registry/catalog …
sailist Jul 30, 2026
729c017
fix(agent-core-v2): reconcile the workspace refactor with main
sailist Jul 30, 2026
618075e
fix(agent-core-v2): export the agent-file parse primitives the v2 pri…
sailist Jul 30, 2026
91b1160
feat(agent-core-v2): return cwd listing for empty fs:search query
sailist Jul 30, 2026
47a9bc1
test: cover cron-fired steer context and titled session creation
sailist Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions .agents/skills/agent-core-dev/align.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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.

Expand Down
60 changes: 35 additions & 25 deletions .agents/skills/agent-core-dev/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand All @@ -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).
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -189,8 +191,9 @@ domain: `<name>` (owning scope: <Scope>)
│ └─ (accessor) <ConsumerDomain> @<Scope> — <what they use me for>
├─ exposes (interfaces I provide, by scope)
│ ├─ App : <IXxxRegistry> — <role>
│ ├─ Session : <ISessionXxx> — <role>
│ └─ Agent : <IAgentXxx> — <role>
│ ├─ Workspace : <IWorkspaceXxx> — <role>
│ ├─ Session : <ISessionXxx> — <role>
│ └─ Agent : <IAgentXxx> — <role>
└─ depends (what I inject) tag = calling style
└─ <DepDomain> @<Scope> direct/event/hook — <what for>
```
Expand Down Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/agent-core-dev/domain-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
16 changes: 9 additions & 7 deletions .agents/skills/agent-core-dev/edge-exposure.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand All @@ -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' }, ... },
};
```

Expand Down Expand Up @@ -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` | `setWorkDir` / `addAdditionalDir` / `removeAdditionalDir` / `resolve` | IWorkspaceContext.* | GET/POST |
| `workspace` | `workDir` / `additionalDirs` / `resolve` | ISessionWorkspaceContext.* | GET |

### Agent (`/api/v2/session/:sid/agent/:aid/:resource:action`)

Expand Down Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/agent-core-dev/implement.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
Expand Down
18 changes: 10 additions & 8 deletions .agents/skills/agent-core-dev/orient.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,23 @@ When writing business code you declare three things; the container handles the r

Classes talk only to interfaces and never care how an implementation is constructed.

## The three `LifecycleScope` tiers
## The four `LifecycleScope` tiers

Lifetimes form a tree, from longest to shortest:

```text
App (0) process-wide, single global instance
└── Session (1) one session
└── Agent (2) one agent
App (0) process-wide, single global instance
└── Workspace (1) one workspace handler (a materialized workspace root)
└── Session (2) one session
└── Agent (3) one agent
```

```ts
export enum LifecycleScope {
App = 0,
Session = 1,
Agent = 2,
Workspace = 1,
Session = 2,
Agent = 3,
}
```

Expand All @@ -51,7 +53,7 @@ Deterministic: **child scopes die first; within one scope, instances dispose in

The `Ln` in a file-header identity line is the domain's **dependency layer** (L0–L7), **not** its `LifecycleScope`. They are easy to confuse because both are small integers, but they answer different questions:

- `LifecycleScope` (App=0 / Session=1 / Agent=2) — **lifetime & visibility** (this stage).
- `LifecycleScope` (App=0 / Workspace=1 / Session=2 / Agent=3) — **lifetime & visibility** (this stage).
- Dependency layer `Ln` (L0–L7) — **who may import whom**: a domain at layer `L` may import only domains at layer `<= L`. Enforced by `lint:domain` from the authoritative `DOMAIN_LAYER` map in `scripts/check-domain-layers.mjs`.

So a Session-scoped service is not "L1" — e.g. `session` is Session-scoped but lives at **L6**. When you write the header, read the number from the layer map, not from the scope.
Expand All @@ -73,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>` domain (Ln) — <one-line role>. `` Keep an existing `(cross-cutting)` label as-is. Write the role as a responsibility ("drives the turn lifecycle"), not a symbol list.
- **Scope is in the filename.** `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** (`<name>.ts`) state the public contract + scope: which `IXxx` they define and what it is for.
- **Impl files** (`<name>Service.ts`) add collaborators + scope: list every imported cross-domain collaborator as a role ("persists records through `records`"); read scope from `registerScopedService(LifecycleScope.X, …)`.
- **Contribution files** (`<targetDomain>.ts` / `<what>.contrib.ts`) state what they register into the target domain (e.g. "registers the `log` config section into `config`").
Expand Down
Loading
Loading