Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/ACP-subagent-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@moonshot-ai/kimi-code': patch
---

ACP: forward subagent lifecycle and streams. `subagent.spawned/started/suspended/completed/failed` events are now emitted as ordinary `tool_call`/`tool_call_update` session updates carrying a `_meta.kimiCode.subagent` payload (subagentId, parentToolCallId, summary/usage on completion), and the subagent's own assistant/thinking/tool frames are forwarded with `_meta.kimiCode.subagentId` instead of being dropped at the main-agent guard. Clients that ignore `_meta` see a flat but complete tool stream; the capability is advertised as `agentCapabilities._meta.kimiCode.subagentEvents`.
23 changes: 23 additions & 0 deletions docs/en/reference/kimi-acp.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,29 @@ The spec divides methods into a **stable** surface and an evolving **unstable**

All methods not listed above return `methodNotFound`.

## Extension frames (`_meta.kimiCode`)

Beyond the stable surface, the adapter attaches vendor metadata under the
`_meta.kimiCode` key (per the ACP extensibility rules; clients that ignore
`_meta` see ordinary standard frames). Support is advertised during
`initialize` as `agentCapabilities._meta.kimiCode.subagentEvents`.

### Subagent lifecycle

`subagent.spawned/started/suspended/completed/failed` engine events are
forwarded as ordinary `tool_call` / `tool_call_update` updates:

- `spawned` creates one card per subagent with `toolCallId:
subagent:<subagentId>` and `_meta.kimiCode.subagent: {event, subagentId,
subagentName, parentToolCallId, description?, swarmIndex?,
runInBackground}`.
- `started` / `suspended` / `completed` / `failed` update that card;
completion carries `resultSummary` / `usage` / `contextTokens`, suspension
carries `reason`, failure carries `error`.
- The subagent's own `assistant.delta` / `thinking.delta` / `tool.call.*`
frames are forwarded with `_meta.kimiCode.subagentId` so clients can nest
them under the subagent's card instead of the main stream.

## MCP Forwarding

When an ACP client provides `mcpServers` in `session/new` or `session/load`, the adapter layer performs the following conversions:
Expand Down
21 changes: 21 additions & 0 deletions docs/zh/reference/kimi-acp.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,27 @@ kimi acp

上述未列出的方法一律返回 `methodNotFound`。

## 扩展帧(`_meta.kimiCode`)

在稳定面之外,适配器遵循 ACP 可扩展性规则,将厂商元数据挂在 `_meta.kimiCode`
键下(忽略 `_meta` 的客户端只会看到普通标准帧)。该能力在 `initialize` 时通过
`agentCapabilities._meta.kimiCode.subagentEvents` 公告。

### 子代理生命周期

引擎的 `subagent.spawned/started/suspended/completed/failed` 事件以普通
`tool_call` / `tool_call_update` 更新转发:

- `spawned` 为每个子代理创建一张卡片,`toolCallId: subagent:<subagentId>`,
并携带 `_meta.kimiCode.subagent: {event, subagentId, subagentName,
parentToolCallId, description?, swarmIndex?, runInBackground}`。
- `started` / `suspended` / `completed` / `failed` 更新该卡片;完成时携带
`resultSummary` / `usage` / `contextTokens`,暂停时携带 `reason`,失败时携带
`error`。
- 子代理自己的 `assistant.delta` / `thinking.delta` / `tool.call.*` 帧带
`_meta.kimiCode.subagentId` 转发,客户端可将其嵌套到子代理卡片下,而不是混进
主流。

## MCP 转发

ACP 客户端在 `session/new` 或 `session/load` 中提供 `mcpServers` 时,适配层做如下转换:
Expand Down
146 changes: 146 additions & 0 deletions packages/acp-adapter/src/events-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import type {
} from '@agentclientprotocol/sdk';
import type {
AssistantDeltaEvent,
SubagentCompletedEvent,
SubagentFailedEvent,
SubagentSpawnedEvent,
SubagentStartedEvent,
SubagentSuspendedEvent,
ThinkingDeltaEvent,
ToolCallDeltaEvent,
ToolCallStartedEvent,
Expand Down Expand Up @@ -525,3 +530,144 @@ export function configOptionUpdateNotification(
},
};
}

/**
* Wire id for a subagent's tool card.
*
* Prefixed so it can never collide with the `${turnId}:${toolCallId}` space
* of real tool calls (see {@link acpToolCallId}), and stable for the whole
* subagent life: `spawned` creates the card once, every later lifecycle event
* is a `tool_call_update` against this id.
*/
export function acpSubagentToolCallId(subagentId: string): string {
return `subagent:${subagentId}`;
}

