mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-15 19:55:36 +00:00
refactor(agent-core-v2): remove the agent RPC aggregation layer (#2871)
* refactor(agent-core-v2): remove the agent RPC aggregation layer
- delete src/agent/rpc/ (AgentRPCService, IAgentRPCService, core-api,
prompt-metadata, types) and sink each method's orchestration into its
owning domain service
- prompt: new submit/submitSteer composing disabledTools gating,
MAIN-only session metadata, and engine-side {turn_id} settlement
- skill: activate now returns PromptLaunchResult and writes session
metadata internally (MAIN-only, unified across prompt/steer/skill/
pluginCommand); node-sdk and kap-server drop their edge-side writes
- pluginCommand: new agent-scope domain owning command activation and
the plugin_command.activated domain event
- permissionMode/loop/fullCompaction: new setModeAndBroadcast /
cancelFromUser / cancel; setMode and loop.cancel stay pure for
internal callers
- klient: agentRpcContract split into per-domain contracts; facade
re-routes to domain channels with its public API unchanged
- node-sdk, kap-server, kimi-inspect and the v2 test harness now call
domain services directly; ctx.rpc keeps its name as a composed
adapter
- externally visible: the agentRPCService debug channel is gone and
session metadata writes are now MAIN-agent-only (see changeset)
* refactor(agent-core-v2): move disabledTools gating out of the prompt domain
Prompt should not own session tool policy: submit no longer accepts or
applies disabledTools. The klient facade keeps its prompt({ disabledTools })
API and composes it edge-side — applying agentToolPolicyService
setSessionDisabledTools before calling agentPromptService.submit, the same
way kap-server's prompt route already does. Over klient, a profile-less
engine now surfaces the raw profile error instead of request.invalid.
Also restores the RPC-removal changeset, which did not make it into the
previous commit.
* chore(agent-core-v2): drop the RPC-removal changeset
* refactor(klient): drop disabledTools from the prompt entry entirely
The prompt path no longer carries session tool gating on any surface:
the klient facade prompt() loses the disabledTools field and calls
agentPromptService.submit directly, and the node-sdk
SessionPromptRpcInput stops accepting or forwarding it (v1 always
ignored the field). Session tool gating remains available through
IAgentToolPolicyService.setSessionDisabledTools, composed at the edge
the way kap-server's prompt route does; the klient toolPolicy contract
added for facade-side composition is removed as unused.
This commit is contained in:
parent
719da94648
commit
23e68eee8b
62 changed files with 1079 additions and 1238 deletions
|
|
@ -45,7 +45,7 @@ A Service method is directly exposable iff **all** hold:
|
|||
3. Errors are `KimiError` (coded).
|
||||
4. It is a command/query, not a factory, stream, byte-store, or sink.
|
||||
|
||||
If any fail → wrap in a **facade** (a Service that takes ids, returns data, throws `KimiError`) and expose the facade. The repo already ships a wire-shaped facade in `rpc/core-api.ts` (`CoreAPI` / `SessionAPI` / `AgentAPI`) behind `IAgentRPCService` / `ISessionRPCService` — prefer building the HTTP edge on top of it rather than re-deriving a new one.
|
||||
If any fail → add a wire-safe orchestration method to the owning domain Service (e.g. `IAgentPromptService.submit` settles `{turn_id}` instead of returning the live `PromptHandle`) or compose several domain Services at the edge — kap-server's `routes/prompts.ts` is the reference for edge-side composition.
|
||||
|
||||
## 3. Per-scope `resource:action` map
|
||||
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ const route = defineRoute(
|
|||
app.post(route.path, route.options, route.handler);
|
||||
```
|
||||
|
||||
**For `/api/v2` (native):** add a `resource:action` entry to `actionMap` ([edge-exposure.md](edge-exposure.md) §3). If the method fails the direct-exposure rules (returns a handle / stream / bytes, takes a live object), wrap it in a wire-shaped facade first (`IAgentRPCService` / `ISessionRPCService`) and map to the facade — as `prompts:*` does via `IAgentRPCService`.
|
||||
**For `/api/v2` (native):** add a `resource:action` entry to `actionMap` ([edge-exposure.md](edge-exposure.md) §3). If the method fails the direct-exposure rules (returns a handle / stream / bytes, takes a live object), add a wire-safe orchestration method to the owning domain Service first — as `prompts:submit` maps to `IAgentPromptService.submit`, which settles `{turn_id}` engine-side instead of returning the live `PromptHandle`.
|
||||
|
||||
### 5. Map errors
|
||||
|
||||
|
|
@ -218,7 +218,7 @@ This is the reference alignment (commits `feat(server-v2): port v1 /sessions/:si
|
|||
|
||||
**The split.**
|
||||
|
||||
- `/api/v2` keeps the native shape — `prompts:submit` / `steer` / `undo` / `clear` / `cancel` map to `IAgentRPCService` (a wire facade over the v2 turn driver) in `actionMap`. The native `IAgentPromptService` is untouched.
|
||||
- `/api/v2` keeps the native shape — `prompts:submit` / `steer` / `undo` / `clear` / `cancel` map to the domain Services (`IAgentPromptService.submit` / `submitSteer`, `IAgentConversationUndoService.undo`, `IAgentLoopService.cancelFromUser`) in `actionMap`.
|
||||
- `/api/v1` gets an `AgentPromptLegacyService` (`prompt/`, `LifecycleScope.Agent`) that re-implements the v1 scheduler — queue, `prompt_id`, steer/abort, auto-start-next — **on top of** the native `IAgentPromptService`. The `/api/v1` routes consume the LegacyService.
|
||||
|
||||
**The schema.** Both surfaces import `promptSubmissionSchema` / `promptSubmitResultSchema` / `promptListResponseSchema` / `promptSteerRequestSchema` / `promptSteerResultSchema` / `promptAbortResponseSchema` from the shared v1 wire schemas (see `packages/kap-server/src/protocol`). The `/api/v1` and `/api/v2` routes are therefore compatible with released clients by construction; the LegacyService projects v2 turn results back into those protocol shapes.
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ File names derive from the interface / class names so that scope and role are vi
|
|||
| Shared-types file | `<domain>.types.ts` | `log.types.ts` |
|
||||
| Errors file | `<name>.errors.ts` | `appendLogStore.errors.ts` |
|
||||
|
||||
Acronym-aware lowerCamelCase lowercases a leading acronym as a group: `ILLMRequester` → `llmRequester.ts`, `IWSGateway` → `wsGateway.ts`, `IOAuthToolkit` → `oauthToolkit.ts`, `IAgentRPCService` → `agentRpcService.ts`.
|
||||
Acronym-aware lowerCamelCase lowercases a leading acronym as a group: `ILLMRequester` → `llmRequester.ts`, `IWSGateway` → `wsGateway.ts`, `IOAuthToolkit` → `oauthToolkit.ts`, `IMcpServerService` → `mcpServerService.ts`.
|
||||
|
||||
Because the impl class always ends in `Service` and the interface file never does, the two files of one service never collide — even for `Store` / `Registry` / `Resolver` interfaces (`IAppendLogStore` → `appendLogStore.ts` + `appendLogStoreService.ts`).
|
||||
|
||||
|
|
|
|||
|
|
@ -31,14 +31,14 @@ describe('ProxyChannel.call', () => {
|
|||
it('POSTs the command to the service base URL; no body and no header without args/token', async () => {
|
||||
const { calls, fetchImpl } = fakeFetch(ok({ id: 's1' }));
|
||||
const channel = new ProxyChannel({
|
||||
baseUrl: 'http://h:1/api/v1/debug/session/s%201/agent/main/agentRPCService',
|
||||
baseUrl: 'http://h:1/api/v1/debug/session/s%201/agent/main/agentLoopService',
|
||||
fetch: fetchImpl,
|
||||
});
|
||||
const result = await channel.call('getModel', []);
|
||||
expect(result).toEqual({ id: 's1' });
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]!.url).toBe(
|
||||
'http://h:1/api/v1/debug/session/s%201/agent/main/agentRPCService/getModel',
|
||||
'http://h:1/api/v1/debug/session/s%201/agent/main/agentLoopService/getModel',
|
||||
);
|
||||
expect(calls[0]!.init?.method).toBe('POST');
|
||||
expect(calls[0]!.init?.body).toBeUndefined();
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
* await client.core(ISessionIndex).listRecent({});
|
||||
* await client.workspace('wd_1').service(ISessionLifecycleService).resume('s1');
|
||||
* await client.session('s1').service(ISessionMetadata).read();
|
||||
* await client.session('s1').agent('main').service(IAgentRPCService).cancel({});
|
||||
* await client.session('s1').agent('main').service(IAgentLoopService).cancelFromUser();
|
||||
*
|
||||
* The `agent-core-v2` service token is the whole key: its type parameter `T`
|
||||
* types the returned proxy, and its decorator id (`String(id)`) is the channel
|
||||
|
|
|
|||
|
|
@ -14,12 +14,14 @@
|
|||
* a full REST refresh; nothing is resynced from the socket itself.
|
||||
*
|
||||
* Rendering is turn-granular (turn → step → frame) and typed entirely by the
|
||||
* transcript data model. Prompts/cancels go through the `IAgentRPCService`
|
||||
* transcript data model. Prompts/cancels go through the `IAgentPromptService`
|
||||
* / `IAgentLoopService` channels
|
||||
* over the debug RPC surface (`/api/v1/debug`); the running indicator
|
||||
* derives from transcript state (`meta.activity` / running turns).
|
||||
*/
|
||||
|
||||
import { IAgentRPCService } from '@moonshot-ai/agent-core-v2/agent/rpc/rpc';
|
||||
import { IAgentLoopService } from '@moonshot-ai/agent-core-v2/agent/loop/loop';
|
||||
import { IAgentPromptService } from '@moonshot-ai/agent-core-v2/agent/prompt/prompt';
|
||||
import { ISessionApprovalService } from '@moonshot-ai/agent-core-v2/session/approval/approval';
|
||||
import {
|
||||
ISessionQuestionService,
|
||||
|
|
@ -561,8 +563,8 @@ export function ChatView({
|
|||
await klient
|
||||
.session(sessionId)
|
||||
.agent(agentId)
|
||||
.service(IAgentRPCService)
|
||||
.prompt({ input: [{ type: 'text', text }] });
|
||||
.service(IAgentPromptService)
|
||||
.submit({ input: [{ type: 'text', text }] });
|
||||
trail?.recordEvent('prompt', text, state);
|
||||
} catch (error) {
|
||||
setSendError(error);
|
||||
|
|
@ -572,7 +574,7 @@ export function ChatView({
|
|||
const cancel = async () => {
|
||||
if (sessionId === null) return;
|
||||
try {
|
||||
await klient.session(sessionId).agent(agentId).service(IAgentRPCService).cancel({});
|
||||
await klient.session(sessionId).agent(agentId).service(IAgentLoopService).cancelFromUser();
|
||||
trail?.recordEvent('cancel', undefined, state);
|
||||
} catch (error) {
|
||||
setSendError(error);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ 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 { IAgentRPCService } from '@moonshot-ai/agent-core-v2/agent/rpc/rpc';
|
||||
import { IAgentSwarmService } from '@moonshot-ai/agent-core-v2/agent/swarm/swarm';
|
||||
import { IAgentTaskService } from '@moonshot-ai/agent-core-v2/agent/task/task';
|
||||
import { IAgentTokenCountingService } from '@moonshot-ai/agent-core-v2/agent/tokenCounting/tokenCounting';
|
||||
|
|
@ -269,17 +268,4 @@ export const AGENT_PANELS: readonly ServicePanelDef[] = [
|
|||
{ label: 'exit', run: (svc) => call(svc, 'exit') },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: String(IAgentRPCService),
|
||||
label: 'AgentRPCService',
|
||||
scope: 'agent',
|
||||
actions: [
|
||||
{ label: 'cancel turn', run: (svc) => call(svc, 'cancel', {}) },
|
||||
{
|
||||
label: 'undoHistory',
|
||||
input: 'Steps',
|
||||
run: (svc, n) => call(svc, 'undoHistory', { count: Number(n) }),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ The DI kernel (`src/_base/di/`) owns the unit layer on top of the scoped registr
|
|||
- `instantiation.ts` — the `@ref(IX)` decorator factory (`LiveRef<T>`: `current` live read + `onDidChange` availability event; observation creates no binding and no graph edge) and `ScopeActivation`.
|
||||
- `src/app/feature/` — `IFeatureManager` (App scope): runtime unit assembly (`provideUnit` / `unprovideUnit` / `updateUnit`) and introspection (`units()` / `onDidChangeUnits`); managed units hang on the manager's own book. External package management stays with `IPluginService`. The `features` assembly (`src/features/featureAssemblyService.ts`) drains the module-level feature table through it.
|
||||
|
||||
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 `agentRPCService.listCommands` / `runCommand`).
|
||||
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`.
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export interface IAgentFullCompactionService {
|
|||
|
||||
readonly compacting: FullCompactionTask | null;
|
||||
begin(input: FullCompactionInput): boolean;
|
||||
cancel(): void;
|
||||
|
||||
readonly hooks: Hooks<{
|
||||
onWillCompact: FullCompactionTask;
|
||||
|
|
|
|||
|
|
@ -247,6 +247,17 @@ export class AgentFullCompactionService extends Service implements IAgentFullCom
|
|||
return this._compacting;
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
const active = this._compacting;
|
||||
if (active !== null) {
|
||||
this.telemetry.track2('cancel', {
|
||||
from: 'compacting',
|
||||
trace_id: active.traceId,
|
||||
});
|
||||
}
|
||||
active?.abortController.abort();
|
||||
}
|
||||
|
||||
private getEffectiveMaxContextTokens(): number {
|
||||
const capability = this.profile.data().modelCapabilities;
|
||||
const configured = capability.max_input_tokens ?? capability.max_context_tokens;
|
||||
|
|
|
|||
|
|
@ -147,6 +147,8 @@ export interface IAgentLoopService {
|
|||
|
||||
cancel(turnId?: number, reason?: unknown): boolean;
|
||||
|
||||
cancelFromUser(turnId?: number): void;
|
||||
|
||||
tryAcquireQuiescence(): IDisposable | undefined;
|
||||
|
||||
settled(): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -250,6 +250,17 @@ export class AgentLoopService extends Disposable implements IAgentLoopService {
|
|||
);
|
||||
}
|
||||
|
||||
cancelFromUser(turnId?: number): void {
|
||||
const status = this.status();
|
||||
if (status.state === 'running') {
|
||||
this.telemetry.track2('cancel', {
|
||||
from: 'streaming',
|
||||
trace_id: status.activeTraceId,
|
||||
});
|
||||
}
|
||||
this.cancel(turnId);
|
||||
}
|
||||
|
||||
tryAcquireQuiescence(): IDisposable | undefined {
|
||||
if (this.disposing) throw abortError('Agent loop disposed');
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export interface IAgentPermissionModeService {
|
|||
|
||||
readonly mode: PermissionMode;
|
||||
setMode(mode: PermissionMode): void;
|
||||
setModeAndBroadcast(mode: PermissionMode): void;
|
||||
|
||||
readonly onDidChangeMode: Event<PermissionModeChangedContext>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@
|
|||
* `PermissionModeModel`, mutating it only through the `permission.set_mode` Op
|
||||
* (`wire.dispatch(setMode({ mode }))`) and reading it through `wire.getModel`.
|
||||
* `setMode` emits `onDidChangeMode` after an actual change, and mode-aware
|
||||
* reminders are registered through the permission-mode injection helper. Bound
|
||||
* at Agent scope.
|
||||
* reminders are registered through the permission-mode injection helper.
|
||||
* `setModeAndBroadcast` is the user-facing entry: on top of `setMode` it
|
||||
* broadcasts the mode to every agent of the session through `agentLifecycle`
|
||||
* (main agent only) and tracks the `yolo_toggle` / `afk_toggle` transitions
|
||||
* through `telemetry`. Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import type { PermissionMode } from '#/agent/permissionPolicy/types';
|
||||
|
|
@ -16,6 +19,12 @@ import { LifecycleScope } from '#/app/scopes';
|
|||
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
|
||||
import { Emitter, type Event } from '#/_base/event';
|
||||
import { PermissionModeInjection } from '#/agent/permissionMode/injection/permissionModeInjection';
|
||||
import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext';
|
||||
import { ITelemetryService } from '#/app/telemetry/telemetry';
|
||||
import {
|
||||
IAgentLifecycleService,
|
||||
MAIN_AGENT_ID,
|
||||
} from '#/session/agentLifecycle/agentLifecycle';
|
||||
import { IWireService } from '#/wire/wire';
|
||||
import { IAgentPermissionModeService, type PermissionModeChangedContext } from './permissionMode';
|
||||
import {
|
||||
|
|
@ -33,6 +42,9 @@ export class AgentPermissionModeService extends Service implements IAgentPermiss
|
|||
constructor(
|
||||
@IWireService private readonly wire: IWireService,
|
||||
@IInstantiationService instantiation: IInstantiationService,
|
||||
@IAgentScopeContext private readonly scopeContext: IAgentScopeContext,
|
||||
@IAgentLifecycleService private readonly agentLifecycle: IAgentLifecycleService,
|
||||
@ITelemetryService private readonly telemetry: ITelemetryService,
|
||||
) {
|
||||
super();
|
||||
this._register(instantiation.createInstance(PermissionModeInjection, this));
|
||||
|
|
@ -49,6 +61,23 @@ export class AgentPermissionModeService extends Service implements IAgentPermiss
|
|||
this.wire.dispatch(setMode({ mode }));
|
||||
if (changed) this._onDidChangeMode.fire({ mode, previousMode });
|
||||
}
|
||||
|
||||
setModeAndBroadcast(mode: PermissionMode): void {
|
||||
const wasYolo = this.mode === 'yolo';
|
||||
const wasAuto = this.mode === 'auto';
|
||||
this.setMode(mode);
|
||||
if (this.scopeContext.agentId === MAIN_AGENT_ID) {
|
||||
this.agentLifecycle.broadcastPermissionMode(mode);
|
||||
}
|
||||
const yoloEnabled = this.mode === 'yolo';
|
||||
if (yoloEnabled !== wasYolo) {
|
||||
this.telemetry.track2('yolo_toggle', { enabled: yoloEnabled });
|
||||
}
|
||||
const afkEnabled = this.mode === 'auto';
|
||||
if (afkEnabled !== wasAuto) {
|
||||
this.telemetry.track2('afk_toggle', { enabled: afkEnabled });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerScopedService(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* `pluginCommand` domain — Agent-scoped plugin command activation contract.
|
||||
*
|
||||
* `IAgentPluginCommandService.activate` drives a user-slash plugin command
|
||||
* into the agent's prompt pipeline: the command definition lives in the
|
||||
* App-scope `plugin` domain, while activation (argument expansion, the
|
||||
* `plugin_command.activated` domain event, prompt enqueue) must run inside the
|
||||
* agent scope. Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
||||
export interface ActivatePluginCommandPayload {
|
||||
readonly pluginId: string;
|
||||
readonly commandName: string;
|
||||
readonly args?: string | undefined;
|
||||
}
|
||||
|
||||
export interface PluginCommandActivatedEvent {
|
||||
readonly type: 'plugin_command.activated';
|
||||
readonly activationId: string;
|
||||
readonly pluginId: string;
|
||||
readonly commandName: string;
|
||||
readonly commandArgs?: string;
|
||||
readonly trigger: 'user-slash';
|
||||
}
|
||||
|
||||
declare module '#/app/event/eventBus' {
|
||||
interface DomainEventMap {
|
||||
'plugin_command.activated': PluginCommandActivatedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IAgentPluginCommandService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
activate(payload: ActivatePluginCommandPayload): Promise<void>;
|
||||
}
|
||||
|
||||
export const IAgentPluginCommandService: ServiceIdentifier<IAgentPluginCommandService> =
|
||||
createDecorator<IAgentPluginCommandService>('agentPluginCommandService');
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* `pluginCommand` domain — `IAgentPluginCommandService` implementation.
|
||||
*
|
||||
* Resolves the command definition through `plugin` (`IPluginService`), expands
|
||||
* its arguments, publishes the `plugin_command.activated` domain event through
|
||||
* `eventBus`, enqueues the expanded body as a user message through `prompt`,
|
||||
* and — for the main agent only — persists the derived title/lastPrompt
|
||||
* through `sessionMetadata`, publishing the live update through `event`.
|
||||
* Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { LifecycleScope } from '#/app/scopes';
|
||||
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
|
||||
import { IEventBus } from '#/app/event/eventBus';
|
||||
import { IEventService } from '#/app/event/event';
|
||||
import { ErrorCodes, Error2 } from '#/errors';
|
||||
import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext';
|
||||
import { MAIN_AGENT_ID } from '#/session/agentLifecycle/agentLifecycle';
|
||||
import { expandCommandArguments } from '#/app/plugin/commands';
|
||||
import { IPluginService } from '#/app/plugin/plugin';
|
||||
import { IAgentPromptService } from '#/agent/prompt/prompt';
|
||||
import { promptMetadataTextFromText } from '#/agent/prompt/promptMetadataText';
|
||||
import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';
|
||||
import { ISessionContext } from '#/session/sessionContext/sessionContext';
|
||||
import { applyPromptMetadataUpdate } from '#/session/sessionMetadata/promptMetadata';
|
||||
|
||||
import {
|
||||
IAgentPluginCommandService,
|
||||
type ActivatePluginCommandPayload,
|
||||
} from './pluginCommand';
|
||||
|
||||
export class AgentPluginCommandService implements IAgentPluginCommandService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IPluginService private readonly plugins: IPluginService,
|
||||
@IAgentPromptService private readonly promptService: IAgentPromptService,
|
||||
@IEventBus private readonly eventBus: IEventBus,
|
||||
@ISessionMetadata private readonly metadata: ISessionMetadata,
|
||||
@IEventService private readonly eventService: IEventService,
|
||||
@ISessionContext private readonly sessionContext: ISessionContext,
|
||||
@IAgentScopeContext private readonly scopeContext: IAgentScopeContext,
|
||||
) { }
|
||||
|
||||
async activate(payload: ActivatePluginCommandPayload): Promise<void> {
|
||||
const commands = await this.plugins.listPluginCommands();
|
||||
const def = commands.find(
|
||||
(command) => command.pluginId === payload.pluginId && command.name === payload.commandName,
|
||||
);
|
||||
if (def === undefined) {
|
||||
throw new Error2(
|
||||
ErrorCodes.REQUEST_INVALID,
|
||||
`Plugin command "${payload.pluginId}:${payload.commandName}" was not found`,
|
||||
);
|
||||
}
|
||||
const commandArgs = payload.args ?? '';
|
||||
const expanded = expandCommandArguments(def.body, commandArgs);
|
||||
const origin = {
|
||||
kind: 'plugin_command' as const,
|
||||
activationId: randomUUID(),
|
||||
pluginId: payload.pluginId,
|
||||
commandName: payload.commandName,
|
||||
commandArgs: payload.args,
|
||||
trigger: 'user-slash' as const,
|
||||
};
|
||||
this.eventBus.publish({
|
||||
type: 'plugin_command.activated',
|
||||
activationId: origin.activationId,
|
||||
pluginId: origin.pluginId,
|
||||
commandName: origin.commandName,
|
||||
commandArgs: origin.commandArgs,
|
||||
trigger: origin.trigger,
|
||||
});
|
||||
await this.promptService.enqueue({ message: {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: expanded }],
|
||||
toolCalls: [],
|
||||
origin,
|
||||
} });
|
||||
if (this.scopeContext.agentId === MAIN_AGENT_ID) {
|
||||
await applyPromptMetadataUpdate(
|
||||
{
|
||||
metadata: this.metadata,
|
||||
eventService: this.eventService,
|
||||
sessionId: this.sessionContext.sessionId,
|
||||
},
|
||||
promptMetadataTextFromPluginCommand(payload),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function promptMetadataTextFromPluginCommand(
|
||||
payload: ActivatePluginCommandPayload,
|
||||
): string | undefined {
|
||||
const args = payload.args?.trim();
|
||||
const command = `/${payload.pluginId}:${payload.commandName}`;
|
||||
return promptMetadataTextFromText(
|
||||
args === undefined || args.length === 0 ? command : `${command} ${args}`,
|
||||
);
|
||||
}
|
||||
|
||||
registerScopedService(
|
||||
LifecycleScope.Agent,
|
||||
IAgentPluginCommandService,
|
||||
AgentPluginCommandService,
|
||||
ScopeActivation.OnScopeCreated,
|
||||
'pluginCommand',
|
||||
);
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { createDecorator } from '#/_base/di/instantiation';
|
||||
import type { ContextMessage } from '#/agent/contextMemory/types';
|
||||
import type { Turn, TurnResult } from '#/agent/loop/loop';
|
||||
import type { ContentPart } from '#/kosong/contract/message';
|
||||
import type { Hooks } from '#/hooks';
|
||||
|
||||
export interface PromptSubmitContext {
|
||||
|
|
@ -47,9 +48,23 @@ export interface PromptQueueSnapshot {
|
|||
readonly pending: readonly PromptSnapshot[];
|
||||
}
|
||||
|
||||
export interface PromptPayload {
|
||||
readonly input: readonly ContentPart[];
|
||||
}
|
||||
|
||||
export interface SteerPayload {
|
||||
readonly input: readonly ContentPart[];
|
||||
}
|
||||
|
||||
export interface PromptLaunchResult {
|
||||
readonly turn_id: number;
|
||||
}
|
||||
|
||||
export interface IAgentPromptService {
|
||||
readonly _serviceBrand: undefined;
|
||||
enqueue(input: PromptInput): Promise<PromptHandle>;
|
||||
submit(payload: PromptPayload): Promise<PromptLaunchResult | undefined>;
|
||||
submitSteer(payload: SteerPayload): Promise<PromptLaunchResult | undefined>;
|
||||
list(): PromptQueueSnapshot;
|
||||
steer(promptIds: readonly string[]): Promise<readonly PromptHandle[]>;
|
||||
abort(promptId: string, reason?: Error): boolean;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,14 @@
|
|||
* Assigns prompt and message identities, serializes user prompts through an
|
||||
* active slot and FIFO, converts selected pending prompts into active-turn
|
||||
* steers, settles lifecycle handles, and keeps system input outside the prompt
|
||||
* resource model. The pure-data `launching` flag is registered into
|
||||
* resource model. `submit` / `submitSteer` are the wire-facing user entry
|
||||
* points: they track `input_steer` through `telemetry`, persist the derived
|
||||
* title/lastPrompt through `sessionMetadata` for the main agent only
|
||||
* (publishing the live update through `event`), enqueue, and settle
|
||||
* `{turn_id}` from the launch handle. Session tool gating is an edge
|
||||
* concern: callers apply `IAgentToolPolicyService.setSessionDisabledTools`
|
||||
* before submitting, the way kap-server's prompt route composes it.
|
||||
* The pure-data `launching` flag is registered into
|
||||
* `agentState` (`IAgentStateService`) and read/written through it; the
|
||||
* `active` / `pending` / `steered` records stay plain fields because their
|
||||
* `Record` values carry Deferred promise handles (the container only holds
|
||||
|
|
@ -31,20 +38,31 @@ import type { ToolDidExecuteContext } from '#/agent/toolExecutor/toolHooks';
|
|||
import { IAgentToolExecutorService } from '#/agent/toolExecutor/toolExecutor';
|
||||
import type { ContentPart } from '#/kosong/contract/message';
|
||||
import { IEventBus } from '#/app/event/eventBus';
|
||||
import { ErrorCodes, Error2 } from '#/errors';
|
||||
import { IEventService } from '#/app/event/event';
|
||||
import { ErrorCodes, Error2, isError2 } from '#/errors';
|
||||
import { OrderedHookSlot } from '#/hooks';
|
||||
import { IWireService } from '#/wire/wire';
|
||||
import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext';
|
||||
import { ITelemetryService } from '#/app/telemetry/telemetry';
|
||||
import { MAIN_AGENT_ID } from '#/session/agentLifecycle/agentLifecycle';
|
||||
import { ISessionContext } from '#/session/sessionContext/sessionContext';
|
||||
import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';
|
||||
import { applyPromptMetadataUpdate } from '#/session/sessionMetadata/promptMetadata';
|
||||
|
||||
import {
|
||||
IAgentPromptService,
|
||||
type PromptCompletion,
|
||||
type PromptHandle,
|
||||
type PromptInput,
|
||||
type PromptLaunchResult,
|
||||
type PromptPayload,
|
||||
type PromptQueueSnapshot,
|
||||
type PromptSnapshot,
|
||||
type PromptState,
|
||||
type PromptSubmitContext,
|
||||
type SteerPayload,
|
||||
} from './prompt';
|
||||
import { promptMetadataTextFromContentParts } from './promptMetadataText';
|
||||
import { PromptStepRequest, RetryStepRequest, SteerStepRequest } from './promptStepRequests';
|
||||
|
||||
declare module '#/app/event/eventBus' {
|
||||
|
|
@ -83,6 +101,11 @@ export class AgentPromptService implements IAgentPromptService {
|
|||
@IWireService private readonly wire: IWireService,
|
||||
@IEventBus private readonly eventBus: IEventBus,
|
||||
@IAgentStateService private readonly states: IAgentStateService,
|
||||
@ITelemetryService private readonly telemetry: ITelemetryService,
|
||||
@ISessionMetadata private readonly metadata: ISessionMetadata,
|
||||
@IEventService private readonly eventService: IEventService,
|
||||
@ISessionContext private readonly sessionContext: ISessionContext,
|
||||
@IAgentScopeContext private readonly scopeContext: IAgentScopeContext,
|
||||
) {
|
||||
this.states.register(promptLaunchingKey);
|
||||
toolExecutor.hooks.onDidExecuteTool.register('prompt-service-delivery', async (ctx, next) => {
|
||||
|
|
@ -129,6 +152,64 @@ export class AgentPromptService implements IAgentPromptService {
|
|||
return record.handle;
|
||||
}
|
||||
|
||||
async submit(payload: PromptPayload): Promise<PromptLaunchResult | undefined> {
|
||||
await this.updatePromptMetadata(promptMetadataTextFromContentParts(payload.input));
|
||||
const handle = await this.enqueue({ message: {
|
||||
role: 'user',
|
||||
content: [...payload.input],
|
||||
toolCalls: [],
|
||||
origin: { kind: 'user' },
|
||||
} });
|
||||
if (handle.state === 'pending') return undefined;
|
||||
const turn = await handle.launched;
|
||||
return turn === undefined ? undefined : { turn_id: turn.id };
|
||||
}
|
||||
|
||||
async submitSteer(payload: SteerPayload): Promise<PromptLaunchResult | undefined> {
|
||||
this.telemetry.track2('input_steer', { parts: payload.input.length });
|
||||
// A steer is user input like a prompt — and can even launch the session's
|
||||
// first turn (e.g. goal mode) — so keep title/lastPrompt in sync the same
|
||||
// way, matching v1.
|
||||
await this.updatePromptMetadata(promptMetadataTextFromContentParts(payload.input));
|
||||
const queued = await this.enqueue({ message: {
|
||||
role: 'user',
|
||||
content: [...payload.input],
|
||||
toolCalls: [],
|
||||
} });
|
||||
if (queued.state !== 'pending') {
|
||||
// No active prompt at enqueue time, so the enqueue itself already
|
||||
// launched this input as its own turn (idle session, or a goal-turn
|
||||
// boundary where the previous turn just ended) — v1's
|
||||
// steer-degrades-to-launch end state. Return that turn instead of
|
||||
// rejecting on a steer-by-id that can never find the record pending.
|
||||
const turn = await queued.launched;
|
||||
return turn === undefined ? undefined : { turn_id: turn.id };
|
||||
}
|
||||
try {
|
||||
const [steered] = await this.steer([queued.id]);
|
||||
const turn = await steered?.launched;
|
||||
return turn === undefined ? undefined : { turn_id: turn.id };
|
||||
} catch (error) {
|
||||
// Pending but nothing active to steer into (a manual compaction holds
|
||||
// the context): the message stays queued and launches once compaction
|
||||
// finishes, so report it as queued rather than failing the steer.
|
||||
if (isError2(error) && error.code === ErrorCodes.PROMPT_NOT_FOUND) return undefined;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async updatePromptMetadata(text: string | undefined): Promise<void> {
|
||||
if (this.scopeContext.agentId !== MAIN_AGENT_ID) return;
|
||||
await applyPromptMetadataUpdate(
|
||||
{
|
||||
metadata: this.metadata,
|
||||
eventService: this.eventService,
|
||||
sessionId: this.sessionContext.sessionId,
|
||||
},
|
||||
text,
|
||||
);
|
||||
}
|
||||
|
||||
list(): PromptQueueSnapshot {
|
||||
return { active: this.active === undefined ? undefined : snapshot(this.active), pending: this.pending.map(snapshot) };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,26 @@ import type { PermissionApprovalResultRecord } from '#/agent/permissionRules/per
|
|||
import type { PermissionData, PermissionMode } from '#/agent/permissionPolicy/types';
|
||||
import type { PlanData } from '#/features/plan/plan';
|
||||
import type { ToolInfo } from '#/tool/toolContract';
|
||||
import type { SessionSummary } from '#/agent/rpc/core-api';
|
||||
import type { UsageStatus } from '#/agent/usage/usage';
|
||||
import type { SessionMeta } from '#/session/sessionMetadata/sessionMetadata';
|
||||
|
||||
export type JsonPrimitive = string | number | boolean | null;
|
||||
export type JsonValue = JsonPrimitive | JsonValue[] | { readonly [key: string]: JsonValue };
|
||||
export type JsonObject = { readonly [key: string]: JsonValue };
|
||||
|
||||
export interface SessionSummary {
|
||||
readonly id: string;
|
||||
readonly title?: string | undefined;
|
||||
readonly lastPrompt?: string;
|
||||
readonly workDir: string;
|
||||
readonly sessionDir: string;
|
||||
readonly createdAt: number;
|
||||
readonly updatedAt: number;
|
||||
readonly archived?: boolean | undefined;
|
||||
readonly metadata?: JsonObject | undefined;
|
||||
readonly additionalDirs?: readonly string[];
|
||||
}
|
||||
|
||||
type AgentType = 'main' | 'sub';
|
||||
|
||||
export type AgentReplayRecordPayload =
|
||||
|
|
|
|||
|
|
@ -1,357 +0,0 @@
|
|||
/**
|
||||
* `rpc` domain — v2 native RPC contract.
|
||||
*
|
||||
* Request/response payloads and event types for the engine's native RPC
|
||||
* surface. `PromptPayload.disabledTools` is the client-managed session
|
||||
* denylist, applied before the prompt is enqueued: full-replace semantics, the profile's own
|
||||
* `disallowedTools` always survive, omitting the field keeps the persisted
|
||||
* value, and `[]` clears the client portion. It is ignored by engines without
|
||||
* profile support.
|
||||
*/
|
||||
|
||||
import type { AgentContextData } from '#/agent/contextMemory/types';
|
||||
import type { AgentCommandInfo } from '#/agent/command/agentCommand';
|
||||
import type {
|
||||
GoalBudgetLimits,
|
||||
GoalBudgetReport,
|
||||
GoalChange,
|
||||
GoalChangeStats,
|
||||
GoalSnapshot,
|
||||
GoalStatus,
|
||||
GoalToolResult,
|
||||
} from '#/agent/goal/types';
|
||||
import type { PermissionMode } from '#/agent/permissionPolicy/types';
|
||||
import type { SwarmModeTrigger } from '#/agent/swarm/swarm';
|
||||
import type { ToolDisclosure, ToolInfo } from '#/tool/toolContract';
|
||||
import type { ResolvedConfig } from '#/app/config/config';
|
||||
import type { ExperimentalFeatureState } from '#/app/flag/flag';
|
||||
import type { ResumeSessionResult } from '#/agent/replayBuilder/types';
|
||||
import type { SessionMeta } from '#/session/sessionMetadata/sessionMetadata';
|
||||
import type { ContentPart } from '#/kosong/contract/message';
|
||||
import type { SessionWarning } from '#/app/sessionLegacy/sessionProtocol';
|
||||
|
||||
import type { ExportSessionPayload, ExportSessionResult } from '#/app/sessionExport/sessionExport';
|
||||
import type { PluginCommandDef, PluginInfo, PluginSummary, ReloadSummary } from '#/app/plugin/types';
|
||||
import type { WithAgentId, WithSessionId } from './types';
|
||||
|
||||
export type { ExportSessionManifest, ExportSessionPayload, ExportSessionResult, ShellEnvironment } from '#/app/sessionExport/sessionExport';
|
||||
|
||||
export type JsonPrimitive = string | number | boolean | null;
|
||||
export type JsonValue = JsonPrimitive | JsonValue[] | { readonly [key: string]: JsonValue };
|
||||
export type JsonObject = { readonly [key: string]: JsonValue };
|
||||
|
||||
export type Unsubscribe = () => void;
|
||||
|
||||
export type TextPromptPart = Extract<ContentPart, { type: 'text' }>;
|
||||
export type PromptPart = Extract<ContentPart, { type: 'text' | 'image_url' | 'video_url' }>;
|
||||
|
||||
export type PromptInput = readonly PromptPart[];
|
||||
|
||||
export type EmptyPayload = {};
|
||||
export type SessionMetadataPatch = Partial<Omit<SessionMeta, 'agents'>>;
|
||||
|
||||
export interface ClientTelemetryInfo {
|
||||
readonly id?: string | undefined;
|
||||
readonly name?: string | undefined;
|
||||
readonly version?: string | undefined;
|
||||
readonly uiMode?: string | undefined;
|
||||
}
|
||||
|
||||
export interface CreateSessionPayload {
|
||||
readonly id?: string | undefined;
|
||||
readonly workDir: string;
|
||||
readonly model?: string | undefined;
|
||||
readonly thinking?: string | undefined;
|
||||
readonly permission?: PermissionMode | undefined;
|
||||
readonly metadata?: JsonObject | undefined;
|
||||
readonly additionalDirs?: readonly string[];
|
||||
readonly client?: ClientTelemetryInfo | undefined;
|
||||
}
|
||||
|
||||
export interface CloseSessionPayload {
|
||||
readonly sessionId: string;
|
||||
}
|
||||
|
||||
export interface ArchiveSessionPayload {
|
||||
readonly sessionId: string;
|
||||
}
|
||||
|
||||
export interface ResumeSessionPayload {
|
||||
readonly sessionId: string;
|
||||
readonly additionalDirs?: readonly string[];
|
||||
}
|
||||
|
||||
export interface ReloadSessionPayload {
|
||||
readonly sessionId: string;
|
||||
readonly forcePluginSessionStartReminder?: boolean | undefined;
|
||||
}
|
||||
|
||||
export interface ForkSessionPayload {
|
||||
readonly sessionId: string;
|
||||
readonly id?: string;
|
||||
readonly title?: string;
|
||||
readonly metadata?: JsonObject;
|
||||
}
|
||||
|
||||
export interface ListSessionsPayload {
|
||||
readonly workDir?: string;
|
||||
readonly sessionId?: string;
|
||||
readonly includeArchive?: boolean;
|
||||
}
|
||||
|
||||
export interface CoreInfo {
|
||||
readonly version: string;
|
||||
}
|
||||
|
||||
export interface SessionSummary {
|
||||
readonly id: string;
|
||||
readonly title?: string | undefined;
|
||||
readonly lastPrompt?: string;
|
||||
readonly workDir: string;
|
||||
readonly sessionDir: string;
|
||||
readonly createdAt: number;
|
||||
readonly updatedAt: number;
|
||||
readonly archived?: boolean | undefined;
|
||||
readonly metadata?: JsonObject | undefined;
|
||||
readonly additionalDirs?: readonly string[];
|
||||
}
|
||||
|
||||
export interface PromptPayload {
|
||||
readonly input: readonly ContentPart[];
|
||||
readonly disabledTools?: readonly string[];
|
||||
}
|
||||
export interface RunShellCommandPayload {
|
||||
readonly command: string;
|
||||
readonly commandId?: string;
|
||||
}
|
||||
export interface ShellCommandResult {
|
||||
readonly stdout: string;
|
||||
readonly stderr: string;
|
||||
readonly isError?: boolean;
|
||||
readonly backgrounded?: boolean;
|
||||
}
|
||||
export interface CancelShellCommandPayload {
|
||||
readonly commandId: string;
|
||||
}
|
||||
export interface SteerPayload {
|
||||
readonly input: readonly ContentPart[];
|
||||
}
|
||||
export interface CancelPayload {
|
||||
readonly turnId?: number;
|
||||
}
|
||||
export interface SetThinkingPayload {
|
||||
readonly level: string;
|
||||
}
|
||||
export interface SetPermissionPayload {
|
||||
readonly mode: PermissionMode;
|
||||
}
|
||||
export interface SetModelPayload {
|
||||
readonly model: string;
|
||||
}
|
||||
export interface SetModelResult {
|
||||
readonly model: string;
|
||||
readonly providerName?: string | undefined;
|
||||
}
|
||||
export interface CancelPlanPayload {
|
||||
readonly id?: string;
|
||||
}
|
||||
export interface EnterSwarmPayload {
|
||||
readonly trigger: SwarmModeTrigger;
|
||||
}
|
||||
export interface BeginCompactionPayload {
|
||||
readonly instruction?: string;
|
||||
}
|
||||
export interface UndoHistoryPayload {
|
||||
readonly count: number;
|
||||
}
|
||||
export interface RegisterToolPayload {
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly parameters: Record<string, unknown>;
|
||||
readonly disclosure?: ToolDisclosure;
|
||||
}
|
||||
export interface UnregisterToolPayload {
|
||||
readonly name: string;
|
||||
}
|
||||
export interface SetActiveToolsPayload {
|
||||
readonly names: readonly string[];
|
||||
}
|
||||
export interface StopTaskPayload {
|
||||
readonly taskId: string;
|
||||
readonly reason?: string;
|
||||
}
|
||||
export interface DetachTaskPayload {
|
||||
readonly taskId: string;
|
||||
}
|
||||
export interface GetTaskOutputPayload {
|
||||
readonly taskId: string;
|
||||
readonly tail?: number;
|
||||
}
|
||||
export interface GetTasksPayload {
|
||||
readonly activeOnly?: boolean;
|
||||
readonly limit?: number;
|
||||
}
|
||||
export interface SkillSummary {
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly path: string;
|
||||
readonly source: 'builtin' | 'user' | 'extra' | 'project';
|
||||
readonly type?: string | undefined;
|
||||
readonly disableModelInvocation?: boolean | undefined;
|
||||
readonly isSubSkill?: boolean | undefined;
|
||||
}
|
||||
|
||||
export interface ActivateSkillPayload {
|
||||
readonly name: string;
|
||||
readonly args?: string | undefined;
|
||||
}
|
||||
|
||||
export interface ActivatePluginCommandPayload {
|
||||
readonly pluginId: string;
|
||||
readonly commandName: string;
|
||||
readonly args?: string | undefined;
|
||||
}
|
||||
|
||||
export interface RunCommandPayload {
|
||||
readonly name: string;
|
||||
readonly args?: string | undefined;
|
||||
}
|
||||
|
||||
export interface McpServerInfo {
|
||||
readonly name: string;
|
||||
readonly transport: 'stdio' | 'http' | 'sse';
|
||||
readonly status: 'pending' | 'connected' | 'failed' | 'disabled' | 'needs-auth' | 'removed';
|
||||
readonly toolCount: number;
|
||||
readonly error?: string;
|
||||
}
|
||||
|
||||
export interface McpStartupMetrics {
|
||||
readonly durationMs: number;
|
||||
}
|
||||
|
||||
export interface ReconnectMcpServerPayload {
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
export interface InstallPluginPayload {
|
||||
readonly source: string;
|
||||
}
|
||||
|
||||
export interface SetPluginEnabledPayload {
|
||||
readonly id: string;
|
||||
readonly enabled: boolean;
|
||||
}
|
||||
|
||||
export interface SetPluginMcpServerEnabledPayload {
|
||||
readonly id: string;
|
||||
readonly server: string;
|
||||
readonly enabled: boolean;
|
||||
}
|
||||
|
||||
export interface RemovePluginPayload {
|
||||
readonly id: string;
|
||||
}
|
||||
|
||||
export interface GetPluginInfoPayload {
|
||||
readonly id: string;
|
||||
}
|
||||
|
||||
export type ReloadPluginsResult = ReloadSummary;
|
||||
export type { PluginSummary, PluginInfo };
|
||||
|
||||
export interface RenameSessionPayload {
|
||||
readonly title: string;
|
||||
}
|
||||
|
||||
export interface UpdateSessionMetadataPayload {
|
||||
readonly metadata: SessionMetadataPatch;
|
||||
}
|
||||
|
||||
export type {
|
||||
GoalBudgetLimits,
|
||||
GoalBudgetReport,
|
||||
GoalChange,
|
||||
GoalChangeStats,
|
||||
GoalSnapshot,
|
||||
GoalStatus,
|
||||
GoalToolResult,
|
||||
};
|
||||
|
||||
export interface CreateGoalPayload {
|
||||
readonly objective: string;
|
||||
readonly replace?: boolean;
|
||||
}
|
||||
|
||||
export interface GetKimiConfigPayload {
|
||||
readonly reload?: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigDiagnostics {
|
||||
readonly warnings: readonly string[];
|
||||
}
|
||||
|
||||
export type SetKimiConfigPayload = ResolvedConfig;
|
||||
|
||||
export interface RemoveKimiProviderPayload {
|
||||
readonly providerId: string;
|
||||
}
|
||||
|
||||
export interface PromptLaunchResult {
|
||||
readonly turn_id: number;
|
||||
}
|
||||
|
||||
export interface AgentAPI {
|
||||
prompt: (payload: PromptPayload) => PromptLaunchResult | undefined;
|
||||
steer: (payload: SteerPayload) => PromptLaunchResult | undefined;
|
||||
cancel: (payload: CancelPayload) => void;
|
||||
undoHistory: (payload: UndoHistoryPayload) => Promise<number>;
|
||||
setPermission: (payload: SetPermissionPayload) => void;
|
||||
cancelCompaction: (payload: EmptyPayload) => void;
|
||||
activateSkill: (payload: ActivateSkillPayload) => PromptLaunchResult | undefined;
|
||||
activatePluginCommand: (payload: ActivatePluginCommandPayload) => void;
|
||||
listCommands: (payload: EmptyPayload) => readonly AgentCommandInfo[];
|
||||
runCommand: (payload: RunCommandPayload) => Promise<void>;
|
||||
getContext: (payload: EmptyPayload) => AgentContextData;
|
||||
getTools: (payload: EmptyPayload) => readonly ToolInfo[];
|
||||
}
|
||||
|
||||
type AgentAPIWithId = WithAgentId<AgentAPI>;
|
||||
|
||||
export interface SessionAPI extends AgentAPIWithId {
|
||||
renameSession: (payload: RenameSessionPayload) => void;
|
||||
updateSessionMetadata: (payload: UpdateSessionMetadataPayload) => void;
|
||||
getSessionMetadata: (payload: EmptyPayload) => SessionMeta;
|
||||
listSkills: (payload: EmptyPayload) => readonly SkillSummary[];
|
||||
listPluginCommands: (payload: EmptyPayload) => readonly PluginCommandDef[];
|
||||
listMcpServers: (payload: EmptyPayload) => readonly McpServerInfo[];
|
||||
getMcpStartupMetrics: (payload: EmptyPayload) => McpStartupMetrics;
|
||||
reconnectMcpServer: (payload: ReconnectMcpServerPayload) => void;
|
||||
generateAgentsMd: (payload: EmptyPayload) => void;
|
||||
getSessionWarnings: (payload: EmptyPayload) => readonly SessionWarning[];
|
||||
}
|
||||
|
||||
type SessionAPIWithId = WithSessionId<SessionAPI>;
|
||||
|
||||
export interface CoreAPI extends SessionAPIWithId {
|
||||
getCoreInfo: (payload: EmptyPayload) => CoreInfo;
|
||||
getExperimentalFeatures: (payload: EmptyPayload) => readonly ExperimentalFeatureState[];
|
||||
getKimiConfig: (payload: GetKimiConfigPayload) => ResolvedConfig;
|
||||
getConfigDiagnostics: (payload: EmptyPayload) => ConfigDiagnostics;
|
||||
setKimiConfig: (payload: SetKimiConfigPayload) => ResolvedConfig;
|
||||
removeKimiProvider: (payload: RemoveKimiProviderPayload) => ResolvedConfig;
|
||||
createSession: (payload: CreateSessionPayload) => SessionSummary;
|
||||
closeSession: (payload: CloseSessionPayload) => void;
|
||||
archiveSession: (payload: ArchiveSessionPayload) => void;
|
||||
resumeSession: (payload: ResumeSessionPayload) => ResumeSessionResult;
|
||||
reloadSession: (payload: ReloadSessionPayload) => ResumeSessionResult;
|
||||
forkSession: (payload: ForkSessionPayload) => ResumeSessionResult;
|
||||
listSessions: (payload: ListSessionsPayload) => readonly SessionSummary[];
|
||||
exportSession: (payload: ExportSessionPayload) => ExportSessionResult;
|
||||
listPlugins: (payload: EmptyPayload) => readonly PluginSummary[];
|
||||
installPlugin: (payload: InstallPluginPayload) => PluginSummary;
|
||||
setPluginEnabled: (payload: SetPluginEnabledPayload) => void;
|
||||
setPluginMcpServerEnabled: (payload: SetPluginMcpServerEnabledPayload) => void;
|
||||
removePlugin: (payload: RemovePluginPayload) => void;
|
||||
reloadPlugins: (payload: EmptyPayload) => ReloadPluginsResult;
|
||||
getPluginInfo: (payload: GetPluginInfoPayload) => PluginInfo;
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
/**
|
||||
* `rpc` domain (Agent) — v1-compatible prompt metadata helpers.
|
||||
*
|
||||
* Derives title and last-prompt text from native and legacy prompt payloads,
|
||||
* persists metadata through `sessionMetadata`, and publishes live updates
|
||||
* through `event`.
|
||||
*/
|
||||
|
||||
import type { IEventService } from '#/app/event/event';
|
||||
import type { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';
|
||||
|
||||
import {
|
||||
promptMetadataTextFromContentParts,
|
||||
promptMetadataTextFromText,
|
||||
titleFromPromptMetadataText,
|
||||
} from '#/agent/prompt/promptMetadataText';
|
||||
|
||||
import type {
|
||||
ActivatePluginCommandPayload,
|
||||
ActivateSkillPayload,
|
||||
PromptPayload,
|
||||
} from './core-api';
|
||||
|
||||
export { promptMetadataTextFromContentParts, titleFromPromptMetadataText };
|
||||
|
||||
export function promptMetadataTextFromPayload(payload: PromptPayload): string | undefined {
|
||||
return promptMetadataTextFromContentParts(payload.input);
|
||||
}
|
||||
|
||||
export function promptMetadataTextFromSkill(payload: ActivateSkillPayload): string | undefined {
|
||||
const args = payload.args?.trim();
|
||||
return promptMetadataTextFromText(
|
||||
args === undefined || args.length === 0 ? `/${payload.name}` : `/${payload.name} ${args}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function promptMetadataTextFromPluginCommand(
|
||||
payload: ActivatePluginCommandPayload,
|
||||
): string | undefined {
|
||||
const args = payload.args?.trim();
|
||||
const command = `/${payload.pluginId}:${payload.commandName}`;
|
||||
return promptMetadataTextFromText(
|
||||
args === undefined || args.length === 0 ? command : `${command} ${args}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function isUntitled(title: string | undefined): boolean {
|
||||
return title === undefined || title.trim().length === 0 || title === 'New Session';
|
||||
}
|
||||
|
||||
export interface PromptMetadataUpdateTarget {
|
||||
readonly metadata: ISessionMetadata;
|
||||
readonly eventService: IEventService;
|
||||
readonly sessionId: string;
|
||||
}
|
||||
|
||||
export async function applyPromptMetadataUpdate(
|
||||
target: PromptMetadataUpdateTarget,
|
||||
text: string | undefined,
|
||||
): Promise<void> {
|
||||
if (text === undefined) return;
|
||||
const current = await target.metadata.read();
|
||||
const patch: { lastPrompt: string; title?: string; isCustomTitle?: boolean } = {
|
||||
lastPrompt: text,
|
||||
};
|
||||
if (!current.isCustomTitle && isUntitled(current.title)) {
|
||||
patch.title = titleFromPromptMetadataText(text);
|
||||
patch.isCustomTitle = false;
|
||||
}
|
||||
await target.metadata.update(patch);
|
||||
target.eventService.publish({
|
||||
type: 'session.meta.updated',
|
||||
payload: {
|
||||
agentId: 'main',
|
||||
sessionId: target.sessionId,
|
||||
title: patch.title,
|
||||
patch: {
|
||||
title: patch.title,
|
||||
isCustomTitle: patch.isCustomTitle,
|
||||
lastPrompt: text,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { createDecorator } from "#/_base/di/instantiation";
|
||||
import type {
|
||||
AgentAPI,
|
||||
SessionAPI,
|
||||
} from './core-api';
|
||||
import type { PromisableMethods } from "#/_base/utils/types";
|
||||
|
||||
export interface IAgentRPCService extends PromisableMethods<AgentAPI> {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export interface ISessionRPCService extends PromisableMethods<SessionAPI> {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export const IAgentRPCService =
|
||||
createDecorator<IAgentRPCService>('agentRPCService');
|
||||
|
||||
export const ISessionRPCService =
|
||||
createDecorator<ISessionRPCService>('agentSessionRPCService');
|
||||
|
|
@ -1,281 +0,0 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
import { LifecycleScope } from '#/app/scopes';
|
||||
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
|
||||
import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory';
|
||||
import { IAgentTokenCountingService } from '#/agent/tokenCounting/tokenCounting';
|
||||
import { IAgentFullCompactionService } from '#/agent/fullCompaction/fullCompaction';
|
||||
import { IEventBus } from '#/app/event/eventBus';
|
||||
import { IEventService } from '#/app/event/event';
|
||||
import { ErrorCodes, Error2, isError2 } from '#/errors';
|
||||
import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode';
|
||||
import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext';
|
||||
import {
|
||||
IAgentLifecycleService,
|
||||
MAIN_AGENT_ID,
|
||||
} from '#/session/agentLifecycle/agentLifecycle';
|
||||
import { IAgentCommandService } from '#/agent/command/agentCommand';
|
||||
import { expandCommandArguments } from '#/app/plugin/commands';
|
||||
import { IPluginService } from '#/app/plugin/plugin';
|
||||
import { ProfileError } from '#/agent/profile/profile';
|
||||
import { IAgentToolPolicyService } from '#/agent/toolPolicy/toolPolicy';
|
||||
import { IAgentPromptService } from '#/agent/prompt/prompt';
|
||||
import { IAgentConversationUndoService } from '#/agent/undo/undo';
|
||||
import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';
|
||||
import { ISessionContext } from '#/session/sessionContext/sessionContext';
|
||||
import { IAgentSkillService } from '#/agent/skill/skill';
|
||||
import { ITelemetryService } from '#/app/telemetry/telemetry';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry';
|
||||
import { IAgentLoopService } from '#/agent/loop/loop';
|
||||
import type {
|
||||
ActivatePluginCommandPayload,
|
||||
ActivateSkillPayload,
|
||||
CancelPayload,
|
||||
EmptyPayload,
|
||||
PromptLaunchResult,
|
||||
PromptPayload,
|
||||
RunCommandPayload,
|
||||
SetPermissionPayload,
|
||||
SteerPayload,
|
||||
UndoHistoryPayload,
|
||||
} from './core-api';
|
||||
import { IAgentRPCService } from './rpc';
|
||||
import {
|
||||
applyPromptMetadataUpdate,
|
||||
promptMetadataTextFromPayload,
|
||||
promptMetadataTextFromPluginCommand,
|
||||
promptMetadataTextFromSkill,
|
||||
} from './prompt-metadata';
|
||||
|
||||
export interface PluginCommandActivatedEvent {
|
||||
readonly type: 'plugin_command.activated';
|
||||
readonly activationId: string;
|
||||
readonly pluginId: string;
|
||||
readonly commandName: string;
|
||||
readonly commandArgs?: string;
|
||||
readonly trigger: 'user-slash';
|
||||
}
|
||||
|
||||
declare module '#/app/event/eventBus' {
|
||||
interface DomainEventMap {
|
||||
'plugin_command.activated': PluginCommandActivatedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export class AgentRPCService implements IAgentRPCService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IAgentPromptService private readonly promptService: IAgentPromptService,
|
||||
@IAgentConversationUndoService
|
||||
private readonly conversationUndo: IAgentConversationUndoService,
|
||||
@IAgentLoopService private readonly loop: IAgentLoopService,
|
||||
@IAgentToolPolicyService private readonly toolPolicy: IAgentToolPolicyService,
|
||||
@IAgentPermissionModeService private readonly permissionMode: IAgentPermissionModeService,
|
||||
@IAgentFullCompactionService private readonly fullCompaction: IAgentFullCompactionService,
|
||||
@IAgentToolRegistryService private readonly toolRegistry: IAgentToolRegistryService,
|
||||
@IAgentContextMemoryService private readonly context: IAgentContextMemoryService,
|
||||
@IAgentTokenCountingService private readonly tokenCounting: IAgentTokenCountingService,
|
||||
@IAgentSkillService private readonly skills: IAgentSkillService,
|
||||
@ITelemetryService private readonly telemetry: ITelemetryService,
|
||||
@IEventBus private readonly eventBus: IEventBus,
|
||||
@IEventService private readonly eventService: IEventService,
|
||||
@IPluginService private readonly plugins: IPluginService,
|
||||
@ISessionMetadata private readonly metadata: ISessionMetadata,
|
||||
@ISessionContext private readonly sessionContext: ISessionContext,
|
||||
@IAgentScopeContext private readonly scopeContext: IAgentScopeContext,
|
||||
@IAgentLifecycleService private readonly agentLifecycle: IAgentLifecycleService,
|
||||
@IAgentCommandService private readonly commands: IAgentCommandService,
|
||||
) { }
|
||||
|
||||
async prompt(payload: PromptPayload): Promise<PromptLaunchResult | undefined> {
|
||||
if (payload.disabledTools !== undefined) {
|
||||
try {
|
||||
await this.toolPolicy.setSessionDisabledTools(payload.disabledTools);
|
||||
} catch (error) {
|
||||
if (error instanceof ProfileError) {
|
||||
throw new Error2(ErrorCodes.REQUEST_INVALID, error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await this.updatePromptMetadata(promptMetadataTextFromPayload(payload));
|
||||
const handle = await this.promptService.enqueue({ message: {
|
||||
role: 'user',
|
||||
content: [...payload.input],
|
||||
toolCalls: [],
|
||||
origin: { kind: 'user' },
|
||||
} });
|
||||
if (handle.state === 'pending') return undefined;
|
||||
const turn = await handle.launched;
|
||||
return turn === undefined ? undefined : { turn_id: turn.id };
|
||||
}
|
||||
|
||||
async steer(payload: SteerPayload): Promise<PromptLaunchResult | undefined> {
|
||||
this.telemetry.track2('input_steer', { parts: payload.input.length });
|
||||
if (this.scopeContext.agentId === MAIN_AGENT_ID) {
|
||||
// A steer is user input like a prompt — and can even launch the
|
||||
// session's first turn (e.g. goal mode) — so keep title/lastPrompt in
|
||||
// sync the same way, matching v1.
|
||||
await this.updatePromptMetadata(promptMetadataTextFromPayload(payload));
|
||||
}
|
||||
const queued = await this.promptService.enqueue({ message: {
|
||||
role: 'user',
|
||||
content: [...payload.input],
|
||||
toolCalls: [],
|
||||
} });
|
||||
if (queued.state !== 'pending') {
|
||||
// No active prompt at enqueue time, so the enqueue itself already
|
||||
// launched this input as its own turn (idle session, or a goal-turn
|
||||
// boundary where the previous turn just ended) — v1's
|
||||
// steer-degrades-to-launch end state. Return that turn instead of
|
||||
// rejecting on a steer-by-id that can never find the record pending.
|
||||
const turn = await queued.launched;
|
||||
return turn === undefined ? undefined : { turn_id: turn.id };
|
||||
}
|
||||
try {
|
||||
const [steered] = await this.promptService.steer([queued.id]);
|
||||
const turn = await steered?.launched;
|
||||
return turn === undefined ? undefined : { turn_id: turn.id };
|
||||
} catch (error) {
|
||||
// Pending but nothing active to steer into (a manual compaction holds
|
||||
// the context): the message stays queued and launches once compaction
|
||||
// finishes, so report it as queued rather than failing the steer.
|
||||
if (isError2(error) && error.code === ErrorCodes.PROMPT_NOT_FOUND) return undefined;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
cancel({ turnId }: CancelPayload): void {
|
||||
if (this.loop.status().state === 'running') {
|
||||
this.telemetry.track2('cancel', {
|
||||
from: 'streaming',
|
||||
trace_id: this.loop.status().activeTraceId,
|
||||
});
|
||||
}
|
||||
this.loop.cancel(turnId);
|
||||
}
|
||||
|
||||
async undoHistory(payload: UndoHistoryPayload): Promise<number> {
|
||||
return this.conversationUndo.undo(payload.count);
|
||||
}
|
||||
|
||||
setPermission(payload: SetPermissionPayload): void {
|
||||
const wasYolo = this.permissionMode.mode === 'yolo';
|
||||
const wasAuto = this.permissionMode.mode === 'auto';
|
||||
this.permissionMode.setMode(payload.mode);
|
||||
if (this.scopeContext.agentId === MAIN_AGENT_ID) {
|
||||
this.agentLifecycle.broadcastPermissionMode(payload.mode);
|
||||
}
|
||||
const enabled = this.permissionMode.mode === 'yolo';
|
||||
if (enabled !== wasYolo) {
|
||||
this.telemetry.track2('yolo_toggle', { enabled });
|
||||
}
|
||||
const afkEnabled = this.permissionMode.mode === 'auto';
|
||||
if (afkEnabled !== wasAuto) {
|
||||
this.telemetry.track2('afk_toggle', { enabled: afkEnabled });
|
||||
}
|
||||
}
|
||||
|
||||
cancelCompaction(_payload: EmptyPayload): void {
|
||||
const active = this.fullCompaction.compacting;
|
||||
if (active !== null) {
|
||||
this.telemetry.track2('cancel', {
|
||||
from: 'compacting',
|
||||
trace_id: active.traceId,
|
||||
});
|
||||
}
|
||||
active?.abortController.abort();
|
||||
}
|
||||
|
||||
async activateSkill(payload: ActivateSkillPayload): Promise<PromptLaunchResult | undefined> {
|
||||
// Awaited (not fire-and-forget): the caller gets the launched turn id and
|
||||
// activation failures (unknown skill, busy) surface instead of vanishing.
|
||||
const turn = await this.skills.activate(payload);
|
||||
await this.updatePromptMetadata(promptMetadataTextFromSkill(payload));
|
||||
return { turn_id: turn.id };
|
||||
}
|
||||
|
||||
async activatePluginCommand(payload: ActivatePluginCommandPayload): Promise<void> {
|
||||
const commands = await this.plugins.listPluginCommands();
|
||||
const def = commands.find(
|
||||
(command) => command.pluginId === payload.pluginId && command.name === payload.commandName,
|
||||
);
|
||||
if (def === undefined) {
|
||||
throw new Error2(
|
||||
ErrorCodes.REQUEST_INVALID,
|
||||
`Plugin command "${payload.pluginId}:${payload.commandName}" was not found`,
|
||||
);
|
||||
}
|
||||
const commandArgs = payload.args ?? '';
|
||||
const expanded = expandCommandArguments(def.body, commandArgs);
|
||||
const origin = {
|
||||
kind: 'plugin_command' as const,
|
||||
activationId: randomUUID(),
|
||||
pluginId: payload.pluginId,
|
||||
commandName: payload.commandName,
|
||||
commandArgs: payload.args,
|
||||
trigger: 'user-slash' as const,
|
||||
};
|
||||
this.eventBus.publish({
|
||||
type: 'plugin_command.activated',
|
||||
activationId: origin.activationId,
|
||||
pluginId: origin.pluginId,
|
||||
commandName: origin.commandName,
|
||||
commandArgs: origin.commandArgs,
|
||||
trigger: origin.trigger,
|
||||
});
|
||||
await this.promptService.enqueue({ message: {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: expanded }],
|
||||
toolCalls: [],
|
||||
origin,
|
||||
} });
|
||||
await this.updatePromptMetadata(promptMetadataTextFromPluginCommand(payload));
|
||||
}
|
||||
|
||||
private async updatePromptMetadata(text: string | undefined): Promise<void> {
|
||||
await applyPromptMetadataUpdate(
|
||||
{
|
||||
metadata: this.metadata,
|
||||
eventService: this.eventService,
|
||||
sessionId: this.sessionContext.sessionId,
|
||||
},
|
||||
text,
|
||||
);
|
||||
}
|
||||
|
||||
getContext(_payload: EmptyPayload) {
|
||||
return {
|
||||
history: this.context.get(),
|
||||
// The externally reported context size, resolved by the
|
||||
// `[token_counting]` strategy inside the service — matching the v1
|
||||
// `context.tokenCount` semantics.
|
||||
tokenCount: this.tokenCounting.statusSize(),
|
||||
};
|
||||
}
|
||||
|
||||
listCommands(_payload: EmptyPayload) {
|
||||
return this.commands.list();
|
||||
}
|
||||
|
||||
async runCommand(payload: RunCommandPayload): Promise<void> {
|
||||
return this.commands.run(payload.name, payload.args);
|
||||
}
|
||||
|
||||
getTools(_payload: EmptyPayload) {
|
||||
return this.toolRegistry.list().map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
active: this.toolPolicy.isToolActive(tool.name, tool.source),
|
||||
source: tool.source,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
registerScopedService(
|
||||
LifecycleScope.Agent,
|
||||
IAgentRPCService,
|
||||
AgentRPCService,
|
||||
ScopeActivation.OnScopeCreated,
|
||||
'rpc',
|
||||
);
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
/**
|
||||
* `rpc` domain (L8) — shared request wrapper types.
|
||||
*/
|
||||
|
||||
export type WithSessionId<T = {}> = T & {
|
||||
readonly sessionId: string;
|
||||
};
|
||||
|
||||
export type WithAgentId<T = {}> = T & {
|
||||
readonly agentId: string;
|
||||
};
|
||||
|
|
@ -1,6 +1,16 @@
|
|||
import { escapeXml } from '#/_base/utils/xml-escape';
|
||||
import { promptMetadataTextFromText } from '#/agent/prompt/promptMetadataText';
|
||||
import type { SkillSource } from '#/app/skillCatalog/types';
|
||||
|
||||
import type { SkillActivationInput } from './skill';
|
||||
|
||||
export function promptMetadataTextFromSkill(input: SkillActivationInput): string | undefined {
|
||||
const args = input.args?.trim();
|
||||
return promptMetadataTextFromText(
|
||||
args === undefined || args.length === 0 ? `/${input.name}` : `/${input.name} ${args}`,
|
||||
);
|
||||
}
|
||||
|
||||
export type SkillPromptTrigger = 'user-slash' | 'model-tool' | 'nested-skill';
|
||||
|
||||
export interface RenderSkillPromptInput {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
import { createDecorator } from "#/_base/di/instantiation";
|
||||
import type { SkillActivationOrigin } from '#/agent/contextMemory/types';
|
||||
import type { Turn } from '#/agent/loop/loop';
|
||||
import type { PromptLaunchResult } from '#/agent/prompt/prompt';
|
||||
import type { ContentPart } from '#/kosong/contract/message';
|
||||
|
||||
export interface SkillActivationInput {
|
||||
|
|
@ -22,7 +22,7 @@ export interface SkillActivationInput {
|
|||
export interface IAgentSkillService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
activate(input: SkillActivationInput): Promise<Turn>;
|
||||
activate(input: SkillActivationInput): Promise<PromptLaunchResult>;
|
||||
recordModelToolActivation(origin: SkillActivationOrigin): void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@
|
|||
* (a stateless, identity-apply Op), derives the `skill.activated` event
|
||||
* through the Op's `toEvent`, drives user-slash activations into a new turn via
|
||||
* `prompt` (attachment parts from the caller ride the same user message after
|
||||
* the rendered prompt), and reports `skill_invoked` / `flow_invoked` through
|
||||
* `telemetry`. `wire.replay` reapplies the fact as a no-op, so neither the
|
||||
* event nor telemetry fires on resume (matching the former `restoring` guard).
|
||||
* Bound at Agent scope.
|
||||
* the rendered prompt), settles `{turn_id}` for the caller, persists the
|
||||
* derived title/lastPrompt through `sessionMetadata` for the main agent only
|
||||
* (publishing the live update through `event`), and reports `skill_invoked` /
|
||||
* `flow_invoked` through `telemetry`. `wire.replay` reapplies the fact as a
|
||||
* no-op, so neither the event nor telemetry fires on resume (matching the
|
||||
* former `restoring` guard). Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
|
@ -19,18 +21,23 @@ import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
|
|||
import type { ContentPart } from '#/kosong/contract/message';
|
||||
|
||||
import type { ContextMessage, SkillActivationOrigin } from '#/agent/contextMemory/types';
|
||||
import { renderUserSlashSkillPrompt } from './prompt';
|
||||
import { promptMetadataTextFromSkill, renderUserSlashSkillPrompt } from './prompt';
|
||||
import { ISessionContext } from '#/session/sessionContext/sessionContext';
|
||||
import { Service } from '#/_base/di/service';
|
||||
import { ErrorCodes, Error2 } from '#/errors';
|
||||
import { isUserActivatableSkillType, type SkillDefinition } from '#/app/skillCatalog/types';
|
||||
import { IAgentPromptService } from '#/agent/prompt/prompt';
|
||||
import { IAgentPromptService, type PromptLaunchResult } from '#/agent/prompt/prompt';
|
||||
import { ITelemetryService } from '#/app/telemetry/telemetry';
|
||||
import type { Turn } from '#/agent/loop/loop';
|
||||
import { IWireService } from '#/wire/wire';
|
||||
import { IAgentSkillService, type SkillActivationInput } from './skill';
|
||||
import { skillActivate } from './skillOps';
|
||||
import { ISessionSkillCatalog } from '#/session/sessionSkillCatalog/skillCatalog';
|
||||
import { IEventService } from '#/app/event/event';
|
||||
import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext';
|
||||
import { MAIN_AGENT_ID } from '#/session/agentLifecycle/agentLifecycle';
|
||||
import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';
|
||||
import { applyPromptMetadataUpdate } from '#/session/sessionMetadata/promptMetadata';
|
||||
|
||||
export class AgentSkillService extends Service implements IAgentSkillService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
|
@ -41,11 +48,14 @@ export class AgentSkillService extends Service implements IAgentSkillService {
|
|||
@IWireService private readonly wire: IWireService,
|
||||
@ITelemetryService private readonly telemetry: ITelemetryService,
|
||||
@ISessionContext private readonly sessionContext: ISessionContext,
|
||||
@ISessionMetadata private readonly metadata: ISessionMetadata,
|
||||
@IEventService private readonly eventService: IEventService,
|
||||
@IAgentScopeContext private readonly scopeContext: IAgentScopeContext,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async activate(input: SkillActivationInput): Promise<Turn> {
|
||||
async activate(input: SkillActivationInput): Promise<PromptLaunchResult> {
|
||||
await this.skillCatalog.ready;
|
||||
const skill = this.skillCatalog.catalog.getSkill(input.name);
|
||||
if (skill === undefined) {
|
||||
|
|
@ -93,7 +103,19 @@ export class AgentSkillService extends Service implements IAgentSkillService {
|
|||
'Cannot activate skill while another turn is active',
|
||||
);
|
||||
}
|
||||
return turn;
|
||||
// Awaited (not fire-and-forget): the caller gets the launched turn id and
|
||||
// activation failures (unknown skill, busy) surface instead of vanishing.
|
||||
if (this.scopeContext.agentId === MAIN_AGENT_ID) {
|
||||
await applyPromptMetadataUpdate(
|
||||
{
|
||||
metadata: this.metadata,
|
||||
eventService: this.eventService,
|
||||
sessionId: this.sessionContext.sessionId,
|
||||
},
|
||||
promptMetadataTextFromSkill(input),
|
||||
);
|
||||
}
|
||||
return { turn_id: turn.id };
|
||||
}
|
||||
|
||||
recordModelToolActivation(origin: SkillActivationOrigin): void {
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ export * from '#/app/sessionIndex/sessionIndexService';
|
|||
export * from '#/app/sessionIndex/sessionIndexMirrorService';
|
||||
export * from '#/session/sessionMetadata/sessionMetadata';
|
||||
export * from '#/session/sessionMetadata/sessionMetadataService';
|
||||
export * from '#/session/sessionMetadata/promptMetadata';
|
||||
export * from '#/session/sessionActivity/sessionActivity';
|
||||
export * from '#/session/sessionActivity/sessionActivityService';
|
||||
export * from '#/session/sessionActivity/sessionOutcomeMirror';
|
||||
|
|
@ -618,19 +619,23 @@ import '#/agent/permissionRules/configSection';
|
|||
export * from '#/agent/permissionRules/permissionRules';
|
||||
export * from '#/agent/permissionRules/matchesRule';
|
||||
export * from '#/agent/permissionRules/permissionRulesService';
|
||||
export * from '#/agent/pluginCommand/pluginCommand';
|
||||
export * from '#/agent/pluginCommand/pluginCommandService';
|
||||
export * from '#/agent/profile/profile';
|
||||
export * from '#/agent/profile/profileService';
|
||||
export * from '#/agent/profile/context';
|
||||
export * from '#/agent/prompt/prompt';
|
||||
export * from '#/agent/prompt/promptService';
|
||||
export * from '#/agent/prompt/promptMetadataText';
|
||||
export * from '#/agent/replayBuilder/types';
|
||||
// `replayBuilder/types` inlines its own `SessionSummary`; keep the barrel's
|
||||
// `SessionSummary` pinned to the session-index one (explicit re-export wins
|
||||
// over the ambiguous `export *` pair).
|
||||
export { type SessionSummary } from '#/app/sessionIndex/sessionIndex';
|
||||
export * from '#/agent/undo/undo';
|
||||
export * from '#/agent/undo/undoService';
|
||||
export * from '#/agent/shellCommand/shellCommand';
|
||||
export * from '#/agent/shellCommand/shellCommandService';
|
||||
export * from '#/agent/rpc/rpc';
|
||||
export * from '#/agent/rpc/rpcService';
|
||||
export * from '#/agent/rpc/prompt-metadata';
|
||||
export * from '#/agent/scopeContext/scopeContext';
|
||||
export * from '#/agent/stepRetry/stepRetry';
|
||||
export * from '#/agent/stepRetry/stepRetryService';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* `sessionMetadata` domain — prompt-derived title / lastPrompt updates.
|
||||
*
|
||||
* Applies the metadata text derived from a prompt-like entry (prompt, steer,
|
||||
* skill or plugin-command activation) to the session's durable metadata:
|
||||
* `lastPrompt` always follows the latest text, while `title` is only derived
|
||||
* for an untitled session without a custom title. Persists through
|
||||
* `sessionMetadata` and publishes the live `session.meta.updated` update
|
||||
* through `event`. Session-scoped by target, called from Agent-scope domains
|
||||
* (main agent only).
|
||||
*/
|
||||
|
||||
import type { IEventService } from '#/app/event/event';
|
||||
|
||||
import { titleFromPromptMetadataText } from '#/agent/prompt/promptMetadataText';
|
||||
|
||||
import type { ISessionMetadata } from './sessionMetadata';
|
||||
|
||||
export function isUntitled(title: string | undefined): boolean {
|
||||
return title === undefined || title.trim().length === 0 || title === 'New Session';
|
||||
}
|
||||
|
||||
export interface PromptMetadataUpdateTarget {
|
||||
readonly metadata: ISessionMetadata;
|
||||
readonly eventService: IEventService;
|
||||
readonly sessionId: string;
|
||||
}
|
||||
|
||||
export async function applyPromptMetadataUpdate(
|
||||
target: PromptMetadataUpdateTarget,
|
||||
text: string | undefined,
|
||||
): Promise<void> {
|
||||
if (text === undefined) return;
|
||||
const current = await target.metadata.read();
|
||||
const patch: { lastPrompt: string; title?: string; isCustomTitle?: boolean } = {
|
||||
lastPrompt: text,
|
||||
};
|
||||
if (!current.isCustomTitle && isUntitled(current.title)) {
|
||||
patch.title = titleFromPromptMetadataText(text);
|
||||
patch.isCustomTitle = false;
|
||||
}
|
||||
await target.metadata.update(patch);
|
||||
target.eventService.publish({
|
||||
type: 'session.meta.updated',
|
||||
payload: {
|
||||
agentId: 'main',
|
||||
sessionId: target.sessionId,
|
||||
title: patch.title,
|
||||
patch: {
|
||||
title: patch.title,
|
||||
isCustomTitle: patch.isCustomTitle,
|
||||
lastPrompt: text,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -72,6 +72,7 @@ export function stubLoopWithHooks(options: StubLoopOptions = {}): StubLoop {
|
|||
async run() { return { type: 'completed', steps: 0, truncated: false }; },
|
||||
status() { return { state: active !== undefined ? 'running' : 'idle', activeTurnId: active?.id, pendingTurnIds: [], hasPendingRequests: queue.hasPendingRequests() }; },
|
||||
cancel(turnId, reason) { cancels.push({ turnId, reason }); if (active === undefined || (turnId !== undefined && active.id !== turnId)) return false; active.cancel(reason); return true; },
|
||||
cancelFromUser(turnId) { stub.cancel(turnId); },
|
||||
tryAcquireQuiescence: () => toDisposable(() => {}),
|
||||
hasPendingRequests: () => queue.hasPendingRequests(), registerLoopErrorHandler: errorHandlers.register,
|
||||
settled: () => Promise.resolve(),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMo
|
|||
import { recordingTelemetry, type TelemetryRecord } from '../../app/telemetry/stubs';
|
||||
import { createTestAgent, telemetryServices, type TestAgentContext } from '../../harness';
|
||||
|
||||
describe('setPermission RPC', () => {
|
||||
describe('setModeAndBroadcast', () => {
|
||||
let ctx: TestAgentContext;
|
||||
let records: TelemetryRecord[];
|
||||
|
||||
|
|
@ -23,6 +23,7 @@ export function stubPermissionModeService(
|
|||
return mode();
|
||||
},
|
||||
setMode: () => {},
|
||||
setModeAndBroadcast: () => {},
|
||||
onDidChangeMode: Event.None as Event<PermissionModeChangedContext>,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
/**
|
||||
* Scenario: `IAgentPluginCommandService.activate` drives a user-slash plugin
|
||||
* command into the prompt pipeline.
|
||||
*
|
||||
* Pins the activation flow: definition lookup (unknown commands reject with
|
||||
* `request.invalid`), argument expansion, the `plugin_command.activated`
|
||||
* domain event, the enqueued user message, and the main-agent prompt-metadata
|
||||
* update. Run: `pnpm --filter @moonshot-ai/agent-core-v2 exec vitest run
|
||||
* test/agent/pluginCommand/pluginCommand.test.ts`.
|
||||
*/
|
||||
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { IEventBus } from '#/app/event/eventBus';
|
||||
import { IPluginService } from '#/app/plugin/plugin';
|
||||
import type { PluginCommandDef } from '#/app/plugin/types';
|
||||
import { ErrorCodes } from '#/errors';
|
||||
import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';
|
||||
|
||||
import {
|
||||
IAgentPluginCommandService,
|
||||
type PluginCommandActivatedEvent,
|
||||
} from '#/agent/pluginCommand/pluginCommand';
|
||||
|
||||
import { appService, createTestAgent, type TestAgentContext } from '../../harness';
|
||||
|
||||
const DEPLOY_COMMAND: PluginCommandDef = {
|
||||
pluginId: 'demo',
|
||||
name: 'deploy',
|
||||
description: 'Deploy',
|
||||
body: 'Deploy body',
|
||||
path: '/plugins/demo/deploy.md',
|
||||
};
|
||||
|
||||
function pluginServiceStub(commands: readonly PluginCommandDef[]): IPluginService {
|
||||
return {
|
||||
_serviceBrand: undefined,
|
||||
onDidReload: () => ({ dispose: () => {} }),
|
||||
onDidMutate: () => ({ dispose: () => {} }),
|
||||
listPlugins: async () => [],
|
||||
installPlugin: async () => ({ id: '' }) as never,
|
||||
setPluginEnabled: async () => {},
|
||||
setPluginMcpServerEnabled: async () => {},
|
||||
removePlugin: async () => {},
|
||||
reloadPlugins: async () => ({ added: [], removed: [], errors: [] }),
|
||||
getPluginInfo: async () => {
|
||||
throw new Error('getPluginInfo is not used by these tests');
|
||||
},
|
||||
listPluginCommands: async () => commands,
|
||||
checkUpdates: async () => [],
|
||||
pluginSkillRoots: async () => [],
|
||||
pluginAgentRoots: async () => [],
|
||||
enabledSessionStarts: async () => [],
|
||||
enabledSystemPrompts: async () => [],
|
||||
enabledMcpServers: async () => ({}),
|
||||
enabledHooks: async () => [],
|
||||
hasLoadedSnapshot: () => true,
|
||||
};
|
||||
}
|
||||
|
||||
describe('AgentPluginCommandService', () => {
|
||||
let ctx: TestAgentContext;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await ctx.expectResumeMatches();
|
||||
} finally {
|
||||
await ctx.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
function agentWithDeployCommand(): TestAgentContext {
|
||||
return createTestAgent(
|
||||
appService(IPluginService, pluginServiceStub([DEPLOY_COMMAND])),
|
||||
);
|
||||
}
|
||||
|
||||
it('publishes the activation event, enqueues the expanded body, and updates metadata', async () => {
|
||||
ctx = agentWithDeployCommand();
|
||||
ctx.mockNextResponse({ type: 'text', text: 'deployed' });
|
||||
|
||||
const events: PluginCommandActivatedEvent[] = [];
|
||||
const sub = ctx
|
||||
.get(IEventBus)
|
||||
.subscribe('plugin_command.activated', (event) => events.push(event));
|
||||
|
||||
await ctx
|
||||
.get(IAgentPluginCommandService)
|
||||
.activate({ pluginId: 'demo', commandName: 'deploy', args: 'prod' });
|
||||
sub.dispose();
|
||||
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0]).toMatchObject({
|
||||
type: 'plugin_command.activated',
|
||||
pluginId: 'demo',
|
||||
commandName: 'deploy',
|
||||
commandArgs: 'prod',
|
||||
trigger: 'user-slash',
|
||||
});
|
||||
|
||||
await ctx.untilTurnEnd();
|
||||
const llmInput = JSON.stringify(ctx.llmInputs());
|
||||
expect(llmInput).toContain('Deploy body');
|
||||
expect(llmInput).toContain('ARGUMENTS: prod');
|
||||
|
||||
const metadata = await ctx.get(ISessionMetadata).read();
|
||||
expect(metadata.title).toBe('/demo:deploy prod');
|
||||
expect(metadata.lastPrompt).toBe('/demo:deploy prod');
|
||||
});
|
||||
|
||||
it('rejects an unknown command with request.invalid', async () => {
|
||||
ctx = agentWithDeployCommand();
|
||||
|
||||
await expect(
|
||||
ctx
|
||||
.get(IAgentPluginCommandService)
|
||||
.activate({ pluginId: 'demo', commandName: 'missing' }),
|
||||
).rejects.toMatchObject({ code: ErrorCodes.REQUEST_INVALID });
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* prompt-metadata — the session title / lastPrompt text derived from a
|
||||
* prompt payload.
|
||||
* promptMetadataText — the session title / lastPrompt text derived from
|
||||
* prompt content parts.
|
||||
*
|
||||
* Tests pin:
|
||||
* - media parts render as `[image]` / `[video]` / `[audio]` placeholders
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { promptMetadataTextFromPayload } from '#/agent/rpc/prompt-metadata';
|
||||
import { promptMetadataTextFromContentParts } from '#/agent/prompt/promptMetadataText';
|
||||
import { buildImageCompressionCaption } from '#/agent/media/image-compress';
|
||||
|
||||
const CAPTION = buildImageCompressionCaption({
|
||||
|
|
@ -20,34 +20,28 @@ const CAPTION = buildImageCompressionCaption({
|
|||
originalPath: '/tmp/originals/shot.png',
|
||||
});
|
||||
|
||||
describe('promptMetadataTextFromPayload', () => {
|
||||
describe('promptMetadataTextFromContentParts', () => {
|
||||
it('renders text and media placeholders', () => {
|
||||
const text = promptMetadataTextFromPayload({
|
||||
input: [
|
||||
{ type: 'text', text: 'look at this' },
|
||||
{ type: 'image_url', imageUrl: { url: 'data:image/png;base64,AAAA' } },
|
||||
],
|
||||
});
|
||||
const text = promptMetadataTextFromContentParts([
|
||||
{ type: 'text', text: 'look at this' },
|
||||
{ type: 'image_url', imageUrl: { url: 'data:image/png;base64,AAAA' } },
|
||||
]);
|
||||
expect(text).toBe('look at this [image]');
|
||||
});
|
||||
|
||||
it('keeps a standalone image-compression caption out of the metadata text', () => {
|
||||
const text = promptMetadataTextFromPayload({
|
||||
input: [
|
||||
{ type: 'text', text: CAPTION },
|
||||
{ type: 'image_url', imageUrl: { url: 'data:image/png;base64,AAAA' } },
|
||||
],
|
||||
});
|
||||
const text = promptMetadataTextFromContentParts([
|
||||
{ type: 'text', text: CAPTION },
|
||||
{ type: 'image_url', imageUrl: { url: 'data:image/png;base64,AAAA' } },
|
||||
]);
|
||||
expect(text).toBe('[image]');
|
||||
});
|
||||
|
||||
it('strips a caption merged into the user text and keeps the rest', () => {
|
||||
const text = promptMetadataTextFromPayload({
|
||||
input: [
|
||||
{ type: 'text', text: `能展示但是没有快捷键提示${CAPTION}` },
|
||||
{ type: 'image_url', imageUrl: { url: 'data:image/png;base64,AAAA' } },
|
||||
],
|
||||
});
|
||||
const text = promptMetadataTextFromContentParts([
|
||||
{ type: 'text', text: `能展示但是没有快捷键提示${CAPTION}` },
|
||||
{ type: 'image_url', imageUrl: { url: 'data:image/png;base64,AAAA' } },
|
||||
]);
|
||||
expect(text).toBe('能展示但是没有快捷键提示 [image]');
|
||||
expect(text).not.toContain('<system>');
|
||||
expect(text).not.toContain('Image compressed');
|
||||
|
|
@ -17,13 +17,18 @@ import { IAgentFullCompactionService } from '#/agent/fullCompaction/fullCompacti
|
|||
import { IAgentLoopService } from '#/agent/loop/loop';
|
||||
import { IAgentPromptService } from '#/agent/prompt/prompt';
|
||||
import { AgentPromptService } from '#/agent/prompt/promptService';
|
||||
import { IAgentScopeContext, makeAgentScopeContext } from '#/agent/scopeContext/scopeContext';
|
||||
import { IAgentSystemReminderService } from '#/agent/systemReminder/systemReminder';
|
||||
import { AgentSystemReminderService } from '#/agent/systemReminder/systemReminderService';
|
||||
import { IAgentToolExecutorService } from '#/agent/toolExecutor/toolExecutor';
|
||||
import { IEventBus } from '#/app/event/eventBus';
|
||||
import { IEventService } from '#/app/event/event';
|
||||
import { EventBusService } from '#/app/event/eventBusService';
|
||||
import { ITelemetryService } from '#/app/telemetry/telemetry';
|
||||
import { ErrorCodes, Error2 } from '#/errors';
|
||||
import { createHooks } from '#/hooks';
|
||||
import { ISessionContext } from '#/session/sessionContext/sessionContext';
|
||||
import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';
|
||||
import { IWireService } from '#/wire/wire';
|
||||
|
||||
import { stubContextMemory } from '../contextMemory/stubs';
|
||||
|
|
@ -57,6 +62,14 @@ function harness() {
|
|||
reg.define(IEventBus, EventBusService);
|
||||
reg.define(IAgentSystemReminderService, AgentSystemReminderService);
|
||||
reg.define(IAgentPromptService, AgentPromptService);
|
||||
reg.definePartialInstance(ITelemetryService, { track: () => {}, track2: () => {} });
|
||||
reg.definePartialInstance(ISessionMetadata, {
|
||||
read: async () => ({ id: 'test-session', createdAt: 0, updatedAt: 0, archived: false }),
|
||||
update: async () => {},
|
||||
});
|
||||
reg.definePartialInstance(IEventService, { publish: () => {} });
|
||||
reg.definePartialInstance(ISessionContext, { sessionId: 'test-session' });
|
||||
reg.defineInstance(IAgentScopeContext, makeAgentScopeContext({ agentId: 'main', agentScope: '' }));
|
||||
}
|
||||
});
|
||||
return { prompt: ix.get(IAgentPromptService), loop, context, fullCompaction, eventBus: ix.get(IEventBus) };
|
||||
|
|
|
|||
82
packages/agent-core-v2/test/agent/prompt/submit.test.ts
Normal file
82
packages/agent-core-v2/test/agent/prompt/submit.test.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* Scenario: `IAgentPromptService.submit` is the wire-facing prompt entry —
|
||||
* prompt-metadata persistence and `{turn_id}` settlement.
|
||||
*
|
||||
* Migrated from the kap-server debug-RPC suite (`test/rpc.test.ts`) when the
|
||||
* RPC aggregation layer was removed: the composition now lives in the prompt
|
||||
* domain. Run: `pnpm --filter @moonshot-ai/agent-core-v2 exec vitest run
|
||||
* test/agent/prompt/submit.test.ts`.
|
||||
*/
|
||||
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { IEventService } from '#/app/event/event';
|
||||
import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';
|
||||
|
||||
import { createTestAgent, type TestAgentContext } from '../../harness';
|
||||
|
||||
describe('prompt submit', () => {
|
||||
let ctx: TestAgentContext;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await ctx.expectResumeMatches();
|
||||
} finally {
|
||||
await ctx.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it('submits a prompt and returns the turn id', async () => {
|
||||
ctx = createTestAgent();
|
||||
ctx.mockNextResponse({ type: 'text', text: 'hi' });
|
||||
|
||||
const launched = await ctx.rpc.prompt({ input: [{ type: 'text', text: 'hello' }] });
|
||||
// Turn ids are 0-based; the point is the launch result came back at all.
|
||||
expect(launched?.turn_id).toBe(0);
|
||||
await ctx.untilTurnEnd();
|
||||
});
|
||||
|
||||
it('derives the session title and lastPrompt from the first prompt', async () => {
|
||||
ctx = createTestAgent();
|
||||
ctx.mockNextResponse({ type: 'text', text: 'hi' });
|
||||
|
||||
const events: { type: string; payload?: unknown }[] = [];
|
||||
const sub = ctx.get(IEventService).subscribe((event) => events.push(event));
|
||||
|
||||
const launched = await ctx.rpc.prompt({ input: [{ type: 'text', text: 'hello title' }] });
|
||||
expect(launched?.turn_id).toBe(0);
|
||||
sub.dispose();
|
||||
|
||||
const metadata = await ctx.get(ISessionMetadata).read();
|
||||
expect(metadata.title).toBe('hello title');
|
||||
expect(metadata.lastPrompt).toBe('hello title');
|
||||
|
||||
const updated = events.find((event) => event.type === 'session.meta.updated');
|
||||
expect(updated).toBeDefined();
|
||||
const payload = updated?.payload as
|
||||
| { title?: string; patch?: { lastPrompt?: string } }
|
||||
| undefined;
|
||||
expect(payload?.title).toBe('hello title');
|
||||
expect(payload?.patch?.lastPrompt).toBe('hello title');
|
||||
|
||||
await ctx.untilTurnEnd();
|
||||
});
|
||||
|
||||
it('keeps a custom title and only refreshes lastPrompt on a later prompt', async () => {
|
||||
ctx = createTestAgent();
|
||||
ctx.mockNextResponse({ type: 'text', text: 'hi' });
|
||||
|
||||
await ctx.get(ISessionMetadata).setTitle('keep-me');
|
||||
|
||||
const launched = await ctx.rpc.prompt({
|
||||
input: [{ type: 'text', text: 'should not become the title' }],
|
||||
});
|
||||
expect(launched?.turn_id).toBe(0);
|
||||
|
||||
const metadata = await ctx.get(ISessionMetadata).read();
|
||||
expect(metadata.title).toBe('keep-me');
|
||||
expect(metadata.lastPrompt).toBe('should not become the title');
|
||||
|
||||
await ctx.untilTurnEnd();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { IAgentContextMemoryService } from '#/index';
|
||||
|
||||
import {
|
||||
createCommandRunner,
|
||||
createTestAgent,
|
||||
execEnvServices,
|
||||
type TestAgentContext,
|
||||
} from '../../harness';
|
||||
|
||||
describe('runShellCommand RPC', () => {
|
||||
let ctx: TestAgentContext;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await ctx.expectResumeMatches();
|
||||
} finally {
|
||||
await ctx.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it('delegates to the shell command service', async () => {
|
||||
ctx = createTestAgent(execEnvServices({ processRunner: createCommandRunner('ok\n', 0) }));
|
||||
const context = ctx.get(IAgentContextMemoryService);
|
||||
|
||||
const result = await ctx.rpc.runShellCommand({ command: 'echo ok' });
|
||||
|
||||
expect(result.isError).toBe(false);
|
||||
expect(context.get().map(({ role, origin }) => ({ role, origin }))).toEqual([
|
||||
{ role: 'user', origin: { kind: 'shell_command', phase: 'input' } },
|
||||
{ role: 'user', origin: { kind: 'shell_command', phase: 'output' } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { ErrorCodes } from '#/errors';
|
||||
|
||||
import {
|
||||
createTestAgent,
|
||||
telemetryServices,
|
||||
type TestAgentContext,
|
||||
} from '../../harness';
|
||||
import { recordingTelemetry, type TelemetryRecord } from '../../app/telemetry/stubs';
|
||||
|
||||
describe('undoHistory RPC', () => {
|
||||
let ctx: TestAgentContext;
|
||||
let records: TelemetryRecord[];
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await ctx.expectResumeMatches();
|
||||
} finally {
|
||||
await ctx.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it('tracks conversation_undo after undoing history', async () => {
|
||||
records = [];
|
||||
ctx = createTestAgent(telemetryServices(recordingTelemetry(records)));
|
||||
ctx.appendUserTurn('undo me');
|
||||
|
||||
const undone = await ctx.rpc.undoHistory({ count: 1 });
|
||||
|
||||
expect(undone).toBe(1);
|
||||
expect(records).toContainEqual({
|
||||
event: 'conversation_undo',
|
||||
properties: { agent_id: 'main', count: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a fractional count without changing persisted history', async () => {
|
||||
records = [];
|
||||
ctx = createTestAgent(telemetryServices(recordingTelemetry(records)));
|
||||
ctx.appendUserTurn('keep me');
|
||||
const history = ctx.context.get();
|
||||
|
||||
await expect(ctx.rpc.undoHistory({ count: 0.5 })).rejects.toMatchObject({
|
||||
code: ErrorCodes.REQUEST_INVALID,
|
||||
details: { field: 'count' },
|
||||
});
|
||||
|
||||
expect(ctx.context.get()).toBe(history);
|
||||
expect(records).not.toContainEqual(expect.objectContaining({ event: 'conversation_undo' }));
|
||||
});
|
||||
});
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
/**
|
||||
* Scenario: `AgentRPCService.activateSkill` is the wire-facing skill
|
||||
* activation entry — awaited, returning the launched turn id.
|
||||
* Scenario: `IAgentSkillService.activate` is the wire-facing skill activation
|
||||
* entry — awaited, returning the launched turn id.
|
||||
*
|
||||
* Unlike `IAgentSkillService.activate` (in-process, returns the live `Turn`
|
||||
* handle), the RPC variant must settle only once the turn has launched and
|
||||
* must surface activation failures (unknown skill, busy agent) to the caller
|
||||
* instead of fire-and-forget. Run: `pnpm --filter @moonshot-ai/agent-core-v2
|
||||
* exec vitest run test/agent/rpc/activateSkill.test.ts`.
|
||||
* The activation settles only once the turn has launched, and activation
|
||||
* failures (unknown skill, busy agent) surface to the caller instead of
|
||||
* fire-and-forget. Run: `pnpm --filter @moonshot-ai/agent-core-v2
|
||||
* exec vitest run test/agent/skill/activateSkill.test.ts`.
|
||||
*/
|
||||
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
|
@ -16,7 +15,7 @@ import { InMemorySkillCatalog } from '#/app/skillCatalog/registry';
|
|||
import { stubSkill } from '../../app/skillCatalog/stubs';
|
||||
import { createTestAgent, skillServices, type TestAgentContext } from '../../harness';
|
||||
|
||||
describe('activateSkill RPC', () => {
|
||||
describe('activateSkill', () => {
|
||||
let ctx: TestAgentContext;
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -11,6 +11,8 @@ import { InMemorySkillCatalog } from '#/app/skillCatalog/registry';
|
|||
import { summarizeSkill } from '#/app/skillCatalog/types';
|
||||
import { ISessionSkillCatalog } from '#/session/sessionSkillCatalog/skillCatalog';
|
||||
import { ISessionContext } from '#/session/sessionContext/sessionContext';
|
||||
import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';
|
||||
import { IEventService } from '#/app/event/event';
|
||||
import { AgentSkillService } from '#/agent/skill/skillService';
|
||||
import {
|
||||
MAX_SKILL_QUERY_DEPTH,
|
||||
|
|
@ -77,6 +79,11 @@ describe('AgentSkillService', () => {
|
|||
reg.definePartialInstance(IAgentToolRegistryService, {
|
||||
register: () => ({ dispose: () => {} }),
|
||||
});
|
||||
reg.definePartialInstance(ISessionMetadata, {
|
||||
read: async () => ({ id: 'test-session', createdAt: 0, updatedAt: 0, archived: false }),
|
||||
update: async () => {},
|
||||
});
|
||||
reg.definePartialInstance(IEventService, { publish: () => {} });
|
||||
reg.defineInstance(ISessionContext, stubSessionContext());
|
||||
reg.defineInstance(IAgentScopeContext, makeAgentScopeContext({ agentId: 'main', agentScope: '' }));
|
||||
},
|
||||
|
|
@ -171,6 +178,11 @@ describe('SkillTool', () => {
|
|||
reg.definePartialInstance(IAgentToolRegistryService, {
|
||||
register: () => ({ dispose: () => {} }),
|
||||
});
|
||||
reg.definePartialInstance(ISessionMetadata, {
|
||||
read: async () => ({ id: 'test-session', createdAt: 0, updatedAt: 0, archived: false }),
|
||||
update: async () => {},
|
||||
});
|
||||
reg.definePartialInstance(IEventService, { publish: () => {} });
|
||||
reg.defineInstance(ISessionContext, stubSessionContext());
|
||||
reg.defineInstance(IAgentScopeContext, makeAgentScopeContext({ agentId: 'main', agentScope: '' }));
|
||||
},
|
||||
|
|
|
|||
|
|
@ -219,6 +219,8 @@ class FakeLoopService implements IAgentLoopService {
|
|||
onDidFinishStep: new OrderedHookSlot<AfterStepContext>(),
|
||||
};
|
||||
|
||||
cancelFromUser(): void {}
|
||||
|
||||
enqueue(_request: StepRequest, _options?: StepEnqueueOptions): EnqueueReceipt {
|
||||
throw new Error('unused in this suite');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ describe('RestGateway', () => {
|
|||
const promptService: IAgentPromptService = {
|
||||
_serviceBrand: undefined,
|
||||
enqueue: ({ message }: { message: ContextMessage }) => { promptCalls.push(message); return Promise.resolve({ id: 'p', launched: Promise.resolve(undefined) } as never); },
|
||||
submit: () => Promise.resolve(undefined),
|
||||
submitSteer: () => Promise.resolve(undefined),
|
||||
steer: () => Promise.resolve([]),
|
||||
list: () => ({ active: undefined, pending: [] }),
|
||||
abort: () => true,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ function permissionMode(mode: PermissionMode = 'auto'): IAgentPermissionModeServ
|
|||
_serviceBrand: undefined,
|
||||
mode,
|
||||
setMode: () => {},
|
||||
setModeAndBroadcast: () => {},
|
||||
onDidChangeMode: () => ({ dispose: () => {} }),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ function permissionMode(): IAgentPermissionModeService {
|
|||
_serviceBrand: undefined,
|
||||
mode: 'auto',
|
||||
setMode: () => {},
|
||||
setModeAndBroadcast: () => {},
|
||||
onDidChangeMode: () => ({ dispose: () => {} }),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,30 +40,51 @@ import { IAgentProfileService, type AgentConfigData } from '#/agent/profile/prof
|
|||
import { IAgentToolPolicyService } from '#/agent/toolPolicy/toolPolicy';
|
||||
import { IAgentPromptService } from '#/agent/prompt/prompt';
|
||||
import type {
|
||||
AgentAPI,
|
||||
BeginCompactionPayload,
|
||||
CancelPlanPayload,
|
||||
CancelShellCommandPayload,
|
||||
CreateGoalPayload,
|
||||
DetachTaskPayload,
|
||||
EmptyPayload,
|
||||
EnterSwarmPayload,
|
||||
GetTaskOutputPayload,
|
||||
GetTasksPayload,
|
||||
GoalSnapshot,
|
||||
GoalToolResult,
|
||||
RegisterToolPayload,
|
||||
RunShellCommandPayload,
|
||||
SetActiveToolsPayload,
|
||||
SetModelPayload,
|
||||
SetModelResult,
|
||||
SetThinkingPayload,
|
||||
ShellCommandResult,
|
||||
StopTaskPayload,
|
||||
UnregisterToolPayload,
|
||||
} from '#/agent/rpc/core-api';
|
||||
PromptLaunchResult,
|
||||
PromptPayload,
|
||||
SteerPayload,
|
||||
} from '#/agent/prompt/prompt';
|
||||
import type { AgentCommandInfo } from '#/agent/command/agentCommand';
|
||||
import { IAgentCommandService } from '#/agent/command/agentCommand';
|
||||
import type { AgentContextData } from '#/agent/contextMemory/types';
|
||||
import type { CreateGoalInput, GoalSnapshot, GoalToolResult } from '#/agent/goal/types';
|
||||
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 { UserToolRegistration } from '#/agent/userTool/userTool';
|
||||
import type { ActivatePluginCommandPayload } from '#/agent/pluginCommand/pluginCommand';
|
||||
import { IAgentPluginCommandService } from '#/agent/pluginCommand/pluginCommand';
|
||||
import type { ToolInfo } from '#/tool/toolContract';
|
||||
|
||||
// Test-facing wire vocabulary, formerly imported from the deleted RPC
|
||||
// aggregation layer; payloads with an owner-domain type are aliased above,
|
||||
// the rest are local to the harness.
|
||||
type EmptyPayload = {};
|
||||
type CreateGoalPayload = CreateGoalInput;
|
||||
type RegisterToolPayload = UserToolRegistration;
|
||||
type RunShellCommandPayload = RunShellCommandInput;
|
||||
type ShellCommandResult = RunShellCommandResult;
|
||||
type SetModelResult = ProfileSetModelResult;
|
||||
interface BeginCompactionPayload { readonly instruction?: string }
|
||||
interface CancelPayload { readonly turnId?: number }
|
||||
interface CancelPlanPayload { readonly id?: string }
|
||||
interface CancelShellCommandPayload { readonly commandId: string }
|
||||
interface DetachTaskPayload { readonly taskId: string }
|
||||
interface EnterSwarmPayload { readonly trigger: SwarmModeTrigger }
|
||||
interface GetTaskOutputPayload { readonly taskId: string; readonly tail?: number }
|
||||
interface GetTasksPayload { readonly activeOnly?: boolean; readonly limit?: number }
|
||||
interface RunCommandPayload { readonly name: string; readonly args?: string }
|
||||
interface SetActiveToolsPayload { readonly names: readonly string[] }
|
||||
interface SetModelPayload { readonly model: string }
|
||||
interface SetPermissionPayload { readonly mode: PermissionMode }
|
||||
interface SetThinkingPayload { readonly level: string }
|
||||
interface StopTaskPayload { readonly taskId: string; readonly reason?: string }
|
||||
interface UndoHistoryPayload { readonly count: number }
|
||||
interface UnregisterToolPayload { readonly name: string }
|
||||
import { type UsageStatus } from '#/agent/usage/usage';
|
||||
import { IAgentSkillService } from '#/agent/skill/skill';
|
||||
import { IAgentSkillService, type SkillActivationInput } from '#/agent/skill/skill';
|
||||
import { AgentSkillService } from '#/agent/skill/skillService';
|
||||
import { IAgentToolDedupeService } from '#/agent/toolDedupe/toolDedupe';
|
||||
import type {
|
||||
|
|
@ -92,7 +113,6 @@ import {
|
|||
InMemoryStorageService,
|
||||
AgentFullCompactionService,
|
||||
IAgentActivityView,
|
||||
IAgentRPCService,
|
||||
IAppendLogStore,
|
||||
IFileSystemStorageService,
|
||||
ISessionApprovalService,
|
||||
|
|
@ -307,6 +327,18 @@ type RpcPromise<T> = Promise<T> & {
|
|||
};
|
||||
|
||||
interface AgentRpcPassthroughAPI {
|
||||
prompt: (payload: PromptPayload) => Promisable<PromptLaunchResult | undefined>;
|
||||
steer: (payload: SteerPayload) => Promisable<PromptLaunchResult | undefined>;
|
||||
cancel: (payload: CancelPayload) => void;
|
||||
undoHistory: (payload: UndoHistoryPayload) => Promisable<number>;
|
||||
setPermission: (payload: SetPermissionPayload) => void;
|
||||
cancelCompaction: (payload: EmptyPayload) => void;
|
||||
activateSkill: (payload: SkillActivationInput) => Promisable<PromptLaunchResult>;
|
||||
activatePluginCommand: (payload: ActivatePluginCommandPayload) => Promisable<void>;
|
||||
listCommands: (payload: EmptyPayload) => readonly AgentCommandInfo[];
|
||||
runCommand: (payload: RunCommandPayload) => Promisable<void>;
|
||||
getContext: (payload: EmptyPayload) => AgentContextData;
|
||||
getTools: (payload: EmptyPayload) => readonly ToolInfo[];
|
||||
runShellCommand: (payload: RunShellCommandPayload) => Promisable<ShellCommandResult>;
|
||||
cancelShellCommand: (payload: CancelShellCommandPayload) => void;
|
||||
setThinking: (payload: SetThinkingPayload) => void;
|
||||
|
|
@ -339,7 +371,7 @@ interface AgentRpcPassthroughAPI {
|
|||
getTasks: (payload: GetTasksPayload) => readonly AgentTaskInfo[];
|
||||
}
|
||||
|
||||
type PromiseAgentAPI = PromisifyMethods<AgentAPI & AgentRpcPassthroughAPI>;
|
||||
type PromiseAgentAPI = PromisifyMethods<AgentRpcPassthroughAPI>;
|
||||
type GenerateFn = typeof kosongGenerate;
|
||||
|
||||
type TestToolResult = ExecutableToolResult & {
|
||||
|
|
@ -1270,8 +1302,7 @@ export class AgentTestContext {
|
|||
}),
|
||||
);
|
||||
|
||||
const rpcMethods = this.get(IAgentRPCService);
|
||||
this.rpc = this.createPromiseAgentApi(rpcMethods);
|
||||
this.rpc = this.createPromiseAgentApi();
|
||||
|
||||
if (options.autoConfigure !== false) {
|
||||
this.configure();
|
||||
|
|
@ -1472,8 +1503,7 @@ export class AgentTestContext {
|
|||
}
|
||||
|
||||
async undoHistory(count: number): Promise<number> {
|
||||
const rpcMethods = this.get(IAgentRPCService);
|
||||
return rpcMethods.undoHistory({ count });
|
||||
return this.get(IAgentConversationUndoService).undo(count);
|
||||
}
|
||||
|
||||
newEvents(): EventSnapshot {
|
||||
|
|
@ -1974,16 +2004,15 @@ export class AgentTestContext {
|
|||
this.recordWire(cloned);
|
||||
}
|
||||
|
||||
private createPromiseAgentApi(agent: IAgentRPCService): PromiseAgentAPI {
|
||||
const passthrough = this.createRpcPassthroughAdapters();
|
||||
return new Proxy(agent, {
|
||||
private createPromiseAgentApi(): PromiseAgentAPI {
|
||||
const adapters = this.createRpcPassthroughAdapters();
|
||||
return new Proxy(adapters, {
|
||||
get(proxyTarget, property, receiver) {
|
||||
const override = Reflect.get(passthrough, property) as unknown;
|
||||
const value = override ?? Reflect.get(proxyTarget, property, receiver);
|
||||
const value = Reflect.get(proxyTarget, property, receiver) as unknown;
|
||||
if (typeof value !== 'function') return value;
|
||||
return (payload: unknown) => {
|
||||
try {
|
||||
return Promise.resolve(value.call(proxyTarget, payload));
|
||||
return Promise.resolve(value(payload));
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
|
@ -1994,6 +2023,23 @@ export class AgentTestContext {
|
|||
|
||||
private createRpcPassthroughAdapters(): AgentRpcPassthroughAPI {
|
||||
return {
|
||||
prompt: (payload) => this.get(IAgentPromptService).submit(payload),
|
||||
steer: (payload) => this.get(IAgentPromptService).submitSteer(payload),
|
||||
cancel: (payload) => this.get(IAgentLoopService).cancelFromUser(payload.turnId),
|
||||
undoHistory: (payload) => this.get(IAgentConversationUndoService).undo(payload.count),
|
||||
setPermission: (payload) =>
|
||||
this.get(IAgentPermissionModeService).setModeAndBroadcast(payload.mode),
|
||||
cancelCompaction: () => this.get(IAgentFullCompactionService).cancel(),
|
||||
activateSkill: (payload) => this.get(IAgentSkillService).activate(payload),
|
||||
activatePluginCommand: (payload) =>
|
||||
this.get(IAgentPluginCommandService).activate(payload),
|
||||
listCommands: () => this.get(IAgentCommandService).list(),
|
||||
runCommand: (payload) => this.get(IAgentCommandService).run(payload.name, payload.args),
|
||||
getContext: () => ({
|
||||
history: this.get(IAgentContextMemoryService).get(),
|
||||
tokenCount: this.get(IAgentTokenCountingService).statusSize(),
|
||||
}),
|
||||
getTools: () => this.toolsData(),
|
||||
runShellCommand: (payload) => this.get(IAgentShellCommandService).run(payload),
|
||||
cancelShellCommand: (payload) =>
|
||||
this.get(IAgentShellCommandService).cancel(payload.commandId),
|
||||
|
|
@ -2125,7 +2171,7 @@ function createWorkspaceContextStub(
|
|||
|
||||
function createPermissionModeService(initialMode: PermissionMode): IAgentPermissionModeService {
|
||||
let mode = initialMode;
|
||||
return {
|
||||
const service: IAgentPermissionModeService = {
|
||||
_serviceBrand: undefined,
|
||||
get mode() {
|
||||
return mode;
|
||||
|
|
@ -2133,8 +2179,12 @@ function createPermissionModeService(initialMode: PermissionMode): IAgentPermiss
|
|||
setMode: (nextMode) => {
|
||||
mode = nextMode;
|
||||
},
|
||||
setModeAndBroadcast: (nextMode) => {
|
||||
service.setMode(nextMode);
|
||||
},
|
||||
onDidChangeMode: Event.None as IAgentPermissionModeService['onDidChangeMode'],
|
||||
};
|
||||
return service;
|
||||
}
|
||||
|
||||
function createPermissionRulesStub(
|
||||
|
|
|
|||
|
|
@ -1429,6 +1429,7 @@ function agentHandle(
|
|||
_serviceBrand: undefined,
|
||||
mode: 'auto',
|
||||
setMode: () => {},
|
||||
setModeAndBroadcast: () => {},
|
||||
onDidChangeMode: Event.None,
|
||||
} as IAgentPermissionModeService;
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ import type {
|
|||
import type { McpOAuthAuthorizationUrlUpdateData } from '@moonshot-ai/agent-core-v2/agent/mcp/tools/auth';
|
||||
import type { PermissionMode } from '@moonshot-ai/agent-core-v2/agent/permissionPolicy/types';
|
||||
import type { WarningEvent } from '@moonshot-ai/agent-core-v2/agent/profile/profileService';
|
||||
import type { PluginCommandActivatedEvent } from '@moonshot-ai/agent-core-v2/agent/rpc/rpcService';
|
||||
import type { PluginCommandActivatedEvent } from '@moonshot-ai/agent-core-v2/agent/pluginCommand/pluginCommand';
|
||||
import type {
|
||||
ShellCompletedEvent,
|
||||
ShellOutputEvent,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
* the native v2 services directly (the workspace handler's
|
||||
* `ISessionLifecycleService.fork` / `archive` / `restore`, reached through the
|
||||
* `sessionIndex` → `IWorkspaceLifecycleService.handlerFor` composition,
|
||||
* `IAgentFullCompactionService.begin`, `IAgentRPCService.cancel`); there is no
|
||||
* `IAgentFullCompactionService.begin`, `IAgentLoopService.cancelFromUser`); there is no
|
||||
* v1-only projection to centralize, so no adapter is involved. `undo` likewise
|
||||
* calls `IAgentConversationUndoService.undo` directly (it throws
|
||||
* `session.undo_unavailable` with a structured reason) and only borrows
|
||||
|
|
@ -81,7 +81,7 @@ import {
|
|||
IAgentProfileService,
|
||||
IAgentConversationUndoService,
|
||||
IAgentFullCompactionService,
|
||||
IAgentRPCService,
|
||||
IAgentLoopService,
|
||||
IAuthSummaryService,
|
||||
ISessionActivityView,
|
||||
ISessionBtwService,
|
||||
|
|
@ -773,7 +773,7 @@ export function registerSessionsRoutes(app: SessionRouteHost, core: Scope): void
|
|||
const agent = await resolveMainAgent(core, parsed.id);
|
||||
// No turnId → cancel whatever turn is active; a safe no-op when idle.
|
||||
// v1 always reports success once the session exists.
|
||||
await agent.accessor.get(IAgentRPCService).cancel({});
|
||||
agent.accessor.get(IAgentLoopService).cancelFromUser();
|
||||
requestLog(req)?.info({ session_id: parsed.id, action: 'abort' }, 'session action completed');
|
||||
reply.send(okEnvelope({ aborted: true }, req.id));
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -40,12 +40,12 @@
|
|||
* for the root, then composes the skill scan at the edge (see above).
|
||||
* - activate → `IAgentSkillService` (Agent scope, on the `main` agent) —
|
||||
* renders the skill prompt and starts a turn with a
|
||||
* `skill_activation` origin. The returned `Turn` handle is
|
||||
* `skill_activation` origin. The returned `{turn_id}` is
|
||||
* discarded; clients follow progress via the `skill.activated`
|
||||
* + `turn.*` events emitted by the service on the WS stream.
|
||||
* The edge then applies the prompt-metadata update
|
||||
* (`applyPromptMetadataUpdate`) so a first `/<skill>`
|
||||
* message titles the session, matching the native RPC path.
|
||||
* The engine applies the prompt-metadata update itself
|
||||
* (main agent only) so a first `/<skill>`
|
||||
* message titles the session, matching the native prompt path.
|
||||
* Optional `attachments` (image/video/file parts, same wire
|
||||
* shape as prompt content) run through the shared prompt
|
||||
* media pipeline (`lib/promptMedia.ts`) and are appended to
|
||||
|
|
@ -84,12 +84,10 @@ import {
|
|||
IAgentSkillService,
|
||||
IBootstrapService,
|
||||
IConfigService,
|
||||
IEventService,
|
||||
IFileService,
|
||||
IPluginService,
|
||||
ISessionContext,
|
||||
ISessionIndex,
|
||||
ISessionMetadata,
|
||||
ISessionSkillCatalog,
|
||||
ISkillDiscovery,
|
||||
ITelemetryService,
|
||||
|
|
@ -100,10 +98,8 @@ import {
|
|||
resumeSessionById,
|
||||
MERGE_ALL_AVAILABLE_SKILLS_SECTION,
|
||||
SKILL_SOURCE_PRIORITY,
|
||||
applyPromptMetadataUpdate,
|
||||
configuredRoots,
|
||||
projectRoots,
|
||||
promptMetadataTextFromSkill,
|
||||
sessionMediaOriginalsDir,
|
||||
userRoots,
|
||||
type ContentPart,
|
||||
|
|
@ -347,19 +343,12 @@ export function registerSkillsRoutes(app: SkillsRouteHost, core: Scope): void {
|
|||
attachmentParts.push(...contentToCoreParts(resolvedContent));
|
||||
}
|
||||
const agent = await ensureMainAgent(resolved.handle);
|
||||
// The engine applies the prompt-metadata update itself (main agent
|
||||
// only), so a first `/<skill>` message titles the session (same as
|
||||
// routes/prompts.ts).
|
||||
await agent.accessor
|
||||
.get(IAgentSkillService)
|
||||
.activate({ name: parsed.id, args: req.body.args, content: attachmentParts });
|
||||
// Keep the easy-title behavior of the native RPC / TUI path: a first
|
||||
// `/<skill>` message titles the session (same as routes/prompts.ts).
|
||||
await applyPromptMetadataUpdate(
|
||||
{
|
||||
metadata: resolved.handle.accessor.get(ISessionMetadata),
|
||||
eventService: core.accessor.get(IEventService),
|
||||
sessionId: session_id,
|
||||
},
|
||||
promptMetadataTextFromSkill({ name: parsed.id, args: req.body.args }),
|
||||
);
|
||||
requestLog(req)?.info({ session_id, skill_name: parsed.id }, 'skill activated');
|
||||
reply.send(okEnvelope({ activated: true, skill_name: parsed.id }, req.id));
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@
|
|||
* `agent/task/taskOps.ts`, `agent/shellCommand/shellCommandService.ts`,
|
||||
* `session/agentLifecycle/mirrorAgentRun.ts`, `session/swarm/sessionSwarmService.ts`,
|
||||
* `agent/goal/goalOps.ts`, `agent/usage/usageOps.ts`, `agent/skill/skillOps.ts`,
|
||||
* `agent/rpc/rpcService.ts`, `session/cron/cronOps.ts`,
|
||||
* `agent/pluginCommand/pluginCommandService.ts`, `session/cron/cronOps.ts`,
|
||||
* `agent/fullCompaction/compactionOps.ts`, `agent/mcp/mcpService.ts`,
|
||||
* `agent/profile/profileService.ts`, `agent/contextMemory/contextMemoryService.ts`).
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ import {
|
|||
IAgentActivityView,
|
||||
IAgentGoalService,
|
||||
IAgentLifecycleService,
|
||||
IAgentRPCService,
|
||||
IAgentPluginCommandService,
|
||||
IAgentPromptService,
|
||||
IAgentShellCommandService,
|
||||
IAppendLogStore,
|
||||
IDebugEventsService,
|
||||
IEventService,
|
||||
IInstantiationService,
|
||||
IPluginService,
|
||||
ISessionIndex,
|
||||
|
|
@ -169,7 +169,7 @@ describe('server-v2 /api/v1/debug RPC', () => {
|
|||
const byName = new Map(body.data.map((c) => [c.name, c]));
|
||||
expect(byName.get('sessionIndex')?.scope).toBe('app');
|
||||
expect(byName.get('sessionMetadata')?.scope).toBe('session');
|
||||
expect(byName.get('agentRPCService')?.scope).toBe('agent');
|
||||
expect(byName.get('agentPromptService')?.scope).toBe('agent');
|
||||
|
||||
const meta = byName.get('sessionMetadata');
|
||||
expect(meta?.methods.map((m) => m.name)).toEqual(
|
||||
|
|
@ -339,92 +339,6 @@ describe('server-v2 /api/v1/debug RPC', () => {
|
|||
|
||||
// --- Agent scope ----------------------------------------------------------
|
||||
|
||||
it('submits a prompt and returns the turn id', async () => {
|
||||
const id = await createSession(home as string);
|
||||
await createMainAgent(id);
|
||||
|
||||
const { body } = await call<{ turn_id: number }>(
|
||||
'POST',
|
||||
rpc('agent', IAgentRPCService, 'prompt', { sid: id, aid: 'main' }),
|
||||
{ input: [{ type: 'text', text: 'hello' }] },
|
||||
);
|
||||
expect(body.code).toBe(0);
|
||||
expect(body.data.turn_id).toBe(0);
|
||||
});
|
||||
|
||||
it('rejects disabledTools before bind without mutating prompt metadata', async () => {
|
||||
const id = await createSession(home as string);
|
||||
await createMainAgent(id);
|
||||
|
||||
const { body } = await call<null>(
|
||||
'POST',
|
||||
rpc('agent', IAgentRPCService, 'prompt', { sid: id, aid: 'main' }),
|
||||
{
|
||||
input: [{ type: 'text', text: 'must not become metadata' }],
|
||||
disabledTools: ['Bash'],
|
||||
},
|
||||
);
|
||||
expect(body.code).toBe(40001);
|
||||
|
||||
const metadata = await call<SessionMetaWire>(
|
||||
'POST',
|
||||
rpc('session', ISessionMetadata, 'read', { sid: id }),
|
||||
);
|
||||
expect(metadata.body.data.title).toBeUndefined();
|
||||
expect(metadata.body.data.lastPrompt).toBeUndefined();
|
||||
});
|
||||
|
||||
it('derives the session title and lastPrompt from the first prompt', async () => {
|
||||
const id = await createSession(home as string);
|
||||
await createMainAgent(id);
|
||||
|
||||
const events: { type: string; payload: unknown }[] = [];
|
||||
const sub = (server as RunningServer).core.accessor
|
||||
.get(IEventService)
|
||||
.subscribe((event) => events.push(event));
|
||||
|
||||
const { body } = await call<{ turn_id: number }>(
|
||||
'POST',
|
||||
rpc('agent', IAgentRPCService, 'prompt', { sid: id, aid: 'main' }),
|
||||
{ input: [{ type: 'text', text: 'hello title' }] },
|
||||
);
|
||||
expect(body.code).toBe(0);
|
||||
sub.dispose();
|
||||
|
||||
const meta = await call<SessionMetaWire>('POST', rpc('session', ISessionMetadata, 'read', { sid: id }));
|
||||
expect(meta.body.code).toBe(0);
|
||||
expect(meta.body.data.title).toBe('hello title');
|
||||
expect(meta.body.data.lastPrompt).toBe('hello title');
|
||||
|
||||
const updated = events.find((e) => e.type === 'session.meta.updated');
|
||||
expect(updated).toBeDefined();
|
||||
const payload = updated?.payload as
|
||||
| { title?: string; patch?: { lastPrompt?: string } }
|
||||
| undefined;
|
||||
expect(payload?.title).toBe('hello title');
|
||||
expect(payload?.patch?.lastPrompt).toBe('hello title');
|
||||
});
|
||||
|
||||
it('keeps a custom title and only refreshes lastPrompt on a later prompt', async () => {
|
||||
const id = await createSession(home as string);
|
||||
await createMainAgent(id);
|
||||
|
||||
const renamed = await call<null>('POST', rpc('session', ISessionMetadata, 'setTitle', { sid: id }), 'keep-me');
|
||||
expect(renamed.body.code).toBe(0);
|
||||
|
||||
const { body } = await call<{ turn_id: number }>(
|
||||
'POST',
|
||||
rpc('agent', IAgentRPCService, 'prompt', { sid: id, aid: 'main' }),
|
||||
{ input: [{ type: 'text', text: 'should not become the title' }] },
|
||||
);
|
||||
expect(body.code).toBe(0);
|
||||
|
||||
const meta = await call<SessionMetaWire>('POST', rpc('session', ISessionMetadata, 'read', { sid: id }));
|
||||
expect(meta.body.code).toBe(0);
|
||||
expect(meta.body.data.title).toBe('keep-me');
|
||||
expect(meta.body.data.lastPrompt).toBe('should not become the title');
|
||||
});
|
||||
|
||||
it('runs a shell command through the shell command service', async () => {
|
||||
const id = await createSession(home as string);
|
||||
await createMainAgent(id);
|
||||
|
|
@ -562,7 +476,7 @@ describe('server-v2 /api/v1/debug RPC', () => {
|
|||
await createMainAgent(sessionId);
|
||||
const activated = await call<null>(
|
||||
'POST',
|
||||
rpc('agent', IAgentRPCService, 'activatePluginCommand', { sid: sessionId, aid: 'main' }),
|
||||
rpc('agent', IAgentPluginCommandService, 'activate', { sid: sessionId, aid: 'main' }),
|
||||
{ pluginId: 'rpc-plugin', commandName: 'deploy', args: 'prod' },
|
||||
);
|
||||
expect(activated.body.code).toBe(0);
|
||||
|
|
@ -575,7 +489,7 @@ describe('server-v2 /api/v1/debug RPC', () => {
|
|||
const id = await createSession(home as string);
|
||||
const { body } = await call<null>(
|
||||
'POST',
|
||||
rpc('agent', IAgentRPCService, 'prompt', { sid: id, aid: 'does-not-exist' }),
|
||||
rpc('agent', IAgentPromptService, 'submit', { sid: id, aid: 'does-not-exist' }),
|
||||
{ input: [{ type: 'text', text: 'hello' }] },
|
||||
);
|
||||
expect(body.code).toBe(40401);
|
||||
|
|
|
|||
|
|
@ -1,20 +1,14 @@
|
|||
/**
|
||||
* `agentRPCService` — the per-agent RPC surface. Mirrors the `AgentAPI`
|
||||
* subset of `agent-core-v2/agent/rpc/core-api.ts`; every method takes one
|
||||
* payload object. Only the methods still implemented by the engine's RPC
|
||||
* facade live here — the domain services the facade calls directly
|
||||
* (shellCommand / profile / usage / plan / task) have their own contracts in
|
||||
* `agent/services.ts`, reusing the payload/result schemas below.
|
||||
* `PromptPayload.input` mirrors the `PromptPart` subset of `ContentPart`
|
||||
* (text / image_url / video_url) from `agent-core-v2/kosong/contract/message.ts`.
|
||||
* Task wire shapes mirror the `TaskInfo` union in `protocol/src/events.ts`.
|
||||
* Shared agent-scope wire schemas — the payload/result vocabulary reused by
|
||||
* the per-domain contracts in `agent/services.ts` and pinned against the
|
||||
* engine types by `test/contract-parity.ts`. `PromptPayload.input` mirrors the
|
||||
* `PromptPart` subset of `ContentPart` (text / image_url / video_url) from
|
||||
* `agent-core-v2/kosong/contract/message.ts`. Task wire shapes mirror the
|
||||
* `TaskInfo` union in `protocol/src/events.ts`.
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { maybe, noResult } from '../helpers.js';
|
||||
import type { ServiceContract } from '../types.js';
|
||||
|
||||
// ── prompt parts ────────────────────────────────────────────────────────────
|
||||
|
||||
const textPartSchema = z.object({
|
||||
|
|
@ -45,9 +39,6 @@ export const emptyPayloadSchema = z.object({});
|
|||
|
||||
export const promptPayloadSchema = z.object({
|
||||
input: z.array(promptPartSchema),
|
||||
// Mirrors `PromptPayload.disabledTools` in the engine (client-managed
|
||||
// session denylist, full-replace).
|
||||
disabledTools: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
/** Same shape as `SteerPayload` in the engine. */
|
||||
|
|
@ -55,7 +46,7 @@ export const steerPayloadSchema = z.object({
|
|||
input: z.array(promptPartSchema),
|
||||
});
|
||||
|
||||
/** Same shape as `ActivateSkillPayload` in the engine. */
|
||||
/** Same shape as `SkillActivationInput`'s wire subset in the engine. */
|
||||
export const activateSkillPayloadSchema = z.object({
|
||||
name: z.string(),
|
||||
args: z.string().optional(),
|
||||
|
|
@ -129,7 +120,7 @@ export const agentCommandInfoSchema = z.object({
|
|||
source: z.string(),
|
||||
});
|
||||
|
||||
/** Same shape as `RunCommandPayload` in the engine. */
|
||||
/** The facade's `runCommand` input shape. */
|
||||
export const runCommandPayloadSchema = z.object({
|
||||
name: z.string(),
|
||||
args: z.string().optional(),
|
||||
|
|
@ -209,22 +200,3 @@ export const getTaskOutputPayloadSchema = z.object({
|
|||
taskId: z.string(),
|
||||
tail: z.number().optional(),
|
||||
});
|
||||
|
||||
// ── contract ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const agentRpcContract = {
|
||||
prompt: { input: z.tuple([promptPayloadSchema]), output: maybe(promptLaunchResultSchema) },
|
||||
steer: { input: z.tuple([steerPayloadSchema]), output: maybe(promptLaunchResultSchema) },
|
||||
activateSkill: {
|
||||
input: z.tuple([activateSkillPayloadSchema]),
|
||||
output: maybe(promptLaunchResultSchema),
|
||||
},
|
||||
cancel: { input: z.tuple([cancelPayloadSchema]), output: noResult },
|
||||
setPermission: { input: z.tuple([setPermissionPayloadSchema]), output: noResult },
|
||||
getContext: { input: z.tuple([emptyPayloadSchema]), output: agentContextDataSchema },
|
||||
listCommands: {
|
||||
input: z.tuple([emptyPayloadSchema]),
|
||||
output: z.array(agentCommandInfoSchema),
|
||||
},
|
||||
runCommand: { input: z.tuple([runCommandPayloadSchema]), output: noResult },
|
||||
} satisfies ServiceContract;
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
/**
|
||||
* Agent-scope domain service contracts. These mirror the positional-arg
|
||||
* signatures of the engine's domain Services (shellCommand / profile / usage /
|
||||
* plan / task) that the agent facade calls directly; payload and result
|
||||
* schemas are shared with `agent/rpc.ts` (they mirror the same wire shapes).
|
||||
* Agent-scope domain service contracts. These mirror the signatures of the
|
||||
* engine's domain Services (prompt / skill / loop / permissionMode / command /
|
||||
* contextMemory / tokenCounting / shellCommand / profile / usage / plan /
|
||||
* task) that the agent facade calls directly; payload and result schemas are
|
||||
* shared in `agent/schemas.ts` (they mirror the same wire shapes).
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
|
@ -10,13 +11,56 @@ import { z } from 'zod';
|
|||
import { maybe, noResult } from '../helpers.js';
|
||||
import type { ServiceContract } from '../types.js';
|
||||
import {
|
||||
activateSkillPayloadSchema,
|
||||
agentCommandInfoSchema,
|
||||
agentTaskInfoSchema,
|
||||
permissionModeSchema,
|
||||
planDataSchema,
|
||||
promptLaunchResultSchema,
|
||||
promptPayloadSchema,
|
||||
runShellCommandPayloadSchema,
|
||||
setModelResultSchema,
|
||||
shellCommandResultSchema,
|
||||
steerPayloadSchema,
|
||||
usageStatusSchema,
|
||||
} from './rpc.js';
|
||||
} from './schemas.js';
|
||||
|
||||
export const agentPromptContract = {
|
||||
submit: {
|
||||
input: z.tuple([promptPayloadSchema]),
|
||||
output: maybe(promptLaunchResultSchema),
|
||||
},
|
||||
submitSteer: {
|
||||
input: z.tuple([steerPayloadSchema]),
|
||||
output: maybe(promptLaunchResultSchema),
|
||||
},
|
||||
} satisfies ServiceContract;
|
||||
|
||||
export const agentSkillContract = {
|
||||
activate: { input: z.tuple([activateSkillPayloadSchema]), output: promptLaunchResultSchema },
|
||||
} satisfies ServiceContract;
|
||||
|
||||
export const agentLoopContract = {
|
||||
cancelFromUser: { input: z.tuple([z.number().optional()]), output: noResult },
|
||||
} satisfies ServiceContract;
|
||||
|
||||
export const agentPermissionModeContract = {
|
||||
setModeAndBroadcast: { input: z.tuple([permissionModeSchema]), output: noResult },
|
||||
} satisfies ServiceContract;
|
||||
|
||||
export const agentCommandContract = {
|
||||
list: { input: z.tuple([]), output: z.array(agentCommandInfoSchema) },
|
||||
run: { input: z.tuple([z.string(), z.string().optional()]), output: noResult },
|
||||
} satisfies ServiceContract;
|
||||
|
||||
/** `history` items are full `ContextMessage`s, mirrored as `unknown`. */
|
||||
export const agentContextMemoryContract = {
|
||||
get: { input: z.tuple([]), output: z.array(z.unknown()) },
|
||||
} satisfies ServiceContract;
|
||||
|
||||
export const agentTokenCountingContract = {
|
||||
statusSize: { input: z.tuple([]), output: z.number() },
|
||||
} satisfies ServiceContract;
|
||||
|
||||
export const agentShellCommandContract = {
|
||||
run: {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export interface SessionArchivedPayload {
|
|||
readonly sessionId: string;
|
||||
}
|
||||
|
||||
/** Payload of `session.meta.updated` on the global bus (`agent/rpc/prompt-metadata.ts`). */
|
||||
/** Payload of `session.meta.updated` on the global bus (`session/sessionMetadata/promptMetadata.ts`). */
|
||||
export interface SessionMetaUpdatedPayload {
|
||||
readonly agentId: string;
|
||||
readonly sessionId: string;
|
||||
|
|
|
|||
|
|
@ -8,14 +8,20 @@
|
|||
|
||||
import type { KlientContract } from './types.js';
|
||||
import { agentActivityViewContract } from './agent/activity.js';
|
||||
import { agentRpcContract } from './agent/rpc.js';
|
||||
import {
|
||||
agentCommandContract,
|
||||
agentContextMemoryContract,
|
||||
agentFullCompactionContract,
|
||||
agentLoopContract,
|
||||
agentMcpContract,
|
||||
agentPermissionModeContract,
|
||||
agentPlanContract,
|
||||
agentProfileContract,
|
||||
agentPromptContract,
|
||||
agentShellCommandContract,
|
||||
agentSkillContract,
|
||||
agentTaskContract,
|
||||
agentTokenCountingContract,
|
||||
agentUsageContract,
|
||||
} from './agent/services.js';
|
||||
import { authContract, authSummaryContract } from './global/auth.js';
|
||||
|
|
@ -67,7 +73,13 @@ export const globalContract: KlientContract = {
|
|||
sessionQuestionService: sessionQuestionContract,
|
||||
sessionSkillCatalog: sessionSkillCatalogContract,
|
||||
// agent scope
|
||||
agentRPCService: agentRpcContract,
|
||||
agentPromptService: agentPromptContract,
|
||||
agentSkillService: agentSkillContract,
|
||||
agentLoopService: agentLoopContract,
|
||||
agentPermissionModeService: agentPermissionModeContract,
|
||||
agentCommandService: agentCommandContract,
|
||||
agentContextMemoryService: agentContextMemoryContract,
|
||||
agentTokenCountingService: agentTokenCountingContract,
|
||||
agentActivityView: agentActivityViewContract,
|
||||
agentShellCommandService: agentShellCommandContract,
|
||||
agentProfileService: agentProfileContract,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
/**
|
||||
* The agent facade — one `session.agent(id)` handle over the agent-scope
|
||||
* services the wire exposes. Turn-driving calls (prompt / steer / cancel) go
|
||||
* through the `agentRPCService` channel; shell commands, model, usage, plan,
|
||||
* and task calls go straight to their domain services. Prompt streaming is
|
||||
* services the wire exposes. Turn-driving calls (prompt / steer / cancel),
|
||||
* skill activation, permission mode, and commands go straight to their domain
|
||||
* services, as do shell commands, model, usage, plan, and task calls;
|
||||
* `getContext` merges two reads client-side. Prompt streaming is
|
||||
* NOT on this interface: it flows through the agent's `events` hub
|
||||
* (`turn.*`, `assistant.delta`, `tool.call.*`, `prompt.completed`, …).
|
||||
*/
|
||||
|
||||
import type { IAgentRPCService } from '@moonshot-ai/agent-core-v2/agent/rpc/rpc';
|
||||
import type { IAgentCommandService } from '@moonshot-ai/agent-core-v2/agent/command/agentCommand';
|
||||
import type { IAgentContextMemoryService } from '@moonshot-ai/agent-core-v2/agent/contextMemory/contextMemory';
|
||||
import type { IAgentMcpService } from '@moonshot-ai/agent-core-v2/agent/mcp/mcp';
|
||||
import type { IAgentPromptService } from '@moonshot-ai/agent-core-v2/agent/prompt/prompt';
|
||||
import type { IAgentTokenCountingService } from '@moonshot-ai/agent-core-v2/agent/tokenCounting/tokenCounting';
|
||||
import type { IAgentPlanService } from '@moonshot-ai/agent-core-v2/features/plan/plan';
|
||||
import type { IAgentProfileService } from '@moonshot-ai/agent-core-v2/agent/profile/profile';
|
||||
import type { IAgentShellCommandService } from '@moonshot-ai/agent-core-v2/agent/shellCommand/shellCommand';
|
||||
|
|
@ -23,22 +26,22 @@ import type { ScopedCaller } from './session.js';
|
|||
|
||||
// Wire-type aliases derived through the engine service interfaces (keeps
|
||||
// klient free of protocol-package imports).
|
||||
export type PromptLaunchResult = Awaited<ReturnType<IAgentRPCService['prompt']>>;
|
||||
export type PromptLaunchResult = Awaited<ReturnType<IAgentPromptService['submit']>>;
|
||||
export type ShellCommandResult = Awaited<ReturnType<IAgentShellCommandService['run']>>;
|
||||
export type SetModelResult = Awaited<ReturnType<IAgentProfileService['setModel']>>;
|
||||
export type ThinkingLevel = ReturnType<IAgentProfileService['getEffectiveThinkingLevel']>;
|
||||
export type UsageStatus = Awaited<ReturnType<IAgentUsageService['status']>>;
|
||||
export type AgentContextData = Awaited<ReturnType<IAgentRPCService['getContext']>>;
|
||||
export type AgentContextData = {
|
||||
history: ReturnType<IAgentContextMemoryService['get']>;
|
||||
tokenCount: ReturnType<IAgentTokenCountingService['statusSize']>;
|
||||
};
|
||||
export type AgentCommandInfo = Awaited<ReturnType<IAgentCommandService['list']>>[number];
|
||||
export type PlanData = Awaited<ReturnType<IAgentPlanService['status']>>;
|
||||
export type AgentTaskInfo = Awaited<ReturnType<IAgentTaskService['list']>>[number];
|
||||
export type McpServerEntry = ReturnType<IAgentMcpService['list']>[number];
|
||||
|
||||
export interface AgentFacade {
|
||||
prompt(input: {
|
||||
input: readonly ContentPart[];
|
||||
disabledTools?: readonly string[];
|
||||
}): Promise<PromptLaunchResult>;
|
||||
prompt(input: { input: readonly ContentPart[] }): Promise<PromptLaunchResult>;
|
||||
steer(input: { input: readonly ContentPart[] }): Promise<PromptLaunchResult>;
|
||||
/**
|
||||
* Activate a skill as a user-slash activation: the engine renders the skill
|
||||
|
|
@ -81,14 +84,17 @@ export interface AgentFacade {
|
|||
}
|
||||
|
||||
export function createAgentFacade(call: ScopedCaller, scope: ScopeRef): AgentFacade {
|
||||
const rpc = (method: string, payload: unknown): Promise<unknown> =>
|
||||
call(scope, 'agentRPCService', method, [payload]);
|
||||
|
||||
return {
|
||||
prompt: (input) => rpc('prompt', input) as Promise<PromptLaunchResult>,
|
||||
steer: (input) => rpc('steer', input) as Promise<PromptLaunchResult>,
|
||||
activateSkill: (input) => rpc('activateSkill', input) as Promise<PromptLaunchResult>,
|
||||
cancel: (input) => rpc('cancel', input ?? {}) as Promise<void>,
|
||||
prompt: (input) =>
|
||||
call(scope, 'agentPromptService', 'submit', [input]) as Promise<PromptLaunchResult>,
|
||||
steer: (input) =>
|
||||
call(scope, 'agentPromptService', 'submitSteer', [input]) as Promise<PromptLaunchResult>,
|
||||
activateSkill: (input) =>
|
||||
call(scope, 'agentSkillService', 'activate', [input]) as Promise<PromptLaunchResult>,
|
||||
cancel: (input) =>
|
||||
// No turnId sends an empty arg list: `[undefined]` would cross the wire
|
||||
// as `[null]`, and `cancelFromUser(null)` would not match the active turn.
|
||||
call(scope, 'agentLoopService', 'cancelFromUser', input?.turnId === undefined ? [] : [input.turnId]) as Promise<void>,
|
||||
runShellCommand: (input) =>
|
||||
call(scope, 'agentShellCommandService', 'run', [input]) as Promise<ShellCommandResult>,
|
||||
cancelShellCommand: (input) =>
|
||||
|
|
@ -100,11 +106,27 @@ export function createAgentFacade(call: ScopedCaller, scope: ScopeRef): AgentFac
|
|||
call(scope, 'agentProfileService', 'getEffectiveThinkingLevel', []) as Promise<ThinkingLevel>,
|
||||
setThinking: (level) =>
|
||||
call(scope, 'agentProfileService', 'setThinking', [level]) as Promise<void>,
|
||||
setPermission: (mode) => rpc('setPermission', { mode }) as Promise<void>,
|
||||
setPermission: (mode) =>
|
||||
call(scope, 'agentPermissionModeService', 'setModeAndBroadcast', [mode]) as Promise<void>,
|
||||
getUsage: () => call(scope, 'agentUsageService', 'status', []) as Promise<UsageStatus>,
|
||||
getContext: () => rpc('getContext', {}) as Promise<AgentContextData>,
|
||||
listCommands: () => rpc('listCommands', {}) as Promise<readonly AgentCommandInfo[]>,
|
||||
runCommand: (input) => rpc('runCommand', input) as Promise<void>,
|
||||
getContext: async () => {
|
||||
const [history, tokenCount] = await Promise.all([
|
||||
call(scope, 'agentContextMemoryService', 'get', []),
|
||||
call(scope, 'agentTokenCountingService', 'statusSize', []),
|
||||
]);
|
||||
return { history, tokenCount } as AgentContextData;
|
||||
},
|
||||
listCommands: () =>
|
||||
call(scope, 'agentCommandService', 'list', []) as Promise<readonly AgentCommandInfo[]>,
|
||||
runCommand: (input) =>
|
||||
// Same `[undefined]` → `[null]` wire hazard as `cancel`: the engine's
|
||||
// `args = ''` default only applies to a missing arg.
|
||||
call(
|
||||
scope,
|
||||
'agentCommandService',
|
||||
'run',
|
||||
input.args === undefined ? [input.name] : [input.name, input.args],
|
||||
) as Promise<void>,
|
||||
getPlan: () => call(scope, 'agentPlanService', 'status', []) as Promise<PlanData>,
|
||||
enterPlan: () => call(scope, 'agentPlanService', 'enter', []) as Promise<void>,
|
||||
clearPlan: () => call(scope, 'agentPlanService', 'clear', []) as Promise<void>,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,13 @@ import { ISessionInteractionService } from '@moonshot-ai/agent-core-v2/session/i
|
|||
import { ISessionApprovalService } from '@moonshot-ai/agent-core-v2/session/approval/approval';
|
||||
import { ISessionQuestionService } from '@moonshot-ai/agent-core-v2/session/question/question';
|
||||
import { ISessionSkillCatalog } from '@moonshot-ai/agent-core-v2/session/sessionSkillCatalog/skillCatalog';
|
||||
import { IAgentRPCService } from '@moonshot-ai/agent-core-v2/agent/rpc/rpc';
|
||||
import { IAgentPromptService } from '@moonshot-ai/agent-core-v2/agent/prompt/prompt';
|
||||
import { IAgentSkillService } from '@moonshot-ai/agent-core-v2/agent/skill/skill';
|
||||
import { IAgentLoopService } from '@moonshot-ai/agent-core-v2/agent/loop/loop';
|
||||
import { IAgentPermissionModeService } from '@moonshot-ai/agent-core-v2/agent/permissionMode/permissionMode';
|
||||
import { IAgentCommandService } from '@moonshot-ai/agent-core-v2/agent/command/agentCommand';
|
||||
import { IAgentContextMemoryService } from '@moonshot-ai/agent-core-v2/agent/contextMemory/contextMemory';
|
||||
import { IAgentTokenCountingService } from '@moonshot-ai/agent-core-v2/agent/tokenCounting/tokenCounting';
|
||||
import { IAgentActivityView } from '@moonshot-ai/agent-core-v2/agent/activityView/activityView';
|
||||
import { IAgentPlanService } from '@moonshot-ai/agent-core-v2/features/plan/plan';
|
||||
import { IAgentProfileService } from '@moonshot-ai/agent-core-v2/agent/profile/profile';
|
||||
|
|
@ -63,7 +69,13 @@ export const serviceTokens: Readonly<Record<string, ServiceIdentifier<unknown>>>
|
|||
sessionApprovalService: ISessionApprovalService,
|
||||
sessionQuestionService: ISessionQuestionService,
|
||||
sessionSkillCatalog: ISessionSkillCatalog,
|
||||
agentRPCService: IAgentRPCService,
|
||||
agentPromptService: IAgentPromptService,
|
||||
agentSkillService: IAgentSkillService,
|
||||
agentLoopService: IAgentLoopService,
|
||||
agentPermissionModeService: IAgentPermissionModeService,
|
||||
agentCommandService: IAgentCommandService,
|
||||
agentContextMemoryService: IAgentContextMemoryService,
|
||||
agentTokenCountingService: IAgentTokenCountingService,
|
||||
agentActivityView: IAgentActivityView,
|
||||
agentShellCommandService: IAgentShellCommandService,
|
||||
agentProfileService: IAgentProfileService,
|
||||
|
|
|
|||
|
|
@ -22,23 +22,15 @@ import type {
|
|||
TurnPhase,
|
||||
} from '@moonshot-ai/agent-core-v2/agent/activityView/activityView';
|
||||
import type { AgentContextData } from '@moonshot-ai/agent-core-v2/agent/contextMemory/types';
|
||||
import type { IAgentCommandService } from '@moonshot-ai/agent-core-v2/agent/command/agentCommand';
|
||||
import type { TurnEndReason } from '@moonshot-ai/agent-core-v2/agent/loop/turnEvents';
|
||||
import type { PermissionMode } from '@moonshot-ai/agent-core-v2/agent/permissionPolicy/types';
|
||||
import type { IAgentProfileService } from '@moonshot-ai/agent-core-v2/agent/profile/profile';
|
||||
import type { IAgentPromptService } from '@moonshot-ai/agent-core-v2/agent/prompt/prompt';
|
||||
import type { IAgentShellCommandService } from '@moonshot-ai/agent-core-v2/agent/shellCommand/shellCommand';
|
||||
import type { IAgentSkillService } from '@moonshot-ai/agent-core-v2/agent/skill/skill';
|
||||
import type { ContentPart } from '@moonshot-ai/agent-core-v2/kosong/contract/message';
|
||||
import type { PlanData } from '@moonshot-ai/agent-core-v2/features/plan/plan';
|
||||
import type {
|
||||
ActivateSkillPayload,
|
||||
AgentAPI,
|
||||
CancelPlanPayload,
|
||||
CancelShellCommandPayload,
|
||||
EmptyPayload,
|
||||
GetTaskOutputPayload,
|
||||
GetTasksPayload,
|
||||
PromptPart,
|
||||
RunShellCommandPayload,
|
||||
SetModelPayload,
|
||||
SetModelResult,
|
||||
ShellCommandResult,
|
||||
StopTaskPayload,
|
||||
} from '@moonshot-ai/agent-core-v2/agent/rpc/core-api';
|
||||
import type { UsageStatus } from '@moonshot-ai/agent-core-v2/agent/usage/usage';
|
||||
import type { SkillSummary } from '@moonshot-ai/agent-core-v2/app/skillCatalog/types';
|
||||
import type { McpServerEntry } from '@moonshot-ai/agent-core-v2/mcpCore/connection-manager';
|
||||
|
|
@ -180,7 +172,7 @@ import {
|
|||
stopTaskPayloadSchema,
|
||||
tokenUsageSchema,
|
||||
usageStatusSchema,
|
||||
} from '../src/contract/agent/rpc.js';
|
||||
} from '../src/contract/agent/schemas.js';
|
||||
import {
|
||||
assistantDeltaEventSchema,
|
||||
compactionBlockedEventSchema,
|
||||
|
|
@ -293,6 +285,7 @@ import {
|
|||
} from '../src/contract/global/workspaces.js';
|
||||
|
||||
import type { AssertWire, MutableDeep } from './helpers/typeAssert.js';
|
||||
import type { AgentFacade } from '../src/core/facade/agent.js';
|
||||
|
||||
/** One-directional: the engine type must be assignable TO the schema's infer. */
|
||||
type AssertEngineToWire<TSchema extends z.ZodType, TEngine> = [MutableDeep<TEngine>] extends [
|
||||
|
|
@ -521,20 +514,32 @@ const _activityViewLifecycle: AssertWire<typeof activityViewLifecycleSchema, Act
|
|||
const _agentActivityState: AssertEngineToWire<typeof agentActivityStateSchema, AgentActivityState> =
|
||||
true;
|
||||
|
||||
// ── agent scope (rpc.ts) ────────────────────────────────────────────────────
|
||||
// Payload/result types for the remaining `AgentAPI` methods are reached
|
||||
// through the interface so the assertions track the exact methods the
|
||||
// contract mirrors; payloads of the domain services the facade calls
|
||||
// directly (shellCommand / profile / usage / plan / task) are imported from
|
||||
// `core-api.ts` (they no longer have `AgentAPI` entries).
|
||||
type PromptPayload = Parameters<AgentAPI['prompt']>[0];
|
||||
type PromptLaunchResult = NonNullable<ReturnType<AgentAPI['prompt']>>;
|
||||
type SteerPayload = Parameters<AgentAPI['steer']>[0];
|
||||
type CancelPayload = Parameters<AgentAPI['cancel']>[0];
|
||||
type SetPermissionPayload = Parameters<AgentAPI['setPermission']>[0];
|
||||
type AgentCommandInfo = Awaited<ReturnType<AgentAPI['listCommands']>>[number];
|
||||
type RunCommandPayload = Parameters<AgentAPI['runCommand']>[0];
|
||||
// ── agent scope (services.ts / schemas.ts) ──────────────────────────────────
|
||||
// Payload/result types are derived from the domain service interfaces the
|
||||
// facade calls, so the assertions track the exact methods the contract
|
||||
// mirrors; facade-only payload shapes (cancel / setPermission / plan / task /
|
||||
// command) derive from the `AgentFacade` input types.
|
||||
type PromptPayload = Parameters<IAgentPromptService['submit']>[0];
|
||||
type PromptLaunchResult = NonNullable<Awaited<ReturnType<IAgentPromptService['submit']>>>;
|
||||
type SteerPayload = Parameters<IAgentPromptService['submitSteer']>[0];
|
||||
type ActivateSkillPayload = Parameters<IAgentSkillService['activate']>[0];
|
||||
type AgentCommandInfo = ReturnType<IAgentCommandService['list']>[number];
|
||||
type RunShellCommandPayload = Parameters<IAgentShellCommandService['run']>[0];
|
||||
type ShellCommandResult = Awaited<ReturnType<IAgentShellCommandService['run']>>;
|
||||
type SetModelResult = Awaited<ReturnType<IAgentProfileService['setModel']>>;
|
||||
type TokenUsage = NonNullable<UsageStatus['total']>;
|
||||
type PromptPart = Extract<ContentPart, { type: 'text' | 'image_url' | 'video_url' }>;
|
||||
|
||||
type EmptyPayload = {};
|
||||
type CancelPayload = NonNullable<Parameters<AgentFacade['cancel']>[0]>;
|
||||
type SetPermissionPayload = { mode: PermissionMode };
|
||||
type RunCommandPayload = Parameters<AgentFacade['runCommand']>[0];
|
||||
type CancelShellCommandPayload = Parameters<AgentFacade['cancelShellCommand']>[0];
|
||||
type SetModelPayload = { model: string };
|
||||
type CancelPlanPayload = NonNullable<Parameters<AgentFacade['cancelPlan']>[0]>;
|
||||
type GetTasksPayload = NonNullable<Parameters<AgentFacade['getTasks']>[0]>;
|
||||
type StopTaskPayload = Parameters<AgentFacade['stopTask']>[0];
|
||||
type GetTaskOutputPayload = Parameters<AgentFacade['getTaskOutput']>[0];
|
||||
|
||||
const _emptyPayload: AssertWire<typeof emptyPayloadSchema, EmptyPayload> = true;
|
||||
const _promptPart: AssertWire<typeof promptPartSchema, PromptPart> = true;
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ describe('session skills routing', () => {
|
|||
expect(seen).toEqual(['workspace']);
|
||||
});
|
||||
|
||||
it('activateSkill routes to agentRPCService with the agent scope', async () => {
|
||||
it('activateSkill routes to agentSkillService with the agent scope', async () => {
|
||||
const channel = new FakeChannel();
|
||||
const klient = createKlientFromChannel(channel);
|
||||
const agent = klient.session('s1').agent('main');
|
||||
|
|
@ -233,11 +233,69 @@ describe('session skills routing', () => {
|
|||
});
|
||||
expect(channel.calls[0]).toEqual({
|
||||
scope: { sessionId: 's1', agentId: 'main' },
|
||||
service: 'agentRPCService',
|
||||
method: 'activateSkill',
|
||||
service: 'agentSkillService',
|
||||
method: 'activate',
|
||||
args: [{ name: 'review', args: 'src/app.ts' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('turn-driving calls route to their domain services with the agent scope', async () => {
|
||||
const channel = new FakeChannel();
|
||||
const klient = createKlientFromChannel(channel);
|
||||
const agent = klient.session('s1').agent('main');
|
||||
const scope = { sessionId: 's1', agentId: 'main' };
|
||||
|
||||
channel.results.set('agentPromptService.submit', { turn_id: 1 });
|
||||
channel.results.set('agentPromptService.submitSteer', { turn_id: 1 });
|
||||
channel.results.set('agentCommandService.list', []);
|
||||
await agent.prompt({ input: [{ type: 'text', text: 'hi' }] });
|
||||
await agent.steer({ input: [{ type: 'text', text: 'steer' }] });
|
||||
await agent.cancel({ turnId: 2 });
|
||||
await agent.cancel();
|
||||
await agent.setPermission('yolo');
|
||||
await agent.listCommands();
|
||||
await agent.runCommand({ name: 'cmd', args: 'a b' });
|
||||
await agent.runCommand({ name: 'plain' });
|
||||
|
||||
expect(channel.calls).toEqual([
|
||||
{
|
||||
scope,
|
||||
service: 'agentPromptService',
|
||||
method: 'submit',
|
||||
args: [{ input: [{ type: 'text', text: 'hi' }] }],
|
||||
},
|
||||
{
|
||||
scope,
|
||||
service: 'agentPromptService',
|
||||
method: 'submitSteer',
|
||||
args: [{ input: [{ type: 'text', text: 'steer' }] }],
|
||||
},
|
||||
{ scope, service: 'agentLoopService', method: 'cancelFromUser', args: [2] },
|
||||
{ scope, service: 'agentLoopService', method: 'cancelFromUser', args: [] },
|
||||
{ scope, service: 'agentPermissionModeService', method: 'setModeAndBroadcast', args: ['yolo'] },
|
||||
{ scope, service: 'agentCommandService', method: 'list', args: [] },
|
||||
{ scope, service: 'agentCommandService', method: 'run', args: ['cmd', 'a b'] },
|
||||
{ scope, service: 'agentCommandService', method: 'run', args: ['plain'] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('getContext merges the contextMemory and tokenCounting reads', async () => {
|
||||
const channel = new FakeChannel();
|
||||
const klient = createKlientFromChannel(channel);
|
||||
const agent = klient.session('s1').agent('main');
|
||||
const scope = { sessionId: 's1', agentId: 'main' };
|
||||
|
||||
channel.results.set('agentContextMemoryService.get', [{ role: 'user' }]);
|
||||
channel.results.set('agentTokenCountingService.statusSize', 42);
|
||||
await expect(agent.getContext()).resolves.toEqual({
|
||||
history: [{ role: 'user' }],
|
||||
tokenCount: 42,
|
||||
});
|
||||
expect(channel.calls).toEqual([
|
||||
{ scope, service: 'agentContextMemoryService', method: 'get', args: [] },
|
||||
{ scope, service: 'agentTokenCountingService', method: 'statusSize', args: [] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('agent mcp / compaction routing', () => {
|
||||
|
|
|
|||
|
|
@ -71,12 +71,6 @@ const MAIN_AGENT_ID = 'main';
|
|||
export interface SessionPromptRpcInput {
|
||||
readonly sessionId: string;
|
||||
readonly input: PromptInput;
|
||||
/**
|
||||
* Client-managed session tool denylist (full-replace semantics), forwarded
|
||||
* to engines with profile tool gating. Omit to keep the persisted value;
|
||||
* `[]` clears the client portion.
|
||||
*/
|
||||
readonly disabledTools?: readonly string[];
|
||||
}
|
||||
|
||||
export interface SessionIdRpcInput {
|
||||
|
|
@ -386,7 +380,6 @@ export abstract class SDKRpcClientBase {
|
|||
sessionId: input.sessionId,
|
||||
agentId,
|
||||
input: input.input,
|
||||
disabledTools: input.disabledTools,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,10 +57,10 @@
|
|||
* too (default-profile bind + permission mode).
|
||||
* - `prompt` / `steer` / `runShellCommand` / `cancelShellCommand` → the
|
||||
* `klient.session(id).agent(id)` facade; `activatePluginCommand` →
|
||||
* `IAgentRPCService` through the agent scope; `activateSkill` →
|
||||
* `IAgentSkillService` through the agent scope (the RPC service's
|
||||
* fire-and-forget variant would swallow v1's synchronous rejections) plus
|
||||
* v1's main-only metadata update; `generateAgentsMd` →
|
||||
* `IAgentPluginCommandService` through the agent scope; `activateSkill` →
|
||||
* `IAgentSkillService` through the agent scope (the engine settles
|
||||
* `{turn_id}` and applies v1's main-only metadata update itself);
|
||||
* `generateAgentsMd` →
|
||||
* `ISessionInitService` through the session scope; `getSessionWarnings` →
|
||||
* rebuilt over the profile's cached AGENTS.md warning plus the engine's
|
||||
* `prepareSystemPromptContext` (no v2 aggregate service exists).
|
||||
|
|
@ -157,7 +157,6 @@ import { wrapSubagentModelError } from '@moonshot-ai/agent-core-v2/session/subag
|
|||
import { loadMcpServers } from '@moonshot-ai/agent-core-v2/workspace/workspaceMcpConfig/internal/config-loader';
|
||||
import type { McpServerConfig as WorkspaceMcpServerConfig } from '@moonshot-ai/agent-core-v2/mcpCore/config-schema';
|
||||
import {
|
||||
applyPromptMetadataUpdate,
|
||||
bootstrap,
|
||||
DEFAULT_AGENT_PROFILE_NAME,
|
||||
drainQueryStoreDisposals,
|
||||
|
|
@ -167,6 +166,7 @@ import {
|
|||
IAgentActivityView,
|
||||
IAgentContextInjectorService,
|
||||
IAgentContextMemoryService,
|
||||
IAgentConversationUndoService,
|
||||
IAgentFullCompactionService,
|
||||
IAgentGoalService,
|
||||
IAgentPluginService,
|
||||
|
|
@ -174,12 +174,14 @@ import {
|
|||
IAgentLoopService,
|
||||
IAgentPermissionModeService,
|
||||
IAgentPermissionRulesService,
|
||||
IAgentPluginCommandService,
|
||||
IAgentProfileService,
|
||||
IAgentRPCService,
|
||||
IAgentSkillService,
|
||||
IAgentSwarmService,
|
||||
IAgentTaskService,
|
||||
IAgentTokenCountingService,
|
||||
IAgentToolPolicyService,
|
||||
IAgentToolRegistryService,
|
||||
IBootstrapService,
|
||||
IConfigService,
|
||||
IEventService,
|
||||
|
|
@ -223,7 +225,6 @@ import {
|
|||
PRINT_WAIT_CEILING_S_DEFAULT,
|
||||
ProfileError,
|
||||
ProfileErrors,
|
||||
promptMetadataTextFromSkill,
|
||||
resolveAgentTaskConfig,
|
||||
resolveConfigPath,
|
||||
resolveKimiHome,
|
||||
|
|
@ -965,6 +966,13 @@ export class SDKRpcClientV2 extends SDKRpcClientBase {
|
|||
foldAgentWireReplay(join(ctx.sessionDir, 'agents', agent.id, 'wire.jsonl')),
|
||||
]);
|
||||
const profile = agent.accessor.get(IAgentProfileService).data();
|
||||
const toolPolicy = agent.accessor.get(IAgentToolPolicyService);
|
||||
const tools = agent.accessor.get(IAgentToolRegistryService).list().map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
active: toolPolicy.isToolActive(tool.name, tool.source),
|
||||
source: tool.source,
|
||||
}));
|
||||
return {
|
||||
type,
|
||||
config: {
|
||||
|
|
@ -985,7 +993,7 @@ export class SDKRpcClientV2 extends SDKRpcClientBase {
|
|||
plan: plan as ResumedAgentState['plan'],
|
||||
swarmMode: agent.accessor.get(IAgentSwarmService).isActive,
|
||||
usage: usage as ResumedAgentState['usage'],
|
||||
tools: agent.accessor.get(IAgentRPCService).getTools({}) as ResumedAgentState['tools'],
|
||||
tools: tools as ResumedAgentState['tools'],
|
||||
toolStore: folded.toolStore,
|
||||
background: background as readonly BackgroundTaskInfo[],
|
||||
};
|
||||
|
|
@ -1525,20 +1533,21 @@ export class SDKRpcClientV2 extends SDKRpcClientBase {
|
|||
return agent.clearPlan();
|
||||
}
|
||||
|
||||
/** Facade (`agentRPCService.listCommands`) — the v2-only contributed-command seam. */
|
||||
/** Facade (`agentCommandService.list`) — the v2-only contributed-command seam. */
|
||||
override async listCommands(input: SessionIdRpcInput): Promise<readonly AgentCommandInfo[]> {
|
||||
const agent = await this.agentFacade(input.sessionId);
|
||||
return agent.listCommands();
|
||||
}
|
||||
|
||||
/** Facade (`agentRPCService.runCommand`) — runs the contribution engine-side. */
|
||||
/** Facade (`agentCommandService.run`) — runs the contribution engine-side. */
|
||||
override async runCommand(input: RunCommandRpcInput): Promise<void> {
|
||||
const agent = await this.agentFacade(input.sessionId);
|
||||
return agent.runCommand({ name: input.name, args: input.args });
|
||||
}
|
||||
|
||||
/**
|
||||
* Facade (`agentRPCService.getContext`). The v2 `AgentContextData` is the
|
||||
* Facade (`getContext`, merged client-side from `agentContextMemoryService.get`
|
||||
* and `agentTokenCountingService.statusSize`). The v2 `AgentContextData` is the
|
||||
* same wire shape as v1's — the cast only bridges the two packages' type
|
||||
* declarations (v2's origin union carries kinds a v1 client never sees in
|
||||
* practice); the data itself crossed the same JSON boundary on both sides.
|
||||
|
|
@ -1615,18 +1624,18 @@ export class SDKRpcClientV2 extends SDKRpcClientBase {
|
|||
}
|
||||
|
||||
/**
|
||||
* Through the agent scope (`IAgentRPCService.cancelCompaction`, the v2 RPC
|
||||
* surface's own cancel) — no klient facade exists. Aborts the in-flight
|
||||
* compaction; a no-op when idle, like v1.
|
||||
* Through the agent scope (`IAgentFullCompactionService.cancel`) — no
|
||||
* klient facade exists. Aborts the in-flight compaction; a no-op when idle,
|
||||
* like v1.
|
||||
*/
|
||||
override async cancelCompaction(input: SessionIdRpcInput): Promise<void> {
|
||||
const agent = await this.agentScope(input.sessionId);
|
||||
await agent.accessor.get(IAgentRPCService).cancelCompaction({});
|
||||
agent.accessor.get(IAgentFullCompactionService).cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Through the agent scope (`IAgentRPCService.undoHistory`, the v2 RPC
|
||||
* surface's own undo) — no klient facade exists; the returned count is
|
||||
* Through the agent scope (`IAgentConversationUndoService.undo`) — no
|
||||
* klient facade exists; the returned count is
|
||||
* dropped (v1 returns void). Failure semantics differ by design: v2
|
||||
* prechecks and rejects atomically with `session.undo_unavailable`, while
|
||||
* v1 splices a partial suffix out of the live history and then throws
|
||||
|
|
@ -1634,7 +1643,7 @@ export class SDKRpcClientV2 extends SDKRpcClientBase {
|
|||
*/
|
||||
override async undoHistory(input: SessionIdRpcInput & { count: number }): Promise<void> {
|
||||
const agent = await this.agentScope(input.sessionId);
|
||||
await agent.accessor.get(IAgentRPCService).undoHistory({ count: input.count });
|
||||
await agent.accessor.get(IAgentConversationUndoService).undo(input.count);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1682,24 +1691,22 @@ export class SDKRpcClientV2 extends SDKRpcClientBase {
|
|||
}
|
||||
|
||||
/**
|
||||
* Facade (`agentRPCService.prompt`). The launch result (`{turn_id}`, or
|
||||
* Facade (`agentPromptService.submit`). The launch result (`{turn_id}`, or
|
||||
* `undefined` when the prompt queued behind a running turn) is dropped —
|
||||
* v1's RPC returns void. The pre-provider surface matches v1: the metadata
|
||||
* update (title/lastPrompt) runs through the same shared helpers before the
|
||||
* turn launches, and a model-less turn fails asynchronously exactly like
|
||||
* v1's. Two enqueue-semantics gaps vs v1, pinned in the migration tracker:
|
||||
* v1's. One enqueue-semantics gap vs v1, pinned in the migration tracker:
|
||||
* v1 drops a prompt submitted while a turn is active (error event only)
|
||||
* where v2 queues it FIFO, and v1 never consumes `disabledTools` (the
|
||||
* payload field reaches the agent RPC but no code reads it) where v2
|
||||
* applies it as the session tool denylist.
|
||||
* where v2 queues it FIFO.
|
||||
*/
|
||||
override async prompt(input: SessionPromptRpcInput): Promise<void> {
|
||||
const agent = await this.agentFacade(input.sessionId);
|
||||
await agent.prompt({ input: input.input, disabledTools: input.disabledTools });
|
||||
await agent.prompt({ input: input.input });
|
||||
}
|
||||
|
||||
/**
|
||||
* Facade (`agentRPCService.steer`). Matches v1 on both paths: mid-turn
|
||||
* Facade (`agentPromptService.submitSteer`). Matches v1 on both paths: mid-turn
|
||||
* steers join the running turn, and an idle-session steer degrades to
|
||||
* launching a fresh turn (the enqueue launches it directly) while
|
||||
* title/lastPrompt are updated like a prompt's.
|
||||
|
|
@ -1738,40 +1745,32 @@ export class SDKRpcClientV2 extends SDKRpcClientBase {
|
|||
}
|
||||
|
||||
/**
|
||||
* Through the agent scope (`IAgentSkillService.activate`) — deliberately
|
||||
* NOT `IAgentRPCService.activateSkill`, whose `void this.skills.activate(...)`
|
||||
* fire-and-forget turns v1's synchronous rejections (`skill.not_found` /
|
||||
* `skill.type_unsupported`) into unhandled rejections. The direct call keeps
|
||||
* v1's semantics: validate first, then render the skill prompt and launch a
|
||||
* turn with it. v1's session layer then updates title/lastPrompt for the
|
||||
* MAIN agent only; replicated here over the engine's shared metadata
|
||||
* helpers. Busy-turn gap vs v1, pinned in the migration tracker: v1 drops
|
||||
* Through the agent scope (`IAgentSkillService.activate`) — the direct call
|
||||
* keeps v1's semantics: validate first (`skill.not_found` /
|
||||
* `skill.type_unsupported` reject synchronously), then render the skill
|
||||
* prompt and launch a turn with it. The engine updates title/lastPrompt for
|
||||
* the MAIN agent only, matching v1's session layer. Busy-turn gap vs v1,
|
||||
* pinned in the migration tracker: v1 drops
|
||||
* the activation into an error event while a turn runs; v2's activate
|
||||
* awaits the queued prompt's launch.
|
||||
*/
|
||||
override async activateSkill(input: ActivateSkillRpcInput): Promise<void> {
|
||||
const agent = await this.agentScope(input.sessionId);
|
||||
await agent.accessor.get(IAgentSkillService).activate({ name: input.name, args: input.args });
|
||||
if (this.interactiveAgentId === MAIN_AGENT_ID) {
|
||||
await this.updatePromptMetadata(input.sessionId, promptMetadataTextFromSkill(input));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Through the agent scope (`IAgentRPCService.activatePluginCommand`) — the
|
||||
* v2 RPC surface's own implementation: the same `request.invalid` rejection
|
||||
* text for an unknown command, the same argument expansion, the activation
|
||||
* event, the prompt enqueue, and the metadata update. Two gaps vs v1,
|
||||
* Through the agent scope (`IAgentPluginCommandService.activate`): the same
|
||||
* `request.invalid` rejection text for an unknown command, the same
|
||||
* argument expansion, the activation event, the prompt enqueue, and the
|
||||
* main-agent-only metadata update. Two gaps vs v1,
|
||||
* pinned in the migration tracker: v1 resolves the command against the
|
||||
* session's creation-time snapshot (v2 uses the app-global live view), and
|
||||
* v1 drops the activation while a turn runs where v2 queues it. v1 also
|
||||
* updates title/lastPrompt for the main agent only, where the v2 RPC does
|
||||
* it unconditionally — only observable through a non-main
|
||||
* `interactiveAgentId`.
|
||||
* v1 drops the activation while a turn runs where v2 queues it.
|
||||
*/
|
||||
override async activatePluginCommand(input: ActivatePluginCommandRpcInput): Promise<void> {
|
||||
const agent = await this.agentScope(input.sessionId);
|
||||
await agent.accessor.get(IAgentRPCService).activatePluginCommand({
|
||||
await agent.accessor.get(IAgentPluginCommandService).activate({
|
||||
pluginId: input.pluginId,
|
||||
commandName: input.commandName,
|
||||
args: input.args,
|
||||
|
|
@ -1796,8 +1795,7 @@ export class SDKRpcClientV2 extends SDKRpcClientBase {
|
|||
}
|
||||
|
||||
/**
|
||||
* No v2 service implements the session-warnings aggregate (`ISessionRPCService`
|
||||
* is an interface without an implementation), so the SDK rebuilds v1's
|
||||
* No v2 service implements the session-warnings aggregate, so the SDK rebuilds v1's
|
||||
* `Session.getSessionWarnings` over v2 primitives: the profile's cached
|
||||
* `agentsMdWarning` (computed on every bind, v1's bootstrap-time cache),
|
||||
* recomputed through the engine's own `prepareSystemPromptContext` when the
|
||||
|
|
@ -1842,23 +1840,6 @@ export class SDKRpcClientV2 extends SDKRpcClientBase {
|
|||
return warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* v1's session-layer prompt-metadata update (title/lastPrompt), rebuilt
|
||||
* over the engine's shared helper so skill/plugin-command activations land
|
||||
* on the same metadata the native v2 prompt path writes.
|
||||
*/
|
||||
private async updatePromptMetadata(sessionId: string, text: string | undefined): Promise<void> {
|
||||
const session = this.requireLiveSession(sessionId);
|
||||
await applyPromptMetadataUpdate(
|
||||
{
|
||||
metadata: session.accessor.get(ISessionMetadata),
|
||||
eventService: this.engineAccessor.get(IEventService),
|
||||
sessionId,
|
||||
},
|
||||
text,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Through the session scope (`ISessionBtwService`) — no klient facade
|
||||
* exists. The v2 service is the port of v1's btw fork: same inherited
|
||||
|
|
|
|||
|
|
@ -2476,8 +2476,8 @@ describe('v1↔v2 agent interaction parity', () => {
|
|||
// Model-less on purpose: parity covers the pre-provider surface — the
|
||||
// call returns without throwing and the metadata update (same shared
|
||||
// helpers on both engines) lands before the turn fails asynchronously.
|
||||
// The enqueue-semantics gaps (v1 drops a mid-turn prompt, v2 queues it;
|
||||
// v1 ignores disabledTools, v2 applies it) are pinned in the tracker.
|
||||
// The enqueue-semantics gap (v1 drops a mid-turn prompt, v2 queues it) is
|
||||
// pinned in the tracker.
|
||||
const pair = await makeSessionParityPair();
|
||||
try {
|
||||
await createOnBoth(pair, { id: 'session_parity_agent_prompt' });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue