Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/kimi-inspect/src/panels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { IAgentPermissionModeService } from '@moonshot-ai/agent-core-v2/agent/pe
import { IAgentPermissionRulesService } from '@moonshot-ai/agent-core-v2/agent/permissionRules/permissionRules';
import { IAgentPlanService } from '@moonshot-ai/agent-core-v2/features/plan/plan';
import { IAgentProfileService } from '@moonshot-ai/agent-core-v2/agent/profile/profile';
import { IAgentSwarmService } from '@moonshot-ai/agent-core-v2/agent/swarm/swarm';
import { IAgentSwarmService } from '@moonshot-ai/agent-core-v2/features/swarm/agent/swarm';
import { IAgentTaskService } from '@moonshot-ai/agent-core-v2/agent/task/task';
import { IAgentTokenCountingService } from '@moonshot-ai/agent-core-v2/agent/tokenCounting/tokenCounting';
import { IAgentToolRegistryService } from '@moonshot-ai/agent-core-v2/agent/toolRegistry/toolRegistry';
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The DI kernel (`src/_base/di/`) owns the unit layer on top of the scoped registr

The four contribution seams (token → fold): config sections — `ConfigSectionContribution` → `ConfigRegistry` fold (`src/app/config/`; module-level `registerConfigSection` stays the static built-in channel drained at construction, a withdrawn runtime record unregisters the section while user TOML values survive); agent tools — `AgentToolContribution` → `AgentToolActivationService` fold (built-in records provided once at App scope by `builtinToolAssemblyService`; `registerAgentToolService` stays the static channel: Agent-scope DI `OnDemand` registration + module table); agent profiles — `AgentProfileContribution` → `IAgentProfileRegistry` fold (see Scopes); wire vocabulary — `WireModelContribution` → `WireService` fold (a record bundles `models` / `ops` / `crossReducers` / `checkpointedModels`; the built-in layer is the module tables drained at fold time — `defineOp` / `defineModel` / `defineCheckpointedModel` stay the static channel — and replaying a withdrawn domain's history lands on the unknown-op skip-and-count path). A fifth seam: executable commands — `CommandContribution` → `IAgentCommandService` fold (`src/agent/command/`; a contributed command runs engine-side — `run(ctx)` gets `ctx.get` resolving through the agent container, valid only during the synchronous part of `run`; name-level dedup, last record wins; surfaced over RPC as `agentCommandService.list` / `run`).

`src/features/` — built-in capabilities authored as self-contained Feature units (`plan` is the first, extracted from `agent/plan` + `agent/tools/plan`). A `Feature` (`src/features/feature.ts`) is an App-scope unit recipe with a `static override readonly name` and `contribute*` helpers composing the seams: `contributeService(scope, id, ctor)` / `contributeAgentService` (per-scope materialization via `ScopeUnits` — provider death retracts everywhere, 连坐), `contributeTool` (per-agent `OnDemand` registration + the `AgentToolContribution` record), `contributeProfiles`, `contributeConfig`, `contributeCommand`, plus `onDispose`. Feature modules self-register at import (`registerFeature`, `src/features/featureRegistry.ts`); the App-scope `IFeatureAssemblyService` drains the table through `IFeatureManager.provideUnit`, so every feature is a named, introspectable, retractable managed unit. Built-in features keep user-facing static contracts — config sections, agent profiles, wire vocabulary — on the static import=register channels (the config/state manifest generators read static tables / call sites; wire records must stay replayable); the Feature unit carries the runtime capabilities (services, tools, commands). The string form of the unit `on(...)` capability (`this.on('turn.ended', …)`) is backed by the production `FiberEventResolver` registered in `src/app/event/fiberEventResolver.ts`, resolving against the scope's `IEventBus`.
`src/features/` — built-in capabilities authored as self-contained Feature units (`plan` was the first, extracted from `agent/plan` + `agent/tools/plan`; `swarm` followed, extracted from `agent/swarm` + `session/swarm` + `agent/tools/agent-swarm` into a scope-organized `agent/` + `session/` + `tools/` layout). A `Feature` (`src/features/feature.ts`) is an App-scope unit recipe with a `static override readonly name` and `contribute*` helpers composing the seams: `contributeService(scope, id, ctor)` / `contributeAgentService` (per-scope materialization via `ScopeUnits` — provider death retracts everywhere, 连坐), `contributeTool` (per-agent `OnDemand` registration + the `AgentToolContribution` record), `contributeProfiles`, `contributeConfig`, `contributeCommand`, plus `onDispose`. Feature modules self-register at import (`registerFeature`, `src/features/featureRegistry.ts`); the App-scope `IFeatureAssemblyService` drains the table through `IFeatureManager.provideUnit`, so every feature is a named, introspectable, retractable managed unit. Built-in features keep user-facing static contracts — config sections, agent profiles, wire vocabulary — on the static import=register channels (the config/state manifest generators read static tables / call sites; wire records must stay replayable); the Feature unit carries the runtime capabilities (services, tools, commands). The string form of the unit `on(...)` capability (`this.on('turn.ended', …)`) is backed by the production `FiberEventResolver` registered in `src/app/event/fiberEventResolver.ts`, resolving against the scope's `IEventBus`.

## Ledger and cascade (L0/L2)

Expand Down
8 changes: 4 additions & 4 deletions packages/agent-core-v2/docs/wire-manifest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@
// plugin.session_start pluginSessionStartSnapshot persisted src/agent/plugin/agentPluginOps.ts
// profile.bind profile persisted src/agent/profile/profileOps.ts
// skill.activate skill transient src/agent/skill/skillOps.ts
// swarm_mode.enter swarm persisted src/agent/swarm/swarmOps.ts
// swarm_mode.exit swarm persisted src/agent/swarm/swarmOps.ts
// swarm_mode.enter swarm persisted src/features/swarm/swarmOps.ts
// swarm_mode.exit swarm persisted src/features/swarm/swarmOps.ts
// task.started task persisted src/agent/task/taskOps.ts
// task.terminated task persisted src/agent/task/taskOps.ts
// token_counting.measured tokenCounting transient src/agent/tokenCounting/tokenCountingOps.ts
Expand Down Expand Up @@ -502,7 +502,7 @@ interface SkillActivatePayload {

/**
* model: swarm · persisted · toEvent
* owner: src/agent/swarm/swarmOps.ts
* owner: src/features/swarm/swarmOps.ts
*/
interface SwarmModeEnterPayload {
_name: 'swarm_mode.enter';
Expand All @@ -512,7 +512,7 @@ interface SwarmModeEnterPayload {

/**
* model: swarm · persisted · toEvent · cross-reducers: contextMemory
* owner: src/agent/swarm/swarmOps.ts
* owner: src/features/swarm/swarmOps.ts
*/
interface SwarmModeExitPayload {
_name: 'swarm_mode.exit';
Expand Down
13 changes: 8 additions & 5 deletions packages/agent-core-v2/src/agent/tools/agent/agentTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@
* Registered via the module-level `registerAgentToolService(ISubagentTool,
* SubagentTool)` at the bottom of this file — the same "import = register"
* pattern used by every agent tool. The per-profile tool listings in the
* description read the full contribution table (not the runtime registry,
* which only holds tools the caller's own Profile activated), plus any
* dynamically registered tools. The description's catalog profile list is
* description read the full `AgentToolContribution` collection — static
* registrations and feature-contributed tools alike — not the runtime
* registry, which only holds tools the caller's own Profile activated,
* plus any dynamically registered tools. The description's catalog profile list is
* snapshotted once the session catalog has loaded and frozen for the agent's
* lifetime: plugin install / enable / disable / remove re-contributes
* profiles mid-session, and a live read would rewrite the tools payload of
Expand All @@ -37,6 +38,7 @@
* Bound at Agent scope.
*/

import { type CollectionView } from '#/_base/di/collection';
import type { IAgentScopeHandle } from '#/_base/di/scope';
import {
isAbortError,
Expand Down Expand Up @@ -67,7 +69,7 @@ import {
type ToolExecution,
} from '#/tool/toolContract';
import {
getAgentToolContributions,
AgentToolContribution,
registerAgentToolService,
} from '#/agent/toolRegistry/toolContribution';
import { IAgentToolRegistryService, type ToolReference } from '#/agent/toolRegistry/toolRegistry';
Expand Down Expand Up @@ -151,6 +153,7 @@ export class SubagentTool implements ISubagentTool {
@IConfigService private readonly config: IConfigService,
@IFlagService private readonly flags: IFlagService,
@IModelCatalog private readonly modelCatalog: IModelCatalog,
@AgentToolContribution private readonly contributions: CollectionView<AgentToolContribution>,
) {
this.callerAgentId = scopeContext.agentId;
this.canRunInBackground = () =>
Expand Down Expand Up @@ -204,7 +207,7 @@ export class SubagentTool implements ISubagentTool {

private knownToolReferences(): ToolReference[] {
const refs = new Map<string, ToolReference>();
for (const contribution of getAgentToolContributions()) {
for (const contribution of this.contributions.items) {
refs.set(contribution.options.name, {
name: contribution.options.name,
source: contribution.options.source ?? 'builtin',
Expand Down
27 changes: 14 additions & 13 deletions packages/agent-core-v2/src/app/sessionLegacy/sessionLegacy.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
/**
* `sessionLegacy` domain (L7 edge adapter) — v1-compatible session actions.
* `sessionLegacy` domain (L7 edge adapter) — v1-compatible session reads.
*
* Implements `POST /sessions/{id}/profile` (`updateProfile` — title rename,
* metadata merge, and the cross-domain `agent_config` patch),
* `GET /sessions/{id}/status` (`status`), and `GET /sessions/{id}/goal`
* (`goal`). The thin pass-through actions (`fork` / `compact` / `abort` /
* `archive`), the `:undo` action, and the `/sessions/{id}/children` endpoints
* are deliberately NOT wrapped here because none of them carries v1-only
* projection worth centralizing; only `updateProfile`, `status`, and `goal`
* stay in this adapter (the `agent_config` patch, the best-effort status
* rollup, and the current-goal read). Bound at App scope — it is a stateless
* dispatcher that resolves the target session/agent per call.
* Implements `GET /sessions/{id}/status` (`status` — the best-effort status
* rollup) and `GET /sessions/{id}/goal` (`goal` — the current-goal read), the
* two endpoints that hold real cross-domain adaptation. Everything else is
* deliberately NOT wrapped here: the thin pass-through actions (`fork` /
* `compact` / `abort` / `archive`), the `:undo` action, the
* `/sessions/{id}/children` endpoints, and `POST /sessions/{id}/profile`
* (title/metadata patch and `agent_config` dispatch) are plain wire-to-native
* translations composed by the kap-server routes directly. `SessionWireFields`
* stays exported here as the profile route's projection shape, consumed by the
* kap-server helper (`routes/sessionProfile.ts`) via deep-path import. Bound
* at App scope — it is a stateless dispatcher that resolves the target
* session/agent per call.
*/

import type { GoalSnapshot } from '#/agent/goal/types';

import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';

import type { SessionStatusResponse, UpdateSessionProfileRequest } from './sessionProtocol';
import type { SessionStatusResponse } from './sessionProtocol';

export interface SessionWireFields {
readonly id: string;
Expand All @@ -35,7 +37,6 @@ export interface SessionWireFields {
export interface ISessionLegacyService {
readonly _serviceBrand: undefined;

updateProfile(sessionId: string, body: UpdateSessionProfileRequest): Promise<SessionWireFields>;
status(sessionId: string): Promise<SessionStatusResponse>;
goal(sessionId: string): Promise<GoalSnapshot | null>;
}
Expand Down
110 changes: 7 additions & 103 deletions packages/agent-core-v2/src/app/sessionLegacy/sessionLegacyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
*
* Stateless App-scope dispatcher: each method resolves the target session (and
* its main agent) per call, delegates to the native v2 services, and projects
* the result into the v1 wire shape. Only `updateProfile` (the cross-domain
* `agent_config` patch), `status` (the best-effort status rollup), and `goal`
* (the current-goal read) live here. No business logic is duplicated here.
* the result into the v1 wire shape. Only `status` (the best-effort status
* rollup) and `goal` (the current-goal read) live here — the profile route's
* title/metadata patch and `agent_config` dispatch are composed by kap-server.
* No business logic is duplicated here.
*/

import type { GoalSnapshot } from '#/agent/goal/types';

import type { SessionStatusResponse, UpdateSessionProfileRequest } from './sessionProtocol';
import type { SessionStatusResponse } from './sessionProtocol';
import { LifecycleScope } from '#/app/scopes';
import {
type IAgentScopeHandle,
Expand All @@ -25,10 +26,9 @@ import {
import { IAgentTokenCountingService } from '#/agent/tokenCounting/tokenCounting';
import { IAgentGoalService } from '#/agent/goal/goal';
import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode';
import type { PermissionMode } from '#/agent/permissionPolicy/types';
import { IAgentPlanService } from '#/features/plan/plan';
import { IAgentProfileService } from '#/agent/profile/profile';
import { IAgentSwarmService } from '#/agent/swarm/swarm';
import { IAgentSwarmService } from '#/features/swarm/agent/swarm';
import {
getLiveSessionById,
resumeSessionById,
Expand All @@ -39,10 +39,8 @@ import { ErrorCodes, Error2 } from '#/errors';
import { ensureMainAgent } from '#/session/agentLifecycle/mainAgent';
import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle';
import { IAgentActivityView } from '#/agent/activityView/activityView';
import { ISessionContext } from '#/session/sessionContext/sessionContext';
import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';

import { ISessionLegacyService, type SessionWireFields } from './sessionLegacy';
import { ISessionLegacyService } from './sessionLegacy';

export class SessionLegacyService implements ISessionLegacyService {
declare readonly _serviceBrand: undefined;
Expand All @@ -59,100 +57,6 @@ export class SessionLegacyService implements ISessionLegacyService {
return resumeSessionById(this.services, sessionId);
}

async updateProfile(
sessionId: string,
body: UpdateSessionProfileRequest,
): Promise<SessionWireFields> {
const session = await this.resume(sessionId);
if (session === undefined) {
throw new Error2(ErrorCodes.SESSION_NOT_FOUND, `session ${sessionId} does not exist`);
}
const metadata = session.accessor.get(ISessionMetadata);

if (typeof body.title === 'string') {
await metadata.setTitle(body.title);
}

const metadataPatch = body.metadata;
if (metadataPatch !== undefined && Object.keys(metadataPatch).length > 0) {
await metadata.update({ custom: { ...(metadataPatch as Record<string, unknown>) } });
}

const agentConfig = body.agent_config;
if (agentConfig !== undefined) {
const agent = await this.resolveMainAgent(sessionId);
await this.applyAgentConfig(agent, agentConfig);
}

const meta = await metadata.read();
const ctx = session.accessor.get(ISessionContext);
return {
id: meta.id,
workspaceId: ctx.workspaceId,
root: ctx.cwd,
title: meta.title,
lastPrompt: meta.lastPrompt,
createdAt: meta.createdAt,
updatedAt: meta.updatedAt,
archived: meta.archived,
archivedAt: meta.archivedAt,
custom: meta.custom,
};
}


private async applyAgentConfig(
agent: IAgentScopeHandle,
agentConfig: NonNullable<UpdateSessionProfileRequest['agent_config']>,
): Promise<void> {
const profile = agent.accessor.get(IAgentProfileService);
if (agentConfig.model !== undefined && agentConfig.model !== '') {
await profile.setModel(agentConfig.model);
}
if (agentConfig.thinking !== undefined) {
profile.setThinking(agentConfig.thinking);
}
if (agentConfig.permission_mode !== undefined) {
agent.accessor
.get(IAgentLifecycleService)
.broadcastPermissionMode(agentConfig.permission_mode as PermissionMode);
}
if (agentConfig.plan_mode !== undefined) {
const plan = agent.accessor.get(IAgentPlanService);
const active = (await plan.status()) !== null;
if (active !== agentConfig.plan_mode) {
if (agentConfig.plan_mode) await plan.enter();
else plan.exit();
}
}
if (agentConfig.swarm_mode !== undefined) {
const swarm = agent.accessor.get(IAgentSwarmService);
if (swarm.isActive !== agentConfig.swarm_mode) {
if (agentConfig.swarm_mode) swarm.enter('manual');
else swarm.exit();
}
}
if (agentConfig.goal_objective !== undefined) {
await agent.accessor
.get(IAgentGoalService)
.createGoal({ objective: agentConfig.goal_objective });
}
if (agentConfig.goal_control !== undefined) {
const goal = agent.accessor.get(IAgentGoalService);
switch (agentConfig.goal_control) {
case 'pause':
await goal.pauseGoal({});
break;
case 'resume':
await goal.resumeGoal({ continueIfPaused: true, continueIfBlocked: true });
break;
case 'cancel':
await goal.cancelGoal({});
break;
}
}
}

private async resolveMainAgent(sessionId: string): Promise<IAgentScopeHandle> {
const session = await this.resume(sessionId);
if (session === undefined) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
* derives `agent.status.updated` from the Ops' `toEvent`, announces the mode
* through the `swarm_mode` context-injection provider (`SwarmInjection`),
* mirrors replayable trailing-enter removal through `contextMemory`, and
* auto-exits on turn end via `turn`. Bound at Agent scope. The service also
* auto-exits on turn end via `turn`. Bound at Agent scope — contributed into
* every Agent scope by `SwarmFeature` (`features/swarm/swarmFeature`). The
* service also
* guards AgentSwarm batch exclusivity through an `onBeforeExecuteTool` veto
* listener: an AgentSwarm call must be the only tool call in its batch;
* anything else is vetoed with a `toolApproval.formatDenyMessage`-formatted
Expand All @@ -15,8 +17,6 @@

import { Service } from '#/_base/di/service';
import { IInstantiationService } from '#/_base/di/instantiation';
import { LifecycleScope } from '#/app/scopes';
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory';
import { IAgentToolApprovalService } from '#/agent/toolApproval/toolApproval';
import { denyToolExecution } from '#/agent/toolExecutor/beforeToolExecuteEvent';
Expand All @@ -26,7 +26,7 @@ import { IWireService } from '#/wire/wire';

import { SwarmInjection } from './injection/swarmInjection';
import { IAgentSwarmService, type SwarmModeTrigger } from './swarm';
import { swarmEnter, swarmExit, SwarmModel } from './swarmOps';
import { swarmEnter, swarmExit, SwarmModel } from '../swarmOps';

export class AgentSwarmService extends Service implements IAgentSwarmService {
declare readonly _serviceBrand: undefined;
Expand Down Expand Up @@ -95,14 +95,6 @@ export class AgentSwarmService extends Service implements IAgentSwarmService {
}
}

registerScopedService(
LifecycleScope.Agent,
IAgentSwarmService,
AgentSwarmService,
ScopeActivation.OnScopeCreated,
'swarm',
);

function multipleAgentSwarmDeniedMessage(hasOtherToolCalls: boolean): string {
const suffix = hasOtherToolCalls
? ' AgentSwarm also must not be combined with other tools in the same response.'
Expand Down
Loading
Loading