/** `_meta` payload carried by every subagent lifecycle frame. */
interface SubagentMeta {
event: 'spawned' | 'started' | 'suspended' | 'completed' | 'failed';
subagentId: string;
subagentName?: string;
/**
* The **raw** (unprefixed) id of the `Agent` tool call that spawned this
* subagent. The wire id of the parent's card is `${turnId}:${rawId}`, so
* clients match by suffix — see acpToolCallId.
*/
parentToolCallId?: string;
description?: string;
swarmIndex?: number;
runInBackground?: boolean;
reason?: string;
resultSummary?: string;
usage?: unknown;
contextTokens?: number;
error?: string;
}

function subagentMeta(meta: SubagentMeta): { kimiCode: { subagent: SubagentMeta } } {
// Absent optional fields stay `undefined` — JSON.stringify drops those
// keys, so nothing ever serializes as an explicit `null`.
return { kimiCode: { subagent: meta } };
}

/**
* Map `subagent.spawned` to a `tool_call` CREATE: one card per subagent,
* nested under the spawning `Agent` call via `_meta.kimiCode.subagent
* .parentToolCallId`. Emitted as an ordinary frame (not a custom update
* kind) because v1 SDKs drop unknown `sessionUpdate` kinds — see
* docs/subagent-frames-spec.md in the ACP UI repo.
*/
export function subagentSpawnedToSessionUpdate(
sessionId: string,
event: SubagentSpawnedEvent,
): SessionNotification {
return {
sessionId,
update: {
sessionUpdate: 'tool_call',
toolCallId: acpSubagentToolCallId(event.subagentId),
title: `Subagent · ${event.subagentName}`,
kind: 'other',
status: 'pending',
_meta: subagentMeta({
event: 'spawned',
subagentId: event.subagentId,
subagentName: event.subagentName,
parentToolCallId: event.parentToolCallId,
description: event.description,
swarmIndex: event.swarmIndex,
runInBackground: event.runInBackground,
}),
},
};
}

/**
* Map `subagent.started` / `.suspended` / `.completed` / `.failed` to a
* `tool_call_update` against the card `spawned` created. Status mirrors the
* ACP tool-call lifecycle; suspension stays `in_progress` with the reason in
* `_meta` (ACP has no paused status).
*/
export function subagentLifecycleToSessionUpdate(
sessionId: string,
event: SubagentStartedEvent | SubagentSuspendedEvent | SubagentCompletedEvent | SubagentFailedEvent,
): SessionNotification {
let status: 'in_progress' | 'completed' | 'failed';
const meta: SubagentMeta = {
event: 'started',
subagentId: event.subagentId,
};
switch (event.type) {
case 'subagent.started':
status = 'in_progress';
break;
case 'subagent.suspended':
status = 'in_progress';
meta.event = 'suspended';
meta.reason = event.reason;
break;
case 'subagent.completed':
status = 'completed';
meta.event = 'completed';
meta.resultSummary = event.resultSummary;
meta.usage = event.usage;
meta.contextTokens = event.contextTokens;
break;
case 'subagent.failed':
status = 'failed';
meta.event = 'failed';
meta.error = event.error;
break;
}
return {
sessionId,
update: {
sessionUpdate: 'tool_call_update',
toolCallId: acpSubagentToolCallId(event.subagentId),
status,
_meta: subagentMeta(meta),
},
};
}

/**
* Attach the nesting marker to a frame that belongs to a subagent's own
* stream (its `assistant.delta`, `tool.call.*`, … events). The client nests
* anything carrying `_meta.kimiCode.subagentId` under the subagent's card;
* frames from the main agent are returned untouched.
*/
export function withSubagentMeta<T extends SessionNotification>(
notification: T,
event: { agentId?: string },
): T {
// 'main' is MAIN_AGENT_ID in session.ts; inlined here because session.ts
// already imports this module — the reverse edge would be a cycle.
if (event.agentId === undefined || event.agentId === 'main') return notification;
return {
...notification,
update: {
...notification.update,
_meta: { kimiCode: { subagentId: event.agentId } },
},
};
}
4 changes: 4 additions & 0 deletions packages/acp-adapter/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,10 @@ export class AcpServer implements Agent {
list: {},
resume: {},
},
// This build forwards subagent lifecycle + streams (see events-map.ts):
// advertised per the spec's SHOULD so clients can gate their subagent UI
// on it instead of sniffing frames.
_meta: { kimiCode: { subagentEvents: true } },
};

return {
Expand Down
Loading