@oh-my-pi/pi-coding-agent 15.1.3 → 15.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/types/async/job-manager.d.ts +3 -2
- package/dist/types/main.d.ts +11 -2
- package/dist/types/modes/acp/acp-agent.d.ts +1 -1
- package/dist/types/modes/acp/acp-event-mapper.d.ts +13 -1
- package/dist/types/modes/acp/acp-mode.d.ts +3 -1
- package/dist/types/plan-mode/approved-plan.d.ts +6 -4
- package/dist/types/session/agent-session.d.ts +6 -2
- package/dist/types/session/client-bridge.d.ts +3 -0
- package/dist/types/tools/ast-edit.d.ts +2 -0
- package/dist/types/tools/ast-grep.d.ts +2 -0
- package/dist/types/tools/render-utils.d.ts +13 -3
- package/package.json +7 -7
- package/src/async/job-manager.ts +111 -13
- package/src/cli/update-cli.ts +1 -5
- package/src/eval/js/shared/runtime.ts +82 -2
- package/src/extensibility/typebox.ts +44 -17
- package/src/main.ts +215 -148
- package/src/modes/acp/acp-agent.ts +115 -32
- package/src/modes/acp/acp-client-bridge.ts +2 -1
- package/src/modes/acp/acp-event-mapper.ts +208 -32
- package/src/modes/acp/acp-mode.ts +11 -3
- package/src/modes/components/tree-selector.ts +26 -7
- package/src/plan-mode/approved-plan.ts +21 -9
- package/src/prompts/agents/oracle.md +56 -0
- package/src/prompts/tools/ask.md +4 -3
- package/src/sdk.ts +3 -1
- package/src/session/agent-session.ts +186 -54
- package/src/session/client-bridge.ts +3 -0
- package/src/task/agents.ts +2 -0
- package/src/tools/ast-edit.ts +19 -11
- package/src/tools/ast-grep.ts +14 -10
- package/src/tools/render-utils.ts +26 -12
|
@@ -66,7 +66,11 @@ import {
|
|
|
66
66
|
import { ACP_BUILTIN_SLASH_COMMANDS, executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
|
|
67
67
|
import { parseThinkingLevel } from "../../thinking";
|
|
68
68
|
import { createAcpClientBridge } from "./acp-client-bridge";
|
|
69
|
-
import {
|
|
69
|
+
import {
|
|
70
|
+
buildToolCallStartUpdate,
|
|
71
|
+
mapAgentSessionEventToAcpSessionUpdates,
|
|
72
|
+
normalizeReplayToolArguments,
|
|
73
|
+
} from "./acp-event-mapper";
|
|
70
74
|
import { ACP_TERMINAL_AUTH_FLAG } from "./terminal-auth";
|
|
71
75
|
|
|
72
76
|
const ACP_DEFAULT_MODE_ID = "default";
|
|
@@ -87,6 +91,8 @@ const SESSION_PAGE_SIZE = 50;
|
|
|
87
91
|
*/
|
|
88
92
|
export const ACP_BOOTSTRAP_RACE_GUARD_MS = 50;
|
|
89
93
|
const ACP_CANCEL_CLEANUP_TIMEOUT_MS = 5_000;
|
|
94
|
+
const ACP_ASYNC_DELIVERY_DRAIN_TIMEOUT_MS = 250;
|
|
95
|
+
const ACP_ASYNC_DELIVERY_DRAIN_MAX_PASSES = 3;
|
|
90
96
|
|
|
91
97
|
type AgentImageContent = {
|
|
92
98
|
type: "image";
|
|
@@ -134,6 +140,7 @@ type ManagedSessionRecord = {
|
|
|
134
140
|
promptQueue: PromptQueueState;
|
|
135
141
|
liveMessageId: string | undefined;
|
|
136
142
|
liveMessageProgress: { textEmitted: boolean; thoughtEmitted: boolean } | undefined;
|
|
143
|
+
toolArgsById: Map<string, unknown>;
|
|
137
144
|
extensionsConfigured: boolean;
|
|
138
145
|
// Installed inside `#scheduleBootstrapUpdates` (post-race-guard); released
|
|
139
146
|
// in `#disposeSessionRecord`. Lives independent of any prompt turn.
|
|
@@ -150,6 +157,14 @@ type ReplayableMessage = {
|
|
|
150
157
|
isError?: boolean;
|
|
151
158
|
};
|
|
152
159
|
|
|
160
|
+
type ReplayableToolItem = {
|
|
161
|
+
type?: unknown;
|
|
162
|
+
id?: unknown;
|
|
163
|
+
name?: unknown;
|
|
164
|
+
arguments?: unknown;
|
|
165
|
+
input?: unknown;
|
|
166
|
+
};
|
|
167
|
+
|
|
153
168
|
type MCPConfigMap = {
|
|
154
169
|
[name: string]: MCPServerConfig;
|
|
155
170
|
};
|
|
@@ -357,7 +372,7 @@ export class AcpAgent implements Agent {
|
|
|
357
372
|
#clientCapabilities: ClientCapabilities | undefined;
|
|
358
373
|
#cancelCleanupTimeoutMs = ACP_CANCEL_CLEANUP_TIMEOUT_MS;
|
|
359
374
|
|
|
360
|
-
constructor(connection: AgentSideConnection,
|
|
375
|
+
constructor(connection: AgentSideConnection, createSession: CreateAcpSession, initialSession?: AgentSession) {
|
|
361
376
|
this.#connection = connection;
|
|
362
377
|
this.#initialSession = initialSession;
|
|
363
378
|
this.#createSession = createSession;
|
|
@@ -634,7 +649,7 @@ export class AcpAgent implements Agent {
|
|
|
634
649
|
const builtinResult = await executeAcpBuiltinSlashCommand(text, {
|
|
635
650
|
session: record.session,
|
|
636
651
|
sessionManager: record.session.sessionManager,
|
|
637
|
-
settings:
|
|
652
|
+
settings: record.session.settings,
|
|
638
653
|
cwd: record.session.sessionManager.getCwd(),
|
|
639
654
|
output: output => this.#emitCommandOutput(record, output),
|
|
640
655
|
refreshCommands: () => this.#emitAvailableCommandsUpdate(record),
|
|
@@ -806,6 +821,9 @@ export class AcpAgent implements Agent {
|
|
|
806
821
|
case "_omp/usage": {
|
|
807
822
|
const [firstRecord] = this.#sessions.values();
|
|
808
823
|
const target = firstRecord?.session ?? this.#initialSession;
|
|
824
|
+
if (!target) {
|
|
825
|
+
return { reports: [] };
|
|
826
|
+
}
|
|
809
827
|
const reports = await target.fetchUsageReports();
|
|
810
828
|
return { reports: reports ?? [] };
|
|
811
829
|
}
|
|
@@ -958,6 +976,7 @@ export class AcpAgent implements Agent {
|
|
|
958
976
|
promptQueue: { promise: Promise.resolve(), release: undefined },
|
|
959
977
|
liveMessageId: undefined,
|
|
960
978
|
liveMessageProgress: undefined,
|
|
979
|
+
toolArgsById: new Map(),
|
|
961
980
|
extensionsConfigured: false,
|
|
962
981
|
lifetimeUnsubscribe: undefined,
|
|
963
982
|
};
|
|
@@ -1020,19 +1039,27 @@ export class AcpAgent implements Agent {
|
|
|
1020
1039
|
return;
|
|
1021
1040
|
}
|
|
1022
1041
|
|
|
1042
|
+
if (event.type === "tool_execution_start" || event.type === "tool_execution_update") {
|
|
1043
|
+
record.toolArgsById.set(event.toolCallId, event.args);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1023
1046
|
this.#prepareLiveAssistantMessage(record, event);
|
|
1024
1047
|
for (const notification of mapAgentSessionEventToAcpSessionUpdates(event, record.session.sessionId, {
|
|
1025
1048
|
getMessageId: message => this.#getLiveMessageId(record, message),
|
|
1026
1049
|
getMessageProgress: message => this.#getLiveMessageProgress(record, message),
|
|
1050
|
+
getToolArgs: toolCallId => record.toolArgsById.get(toolCallId),
|
|
1027
1051
|
cwd: record.session.sessionManager.getCwd(),
|
|
1028
1052
|
})) {
|
|
1029
1053
|
await this.#connection.sessionUpdate(notification);
|
|
1030
1054
|
}
|
|
1055
|
+
if (event.type === "tool_execution_end") {
|
|
1056
|
+
record.toolArgsById.delete(event.toolCallId);
|
|
1057
|
+
}
|
|
1031
1058
|
this.#clearLiveAssistantMessageAfterEvent(record, event);
|
|
1032
1059
|
|
|
1033
1060
|
if (event.type === "agent_end") {
|
|
1034
1061
|
await this.#emitEndOfTurnUpdates(record);
|
|
1035
|
-
await record
|
|
1062
|
+
await this.#waitForAcpPromptIdle(record);
|
|
1036
1063
|
this.#finishPrompt(record, {
|
|
1037
1064
|
stopReason: this.#resolveStopReason(event, promptTurn.cancelRequested),
|
|
1038
1065
|
usage: this.#buildTurnUsage(promptTurn.usageBaseline, record.session.sessionManager.getUsageStatistics()),
|
|
@@ -1041,6 +1068,20 @@ export class AcpAgent implements Agent {
|
|
|
1041
1068
|
}
|
|
1042
1069
|
}
|
|
1043
1070
|
|
|
1071
|
+
async #waitForAcpPromptIdle(record: ManagedSessionRecord): Promise<void> {
|
|
1072
|
+
for (let pass = 0; pass < ACP_ASYNC_DELIVERY_DRAIN_MAX_PASSES; pass++) {
|
|
1073
|
+
await record.session.waitForIdle();
|
|
1074
|
+
const delivered = await record.session.drainAsyncJobDeliveriesForAcp({
|
|
1075
|
+
timeoutMs: ACP_ASYNC_DELIVERY_DRAIN_TIMEOUT_MS,
|
|
1076
|
+
});
|
|
1077
|
+
if (!delivered) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
await record.session.waitForIdle();
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1044
1085
|
#prepareLiveAssistantMessage(record: ManagedSessionRecord, event: AgentSessionEvent): void {
|
|
1045
1086
|
if (
|
|
1046
1087
|
(event.type === "message_start" || event.type === "message_update" || event.type === "message_end") &&
|
|
@@ -1297,7 +1338,7 @@ export class AcpAgent implements Agent {
|
|
|
1297
1338
|
|
|
1298
1339
|
#getAvailableModes(session: AgentSession): Array<{ id: string; name: string; description: string }> {
|
|
1299
1340
|
const modes = [{ id: ACP_DEFAULT_MODE_ID, name: "Default", description: "Standard ACP headless mode" }];
|
|
1300
|
-
if (
|
|
1341
|
+
if (session.settings.get("plan.enabled")) {
|
|
1301
1342
|
modes.push({
|
|
1302
1343
|
id: ACP_PLAN_MODE_ID,
|
|
1303
1344
|
name: "Plan",
|
|
@@ -1581,16 +1622,30 @@ export class AcpAgent implements Agent {
|
|
|
1581
1622
|
|
|
1582
1623
|
async #replaySessionHistory(record: ManagedSessionRecord): Promise<void> {
|
|
1583
1624
|
const cwd = record.session.sessionManager.getCwd();
|
|
1625
|
+
const replayedToolCallIds = new Set<string>();
|
|
1626
|
+
const replayedToolCallArgs = new Map<string, unknown>();
|
|
1584
1627
|
for (const message of record.session.sessionManager.buildSessionContext().messages as ReplayableMessage[]) {
|
|
1585
|
-
for (const notification of this.#messageToReplayNotifications(
|
|
1628
|
+
for (const notification of this.#messageToReplayNotifications(
|
|
1629
|
+
record.session.sessionId,
|
|
1630
|
+
message,
|
|
1631
|
+
cwd,
|
|
1632
|
+
replayedToolCallIds,
|
|
1633
|
+
replayedToolCallArgs,
|
|
1634
|
+
)) {
|
|
1586
1635
|
await this.#connection.sessionUpdate(notification);
|
|
1587
1636
|
}
|
|
1588
1637
|
}
|
|
1589
1638
|
}
|
|
1590
1639
|
|
|
1591
|
-
#messageToReplayNotifications(
|
|
1640
|
+
#messageToReplayNotifications(
|
|
1641
|
+
sessionId: string,
|
|
1642
|
+
message: ReplayableMessage,
|
|
1643
|
+
cwd: string,
|
|
1644
|
+
replayedToolCallIds: Set<string>,
|
|
1645
|
+
replayedToolCallArgs: Map<string, unknown>,
|
|
1646
|
+
): SessionNotification[] {
|
|
1592
1647
|
if (message.role === "assistant") {
|
|
1593
|
-
return this.#replayAssistantMessage(sessionId, message);
|
|
1648
|
+
return this.#replayAssistantMessage(sessionId, message, cwd, replayedToolCallIds, replayedToolCallArgs);
|
|
1594
1649
|
}
|
|
1595
1650
|
if (
|
|
1596
1651
|
message.role === "user" ||
|
|
@@ -1610,11 +1665,19 @@ export class AcpAgent implements Agent {
|
|
|
1610
1665
|
typeof message.toolCallId === "string" &&
|
|
1611
1666
|
typeof message.toolName === "string"
|
|
1612
1667
|
) {
|
|
1613
|
-
return this.#replayToolResult(
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1668
|
+
return this.#replayToolResult(
|
|
1669
|
+
sessionId,
|
|
1670
|
+
cwd,
|
|
1671
|
+
{
|
|
1672
|
+
...message,
|
|
1673
|
+
toolCallId: message.toolCallId,
|
|
1674
|
+
toolName: message.toolName,
|
|
1675
|
+
},
|
|
1676
|
+
{
|
|
1677
|
+
includeStart: !replayedToolCallIds.has(message.toolCallId),
|
|
1678
|
+
toolArgs: replayedToolCallArgs.get(message.toolCallId),
|
|
1679
|
+
},
|
|
1680
|
+
);
|
|
1618
1681
|
}
|
|
1619
1682
|
if (
|
|
1620
1683
|
message.role === "bashExecution" ||
|
|
@@ -1631,7 +1694,13 @@ export class AcpAgent implements Agent {
|
|
|
1631
1694
|
return [];
|
|
1632
1695
|
}
|
|
1633
1696
|
|
|
1634
|
-
#replayAssistantMessage(
|
|
1697
|
+
#replayAssistantMessage(
|
|
1698
|
+
sessionId: string,
|
|
1699
|
+
message: ReplayableMessage,
|
|
1700
|
+
cwd: string,
|
|
1701
|
+
replayedToolCallIds: Set<string>,
|
|
1702
|
+
replayedToolCallArgs: Map<string, unknown>,
|
|
1703
|
+
): SessionNotification[] {
|
|
1635
1704
|
const notifications: SessionNotification[] = [];
|
|
1636
1705
|
const messageId = crypto.randomUUID();
|
|
1637
1706
|
if (Array.isArray(message.content)) {
|
|
@@ -1666,24 +1735,23 @@ export class AcpAgent implements Agent {
|
|
|
1666
1735
|
});
|
|
1667
1736
|
continue;
|
|
1668
1737
|
}
|
|
1738
|
+
const toolItem = item as ReplayableToolItem;
|
|
1669
1739
|
if (
|
|
1670
|
-
(
|
|
1671
|
-
|
|
1672
|
-
typeof
|
|
1673
|
-
"name" in item &&
|
|
1674
|
-
typeof item.name === "string"
|
|
1740
|
+
(toolItem.type === "toolCall" || toolItem.type === "tool_use") &&
|
|
1741
|
+
typeof toolItem.id === "string" &&
|
|
1742
|
+
typeof toolItem.name === "string"
|
|
1675
1743
|
) {
|
|
1676
|
-
const
|
|
1677
|
-
|
|
1678
|
-
toolCallId:
|
|
1679
|
-
|
|
1680
|
-
|
|
1744
|
+
const args = this.#buildReplayAssistantToolArgs(toolItem);
|
|
1745
|
+
const update = buildToolCallStartUpdate({
|
|
1746
|
+
toolCallId: toolItem.id,
|
|
1747
|
+
toolName: toolItem.name,
|
|
1748
|
+
args,
|
|
1681
1749
|
status: "completed",
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
update.rawInput = item.arguments;
|
|
1685
|
-
}
|
|
1750
|
+
cwd,
|
|
1751
|
+
});
|
|
1686
1752
|
notifications.push({ sessionId, update });
|
|
1753
|
+
replayedToolCallIds.add(toolItem.id);
|
|
1754
|
+
replayedToolCallArgs.set(toolItem.id, args);
|
|
1687
1755
|
}
|
|
1688
1756
|
}
|
|
1689
1757
|
}
|
|
@@ -1700,10 +1768,21 @@ export class AcpAgent implements Agent {
|
|
|
1700
1768
|
return notifications;
|
|
1701
1769
|
}
|
|
1702
1770
|
|
|
1771
|
+
#buildReplayAssistantToolArgs(item: ReplayableToolItem): unknown {
|
|
1772
|
+
if ("arguments" in item) {
|
|
1773
|
+
return normalizeReplayToolArguments(item.arguments).args;
|
|
1774
|
+
}
|
|
1775
|
+
if (item.type === "tool_use" && "input" in item) {
|
|
1776
|
+
return item.input;
|
|
1777
|
+
}
|
|
1778
|
+
return {};
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1703
1781
|
#replayToolResult(
|
|
1704
1782
|
sessionId: string,
|
|
1705
1783
|
cwd: string,
|
|
1706
1784
|
message: Required<Pick<ReplayableMessage, "toolCallId" | "toolName">> & ReplayableMessage,
|
|
1785
|
+
options: { includeStart?: boolean; toolArgs?: unknown } = {},
|
|
1707
1786
|
): SessionNotification[] {
|
|
1708
1787
|
const args = this.#buildReplayToolArgs(message.details);
|
|
1709
1788
|
const startEvent: AgentSessionEvent = {
|
|
@@ -1723,10 +1802,14 @@ export class AcpAgent implements Agent {
|
|
|
1723
1802
|
errorMessage: message.errorMessage,
|
|
1724
1803
|
},
|
|
1725
1804
|
};
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1805
|
+
const notifications = mapAgentSessionEventToAcpSessionUpdates(endEvent, sessionId, {
|
|
1806
|
+
cwd,
|
|
1807
|
+
getToolArgs: toolCallId => (toolCallId === message.toolCallId ? options.toolArgs : undefined),
|
|
1808
|
+
});
|
|
1809
|
+
if (options.includeStart === false) {
|
|
1810
|
+
return notifications;
|
|
1811
|
+
}
|
|
1812
|
+
return [...mapAgentSessionEventToAcpSessionUpdates(startEvent, sessionId, { cwd }), ...notifications];
|
|
1730
1813
|
}
|
|
1731
1814
|
|
|
1732
1815
|
#buildReplayToolArgs(details: unknown): { path?: string } {
|
|
@@ -36,7 +36,7 @@ export function createAcpClientBridge(
|
|
|
36
36
|
requestPermission: true,
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
const bridge: ClientBridge = { capabilities };
|
|
39
|
+
const bridge: ClientBridge = { capabilities, deferAgentInitiatedTurns: true };
|
|
40
40
|
|
|
41
41
|
if (capabilities.readTextFile) {
|
|
42
42
|
bridge.readTextFile = async params => {
|
|
@@ -122,6 +122,7 @@ async function requestPermission(
|
|
|
122
122
|
toolCallId: toolCall.toolCallId,
|
|
123
123
|
title: toolCall.title,
|
|
124
124
|
...(toolCall.kind ? { kind: toolCall.kind as ToolCallUpdate["kind"] } : {}),
|
|
125
|
+
...(toolCall.status ? { status: toolCall.status as ToolCallUpdate["status"] } : {}),
|
|
125
126
|
...(toolCall.rawInput !== undefined ? { rawInput: toolCall.rawInput } : {}),
|
|
126
127
|
...(toolCall.locations ? { locations: toolCall.locations } : {}),
|
|
127
128
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
SessionNotification,
|
|
3
3
|
SessionUpdate,
|
|
4
|
+
ToolCall,
|
|
4
5
|
ToolCallContent,
|
|
5
6
|
ToolCallLocation,
|
|
6
7
|
ToolKind,
|
|
@@ -17,6 +18,7 @@ interface MessageProgress {
|
|
|
17
18
|
interface AcpEventMapperOptions {
|
|
18
19
|
getMessageId?: (message: unknown) => string | undefined;
|
|
19
20
|
getMessageProgress?: (message: unknown) => MessageProgress | undefined;
|
|
21
|
+
getToolArgs?: (toolCallId: string) => unknown;
|
|
20
22
|
/**
|
|
21
23
|
* Session cwd. Tool call locations sent to ACP clients must be absolute
|
|
22
24
|
* (the editor host needs them to open or focus files). When provided,
|
|
@@ -30,6 +32,10 @@ interface ContentArrayContainer {
|
|
|
30
32
|
content?: unknown;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
interface DetailsContainer {
|
|
36
|
+
details?: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
interface TypedValue {
|
|
34
40
|
type?: unknown;
|
|
35
41
|
}
|
|
@@ -38,6 +44,10 @@ interface TextLikeContent extends TypedValue {
|
|
|
38
44
|
text?: unknown;
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
interface TerminalIdContainer {
|
|
48
|
+
terminalId?: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
interface BinaryLikeContent extends TypedValue {
|
|
42
52
|
data?: unknown;
|
|
43
53
|
mimeType?: unknown;
|
|
@@ -118,6 +128,8 @@ export function mapToolKind(toolName: string): ToolKind {
|
|
|
118
128
|
case "move":
|
|
119
129
|
return "move";
|
|
120
130
|
case "bash":
|
|
131
|
+
case "shell":
|
|
132
|
+
case "exec":
|
|
121
133
|
case "eval":
|
|
122
134
|
return "execute";
|
|
123
135
|
case "search":
|
|
@@ -144,24 +156,20 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
144
156
|
case "message_end":
|
|
145
157
|
return mapAssistantMessageEnd(event, sessionId, options);
|
|
146
158
|
case "tool_execution_start": {
|
|
147
|
-
const update
|
|
148
|
-
sessionUpdate: "tool_call",
|
|
159
|
+
const update = buildToolCallStartUpdate({
|
|
149
160
|
toolCallId: event.toolCallId,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
};
|
|
155
|
-
const locations = extractToolLocations(event.args, options.cwd);
|
|
156
|
-
if (locations.length > 0) {
|
|
157
|
-
update.locations = locations;
|
|
158
|
-
}
|
|
161
|
+
toolName: event.toolName,
|
|
162
|
+
args: event.args,
|
|
163
|
+
intent: event.intent,
|
|
164
|
+
cwd: options.cwd,
|
|
165
|
+
});
|
|
159
166
|
return [toSessionNotification(sessionId, update)];
|
|
160
167
|
}
|
|
161
168
|
case "tool_execution_update": {
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
169
|
+
const content = mergeToolUpdateContent(
|
|
170
|
+
buildToolStartContent(event.toolName, event.args),
|
|
171
|
+
extractToolCallContent(event.partialResult),
|
|
172
|
+
);
|
|
165
173
|
const update: SessionUpdate = {
|
|
166
174
|
sessionUpdate: "tool_call_update",
|
|
167
175
|
toolCallId: event.toolCallId,
|
|
@@ -178,10 +186,11 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
178
186
|
return [toSessionNotification(sessionId, update)];
|
|
179
187
|
}
|
|
180
188
|
case "tool_execution_end": {
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
189
|
+
const resultContent = [...extractDiffToolCallContent(event.result), ...extractToolCallContent(event.result)];
|
|
190
|
+
const content = mergeToolUpdateContent(
|
|
191
|
+
buildToolStartContent(event.toolName, getToolExecutionEndArgs(event, options)),
|
|
192
|
+
resultContent,
|
|
193
|
+
);
|
|
185
194
|
const update: SessionUpdate = {
|
|
186
195
|
sessionUpdate: "tool_call_update",
|
|
187
196
|
toolCallId: event.toolCallId,
|
|
@@ -195,7 +204,12 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
195
204
|
if (locations.length > 0) {
|
|
196
205
|
update.locations = locations;
|
|
197
206
|
}
|
|
198
|
-
|
|
207
|
+
const notifications = [toSessionNotification(sessionId, update)];
|
|
208
|
+
const planUpdate = mapTodoWriteResultToPlanUpdate(event);
|
|
209
|
+
if (planUpdate) {
|
|
210
|
+
notifications.push(toSessionNotification(sessionId, planUpdate));
|
|
211
|
+
}
|
|
212
|
+
return notifications;
|
|
199
213
|
}
|
|
200
214
|
case "todo_reminder": {
|
|
201
215
|
const entries = event.todos.map(todo => ({
|
|
@@ -312,6 +326,144 @@ function mapTodoStatus(status: TodoStatus): "pending" | "in_progress" | "complet
|
|
|
312
326
|
return todoStatusMap[status];
|
|
313
327
|
}
|
|
314
328
|
|
|
329
|
+
function mapTodoWriteResultToPlanUpdate(
|
|
330
|
+
event: Extract<AgentSessionEvent, { type: "tool_execution_end" }>,
|
|
331
|
+
): SessionUpdate | undefined {
|
|
332
|
+
if (event.toolName !== "todo_write" || event.isError) {
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
const phases = extractTodoWritePhases(event.result);
|
|
336
|
+
if (!Array.isArray(phases)) {
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
sessionUpdate: "plan",
|
|
341
|
+
entries: extractTodoEntries(phases).map(todo => ({
|
|
342
|
+
content: todo.content,
|
|
343
|
+
priority: "medium" as const,
|
|
344
|
+
status: mapTodoStatus(todo.status),
|
|
345
|
+
})),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function extractTodoWritePhases(result: unknown): unknown {
|
|
350
|
+
if (typeof result !== "object" || result === null || !("details" in result)) {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
const details = (result as { details?: unknown }).details;
|
|
354
|
+
if (typeof details !== "object" || details === null || !("phases" in details)) {
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
return (details as { phases?: unknown }).phases;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function extractTodoEntries(phases: unknown[]): Array<{ content: string; status: TodoStatus }> {
|
|
361
|
+
const entries: Array<{ content: string; status: TodoStatus }> = [];
|
|
362
|
+
for (const phase of phases) {
|
|
363
|
+
if (typeof phase !== "object" || phase === null || !("tasks" in phase)) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const tasks = (phase as { tasks?: unknown }).tasks;
|
|
367
|
+
if (!Array.isArray(tasks)) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
for (const task of tasks) {
|
|
371
|
+
if (typeof task !== "object" || task === null || !("content" in task)) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const content = (task as { content?: unknown }).content;
|
|
375
|
+
if (typeof content !== "string" || content.length === 0) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const status = (task as { status?: TodoStatus }).status;
|
|
379
|
+
entries.push({ content, status: isTodoStatus(status) ? status : "pending" });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return entries;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function isTodoStatus(status: unknown): status is TodoStatus {
|
|
386
|
+
return status === "pending" || status === "in_progress" || status === "completed" || status === "abandoned";
|
|
387
|
+
}
|
|
388
|
+
export function buildToolCallStartUpdate(input: {
|
|
389
|
+
toolCallId: string;
|
|
390
|
+
toolName: string;
|
|
391
|
+
args: unknown;
|
|
392
|
+
intent?: string;
|
|
393
|
+
cwd?: string;
|
|
394
|
+
status?: "pending" | "completed";
|
|
395
|
+
}): SessionUpdate {
|
|
396
|
+
const update: ToolCall & { sessionUpdate: "tool_call" } = {
|
|
397
|
+
sessionUpdate: "tool_call",
|
|
398
|
+
toolCallId: input.toolCallId,
|
|
399
|
+
title: buildToolTitle(input.toolName, input.args, input.intent),
|
|
400
|
+
kind: mapToolKind(input.toolName),
|
|
401
|
+
status: input.status ?? "pending",
|
|
402
|
+
rawInput: input.args,
|
|
403
|
+
};
|
|
404
|
+
const content = buildToolStartContent(input.toolName, input.args);
|
|
405
|
+
if (content.length > 0) {
|
|
406
|
+
update.content = content;
|
|
407
|
+
}
|
|
408
|
+
const locations = extractToolLocations(input.args, input.cwd);
|
|
409
|
+
if (locations.length > 0) {
|
|
410
|
+
update.locations = locations;
|
|
411
|
+
}
|
|
412
|
+
return update;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function normalizeReplayToolArguments(value: unknown): { args: unknown } {
|
|
416
|
+
if (typeof value !== "string") {
|
|
417
|
+
return { args: value ?? {} };
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
const parsed: unknown = JSON.parse(value);
|
|
421
|
+
return { args: parsed };
|
|
422
|
+
} catch {
|
|
423
|
+
return { args: value };
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function getToolExecutionEndArgs(
|
|
428
|
+
event: Extract<AgentSessionEvent, { type: "tool_execution_end" }>,
|
|
429
|
+
options: AcpEventMapperOptions,
|
|
430
|
+
): unknown {
|
|
431
|
+
if ("args" in event) {
|
|
432
|
+
return (event as { args?: unknown }).args;
|
|
433
|
+
}
|
|
434
|
+
return options.getToolArgs?.(event.toolCallId);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function buildToolStartContent(toolName: string, args: unknown): ToolCallContent[] {
|
|
438
|
+
if (!isCommandToolName(toolName)) {
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
const command = extractStringProperty<CommandContainer>(args, "command");
|
|
442
|
+
return command ? [textToolCallContent(`$ ${command}`)] : [];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function mergeToolUpdateContent(startContent: ToolCallContent[], resultContent: ToolCallContent[]): ToolCallContent[] {
|
|
446
|
+
if (startContent.length === 0) {
|
|
447
|
+
return resultContent;
|
|
448
|
+
}
|
|
449
|
+
const merged = [...startContent];
|
|
450
|
+
for (const item of resultContent) {
|
|
451
|
+
if (
|
|
452
|
+
item.type === "content" &&
|
|
453
|
+
item.content.type === "text" &&
|
|
454
|
+
hasEquivalentTextContent(merged, item.content.text)
|
|
455
|
+
) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
merged.push(item);
|
|
459
|
+
}
|
|
460
|
+
return merged;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function isCommandToolName(toolName: string): boolean {
|
|
464
|
+
return toolName === "bash" || toolName === "shell" || toolName === "exec";
|
|
465
|
+
}
|
|
466
|
+
|
|
315
467
|
function buildToolTitle(toolName: string, args: unknown, intent: string | undefined): string {
|
|
316
468
|
const trimmedIntent = intent?.trim();
|
|
317
469
|
if (trimmedIntent) {
|
|
@@ -418,26 +570,33 @@ function buildDiffContent(entry: unknown): ToolCallContent | undefined {
|
|
|
418
570
|
};
|
|
419
571
|
}
|
|
420
572
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
573
|
+
function extractTerminalId(value: unknown): string | undefined {
|
|
574
|
+
const direct = extractStringProperty<TerminalIdContainer>(value, "terminalId");
|
|
575
|
+
if (direct) return direct;
|
|
576
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
577
|
+
const details = (value as DetailsContainer).details;
|
|
578
|
+
return extractStringProperty<TerminalIdContainer>(details, "terminalId");
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function terminalToolCallContent(terminalId: string): ToolCallContent {
|
|
582
|
+
return { type: "terminal", terminalId };
|
|
429
583
|
}
|
|
430
584
|
|
|
431
585
|
function extractToolCallContent(value: unknown): ToolCallContent[] {
|
|
432
586
|
const richContent = extractStructuredToolCallContent(value);
|
|
587
|
+
const terminalId = extractTerminalId(value);
|
|
588
|
+
const content =
|
|
589
|
+
terminalId && !hasTerminalContent(richContent, terminalId)
|
|
590
|
+
? [...richContent, terminalToolCallContent(terminalId)]
|
|
591
|
+
: richContent;
|
|
433
592
|
const fallbackText = extractReadableText(value);
|
|
434
593
|
if (!fallbackText) {
|
|
435
|
-
return
|
|
594
|
+
return content;
|
|
436
595
|
}
|
|
437
|
-
if (hasEquivalentTextContent(
|
|
438
|
-
return
|
|
596
|
+
if (hasEquivalentTextContent(content, fallbackText)) {
|
|
597
|
+
return content;
|
|
439
598
|
}
|
|
440
|
-
return [...
|
|
599
|
+
return [...content, textToolCallContent(fallbackText)];
|
|
441
600
|
}
|
|
442
601
|
|
|
443
602
|
function extractStructuredToolCallContent(value: unknown): ToolCallContent[] {
|
|
@@ -596,6 +755,10 @@ function hasEquivalentTextContent(content: ToolCallContent[], text: string): boo
|
|
|
596
755
|
return content.some(item => item.type === "content" && item.content.type === "text" && item.content.text === text);
|
|
597
756
|
}
|
|
598
757
|
|
|
758
|
+
function hasTerminalContent(content: ToolCallContent[], terminalId: string): boolean {
|
|
759
|
+
return content.some(item => item.type === "terminal" && item.terminalId === terminalId);
|
|
760
|
+
}
|
|
761
|
+
|
|
599
762
|
function extractReadableText(value: unknown): string | undefined {
|
|
600
763
|
if (typeof value === "string") {
|
|
601
764
|
return normalizeText(value);
|
|
@@ -625,11 +788,24 @@ function extractReadableText(value: unknown): string | undefined {
|
|
|
625
788
|
return normalizeText(text);
|
|
626
789
|
}
|
|
627
790
|
}
|
|
628
|
-
|
|
791
|
+
if (isTerminalOnlyDetails(value)) {
|
|
792
|
+
return undefined;
|
|
793
|
+
}
|
|
629
794
|
const serialized = safeJsonStringify(value);
|
|
630
795
|
return normalizeText(serialized);
|
|
631
796
|
}
|
|
632
797
|
|
|
798
|
+
function isTerminalOnlyDetails(value: unknown): boolean {
|
|
799
|
+
if (typeof value !== "object" || value === null) {
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
if (extractTerminalId(value) === undefined) {
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
const content = (value as ContentArrayContainer).content;
|
|
806
|
+
return content === undefined || (Array.isArray(content) && content.length === 0);
|
|
807
|
+
}
|
|
808
|
+
|
|
633
809
|
function extractAssistantMessageText(value: unknown): string {
|
|
634
810
|
if (typeof value !== "object" || value === null || !("content" in value)) {
|
|
635
811
|
return "";
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import * as stream from "node:stream";
|
|
2
|
-
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
2
|
+
import { AgentSideConnection, ndJsonStream, type Stream } from "@agentclientprotocol/sdk";
|
|
3
3
|
import type { AgentSession } from "../../session/agent-session";
|
|
4
4
|
import { AcpAgent } from "./acp-agent";
|
|
5
5
|
|
|
6
6
|
export type AcpSessionFactory = (cwd: string) => Promise<AgentSession>;
|
|
7
7
|
|
|
8
|
-
export
|
|
8
|
+
export function createAcpConnection(
|
|
9
|
+
transport: Stream,
|
|
10
|
+
createSession: AcpSessionFactory,
|
|
11
|
+
initialSession?: AgentSession,
|
|
12
|
+
): AgentSideConnection {
|
|
13
|
+
return new AgentSideConnection(conn => new AcpAgent(conn, createSession, initialSession), transport);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function runAcpMode(createSession: AcpSessionFactory, initialSession?: AgentSession): Promise<never> {
|
|
9
17
|
const input = stream.Writable.toWeb(process.stdout);
|
|
10
18
|
const output = stream.Readable.toWeb(process.stdin);
|
|
11
19
|
const transport = ndJsonStream(input, output);
|
|
12
|
-
const connection =
|
|
20
|
+
const connection = createAcpConnection(transport, createSession, initialSession);
|
|
13
21
|
await connection.closed;
|
|
14
22
|
process.exit(0);
|
|
15
23
|
}
|