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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/agent-core-dev/edge-exposure.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions .agents/skills/agent-core-dev/server-align.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/agent-core-dev/service-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).

Expand Down
4 changes: 2 additions & 2 deletions apps/kimi-inspect/src/channel/channel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion apps/kimi-inspect/src/channel/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 7 additions & 5 deletions apps/kimi-inspect/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
14 changes: 0 additions & 14 deletions apps/kimi-inspect/src/panels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) }),
},
],
},
];
2 changes: 1 addition & 1 deletion packages/agent-core-v2/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface IAgentFullCompactionService {

readonly compacting: FullCompactionTask | null;
begin(input: FullCompactionInput): boolean;
cancel(): void;

readonly hooks: Hooks<{
onWillCompact: FullCompactionTask;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,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;
Expand Down
2 changes: 2 additions & 0 deletions packages/agent-core-v2/src/agent/loop/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ export interface IAgentLoopService {

cancel(turnId?: number, reason?: unknown): boolean;

cancelFromUser(turnId?: number): void;

tryAcquireQuiescence(): IDisposable | undefined;

settled(): Promise<void>;
Expand Down
11 changes: 11 additions & 0 deletions packages/agent-core-v2/src/agent/loop/loopService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (this.activeTurnJob !== undefined || this.hasPendingRequests()) return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface IAgentPermissionModeService {

readonly mode: PermissionMode;
setMode(mode: PermissionMode): void;
setModeAndBroadcast(mode: PermissionMode): void;

readonly onDidChangeMode: Event<PermissionModeChangedContext>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand All @@ -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));
Expand All @@ -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(
Expand Down
41 changes: 41 additions & 0 deletions packages/agent-core-v2/src/agent/pluginCommand/pluginCommand.ts
Original file line number Diff line number Diff line change
@@ -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');
Loading
Loading