mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-19 13:45:28 +00:00
refactor(agent-core-v2): extract swarm into a scope-organized feature (#2874)
* refactor(agent-core-v2): extract swarm into a scope-organized feature
- move src/agent/swarm, src/session/swarm, and src/agent/tools/agent-swarm
into src/features/swarm/{agent,session,tools/agent-swarm}; swarmOps.ts
stays a static import=register wire channel at the feature root
- add SwarmFeature carrying the three runtime registrations
(IAgentSwarmService, ISessionSwarmService, IAgentSwarmTool) with
ScopeActivation.OnScopeCreated preserved
- switch src/index.ts to precise leaf exports and update import sites,
including the kap-server and kimi-inspect deep-path imports
- move tests to test/features/swarm and re-assert service overrides in
the test harness so stubs keep winning over feature contributions
* fix(agent-core-v2): keep feature-contributed tools in Agent tool descriptions
SubagentTool.knownToolReferences() now reads the full AgentToolContribution
collection (static registrations and feature contributions alike) instead
of the static contribution table. A caller profile that does not activate
a feature-contributed tool (e.g. AgentSwarm) no longer drops it from the
per-profile tool listings the description advertises for spawned profiles
when a workspace/session restriction forces explicit enumeration.
Add a regression test with a caller profile lacking AgentSwarm under a
global tool restriction.
* refactor(kap-server): lift session profile updates to the route edge
- add sessionProfile.ts/sessionAgentConfig.ts route helpers that resume
the session and dispatch title/metadata and the agent_config patch to
the native v2 services directly
- drop updateProfile from ISessionLegacyService, leaving only the
status rollup and the goal read in the legacy adapter
- wire shape and client-visible behavior unchanged
This commit is contained in:
parent
6e31722df1
commit
314b39489e
35 changed files with 362 additions and 252 deletions
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -37,6 +38,7 @@
|
|||
* Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { type CollectionView } from '#/_base/di/collection';
|
||||
import type { IAgentScopeHandle } from '#/_base/di/scope';
|
||||
import {
|
||||
isAbortError,
|
||||
|
|
@ -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';
|
||||
|
|
@ -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 = () =>
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
|
|
@ -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;
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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';
|
||||
|
|
@ -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;
|
||||
|
|
@ -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.'
|
||||
|
|
@ -15,13 +15,12 @@
|
|||
* bindings are resolved through the model catalog before lifecycle allocation.
|
||||
* Resumed agents keep the model recorded in their own wire journal — with
|
||||
* per-subagent models there is no "child follows the parent's current model"
|
||||
* invariant to enforce. Bound at Session scope.
|
||||
* invariant to enforce. Bound at Session scope — contributed into every
|
||||
* Session scope by `SwarmFeature` (`features/swarm/swarmFeature`).
|
||||
*/
|
||||
|
||||
import type { TokenUsage } from '#/kosong/contract/usage';
|
||||
import { IModelCatalog } from '#/kosong/model/catalog';
|
||||
import { LifecycleScope } from '#/app/scopes';
|
||||
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
|
||||
import { Error2, ErrorCodes } from '#/errors';
|
||||
import { linkAbortSignal } from '#/_base/utils/abort';
|
||||
import type { IAgentScopeHandle } from '#/_base/di/scope';
|
||||
|
|
@ -298,11 +297,3 @@ export class SessionSwarmService implements ISessionSwarmService {
|
|||
}
|
||||
|
||||
export type _AgentRunUsage = TokenUsage;
|
||||
|
||||
registerScopedService(
|
||||
LifecycleScope.Session,
|
||||
ISessionSwarmService,
|
||||
SessionSwarmService,
|
||||
ScopeActivation.OnScopeCreated,
|
||||
'sessionSwarm',
|
||||
);
|
||||
44
packages/agent-core-v2/src/features/swarm/swarmFeature.ts
Normal file
44
packages/agent-core-v2/src/features/swarm/swarmFeature.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* `swarm` domain — `SwarmFeature`: the agent-swarm capability assembled as
|
||||
* one App-scope Feature unit.
|
||||
*
|
||||
* Contributes the per-Agent `IAgentSwarmService` (swarm mode), the
|
||||
* per-Session `ISessionSwarmService` (batch scheduler), and the `AgentSwarm`
|
||||
* agent tool through the `features` base-class seams; retracting the unit
|
||||
* withdraws all of them across the scope tree. Both services keep
|
||||
* `OnScopeCreated` activation — `AgentSwarmService` subscribes the
|
||||
* `turn.ended` auto-exit and the AgentSwarm batch-exclusivity veto in its
|
||||
* constructor. The `swarm` wire vocabulary (`features/swarm/swarmOps`) stays
|
||||
* on its static import=register channel — wire records must remain
|
||||
* replayable even when the feature unit is retracted. Registered into the
|
||||
* feature table at import.
|
||||
*/
|
||||
|
||||
import { ScopeActivation } from '#/_base/di/instantiation';
|
||||
import { LifecycleScope } from '#/app/scopes';
|
||||
import { Feature } from '#/features/feature';
|
||||
import { registerFeature } from '#/features/featureRegistry';
|
||||
|
||||
import { IAgentSwarmService } from './agent/swarm';
|
||||
import { AgentSwarmService } from './agent/swarmService';
|
||||
import { ISessionSwarmService } from './session/sessionSwarm';
|
||||
import { SessionSwarmService } from './session/sessionSwarmService';
|
||||
import { IAgentSwarmTool } from './tools/agent-swarm/agent-swarm';
|
||||
import { AgentSwarmTool } from './tools/agent-swarm/agentSwarmTool';
|
||||
|
||||
export class SwarmFeature extends Feature {
|
||||
static override readonly name = 'swarm';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.contributeAgentService(IAgentSwarmService, AgentSwarmService, {
|
||||
activation: ScopeActivation.OnScopeCreated,
|
||||
});
|
||||
this.contributeService(LifecycleScope.Session, ISessionSwarmService, SessionSwarmService, {
|
||||
activation: ScopeActivation.OnScopeCreated,
|
||||
});
|
||||
this.contributeTool(IAgentSwarmTool, AgentSwarmTool, { name: 'AgentSwarm', domain: 'swarm' });
|
||||
}
|
||||
}
|
||||
|
||||
registerFeature(SwarmFeature);
|
||||
|
|
@ -13,7 +13,7 @@ import { z } from 'zod';
|
|||
|
||||
import { defineModel } from '#/wire/model';
|
||||
|
||||
import type { SwarmModeTrigger } from './swarm';
|
||||
import type { SwarmModeTrigger } from './agent/swarm';
|
||||
|
||||
export const SwarmModel = defineModel<SwarmModeTrigger | null>('swarm', () => null);
|
||||
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
/**
|
||||
* `tools` domain — `IAgentSwarmTool` contract (the `AgentSwarm` tool).
|
||||
* `swarm` domain — `IAgentSwarmTool` contract (the `AgentSwarm` tool).
|
||||
*
|
||||
* Public contract of the `AgentSwarm` collaboration tool: the input zod
|
||||
* schema the model-facing parameters are derived from, the tool-owned
|
||||
* constants the schema is built around (prompt template placeholder, maximum
|
||||
* subagent count), and the `IAgentSwarmTool` DI decorator that the
|
||||
* implementation registers against via `registerAgentToolService`. Bound at
|
||||
* Agent scope.
|
||||
* subagent count), and the `IAgentSwarmTool` DI decorator used to resolve the
|
||||
* implementation through the container. Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* `tools` domain — `AgentSwarmTool` implementation (the `AgentSwarm`
|
||||
* `swarm` domain — `AgentSwarmTool` implementation (the `AgentSwarm`
|
||||
* tool).
|
||||
*
|
||||
* Launches a batch of child agents (an ordinary Agent scope each) through the
|
||||
|
|
@ -18,11 +18,8 @@
|
|||
* (or under `[secondary_model].force`) the parameter is not advertised at
|
||||
* all. Swarm mode is
|
||||
* entered through `IAgentSwarmService`; the caller's agent id comes from
|
||||
* `IAgentScopeContext`. Pure tool — owns no scoped state.
|
||||
*
|
||||
* Registered via the module-level `registerAgentToolService(IAgentSwarmTool,
|
||||
* AgentSwarmTool)` at the bottom of this file — the same "import = register"
|
||||
* pattern used by every agent tool. Bound at Agent scope.
|
||||
* `IAgentScopeContext`. Pure tool — owns no scoped state. Bound at Agent
|
||||
* scope — contributed by `SwarmFeature` (`features/swarm/swarmFeature`).
|
||||
*/
|
||||
|
||||
import {
|
||||
|
|
@ -32,11 +29,10 @@ import {
|
|||
type ToolExecution,
|
||||
} from '#/tool/toolContract';
|
||||
import { Error2, ErrorCodes } from '#/errors';
|
||||
import { registerAgentToolService } from '#/agent/toolRegistry/toolContribution';
|
||||
import { toInputJsonSchema } from '#/tool/input-schema';
|
||||
import { IConfigService } from '#/app/config/config';
|
||||
import { IFlagService } from '#/app/flag/flag';
|
||||
import { ISessionSwarmService, type SessionSwarmTask } from '#/session/swarm/sessionSwarm';
|
||||
import { ISessionSwarmService, type SessionSwarmTask } from '#/features/swarm/session/sessionSwarm';
|
||||
import { ISessionAgentProfileCatalog } from '#/session/sessionAgentProfileCatalog/sessionAgentProfileCatalog';
|
||||
import { IAgentProfileService } from '#/agent/profile/profile';
|
||||
import {
|
||||
|
|
@ -44,7 +40,7 @@ import {
|
|||
subagentTypeNotAllowedMessage,
|
||||
} from '#/app/agentProfileCatalog/profile-shared';
|
||||
import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext';
|
||||
import { IAgentSwarmService } from '#/agent/swarm/swarm';
|
||||
import { IAgentSwarmService } from '#/features/swarm/agent/swarm';
|
||||
import {
|
||||
buildSubagentModelDescriptions,
|
||||
exposesSubagentModelChoice,
|
||||
|
|
@ -235,8 +231,6 @@ export class AgentSwarmTool implements IAgentSwarmTool {
|
|||
}
|
||||
}
|
||||
|
||||
registerAgentToolService(IAgentSwarmTool, AgentSwarmTool, { name: 'AgentSwarm', domain: 'swarm' });
|
||||
|
||||
async function createAgentSwarmSpecs(
|
||||
args: AgentSwarmToolInput,
|
||||
getResumeItem: (agentId: string) => Promise<string | undefined>,
|
||||
|
|
@ -299,6 +299,13 @@ import '#/features/plan/planFeature';
|
|||
export * from '#/features/debugEvents/debugEvents';
|
||||
export * from '#/features/debugEvents/debugEventsService';
|
||||
import '#/features/debugEvents/debugEventsFeature';
|
||||
export * from '#/features/swarm/agent/swarm';
|
||||
export * from '#/features/swarm/agent/swarmService';
|
||||
export * from '#/features/swarm/session/sessionSwarm';
|
||||
export * from '#/features/swarm/session/sessionSwarmService';
|
||||
export * from '#/features/swarm/tools/agent-swarm/agent-swarm';
|
||||
import '#/features/swarm/tools/agent-swarm/agentSwarmTool';
|
||||
import '#/features/swarm/swarmFeature';
|
||||
export * from '#/agent/tools/goal/create-goal/create-goal';
|
||||
import '#/agent/tools/goal/create-goal/createGoalTool';
|
||||
export * from '#/agent/tools/goal/get-goal/get-goal';
|
||||
|
|
@ -312,10 +319,6 @@ import '#/agent/goal/goalDeadlineSchedulerService';
|
|||
export * from '#/agent/goal/goal';
|
||||
export * from '#/agent/goal/goalService';
|
||||
export * from '#/agent/goal/types';
|
||||
export * from '#/agent/tools/agent-swarm/agent-swarm';
|
||||
import '#/agent/tools/agent-swarm/agentSwarmTool';
|
||||
export * from '#/agent/swarm/swarm';
|
||||
export * from '#/agent/swarm/swarmService';
|
||||
export * from '#/agent/usage/usage';
|
||||
export * from '#/agent/usage/usageService';
|
||||
export * from '#/agent/toolDedupe/toolDedupe';
|
||||
|
|
@ -635,8 +638,6 @@ export * from '#/agent/stepRetry/stepRetryService';
|
|||
export * from '#/session/sessionInit/sessionInit';
|
||||
export * from '#/session/sessionInit/sessionInitService';
|
||||
export * from '#/session/sessionInit/profile/init';
|
||||
export * from '#/session/swarm/sessionSwarm';
|
||||
export * from '#/session/swarm/sessionSwarmService';
|
||||
export * from '#/session/todo/todoItem';
|
||||
export * from '#/session/todo/todoListReminder';
|
||||
export * from '#/session/todo/sessionTodo';
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import {
|
|||
} from '#/agent/loop/loop';
|
||||
import { MessageStepRequest } from '#/agent/loop/stepRequest';
|
||||
import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext';
|
||||
import { IAgentSwarmService } from '#/agent/swarm/swarm';
|
||||
import { IAgentSwarmService } from '#/features/swarm/agent/swarm';
|
||||
import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode';
|
||||
import type { PermissionMode, PermissionPolicyResult } from '#/agent/permissionPolicy/types';
|
||||
import { IAgentToolApprovalService } from '#/agent/toolApproval/toolApproval';
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory'
|
|||
import { IAgentGoalService } from '#/agent/goal/goal';
|
||||
import { type AgentGoalService } from '#/agent/goal/goalService';
|
||||
import { IAgentProfileService } from '#/agent/profile/profile';
|
||||
import { IAgentSwarmService } from '#/agent/swarm/swarm';
|
||||
import { IAgentSwarmService } from '#/features/swarm/agent/swarm';
|
||||
import {
|
||||
InMemoryWireRecordPersistence,
|
||||
agentService,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Shared stubs for goal tests.
|
||||
*/
|
||||
|
||||
import type { IAgentSwarmService } from '#/agent/swarm/swarm';
|
||||
import type { IAgentSwarmService } from '#/features/swarm/agent/swarm';
|
||||
|
||||
export function stubAgentSwarm(): IAgentSwarmService {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { UpdateGoalToolInputSchema } from '#/agent/tools/goal/update-goal/update
|
|||
import { UpdateGoalTool } from '#/agent/tools/goal/update-goal/updateGoalTool';
|
||||
import { IAgentLoopService } from '#/agent/loop/loop';
|
||||
import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext';
|
||||
import { IAgentSwarmService } from '#/agent/swarm/swarm';
|
||||
import { IAgentSwarmService } from '#/features/swarm/agent/swarm';
|
||||
import {
|
||||
IAgentToolExecutorService,
|
||||
type ToolExecutionResult,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry';
|
|||
import { AgentToolRegistryService } from '#/agent/toolRegistry/toolRegistryService';
|
||||
import { ISessionToolPolicyGate } from '#/session/sessionToolPolicyGate/sessionToolPolicyGate';
|
||||
import type { AgentTool, ToolExecution } from '#/tool/toolContract';
|
||||
import '#/agent/tools/agent-swarm/agentSwarmTool';
|
||||
import '#/agent/tools/agent/agentTool';
|
||||
import '#/agent/tools/ask-user-question/askUserQuestionTool';
|
||||
import '#/agent/tools/edit/editTool';
|
||||
|
|
@ -412,7 +411,7 @@ describe('AgentToolActivationService', () => {
|
|||
});
|
||||
|
||||
it('feeds every built-in contribution through the App-scope assembly unchanged', async () => {
|
||||
expect(savedContributions).toHaveLength(21);
|
||||
expect(savedContributions).toHaveLength(20);
|
||||
for (const contribution of savedContributions) {
|
||||
registerAgentToolService(contribution.id, contribution.ctor, contribution.options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
* current model catalog.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { SyncDescriptor } from '#/_base/di/descriptors';
|
||||
import type { ServiceIdentifier, ServicesAccessor } from '#/_base/di/instantiation';
|
||||
|
|
@ -19,7 +19,7 @@ import { IAgentTokenCountingService } from '#/agent/tokenCounting/tokenCounting'
|
|||
import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode';
|
||||
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 { UNKNOWN_CAPABILITY } from '#/kosong/contract/capability';
|
||||
import { IModelCatalog } from '#/kosong/model/catalog';
|
||||
import { IModelService } from '#/kosong/model/model';
|
||||
|
|
@ -30,9 +30,7 @@ import { IWorkspaceLifecycleService } from '#/app/workspaceLifecycle/workspaceLi
|
|||
import { ISessionLifecycleService } from '#/workspace/sessionLifecycle/sessionLifecycle';
|
||||
import { IAgentActivityView } from '#/agent/activityView/activityView';
|
||||
import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle';
|
||||
import { ISessionContext } from '#/session/sessionContext/sessionContext';
|
||||
import { ISessionCronService } from '#/session/cron/sessionCronService';
|
||||
import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';
|
||||
|
||||
function accessor(
|
||||
entries: ReadonlyArray<readonly [ServiceIdentifier<unknown>, unknown]>,
|
||||
|
|
@ -365,47 +363,4 @@ describe('Session legacy status (best-effort runtime state)', () => {
|
|||
context_usage: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('fans a permission_mode patch out through the session agent registry', async () => {
|
||||
const broadcastPermissionMode = vi.fn();
|
||||
const agent: IAgentScopeHandle = {
|
||||
id: 'main',
|
||||
kind: LifecycleScope.Agent,
|
||||
accessor: accessor([
|
||||
[IAgentProfileService, { _serviceBrand: undefined }],
|
||||
[IAgentLifecycleService, { broadcastPermissionMode }],
|
||||
]),
|
||||
dispose: () => {},
|
||||
};
|
||||
const agents = {
|
||||
create: () => Promise.resolve(agent),
|
||||
whenReady: () => Promise.resolve(agent),
|
||||
list: () => [agent],
|
||||
broadcastPermissionMode,
|
||||
} as unknown as IAgentLifecycleService;
|
||||
const session: ISessionScopeHandle = {
|
||||
id: 'session-test',
|
||||
kind: LifecycleScope.Session,
|
||||
accessor: accessor([
|
||||
[IAgentLifecycleService, agents],
|
||||
[
|
||||
ISessionMetadata,
|
||||
{
|
||||
read: () =>
|
||||
Promise.resolve({ id: 'session-test', createdAt: 0, updatedAt: 0, archived: false }),
|
||||
},
|
||||
],
|
||||
[ISessionContext, { workspaceId: 'ws-test', cwd: '/workspace' }],
|
||||
]),
|
||||
dispose: () => {},
|
||||
};
|
||||
stubSessionChain(ix, session);
|
||||
ix.set(ISessionLegacyService, new SyncDescriptor(SessionLegacyService));
|
||||
|
||||
await ix.get(ISessionLegacyService).updateProfile('session-test', {
|
||||
agent_config: { permission_mode: 'yolo' },
|
||||
});
|
||||
|
||||
expect(broadcastPermissionMode).toHaveBeenCalledWith('yolo');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -48,11 +48,11 @@ import {
|
|||
type AgentRunSuspendedEvent,
|
||||
type AgentSpawnAttemptOptions,
|
||||
type QueuedAgentRunTask,
|
||||
} from '#/session/swarm/agentRunBatch';
|
||||
import { ISessionSwarmService, type SessionSwarmSpawnTask, type SessionSwarmTask } from '#/session/swarm/sessionSwarm';
|
||||
} from '#/features/swarm/session/agentRunBatch';
|
||||
import { ISessionSwarmService, type SessionSwarmSpawnTask, type SessionSwarmTask } from '#/features/swarm/session/sessionSwarm';
|
||||
import { Error2 } from '#/_base/errors/errors';
|
||||
import { ConfigErrors } from '#/app/config/errors';
|
||||
import { SessionSwarmService } from '#/session/swarm/sessionSwarmService';
|
||||
import { SessionSwarmService } from '#/features/swarm/session/sessionSwarmService';
|
||||
|
||||
import { stubLog } from '../../_base/log/stubs';
|
||||
import { stubFlag } from '../../app/flag/stubs';
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Exercises the Agent-scoped service through DI and public loop boundaries,
|
||||
* with storage, session swarm execution, and approvals stubbed. Run:
|
||||
* `pnpm --filter @moonshot-ai/agent-core-v2 exec vitest run
|
||||
* test/agent/swarm/swarm.test.ts`.
|
||||
* test/features/swarm/swarm.test.ts`.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
|
|
@ -24,7 +24,7 @@ import { AgentContextMemoryService } from '#/agent/contextMemory/contextMemorySe
|
|||
import type { ContextMessage } from '#/agent/contextMemory/types';
|
||||
import { DEFAULT_SUBAGENT_TIMEOUT_MS } from '#/session/subagent/configSection';
|
||||
import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle';
|
||||
import { ISessionSwarmService, type SessionSwarmRunResult, type SessionSwarmTask } from '#/session/swarm/sessionSwarm';
|
||||
import { ISessionSwarmService, type SessionSwarmRunResult, type SessionSwarmTask } from '#/features/swarm/session/sessionSwarm';
|
||||
import { IAgentStateService } from '#/agent/state/agentState';
|
||||
import { AgentStateService } from '#/agent/state/agentStateService';
|
||||
import { IAgentTokenCountingService } from '#/agent/tokenCounting/tokenCounting';
|
||||
|
|
@ -33,12 +33,12 @@ import {
|
|||
wrapSystemReminder,
|
||||
} from '#/agent/systemReminder/systemReminder';
|
||||
import { AgentSystemReminderService } from '#/agent/systemReminder/systemReminderService';
|
||||
import { IAgentSwarmService } from '#/agent/swarm/swarm';
|
||||
import { AgentSwarmService } from '#/agent/swarm/swarmService';
|
||||
import SWARM_MODE_ENTER_REMINDER from '../../../src/agent/swarm/enter-reminder.md?raw';
|
||||
import { SwarmModel } from '#/agent/swarm/swarmOps';
|
||||
import { AgentSwarmToolInputSchema } from '#/agent/tools/agent-swarm/agent-swarm';
|
||||
import { AgentSwarmTool } from '#/agent/tools/agent-swarm/agentSwarmTool';
|
||||
import { IAgentSwarmService } from '#/features/swarm/agent/swarm';
|
||||
import { AgentSwarmService } from '#/features/swarm/agent/swarmService';
|
||||
import SWARM_MODE_ENTER_REMINDER from '../../../src/features/swarm/agent/enter-reminder.md?raw';
|
||||
import { SwarmModel } from '#/features/swarm/swarmOps';
|
||||
import { AgentSwarmToolInputSchema } from '#/features/swarm/tools/agent-swarm/agent-swarm';
|
||||
import { AgentSwarmTool } from '#/features/swarm/tools/agent-swarm/agentSwarmTool';
|
||||
import { IAgentToolApprovalService } from '#/agent/toolApproval/toolApproval';
|
||||
import { IAgentToolExecutorService } from '#/agent/toolExecutor/toolExecutor';
|
||||
import type {
|
||||
|
|
@ -65,8 +65,8 @@ import { EventBusService } from '#/app/event/eventBusService';
|
|||
|
||||
import { executeTool } from '../../tools/fixtures/execute-tool';
|
||||
import { registerTestAgentWire, restoreTestAgentWire, testWireScope } from '../../wire/stubs';
|
||||
import { stubLoopWithHooks } from '../loop/stubs';
|
||||
import { stubToolExecutorEvents, type ToolExecutorEventStubs } from '../toolExecutor/stubs';
|
||||
import { stubLoopWithHooks } from '../../agent/loop/stubs';
|
||||
import { stubToolExecutorEvents, type ToolExecutorEventStubs } from '../../agent/toolExecutor/stubs';
|
||||
import { createTestAgent } from '../../harness';
|
||||
|
||||
const signal = new AbortController().signal;
|
||||
|
|
@ -6,7 +6,9 @@ import { createControlledPromise } from '@antfu/utils';
|
|||
import { expect, vi } from 'vitest';
|
||||
|
||||
import { toDisposable } from '#/_base/di/lifecycle';
|
||||
import type { IInstantiationService } from '#/_base/di/instantiation';
|
||||
import type { IAgentScopeHandle } from '#/_base/di/scope';
|
||||
import { getContributedServices } from '#/features/featureRegistry';
|
||||
import { Emitter, Event } from '#/_base/event';
|
||||
import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle';
|
||||
import type { Promisable, PromisifyMethods } from '#/_base/utils/types';
|
||||
|
|
@ -52,7 +54,7 @@ import { IAgentConversationUndoService } from '#/agent/undo/undo';
|
|||
import { IAgentLoopService } from '#/agent/loop/loop';
|
||||
import type { RunShellCommandInput, RunShellCommandResult } from '#/agent/shellCommand/shellCommand';
|
||||
import type { ProfileSetModelResult } from '#/agent/profile/profile';
|
||||
import type { SwarmModeTrigger } from '#/agent/swarm/swarm';
|
||||
import type { SwarmModeTrigger } from '#/features/swarm/agent/swarm';
|
||||
import type { UserToolRegistration } from '#/agent/userTool/userTool';
|
||||
import type { ActivatePluginCommandPayload } from '#/agent/pluginCommand/pluginCommand';
|
||||
import { IAgentPluginCommandService } from '#/agent/pluginCommand/pluginCommand';
|
||||
|
|
@ -207,7 +209,7 @@ import {
|
|||
import type { IProcess } from '#/session/process/processRunner';
|
||||
import { ISessionQuestionService, type QuestionResult } from '#/session/question/question';
|
||||
import { ISessionSkillCatalog } from '#/session/sessionSkillCatalog/skillCatalog';
|
||||
import { ISessionSwarmService } from '#/session/swarm/sessionSwarm';
|
||||
import { ISessionSwarmService } from '#/features/swarm/session/sessionSwarm';
|
||||
import type { PathAccessOperation } from '#/session/workspaceContext/workspaceContext';
|
||||
|
||||
import { stubAgentIdentity } from '../app/agentIdentity/stubs';
|
||||
|
|
@ -897,6 +899,45 @@ function collectScopeSeed(
|
|||
return seed;
|
||||
}
|
||||
|
||||
// Feature contributions (`ScopeUnits` fold) provide into a scope through the
|
||||
// cascade and would replace a same-token seed instance installed at creation;
|
||||
// re-asserting overrides of feature-contributed tokens through the live
|
||||
// container right after scope creation keeps test stubs winning, as they did
|
||||
// over static registrations (which `provideScopeServices` skips when seeded).
|
||||
function reassertServiceOverrides(
|
||||
overrides: readonly TestAgentScopedServiceOverride[],
|
||||
scope: TestAgentServiceScope,
|
||||
instantiation: IInstantiationService,
|
||||
): void {
|
||||
const contributed = new Set(
|
||||
getContributedServices()
|
||||
.filter((entry) => entry.scope === scope)
|
||||
.map((entry) => entry.id),
|
||||
);
|
||||
if (contributed.size === 0) {
|
||||
return;
|
||||
}
|
||||
const reg: TestAgentServiceRegistration = {
|
||||
define: (id, ctor) => {
|
||||
if (contributed.has(id)) instantiation.provide(id, new SyncDescriptor(ctor));
|
||||
},
|
||||
defineDescriptor: (id, descriptor) => {
|
||||
if (contributed.has(id)) instantiation.provide(id, descriptor);
|
||||
},
|
||||
defineInstance: (id, instance) => {
|
||||
if (contributed.has(id)) instantiation.provide(id, instance);
|
||||
},
|
||||
definePartialInstance: (id, instance) => {
|
||||
if (contributed.has(id)) instantiation.provide(id, instance as never);
|
||||
},
|
||||
};
|
||||
for (const override of overrides) {
|
||||
if (override.scope === scope) {
|
||||
override.register(reg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PersistenceAppendLogStore implements IAppendLogStore {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
private readonly history: WireRecord[] = [];
|
||||
|
|
@ -1236,6 +1277,7 @@ export class AgentTestContext {
|
|||
'session',
|
||||
),
|
||||
});
|
||||
reassertServiceOverrides(this.serviceOverrides, 'session', this.session.instantiation);
|
||||
const workspace = this.session.accessor.get(ISessionWorkspaceContext);
|
||||
|
||||
this.agent = this.session.createChild(LifecycleScope.Agent, agentId, {
|
||||
|
|
@ -1289,6 +1331,7 @@ export class AgentTestContext {
|
|||
'agent',
|
||||
),
|
||||
});
|
||||
reassertServiceOverrides(this.serviceOverrides, 'agent', this.agent.instantiation);
|
||||
|
||||
this.initializeRestorableServices();
|
||||
this.get(IAgentActivityView);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { IAgentTaskService } from '#/agent/task/task';
|
|||
import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory';
|
||||
import { IAgentTokenCountingService } from '#/agent/tokenCounting/tokenCounting';
|
||||
import { makeHookRunner } from '../agent/externalHooks/runner-stub';
|
||||
import { IAgentProfileService } from '#/agent/profile/profile';
|
||||
import { IAgentProfileService, type ProfileData } from '#/agent/profile/profile';
|
||||
import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode';
|
||||
import { ToolAccesses, type ExecutableTool } from '#/tool/toolContract';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry';
|
||||
|
|
@ -28,7 +28,7 @@ import { IAgentUserToolService, type UserToolRegistration } from '#/agent/userTo
|
|||
import {
|
||||
AgentSwarmToolInputSchema,
|
||||
type AgentSwarmToolInput,
|
||||
} from '#/agent/tools/agent-swarm/agent-swarm';
|
||||
} from '#/features/swarm/tools/agent-swarm/agent-swarm';
|
||||
import {
|
||||
SubagentToolInputSchema,
|
||||
type SubagentToolInput,
|
||||
|
|
@ -58,13 +58,14 @@ import type {
|
|||
ISessionSwarmService,
|
||||
SessionSwarmRunArgs,
|
||||
SessionSwarmRunResult,
|
||||
} from '#/session/swarm/sessionSwarm';
|
||||
} from '#/features/swarm/session/sessionSwarm';
|
||||
import type { IProcess, ISessionProcessRunner } from '#/session/process/processRunner';
|
||||
import { IWireService } from '#/wire/wire';
|
||||
import { createFakeProcessRunner } from '../tools/fixtures/fake-exec';
|
||||
import { StubConfigService } from '../kosong/stubs';
|
||||
import { stubFlag } from '../app/flag/stubs';
|
||||
import {
|
||||
agentService,
|
||||
appService,
|
||||
configServices,
|
||||
createCommandRunner,
|
||||
|
|
@ -552,6 +553,37 @@ describe('Agent tool description', () => {
|
|||
expect(coderTools).not.toContain('Bash');
|
||||
});
|
||||
|
||||
it('lists contributed tools the caller profile does not activate', () => {
|
||||
// The caller binds a profile without AgentSwarm, so the runtime registry
|
||||
// never holds it; a global restriction forces the explicit enumeration
|
||||
// branch. The listing must still draw from the full contribution
|
||||
// collection — spawned `agent` profiles can use AgentSwarm.
|
||||
const callerData = {
|
||||
profileName: 'orchestrator',
|
||||
activeToolNames: ['Agent', 'Bash', 'Read'],
|
||||
disallowedTools: [],
|
||||
} as unknown as ProfileData;
|
||||
ctx = createTestAgent(
|
||||
{ autoConfigure: false },
|
||||
agentService(IAgentProfileService, {
|
||||
_serviceBrand: undefined,
|
||||
data: () => callerData,
|
||||
onDidChange: Event.None,
|
||||
} as unknown as IAgentProfileService),
|
||||
configServices(() => ({
|
||||
providers: {},
|
||||
tools: { disabled: ['Write'] },
|
||||
})),
|
||||
);
|
||||
|
||||
const description = agentDescription();
|
||||
const agentTools = description.match(/- agent: [^\n]*\n Tools: ([^\n]*)/)?.[1];
|
||||
|
||||
expect(agentTools).toBeDefined();
|
||||
expect(agentTools).toContain('AgentSwarm');
|
||||
expect(agentTools).not.toContain('Write');
|
||||
});
|
||||
|
||||
it('renders effective tools after applying disallowedTools', () => {
|
||||
const restricted: AgentProfile = normalizeAgentProfile({
|
||||
name: 'restricted',
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ import type {
|
|||
SubagentSpawnedEvent,
|
||||
SubagentStartedEvent,
|
||||
} from '@moonshot-ai/agent-core-v2/session/subagent/mirrorAgentRun';
|
||||
import type { SubagentSuspendedEvent } from '@moonshot-ai/agent-core-v2/session/swarm/sessionSwarmService';
|
||||
import type { SubagentSuspendedEvent } from '@moonshot-ai/agent-core-v2/features/swarm/session/sessionSwarmService';
|
||||
import type { ToolUpdate } from '@moonshot-ai/agent-core-v2/tool/toolContract';
|
||||
|
||||
import { ToolInputDisplaySchema } from './display';
|
||||
|
|
|
|||
84
packages/kap-server/src/routes/sessionAgentConfig.ts
Normal file
84
packages/kap-server/src/routes/sessionAgentConfig.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* `agent_config` patch dispatch for `POST /sessions/{session_id}/profile`.
|
||||
*
|
||||
* The `agent_config` body field is a wire-to-native translation, not a v1-only
|
||||
* projection, so it lives at the server edge alongside the other routes that
|
||||
* call the native v2 services directly (`fork` / `compact` / `undo` / `abort`)
|
||||
* instead of inside `ISessionLegacyService`. The helper resumes the session
|
||||
* (cold-load if needed), resolves its main agent, and fans each present field
|
||||
* out to the owning Agent-scope service, preserving the per-field
|
||||
* apply-only-when-set semantics and the plan/swarm idempotency guards.
|
||||
*/
|
||||
|
||||
import {
|
||||
ErrorCodes,
|
||||
Error2,
|
||||
IAgentGoalService,
|
||||
IAgentLifecycleService,
|
||||
IAgentPlanService,
|
||||
IAgentProfileService,
|
||||
IAgentSwarmService,
|
||||
resumeSessionById,
|
||||
type PermissionMode,
|
||||
type Scope,
|
||||
} from '@moonshot-ai/agent-core-v2';
|
||||
import type { SessionAgentConfigPartial } from '@moonshot-ai/agent-core-v2/app/sessionLegacy/sessionProtocol';
|
||||
|
||||
import { ensureMainAgent } from '../transport/mainAgent';
|
||||
|
||||
export async function applySessionAgentConfig(
|
||||
core: Scope,
|
||||
sessionId: string,
|
||||
agentConfig: SessionAgentConfigPartial,
|
||||
): Promise<void> {
|
||||
const session = await resumeSessionById(core.accessor, sessionId);
|
||||
if (session === undefined) {
|
||||
throw new Error2(ErrorCodes.SESSION_NOT_FOUND, `session ${sessionId} does not exist`);
|
||||
}
|
||||
const agent = await ensureMainAgent(session);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
packages/kap-server/src/routes/sessionProfile.ts
Normal file
58
packages/kap-server/src/routes/sessionProfile.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* title/metadata patch for `POST /sessions/{session_id}/profile`.
|
||||
*
|
||||
* Like the `agent_config` dispatch (`sessionAgentConfig.ts`), the
|
||||
* title/metadata update is a wire-to-native translation with no v1-only
|
||||
* projection, so it lives at the server edge instead of inside
|
||||
* `ISessionLegacyService`. The helper resumes the session (cold-load if
|
||||
* needed), applies the patch through `ISessionMetadata`, and reads the
|
||||
* metadata document back together with `ISessionContext` to assemble the
|
||||
* `SessionWireFields` shape the route feeds to `toWireSession`.
|
||||
*/
|
||||
|
||||
import {
|
||||
ErrorCodes,
|
||||
Error2,
|
||||
ISessionContext,
|
||||
ISessionMetadata,
|
||||
resumeSessionById,
|
||||
type Scope,
|
||||
} from '@moonshot-ai/agent-core-v2';
|
||||
import type { SessionWireFields } from '@moonshot-ai/agent-core-v2/app/sessionLegacy/sessionLegacy';
|
||||
import type { UpdateSessionProfileRequest } from '@moonshot-ai/agent-core-v2/app/sessionLegacy/sessionProtocol';
|
||||
|
||||
export async function updateSessionProfile(
|
||||
core: Scope,
|
||||
sessionId: string,
|
||||
body: Pick<UpdateSessionProfileRequest, 'title' | 'metadata'>,
|
||||
): Promise<SessionWireFields> {
|
||||
const session = await resumeSessionById(core.accessor, 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 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,
|
||||
};
|
||||
}
|
||||
|
|
@ -29,10 +29,12 @@
|
|||
* `/sessions/{id}/children` endpoints call `ISessionLifecycleService.createChild`
|
||||
* and `ISessionIndex.list({ childOf })` directly — the child markers and
|
||||
* parent-title default live in the lifecycle, and the child filter lives in the
|
||||
* index. Only `POST /sessions/{id}/profile` (`updateProfile`),
|
||||
* `GET /sessions/{id}/status`, and `GET /sessions/{id}/goal` go through
|
||||
* `ISessionLegacyService` (the `agent_config` patch, the status rollup, and the
|
||||
* current-goal read hold real cross-domain adaptation);
|
||||
* index. `POST /sessions/{id}/profile` is composed at the edge too: both the
|
||||
* title/metadata patch (`sessionProfile.ts`) and the `agent_config` dispatch
|
||||
* (`sessionAgentConfig.ts`) are wire-to-native translations over the native v2
|
||||
* services. Only `GET /sessions/{id}/status` and `GET /sessions/{id}/goal` go
|
||||
* through `ISessionLegacyService` (the status rollup and the current-goal read
|
||||
* hold real cross-domain adaptation);
|
||||
* the route forwards each adapter result verbatim, mirroring v1's thin handler.
|
||||
* `create`, `fork`, and child creation publish `event.session.created` on the
|
||||
* core event bus, matching v1.
|
||||
|
|
@ -135,6 +137,8 @@ import { requestLog } from '../lib/requestLog';
|
|||
import { defineRoute } from '../middleware/defineRoute';
|
||||
import { ensureMainAgent } from '../transport/mainAgent';
|
||||
import { parseActionSuffix } from './action-suffix';
|
||||
import { applySessionAgentConfig } from './sessionAgentConfig';
|
||||
import { updateSessionProfile } from './sessionProfile';
|
||||
|
||||
interface SessionRouteHost {
|
||||
post(
|
||||
|
|
@ -611,9 +615,15 @@ export function registerSessionsRoutes(app: SessionRouteHost, core: Scope): void
|
|||
async (req, reply) => {
|
||||
try {
|
||||
const { session_id } = req.params;
|
||||
const fields = await core.accessor
|
||||
.get(ISessionLegacyService)
|
||||
.updateProfile(session_id, req.body);
|
||||
const { agent_config, ...profileBody } = req.body;
|
||||
// Both halves of the profile patch are wire-to-native translations
|
||||
// dispatched to the native v2 services at the edge (same direct-call
|
||||
// pattern as fork/compact/undo); title/metadata applies first,
|
||||
// matching the original in-adapter ordering.
|
||||
const fields = await updateSessionProfile(core, session_id, profileBody);
|
||||
if (agent_config !== undefined) {
|
||||
await applySessionAgentConfig(core, session_id, agent_config);
|
||||
}
|
||||
const session = toWireSession(fields, fields.root, resolveSessionFacts(core, fields.id));
|
||||
// Broadcast the title change to every connection (including clients not
|
||||
// subscribed to this session, and covering inactive sessions), so session
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@
|
|||
* `DomainEventMap` augmentations in `packages/agent-core-v2/src`, e.g.
|
||||
* `agent/loop/loopService.ts`, `agent/toolExecutor/toolExecutorService.ts`,
|
||||
* `agent/task/taskOps.ts`, `agent/shellCommand/shellCommandService.ts`,
|
||||
* `session/agentLifecycle/mirrorAgentRun.ts`, `session/swarm/sessionSwarmService.ts`,
|
||||
* `session/agentLifecycle/mirrorAgentRun.ts`, `features/swarm/session/sessionSwarmService.ts`,
|
||||
* `agent/goal/goalOps.ts`, `agent/usage/usageOps.ts`, `agent/skill/skillOps.ts`,
|
||||
* `agent/pluginCommand/pluginCommandService.ts`, `session/cron/cronOps.ts`,
|
||||
* `agent/fullCompaction/compactionOps.ts`, `agent/mcp/mcpService.ts`,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue