linkshell-cli 0.3.12 → 0.3.14
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/dist/cli/src/runtime/acp/acp-client.d.ts +4 -0
- package/dist/cli/src/runtime/acp/acp-client.js +11 -0
- package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +17 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js +674 -50
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-sessions.d.ts +3 -1
- package/dist/cli/src/runtime/acp/claude-sessions.js +16 -8
- package/dist/cli/src/runtime/acp/claude-sessions.js.map +1 -1
- package/dist/cli/src/runtime/acp/codex-sessions.d.ts +5 -1
- package/dist/cli/src/runtime/acp/codex-sessions.js +2 -2
- package/dist/cli/src/runtime/acp/codex-sessions.js.map +1 -1
- package/dist/cli/src/runtime/bridge-session.js +4 -3
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +6361 -2337
- package/dist/shared-protocol/src/index.js +69 -0
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +3 -3
- package/src/runtime/acp/acp-client.ts +12 -0
- package/src/runtime/acp/agent-workspace.ts +741 -52
- package/src/runtime/acp/claude-sessions.ts +15 -6
- package/src/runtime/acp/codex-sessions.ts +4 -1
- package/src/runtime/bridge-session.ts +4 -5
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { basename, join, relative } from "node:path";
|
|
4
5
|
import { createEnvelope, parseTypedPayload, } from "@linkshell/protocol";
|
|
@@ -12,9 +13,43 @@ const PERMISSION_TIMEOUT_MS = 5 * 60_000;
|
|
|
12
13
|
const MAX_TIMELINE_ITEMS = 200;
|
|
13
14
|
const MAX_SNAPSHOT_ITEMS = 80;
|
|
14
15
|
const MAX_SNAPSHOT_TEXT_BYTES = 128 * 1024;
|
|
16
|
+
const MAX_DELTA_EVENTS = 500;
|
|
17
|
+
const HISTORY_PAGE_MAX_ITEMS = 500;
|
|
15
18
|
function id(prefix) {
|
|
16
19
|
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
17
20
|
}
|
|
21
|
+
function clampHistoryCursor(value, fallback, max) {
|
|
22
|
+
if (!value)
|
|
23
|
+
return fallback;
|
|
24
|
+
const parsed = Number.parseInt(value, 10);
|
|
25
|
+
if (!Number.isFinite(parsed))
|
|
26
|
+
return fallback;
|
|
27
|
+
return Math.max(0, Math.min(max, parsed));
|
|
28
|
+
}
|
|
29
|
+
function appServerText(value) {
|
|
30
|
+
if (typeof value === "string") {
|
|
31
|
+
const text = value.trim();
|
|
32
|
+
return text || undefined;
|
|
33
|
+
}
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
const text = value
|
|
36
|
+
.map((part) => appServerText(part))
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.join("\n")
|
|
39
|
+
.trim();
|
|
40
|
+
return text || undefined;
|
|
41
|
+
}
|
|
42
|
+
const record = asRecord(value);
|
|
43
|
+
if (!record)
|
|
44
|
+
return undefined;
|
|
45
|
+
if (typeof record.text === "string")
|
|
46
|
+
return appServerText(record.text);
|
|
47
|
+
if (typeof record.content === "string")
|
|
48
|
+
return appServerText(record.content);
|
|
49
|
+
if (typeof record.message === "string")
|
|
50
|
+
return appServerText(record.message);
|
|
51
|
+
return appServerText(record.content ?? record.message ?? record.parts);
|
|
52
|
+
}
|
|
18
53
|
function stringify(value) {
|
|
19
54
|
if (typeof value === "string")
|
|
20
55
|
return value;
|
|
@@ -616,6 +651,23 @@ function providerLabel(provider) {
|
|
|
616
651
|
const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"];
|
|
617
652
|
const CLAUDE_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
|
|
618
653
|
const AGENT_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"];
|
|
654
|
+
const COMMAND_OUTPUT_MAX_BYTES = 96 * 1024;
|
|
655
|
+
const LINKSHELL_NATIVE_COMMANDS = [
|
|
656
|
+
{ name: "status", description: "Show current Agent and workspace status", category: "LinkShell", argsMode: "none" },
|
|
657
|
+
{ name: "plan", description: "Enter Plan mode for the next turn", category: "Agent", argsMode: "none" },
|
|
658
|
+
{ name: "exit-plan", description: "Exit Plan mode", category: "Agent", argsMode: "none" },
|
|
659
|
+
{ name: "review", description: "Ask the Agent to review current local changes", category: "Agent", argsMode: "optional" },
|
|
660
|
+
{ name: "subagents", description: "Ask the Agent to split work across subagents when useful", category: "Agent", argsMode: "optional" },
|
|
661
|
+
{ name: "compact", description: "Compact the active Codex context", category: "Codex", argsMode: "none", providers: ["codex"] },
|
|
662
|
+
{ name: "clear", description: "Start a fresh Agent context for this conversation", category: "Agent", argsMode: "none", destructive: true },
|
|
663
|
+
{ name: "git-status", description: "Show branch and working tree status", category: "Git", argsMode: "none" },
|
|
664
|
+
{ name: "git-diff", description: "Show a compact diffstat for current changes", category: "Git", argsMode: "none" },
|
|
665
|
+
{ name: "git-commit", description: "Commit staged changes with the given message", category: "Git", argsMode: "required" },
|
|
666
|
+
{ name: "git-pull", description: "Pull with fast-forward only", category: "Git", argsMode: "none" },
|
|
667
|
+
{ name: "git-push", description: "Push the current branch", category: "Git", argsMode: "none" },
|
|
668
|
+
{ name: "git-stash", description: "Stash current working tree changes", category: "Git", argsMode: "optional" },
|
|
669
|
+
{ name: "git-stash-pop", description: "Pop the latest stash", category: "Git", argsMode: "none" },
|
|
670
|
+
];
|
|
619
671
|
const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
|
|
620
672
|
"add-dir",
|
|
621
673
|
"agents",
|
|
@@ -877,17 +929,30 @@ function customClaudeCommands(cwd) {
|
|
|
877
929
|
}
|
|
878
930
|
function defaultProviderCommands(provider, cwd, enabled) {
|
|
879
931
|
const disabledReason = enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`;
|
|
932
|
+
const linkshellCommands = LINKSHELL_NATIVE_COMMANDS
|
|
933
|
+
.filter((command) => !command.providers || command.providers.includes(provider))
|
|
934
|
+
.map((command) => makeCommand({
|
|
935
|
+
provider,
|
|
936
|
+
name: command.name,
|
|
937
|
+
description: command.description,
|
|
938
|
+
source: "linkshell",
|
|
939
|
+
category: command.category,
|
|
940
|
+
argsMode: command.argsMode ?? "optional",
|
|
941
|
+
destructive: command.destructive,
|
|
942
|
+
disabledReason,
|
|
943
|
+
executionKind: "native",
|
|
944
|
+
}));
|
|
880
945
|
if (provider === "codex") {
|
|
881
|
-
return
|
|
946
|
+
return linkshellCommands;
|
|
882
947
|
}
|
|
883
948
|
if (provider === "claude") {
|
|
884
949
|
const custom = customClaudeCommands(cwd).map((command) => ({
|
|
885
950
|
...command,
|
|
886
951
|
disabledReason: command.disabledReason ?? disabledReason,
|
|
887
952
|
}));
|
|
888
|
-
return custom;
|
|
953
|
+
return [...linkshellCommands, ...custom];
|
|
889
954
|
}
|
|
890
|
-
return
|
|
955
|
+
return linkshellCommands;
|
|
891
956
|
}
|
|
892
957
|
function mergeCommands(...groups) {
|
|
893
958
|
const map = new Map();
|
|
@@ -904,6 +969,76 @@ function mergeCommands(...groups) {
|
|
|
904
969
|
}
|
|
905
970
|
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
906
971
|
}
|
|
972
|
+
function isGitNativeCommand(name) {
|
|
973
|
+
return name === "git-status" ||
|
|
974
|
+
name === "git-diff" ||
|
|
975
|
+
name === "git-commit" ||
|
|
976
|
+
name === "git-pull" ||
|
|
977
|
+
name === "git-push" ||
|
|
978
|
+
name === "git-stash" ||
|
|
979
|
+
name === "git-stash-pop";
|
|
980
|
+
}
|
|
981
|
+
function gitCommandArgs(name, args) {
|
|
982
|
+
const message = args?.trim();
|
|
983
|
+
switch (name) {
|
|
984
|
+
case "git-status":
|
|
985
|
+
return { display: "git status --short --branch", argv: ["status", "--short", "--branch"] };
|
|
986
|
+
case "git-diff":
|
|
987
|
+
return { display: "git diff --stat", argv: ["diff", "--stat"] };
|
|
988
|
+
case "git-commit":
|
|
989
|
+
if (!message)
|
|
990
|
+
throw new Error("请先输入提交信息,例如 /git-commit fix mobile agent timeline");
|
|
991
|
+
return { display: `git commit -m ${JSON.stringify(message)}`, argv: ["commit", "-m", message] };
|
|
992
|
+
case "git-pull":
|
|
993
|
+
return { display: "git pull --ff-only", argv: ["pull", "--ff-only"] };
|
|
994
|
+
case "git-push":
|
|
995
|
+
return { display: "git push", argv: ["push"] };
|
|
996
|
+
case "git-stash":
|
|
997
|
+
return {
|
|
998
|
+
display: "git stash push -u",
|
|
999
|
+
argv: ["stash", "push", "-u", "-m", message || "LinkShell mobile stash"],
|
|
1000
|
+
};
|
|
1001
|
+
case "git-stash-pop":
|
|
1002
|
+
return { display: "git stash pop", argv: ["stash", "pop"] };
|
|
1003
|
+
default:
|
|
1004
|
+
throw new Error(`未知 Git 命令:/${name}`);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function runProcess(command, args, options) {
|
|
1008
|
+
return new Promise((resolve, reject) => {
|
|
1009
|
+
const child = spawn(command, args, {
|
|
1010
|
+
cwd: options.cwd,
|
|
1011
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1012
|
+
windowsHide: true,
|
|
1013
|
+
});
|
|
1014
|
+
const maxBytes = options.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES;
|
|
1015
|
+
let output = "";
|
|
1016
|
+
let bytes = 0;
|
|
1017
|
+
let truncated = false;
|
|
1018
|
+
const append = (chunk) => {
|
|
1019
|
+
if (bytes >= maxBytes) {
|
|
1020
|
+
truncated = true;
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
const remaining = maxBytes - bytes;
|
|
1024
|
+
const slice = chunk.byteLength > remaining ? chunk.subarray(0, remaining) : chunk;
|
|
1025
|
+
output += slice.toString("utf8");
|
|
1026
|
+
bytes += slice.byteLength;
|
|
1027
|
+
if (slice.byteLength < chunk.byteLength)
|
|
1028
|
+
truncated = true;
|
|
1029
|
+
};
|
|
1030
|
+
child.stdout.on("data", append);
|
|
1031
|
+
child.stderr.on("data", append);
|
|
1032
|
+
child.once("error", reject);
|
|
1033
|
+
child.once("close", (exitCode, signal) => {
|
|
1034
|
+
resolve({
|
|
1035
|
+
output: `${output.trimEnd()}${truncated ? "\n\n[truncated by LinkShell]" : ""}`,
|
|
1036
|
+
exitCode,
|
|
1037
|
+
signal,
|
|
1038
|
+
});
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
907
1042
|
function runtimeCommands(provider, value) {
|
|
908
1043
|
const raw = asRecord(value);
|
|
909
1044
|
const commandsValue = Array.isArray(value) ? value :
|
|
@@ -1018,10 +1153,26 @@ function parseRemoteSessions(value) {
|
|
|
1018
1153
|
createdAt: parseTimestamp(source.createdAt ?? source.created_at),
|
|
1019
1154
|
lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
|
|
1020
1155
|
archived: typeof source.archived === "boolean" ? source.archived : undefined,
|
|
1156
|
+
status: normalizeAgentStatus(firstString(source, ["status", "state", "phase"])),
|
|
1157
|
+
runningTurnId: firstString(source, ["runningTurnId", "running_turn_id", "turnId", "activeTurnId"]),
|
|
1021
1158
|
});
|
|
1022
1159
|
}
|
|
1023
1160
|
return result;
|
|
1024
1161
|
}
|
|
1162
|
+
function normalizeAgentStatus(value) {
|
|
1163
|
+
if (!value)
|
|
1164
|
+
return undefined;
|
|
1165
|
+
const normalized = value.toLowerCase();
|
|
1166
|
+
if (normalized === "running" || normalized === "in_progress" || normalized === "busy")
|
|
1167
|
+
return "running";
|
|
1168
|
+
if (normalized === "waiting_permission" || normalized === "waiting" || normalized === "blocked")
|
|
1169
|
+
return "waiting_permission";
|
|
1170
|
+
if (normalized === "error" || normalized === "failed")
|
|
1171
|
+
return "error";
|
|
1172
|
+
if (normalized === "idle" || normalized === "completed" || normalized === "done")
|
|
1173
|
+
return "idle";
|
|
1174
|
+
return undefined;
|
|
1175
|
+
}
|
|
1025
1176
|
export class AgentWorkspaceProxy {
|
|
1026
1177
|
input;
|
|
1027
1178
|
clients = new Map();
|
|
@@ -1036,6 +1187,8 @@ export class AgentWorkspaceProxy {
|
|
|
1036
1187
|
conversations = new Map();
|
|
1037
1188
|
conversationByAgentSessionId = new Map();
|
|
1038
1189
|
timelines = new Map();
|
|
1190
|
+
conversationRevisions = new Map();
|
|
1191
|
+
revisionEvents = new Map();
|
|
1039
1192
|
toolOutputBuffers = new Map();
|
|
1040
1193
|
pendingPermissions = new Map();
|
|
1041
1194
|
permissionWaiters = new Map();
|
|
@@ -1065,7 +1218,7 @@ export class AgentWorkspaceProxy {
|
|
|
1065
1218
|
this.input.send(createEnvelope({
|
|
1066
1219
|
type: "agent.v2.conversation.list.result",
|
|
1067
1220
|
hostDeviceId: this.input.hostDeviceId,
|
|
1068
|
-
payload: { conversations },
|
|
1221
|
+
payload: { conversations: conversations.map((conversation) => this.conversationSnapshot(conversation)) },
|
|
1069
1222
|
}));
|
|
1070
1223
|
break;
|
|
1071
1224
|
}
|
|
@@ -1074,6 +1227,16 @@ export class AgentWorkspaceProxy {
|
|
|
1074
1227
|
this.sendSnapshot(payload.conversationId);
|
|
1075
1228
|
break;
|
|
1076
1229
|
}
|
|
1230
|
+
case "agent.v2.history.request": {
|
|
1231
|
+
const payload = parseTypedPayload("agent.v2.history.request", envelope.payload);
|
|
1232
|
+
await this.sendHistoryPage(payload);
|
|
1233
|
+
break;
|
|
1234
|
+
}
|
|
1235
|
+
case "agent.v2.delta.request": {
|
|
1236
|
+
const payload = parseTypedPayload("agent.v2.delta.request", envelope.payload);
|
|
1237
|
+
this.sendDelta(payload);
|
|
1238
|
+
break;
|
|
1239
|
+
}
|
|
1077
1240
|
case "agent.v2.prompt": {
|
|
1078
1241
|
const payload = parseTypedPayload("agent.v2.prompt", envelope.payload);
|
|
1079
1242
|
await this.sendPrompt(payload);
|
|
@@ -1276,14 +1439,21 @@ export class AgentWorkspaceProxy {
|
|
|
1276
1439
|
reasoningEffort: existing?.reasoningEffort,
|
|
1277
1440
|
permissionMode: existing?.permissionMode,
|
|
1278
1441
|
collaborationMode: existing?.collaborationMode,
|
|
1279
|
-
status: existing?.status ?? "idle",
|
|
1442
|
+
status: remote.status ?? existing?.status ?? "idle",
|
|
1280
1443
|
archived: remote.archived ?? existing?.archived ?? false,
|
|
1444
|
+
timelineRevision: existing?.timelineRevision ?? this.getRevision(conversationId),
|
|
1445
|
+
historyComplete: existing?.historyComplete ?? false,
|
|
1446
|
+
runningTurnId: remote.runningTurnId ?? this.currentTurnIds.get(conversationId),
|
|
1447
|
+
source: "device",
|
|
1448
|
+
canonical: true,
|
|
1281
1449
|
lastMessagePreview: existing?.lastMessagePreview,
|
|
1282
1450
|
lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
|
|
1283
1451
|
createdAt: remote.createdAt ?? existing?.createdAt ?? now,
|
|
1284
1452
|
};
|
|
1285
1453
|
this.conversations.set(conversation.id, conversation);
|
|
1286
1454
|
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
1455
|
+
if (remote.runningTurnId)
|
|
1456
|
+
this.rememberTurnConversationId(conversation.id, remote.runningTurnId);
|
|
1287
1457
|
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
1288
1458
|
}
|
|
1289
1459
|
}
|
|
@@ -1361,12 +1531,18 @@ export class AgentWorkspaceProxy {
|
|
|
1361
1531
|
}
|
|
1362
1532
|
this.hydrateStoredTimeline(existingConversation);
|
|
1363
1533
|
this.activeConversationId = existingConversation.id;
|
|
1534
|
+
const snapshot = this.latestSnapshot(existingConversation.id);
|
|
1364
1535
|
this.input.send(createEnvelope({
|
|
1365
1536
|
type: "agent.v2.conversation.opened",
|
|
1366
1537
|
hostDeviceId: this.input.hostDeviceId,
|
|
1367
1538
|
payload: {
|
|
1368
|
-
conversation: existingConversation,
|
|
1369
|
-
snapshot:
|
|
1539
|
+
conversation: this.conversationSnapshot(existingConversation),
|
|
1540
|
+
snapshot: snapshot.items,
|
|
1541
|
+
revision: this.getRevision(existingConversation.id),
|
|
1542
|
+
cursor: snapshot.cursor,
|
|
1543
|
+
hasMore: snapshot.hasMore,
|
|
1544
|
+
source: "device-history",
|
|
1545
|
+
canonical: true,
|
|
1370
1546
|
},
|
|
1371
1547
|
}));
|
|
1372
1548
|
return existingConversation;
|
|
@@ -1401,6 +1577,11 @@ export class AgentWorkspaceProxy {
|
|
|
1401
1577
|
collaborationMode: payload.collaborationMode ?? existingConversation?.collaborationMode,
|
|
1402
1578
|
status: "idle",
|
|
1403
1579
|
archived: existingConversation?.archived ?? false,
|
|
1580
|
+
timelineRevision: existingConversation?.timelineRevision ?? this.getRevision(conversationId),
|
|
1581
|
+
historyComplete: existingConversation?.historyComplete ?? false,
|
|
1582
|
+
runningTurnId: this.currentTurnIds.get(conversationId),
|
|
1583
|
+
source: "device",
|
|
1584
|
+
canonical: true,
|
|
1404
1585
|
lastMessagePreview: existingConversation?.status === "error" ? undefined : existingConversation?.lastMessagePreview,
|
|
1405
1586
|
lastActivityAt: now,
|
|
1406
1587
|
createdAt: existingConversation?.createdAt ?? now,
|
|
@@ -1410,10 +1591,19 @@ export class AgentWorkspaceProxy {
|
|
|
1410
1591
|
this.activeConversationId = conversation.id;
|
|
1411
1592
|
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
1412
1593
|
this.hydrateStoredTimeline(conversation);
|
|
1594
|
+
const snapshot = this.latestSnapshot(conversation.id);
|
|
1413
1595
|
this.input.send(createEnvelope({
|
|
1414
1596
|
type: "agent.v2.conversation.opened",
|
|
1415
1597
|
hostDeviceId: this.input.hostDeviceId,
|
|
1416
|
-
payload: {
|
|
1598
|
+
payload: {
|
|
1599
|
+
conversation: this.conversationSnapshot(conversation),
|
|
1600
|
+
snapshot: snapshot.items,
|
|
1601
|
+
revision: this.getRevision(conversation.id),
|
|
1602
|
+
cursor: snapshot.cursor,
|
|
1603
|
+
hasMore: snapshot.hasMore,
|
|
1604
|
+
source: "device-history",
|
|
1605
|
+
canonical: true,
|
|
1606
|
+
},
|
|
1417
1607
|
}));
|
|
1418
1608
|
return conversation;
|
|
1419
1609
|
}
|
|
@@ -1436,6 +1626,10 @@ export class AgentWorkspaceProxy {
|
|
|
1436
1626
|
collaborationMode: payload.collaborationMode,
|
|
1437
1627
|
status: "error",
|
|
1438
1628
|
archived: false,
|
|
1629
|
+
timelineRevision: this.getRevision(fallbackId),
|
|
1630
|
+
historyComplete: false,
|
|
1631
|
+
source: "device",
|
|
1632
|
+
canonical: true,
|
|
1439
1633
|
lastMessagePreview: message,
|
|
1440
1634
|
lastActivityAt: now,
|
|
1441
1635
|
createdAt: now,
|
|
@@ -1452,7 +1646,14 @@ export class AgentWorkspaceProxy {
|
|
|
1452
1646
|
this.input.send(createEnvelope({
|
|
1453
1647
|
type: "agent.v2.conversation.opened",
|
|
1454
1648
|
hostDeviceId: this.input.hostDeviceId,
|
|
1455
|
-
payload: {
|
|
1649
|
+
payload: {
|
|
1650
|
+
conversation: this.conversationSnapshot(conversation),
|
|
1651
|
+
snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []),
|
|
1652
|
+
revision: this.getRevision(conversation.id),
|
|
1653
|
+
hasMore: false,
|
|
1654
|
+
source: "device",
|
|
1655
|
+
canonical: true,
|
|
1656
|
+
},
|
|
1456
1657
|
}));
|
|
1457
1658
|
return conversation;
|
|
1458
1659
|
}
|
|
@@ -1640,14 +1841,8 @@ export class AgentWorkspaceProxy {
|
|
|
1640
1841
|
this.emitStatus(conversation.id, conversation.status, `${providerLabel(conversation.provider)} · ${conversation.collaborationMode === "plan" ? "Plan mode" : "Default mode"} · ${conversation.cwd}`);
|
|
1641
1842
|
return;
|
|
1642
1843
|
}
|
|
1643
|
-
if (
|
|
1644
|
-
this.
|
|
1645
|
-
id: id("error"),
|
|
1646
|
-
conversationId: conversation.id,
|
|
1647
|
-
type: "error",
|
|
1648
|
-
error: `${command.title} 暂无 ${providerLabel(conversation.provider)} 原生实现。`,
|
|
1649
|
-
createdAt: now,
|
|
1650
|
-
});
|
|
1844
|
+
if (isGitNativeCommand(command.name)) {
|
|
1845
|
+
await this.executeGitNativeCommand(conversation, command.name, args);
|
|
1651
1846
|
return;
|
|
1652
1847
|
}
|
|
1653
1848
|
if (command.name === "plan" || command.name === "exit-plan") {
|
|
@@ -1662,6 +1857,21 @@ export class AgentWorkspaceProxy {
|
|
|
1662
1857
|
: "已退出 Plan mode。");
|
|
1663
1858
|
return;
|
|
1664
1859
|
}
|
|
1860
|
+
if (command.name === "review" || command.name === "subagents") {
|
|
1861
|
+
const prompt = command.name === "review"
|
|
1862
|
+
? args || "Review the current local changes."
|
|
1863
|
+
: args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
|
|
1864
|
+
await this.sendPrompt({
|
|
1865
|
+
conversationId: conversation.id,
|
|
1866
|
+
clientMessageId: id(command.name),
|
|
1867
|
+
contentBlocks: [{ type: "text", text: prompt }],
|
|
1868
|
+
model: conversation.model,
|
|
1869
|
+
reasoningEffort: conversation.reasoningEffort,
|
|
1870
|
+
permissionMode: conversation.permissionMode,
|
|
1871
|
+
collaborationMode: conversation.collaborationMode,
|
|
1872
|
+
});
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1665
1875
|
if (command.name === "compact") {
|
|
1666
1876
|
if (!(client instanceof AcpClient))
|
|
1667
1877
|
throw new Error("当前 Codex runtime 不支持原生 compact。");
|
|
@@ -1700,21 +1910,6 @@ export class AgentWorkspaceProxy {
|
|
|
1700
1910
|
this.emitStatus(conversation.id, "idle", "上下文已重置,已创建新的 Codex thread。");
|
|
1701
1911
|
return;
|
|
1702
1912
|
}
|
|
1703
|
-
if (command.name === "review" || command.name === "subagents") {
|
|
1704
|
-
const prompt = command.name === "review"
|
|
1705
|
-
? args || "Review the current local changes."
|
|
1706
|
-
: args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
|
|
1707
|
-
await this.sendPrompt({
|
|
1708
|
-
conversationId: conversation.id,
|
|
1709
|
-
clientMessageId: id(command.name),
|
|
1710
|
-
contentBlocks: [{ type: "text", text: prompt }],
|
|
1711
|
-
model: conversation.model,
|
|
1712
|
-
reasoningEffort: conversation.reasoningEffort,
|
|
1713
|
-
permissionMode: conversation.permissionMode,
|
|
1714
|
-
collaborationMode: conversation.collaborationMode,
|
|
1715
|
-
});
|
|
1716
|
-
return;
|
|
1717
|
-
}
|
|
1718
1913
|
throw new Error(`命令暂未实现:/${command.name}`);
|
|
1719
1914
|
}
|
|
1720
1915
|
catch (error) {
|
|
@@ -1729,6 +1924,41 @@ export class AgentWorkspaceProxy {
|
|
|
1729
1924
|
});
|
|
1730
1925
|
}
|
|
1731
1926
|
}
|
|
1927
|
+
async executeGitNativeCommand(conversation, commandName, args) {
|
|
1928
|
+
const git = gitCommandArgs(commandName, args);
|
|
1929
|
+
const toolId = id(commandName);
|
|
1930
|
+
const now = Date.now();
|
|
1931
|
+
conversation.status = "running";
|
|
1932
|
+
conversation.lastMessagePreview = git.display;
|
|
1933
|
+
conversation.lastActivityAt = now;
|
|
1934
|
+
this.emitConversation(conversation);
|
|
1935
|
+
this.upsertTool(conversation.id, {
|
|
1936
|
+
id: toolId,
|
|
1937
|
+
name: "命令",
|
|
1938
|
+
input: `${git.display}\n\ncwd: ${conversation.cwd}`,
|
|
1939
|
+
createdAt: now,
|
|
1940
|
+
status: "running",
|
|
1941
|
+
});
|
|
1942
|
+
const result = await runProcess("git", git.argv, {
|
|
1943
|
+
cwd: conversation.cwd,
|
|
1944
|
+
maxBytes: COMMAND_OUTPUT_MAX_BYTES,
|
|
1945
|
+
});
|
|
1946
|
+
const ok = result.exitCode === 0;
|
|
1947
|
+
this.upsertTool(conversation.id, {
|
|
1948
|
+
id: toolId,
|
|
1949
|
+
name: "命令",
|
|
1950
|
+
input: `${git.display}\n\ncwd: ${conversation.cwd}`,
|
|
1951
|
+
output: result.output || (ok ? "完成" : `退出码 ${result.exitCode ?? "unknown"}`),
|
|
1952
|
+
createdAt: now,
|
|
1953
|
+
status: ok ? "completed" : "failed",
|
|
1954
|
+
});
|
|
1955
|
+
conversation.status = ok ? "idle" : "error";
|
|
1956
|
+
conversation.lastMessagePreview = ok
|
|
1957
|
+
? `${git.display} 完成`
|
|
1958
|
+
: `${git.display} 失败`;
|
|
1959
|
+
conversation.lastActivityAt = Date.now();
|
|
1960
|
+
this.emitConversation(conversation);
|
|
1961
|
+
}
|
|
1732
1962
|
handleRequest(method, params) {
|
|
1733
1963
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
1734
1964
|
return this.handleStructuredInput(params, true);
|
|
@@ -2380,6 +2610,14 @@ export class AgentWorkspaceProxy {
|
|
|
2380
2610
|
this.permissionWaiters.delete(requestId);
|
|
2381
2611
|
this.permissionSources.delete(requestId);
|
|
2382
2612
|
resolve(formatPermissionResponse(source, "cancelled", "cancelled"));
|
|
2613
|
+
this.markPermission(conversationId, requestId, {
|
|
2614
|
+
permissionOutcome: "cancelled",
|
|
2615
|
+
optionId: "cancelled",
|
|
2616
|
+
permissionLive: false,
|
|
2617
|
+
permissionPending: false,
|
|
2618
|
+
permissionExpired: true,
|
|
2619
|
+
permissionError: "等待授权超时",
|
|
2620
|
+
});
|
|
2383
2621
|
this.updateConversationStatus(conversationId, "idle");
|
|
2384
2622
|
}, PERMISSION_TIMEOUT_MS);
|
|
2385
2623
|
this.permissionWaiters.set(requestId, { resolve, timer });
|
|
@@ -2409,6 +2647,8 @@ export class AgentWorkspaceProxy {
|
|
|
2409
2647
|
this.markPermission(payload.conversationId, payload.requestId, {
|
|
2410
2648
|
permissionOutcome: payload.outcome,
|
|
2411
2649
|
optionId: selectedOptionId,
|
|
2650
|
+
permissionLive: false,
|
|
2651
|
+
permissionExpired: false,
|
|
2412
2652
|
permissionError: undefined,
|
|
2413
2653
|
permissionPending: false,
|
|
2414
2654
|
});
|
|
@@ -2455,6 +2695,209 @@ export class AgentWorkspaceProxy {
|
|
|
2455
2695
|
updatedAt: Date.now(),
|
|
2456
2696
|
});
|
|
2457
2697
|
}
|
|
2698
|
+
getRevision(conversationId) {
|
|
2699
|
+
return this.conversationRevisions.get(conversationId) ??
|
|
2700
|
+
this.conversations.get(conversationId)?.timelineRevision ??
|
|
2701
|
+
0;
|
|
2702
|
+
}
|
|
2703
|
+
setRevisionFloor(conversationId, revision) {
|
|
2704
|
+
const nextRevision = Math.max(this.getRevision(conversationId), revision);
|
|
2705
|
+
this.conversationRevisions.set(conversationId, nextRevision);
|
|
2706
|
+
const conversation = this.conversations.get(conversationId);
|
|
2707
|
+
if (conversation) {
|
|
2708
|
+
conversation.timelineRevision = nextRevision;
|
|
2709
|
+
conversation.runningTurnId = this.currentTurnIds.get(conversationId);
|
|
2710
|
+
}
|
|
2711
|
+
return nextRevision;
|
|
2712
|
+
}
|
|
2713
|
+
conversationSnapshot(conversation) {
|
|
2714
|
+
return {
|
|
2715
|
+
...conversation,
|
|
2716
|
+
timelineRevision: this.getRevision(conversation.id),
|
|
2717
|
+
historyComplete: conversation.historyComplete ?? false,
|
|
2718
|
+
runningTurnId: this.currentTurnIds.get(conversation.id),
|
|
2719
|
+
source: conversation.source ?? "device",
|
|
2720
|
+
canonical: conversation.canonical ?? true,
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
annotateTimelineItem(item, revision, source) {
|
|
2724
|
+
return {
|
|
2725
|
+
...item,
|
|
2726
|
+
revision: revision ?? item.revision,
|
|
2727
|
+
source: item.source ?? source,
|
|
2728
|
+
canonical: item.canonical ?? true,
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
recordRevisionEvent(conversationId, change) {
|
|
2732
|
+
const revision = this.getRevision(conversationId) + 1;
|
|
2733
|
+
this.conversationRevisions.set(conversationId, revision);
|
|
2734
|
+
const conversation = this.conversations.get(conversationId);
|
|
2735
|
+
if (conversation) {
|
|
2736
|
+
conversation.timelineRevision = revision;
|
|
2737
|
+
conversation.runningTurnId = this.currentTurnIds.get(conversationId);
|
|
2738
|
+
}
|
|
2739
|
+
const event = {
|
|
2740
|
+
revision,
|
|
2741
|
+
item: change.item ? this.annotateTimelineItem(change.item, revision, "device-live") : undefined,
|
|
2742
|
+
conversation: change.conversation ? this.conversationSnapshot(change.conversation) : undefined,
|
|
2743
|
+
};
|
|
2744
|
+
const events = this.revisionEvents.get(conversationId) ?? [];
|
|
2745
|
+
events.push(event);
|
|
2746
|
+
if (events.length > MAX_DELTA_EVENTS) {
|
|
2747
|
+
events.splice(0, events.length - MAX_DELTA_EVENTS);
|
|
2748
|
+
}
|
|
2749
|
+
this.revisionEvents.set(conversationId, events);
|
|
2750
|
+
return event;
|
|
2751
|
+
}
|
|
2752
|
+
storedTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
|
|
2753
|
+
if (!conversation.agentSessionId)
|
|
2754
|
+
return [];
|
|
2755
|
+
const result = conversation.provider === "codex"
|
|
2756
|
+
? loadCodexStoredTimeline(conversation.agentSessionId, conversation.id, conversation.cwd || this.input.cwd, { maxItems })
|
|
2757
|
+
: conversation.provider === "claude"
|
|
2758
|
+
? loadClaudeStoredTimeline(conversation.agentSessionId, conversation.id, { maxItems })
|
|
2759
|
+
: { items: [] };
|
|
2760
|
+
return result.items
|
|
2761
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
2762
|
+
.map((item, index) => this.annotateTimelineItem(item, index + 1, "device-history"));
|
|
2763
|
+
}
|
|
2764
|
+
canonicalTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
|
|
2765
|
+
const merged = new Map();
|
|
2766
|
+
for (const item of this.storedTimeline(conversation, maxItems)) {
|
|
2767
|
+
merged.set(item.id, item);
|
|
2768
|
+
}
|
|
2769
|
+
for (const item of this.timelines.get(conversation.id) ?? []) {
|
|
2770
|
+
merged.set(item.id, this.annotateTimelineItem(item, item.revision, item.source ?? "device-live"));
|
|
2771
|
+
}
|
|
2772
|
+
const items = [...merged.values()]
|
|
2773
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
2774
|
+
.slice(-maxItems);
|
|
2775
|
+
this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
|
|
2776
|
+
return items;
|
|
2777
|
+
}
|
|
2778
|
+
async appServerTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
|
|
2779
|
+
if (conversation.provider !== "codex" || !conversation.agentSessionId)
|
|
2780
|
+
return [];
|
|
2781
|
+
if (this.protocolForProvider(conversation.provider) !== "codex-app-server")
|
|
2782
|
+
return [];
|
|
2783
|
+
const client = this.clientForProvider(conversation.provider);
|
|
2784
|
+
const listTurns = client?.listTurns;
|
|
2785
|
+
if (typeof listTurns !== "function")
|
|
2786
|
+
return [];
|
|
2787
|
+
try {
|
|
2788
|
+
const result = await listTurns.call(client, { sessionId: conversation.agentSessionId, limit: maxItems });
|
|
2789
|
+
return this.timelineFromAppServerTurns(conversation.id, conversation.agentSessionId, result);
|
|
2790
|
+
}
|
|
2791
|
+
catch (error) {
|
|
2792
|
+
if (this.input.verbose) {
|
|
2793
|
+
process.stderr.write(`[agent:v2] thread/turns/list failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2794
|
+
}
|
|
2795
|
+
return [];
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
timelineFromAppServerTurns(conversationId, agentSessionId, value) {
|
|
2799
|
+
const raw = asRecord(value);
|
|
2800
|
+
const turns = Array.isArray(value) ? value :
|
|
2801
|
+
Array.isArray(raw?.turns) ? raw.turns :
|
|
2802
|
+
Array.isArray(raw?.items) ? raw.items :
|
|
2803
|
+
Array.isArray(raw?.entries) ? raw.entries :
|
|
2804
|
+
[];
|
|
2805
|
+
const items = [];
|
|
2806
|
+
turns.forEach((entry, index) => {
|
|
2807
|
+
const turn = asRecord(entry);
|
|
2808
|
+
if (!turn)
|
|
2809
|
+
return;
|
|
2810
|
+
const turnId = firstString(turn, ["id", "turnId", "turn_id"]) ?? `turn-${index + 1}`;
|
|
2811
|
+
const createdAt = parseTimestamp(turn.createdAt ?? turn.created_at ?? turn.startedAt ?? turn.started_at) ?? Date.now() + index;
|
|
2812
|
+
const updatedAt = parseTimestamp(turn.updatedAt ?? turn.updated_at ?? turn.completedAt ?? turn.completed_at);
|
|
2813
|
+
const userText = appServerText(turn.input ?? turn.prompt ?? turn.user ?? turn.userMessage ?? turn.request);
|
|
2814
|
+
if (userText) {
|
|
2815
|
+
items.push({
|
|
2816
|
+
id: `app-server:${agentSessionId}:${turnId}:user`,
|
|
2817
|
+
conversationId,
|
|
2818
|
+
type: "message",
|
|
2819
|
+
kind: "chat",
|
|
2820
|
+
turnId,
|
|
2821
|
+
role: "user",
|
|
2822
|
+
content: [{ type: "text", text: userText }],
|
|
2823
|
+
text: userText,
|
|
2824
|
+
createdAt,
|
|
2825
|
+
updatedAt,
|
|
2826
|
+
metadata: { source: "app-server", provider: "codex" },
|
|
2827
|
+
});
|
|
2828
|
+
}
|
|
2829
|
+
const assistantText = appServerText(turn.output ?? turn.response ?? turn.assistant ?? turn.assistantMessage ?? turn.result);
|
|
2830
|
+
if (assistantText) {
|
|
2831
|
+
items.push({
|
|
2832
|
+
id: `app-server:${agentSessionId}:${turnId}:assistant`,
|
|
2833
|
+
conversationId,
|
|
2834
|
+
type: "message",
|
|
2835
|
+
kind: "chat",
|
|
2836
|
+
turnId,
|
|
2837
|
+
role: "assistant",
|
|
2838
|
+
content: [{ type: "text", text: assistantText }],
|
|
2839
|
+
text: assistantText,
|
|
2840
|
+
createdAt: updatedAt ?? createdAt + 1,
|
|
2841
|
+
updatedAt,
|
|
2842
|
+
metadata: { source: "app-server", provider: "codex" },
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
const nestedItems = Array.isArray(turn.items) ? turn.items : Array.isArray(turn.messages) ? turn.messages : [];
|
|
2846
|
+
for (const nested of nestedItems) {
|
|
2847
|
+
const nestedRecord = asRecord(nested);
|
|
2848
|
+
if (!nestedRecord)
|
|
2849
|
+
continue;
|
|
2850
|
+
const text = appServerText(nestedRecord.content ?? nestedRecord.text ?? nestedRecord.message);
|
|
2851
|
+
const role = nestedRecord.role === "user" || nestedRecord.role === "assistant" || nestedRecord.role === "system"
|
|
2852
|
+
? nestedRecord.role
|
|
2853
|
+
: undefined;
|
|
2854
|
+
if (!text || !role)
|
|
2855
|
+
continue;
|
|
2856
|
+
const itemId = firstString(nestedRecord, ["id", "itemId", "messageId"]) ?? `${role}-${items.length + 1}`;
|
|
2857
|
+
items.push({
|
|
2858
|
+
id: `app-server:${agentSessionId}:${turnId}:${itemId}`,
|
|
2859
|
+
conversationId,
|
|
2860
|
+
type: "message",
|
|
2861
|
+
kind: "chat",
|
|
2862
|
+
turnId,
|
|
2863
|
+
itemId,
|
|
2864
|
+
role,
|
|
2865
|
+
content: [{ type: "text", text }],
|
|
2866
|
+
text,
|
|
2867
|
+
createdAt: parseTimestamp(nestedRecord.createdAt ?? nestedRecord.created_at) ?? createdAt + items.length,
|
|
2868
|
+
updatedAt: parseTimestamp(nestedRecord.updatedAt ?? nestedRecord.updated_at),
|
|
2869
|
+
metadata: { source: "app-server", provider: "codex" },
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
});
|
|
2873
|
+
return items
|
|
2874
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
2875
|
+
.map((item, index) => this.annotateTimelineItem(item, index + 1, "app-server"));
|
|
2876
|
+
}
|
|
2877
|
+
mergeCanonicalTimelineItems(items, maxItems) {
|
|
2878
|
+
const byKey = new Map();
|
|
2879
|
+
for (const item of items.sort((a, b) => a.createdAt - b.createdAt)) {
|
|
2880
|
+
const key = item.type === "message" && item.role && item.text
|
|
2881
|
+
? `${item.role}:${item.text.replace(/\s+/g, " ").trim().slice(0, 500)}`
|
|
2882
|
+
: item.id;
|
|
2883
|
+
const existing = byKey.get(key);
|
|
2884
|
+
if (!existing || item.source === "app-server" || (item.updatedAt ?? item.createdAt) >= (existing.updatedAt ?? existing.createdAt)) {
|
|
2885
|
+
byKey.set(key, item);
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
return [...byKey.values()]
|
|
2889
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
2890
|
+
.slice(-maxItems);
|
|
2891
|
+
}
|
|
2892
|
+
latestSnapshot(conversationId) {
|
|
2893
|
+
const timeline = this.timelines.get(conversationId) ?? [];
|
|
2894
|
+
const start = Math.max(0, timeline.length - MAX_SNAPSHOT_ITEMS);
|
|
2895
|
+
return {
|
|
2896
|
+
items: timeline.slice(start).map((item) => snapshotTimelineItem(item)),
|
|
2897
|
+
cursor: start > 0 ? String(start) : undefined,
|
|
2898
|
+
hasMore: start > 0,
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2458
2901
|
addItem(conversationId, item) {
|
|
2459
2902
|
this.rememberItemConversationId(conversationId, item);
|
|
2460
2903
|
const timeline = this.timelines.get(conversationId) ?? [];
|
|
@@ -2572,6 +3015,17 @@ export class AgentWorkspaceProxy {
|
|
|
2572
3015
|
this.timelines.set(newId, [...mergedTimeline.values()]
|
|
2573
3016
|
.sort((a, b) => a.createdAt - b.createdAt)
|
|
2574
3017
|
.slice(-MAX_TIMELINE_ITEMS));
|
|
3018
|
+
const oldRevision = this.conversationRevisions.get(oldId) ?? 0;
|
|
3019
|
+
if (oldRevision > 0) {
|
|
3020
|
+
this.conversationRevisions.delete(oldId);
|
|
3021
|
+
this.setRevisionFloor(newId, oldRevision);
|
|
3022
|
+
}
|
|
3023
|
+
const oldEvents = this.revisionEvents.get(oldId);
|
|
3024
|
+
if (oldEvents) {
|
|
3025
|
+
this.revisionEvents.delete(oldId);
|
|
3026
|
+
const existingEvents = this.revisionEvents.get(newId) ?? [];
|
|
3027
|
+
this.revisionEvents.set(newId, [...existingEvents, ...oldEvents].slice(-MAX_DELTA_EVENTS));
|
|
3028
|
+
}
|
|
2575
3029
|
for (const [agentSessionId, conversationId] of this.conversationByAgentSessionId) {
|
|
2576
3030
|
if (conversationId === oldId) {
|
|
2577
3031
|
this.conversationByAgentSessionId.set(agentSessionId, newId);
|
|
@@ -2605,17 +3059,47 @@ export class AgentWorkspaceProxy {
|
|
|
2605
3059
|
}
|
|
2606
3060
|
emitItem(conversationId, item) {
|
|
2607
3061
|
const conversation = this.conversations.get(conversationId);
|
|
3062
|
+
const itemSnapshot = snapshotTimelineItem(item, { stripImages: false });
|
|
3063
|
+
const event = this.recordRevisionEvent(conversationId, { conversation, item: itemSnapshot });
|
|
2608
3064
|
this.input.send(createEnvelope({
|
|
2609
3065
|
type: "agent.v2.event",
|
|
2610
3066
|
hostDeviceId: this.input.hostDeviceId,
|
|
2611
|
-
payload: {
|
|
3067
|
+
payload: {
|
|
3068
|
+
conversationId,
|
|
3069
|
+
conversation: event.conversation,
|
|
3070
|
+
item: event.item,
|
|
3071
|
+
revision: event.revision,
|
|
3072
|
+
source: "device-live",
|
|
3073
|
+
canonical: true,
|
|
3074
|
+
},
|
|
2612
3075
|
}));
|
|
2613
3076
|
}
|
|
2614
3077
|
emitConversation(conversation) {
|
|
3078
|
+
const event = this.recordRevisionEvent(conversation.id, { conversation });
|
|
2615
3079
|
this.input.send(createEnvelope({
|
|
2616
3080
|
type: "agent.v2.event",
|
|
2617
3081
|
hostDeviceId: this.input.hostDeviceId,
|
|
2618
|
-
payload: {
|
|
3082
|
+
payload: {
|
|
3083
|
+
conversationId: conversation.id,
|
|
3084
|
+
conversation: event.conversation,
|
|
3085
|
+
revision: event.revision,
|
|
3086
|
+
source: "device-live",
|
|
3087
|
+
canonical: true,
|
|
3088
|
+
},
|
|
3089
|
+
}));
|
|
3090
|
+
this.input.send(createEnvelope({
|
|
3091
|
+
type: "agent.v2.running_state",
|
|
3092
|
+
hostDeviceId: this.input.hostDeviceId,
|
|
3093
|
+
payload: {
|
|
3094
|
+
conversationId: conversation.id,
|
|
3095
|
+
status: conversation.status,
|
|
3096
|
+
runningTurnId: this.currentTurnIds.get(conversation.id),
|
|
3097
|
+
revision: event.revision,
|
|
3098
|
+
error: conversation.status === "error" ? conversation.lastMessagePreview : undefined,
|
|
3099
|
+
updatedAt: conversation.lastActivityAt,
|
|
3100
|
+
source: "device-live",
|
|
3101
|
+
canonical: true,
|
|
3102
|
+
},
|
|
2619
3103
|
}));
|
|
2620
3104
|
}
|
|
2621
3105
|
emitStatus(conversationId, status, text) {
|
|
@@ -2659,17 +3143,126 @@ export class AgentWorkspaceProxy {
|
|
|
2659
3143
|
if (conversation)
|
|
2660
3144
|
this.hydrateStoredTimeline(conversation);
|
|
2661
3145
|
}
|
|
2662
|
-
const conversations = [...this.conversations.values()];
|
|
2663
|
-
const
|
|
2664
|
-
? snapshotTimelineItems(this.timelines.get(conversationId) ?? [])
|
|
2665
|
-
: [];
|
|
3146
|
+
const conversations = [...this.conversations.values()].map((conversation) => this.conversationSnapshot(conversation));
|
|
3147
|
+
const snapshot = conversationId ? this.latestSnapshot(conversationId) : { items: [], cursor: undefined, hasMore: false };
|
|
2666
3148
|
this.input.send(createEnvelope({
|
|
2667
3149
|
type: "agent.v2.snapshot",
|
|
2668
3150
|
hostDeviceId: this.input.hostDeviceId,
|
|
2669
3151
|
payload: {
|
|
2670
3152
|
conversations,
|
|
2671
3153
|
activeConversationId: this.activeConversationId,
|
|
2672
|
-
items,
|
|
3154
|
+
items: snapshot.items,
|
|
3155
|
+
revision: conversationId ? this.getRevision(conversationId) : undefined,
|
|
3156
|
+
cursor: snapshot.cursor,
|
|
3157
|
+
hasMore: snapshot.hasMore,
|
|
3158
|
+
source: "device-history",
|
|
3159
|
+
canonical: true,
|
|
3160
|
+
},
|
|
3161
|
+
}));
|
|
3162
|
+
}
|
|
3163
|
+
async sendHistoryPage(payload) {
|
|
3164
|
+
const conversation = this.conversations.get(payload.conversationId);
|
|
3165
|
+
if (!conversation)
|
|
3166
|
+
return;
|
|
3167
|
+
const limit = Math.min(Math.max(payload.limit ?? MAX_SNAPSHOT_ITEMS, 1), 200);
|
|
3168
|
+
const items = this.mergeCanonicalTimelineItems([
|
|
3169
|
+
...this.canonicalTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
|
|
3170
|
+
...await this.appServerTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
|
|
3171
|
+
], HISTORY_PAGE_MAX_ITEMS);
|
|
3172
|
+
this.timelines.set(conversation.id, items.slice(-MAX_TIMELINE_ITEMS).map((item) => this.annotateTimelineItem(item, item.revision, item.source ?? "device-history")));
|
|
3173
|
+
for (const item of this.timelines.get(conversation.id) ?? []) {
|
|
3174
|
+
this.rememberItemConversationId(conversation.id, item);
|
|
3175
|
+
}
|
|
3176
|
+
const direction = payload.direction ?? "older";
|
|
3177
|
+
let page;
|
|
3178
|
+
let cursor;
|
|
3179
|
+
let hasMore = false;
|
|
3180
|
+
if (direction === "newer") {
|
|
3181
|
+
const start = clampHistoryCursor(payload.cursor, 0, items.length);
|
|
3182
|
+
const end = Math.min(items.length, start + limit);
|
|
3183
|
+
page = items.slice(start, end);
|
|
3184
|
+
hasMore = end < items.length;
|
|
3185
|
+
cursor = hasMore ? String(end) : undefined;
|
|
3186
|
+
}
|
|
3187
|
+
else {
|
|
3188
|
+
const end = clampHistoryCursor(payload.cursor, items.length, items.length);
|
|
3189
|
+
const start = Math.max(0, end - limit);
|
|
3190
|
+
page = items.slice(start, end);
|
|
3191
|
+
hasMore = start > 0;
|
|
3192
|
+
cursor = hasMore ? String(start) : undefined;
|
|
3193
|
+
}
|
|
3194
|
+
conversation.historyComplete = !hasMore;
|
|
3195
|
+
const revision = this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
|
|
3196
|
+
this.input.send(createEnvelope({
|
|
3197
|
+
type: "agent.v2.history.page",
|
|
3198
|
+
hostDeviceId: this.input.hostDeviceId,
|
|
3199
|
+
payload: {
|
|
3200
|
+
conversationId: conversation.id,
|
|
3201
|
+
conversation: this.conversationSnapshot(conversation),
|
|
3202
|
+
items: page.map((item) => snapshotTimelineItem(item)),
|
|
3203
|
+
revision,
|
|
3204
|
+
cursor,
|
|
3205
|
+
hasMore,
|
|
3206
|
+
source: "device-history",
|
|
3207
|
+
canonical: true,
|
|
3208
|
+
},
|
|
3209
|
+
}));
|
|
3210
|
+
}
|
|
3211
|
+
sendDelta(payload) {
|
|
3212
|
+
const conversation = this.conversations.get(payload.conversationId);
|
|
3213
|
+
if (!conversation)
|
|
3214
|
+
return;
|
|
3215
|
+
this.hydrateStoredTimeline(conversation);
|
|
3216
|
+
const sinceRevision = payload.sinceRevision ?? 0;
|
|
3217
|
+
const limit = Math.min(Math.max(payload.limit ?? 100, 1), 500);
|
|
3218
|
+
const events = this.revisionEvents.get(conversation.id) ?? [];
|
|
3219
|
+
const oldestAvailable = events[0]?.revision ?? this.getRevision(conversation.id);
|
|
3220
|
+
const newestRevision = this.getRevision(conversation.id);
|
|
3221
|
+
const reset = sinceRevision > 0 &&
|
|
3222
|
+
(sinceRevision > newestRevision ||
|
|
3223
|
+
(sinceRevision < newestRevision &&
|
|
3224
|
+
(events.length === 0 || sinceRevision < oldestAvailable - 1)));
|
|
3225
|
+
if (reset) {
|
|
3226
|
+
const snapshot = this.latestSnapshot(conversation.id);
|
|
3227
|
+
this.input.send(createEnvelope({
|
|
3228
|
+
type: "agent.v2.delta",
|
|
3229
|
+
hostDeviceId: this.input.hostDeviceId,
|
|
3230
|
+
payload: {
|
|
3231
|
+
conversationId: conversation.id,
|
|
3232
|
+
conversation: this.conversationSnapshot(conversation),
|
|
3233
|
+
items: snapshot.items,
|
|
3234
|
+
sinceRevision,
|
|
3235
|
+
revision: newestRevision,
|
|
3236
|
+
reset: true,
|
|
3237
|
+
cursor: snapshot.cursor,
|
|
3238
|
+
hasMore: snapshot.hasMore,
|
|
3239
|
+
source: "device-history",
|
|
3240
|
+
canonical: true,
|
|
3241
|
+
},
|
|
3242
|
+
}));
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
3245
|
+
const changed = events
|
|
3246
|
+
.filter((event) => event.revision > sinceRevision)
|
|
3247
|
+
.slice(-limit);
|
|
3248
|
+
const itemsById = new Map();
|
|
3249
|
+
for (const event of changed) {
|
|
3250
|
+
if (event.item)
|
|
3251
|
+
itemsById.set(event.item.id, event.item);
|
|
3252
|
+
}
|
|
3253
|
+
this.input.send(createEnvelope({
|
|
3254
|
+
type: "agent.v2.delta",
|
|
3255
|
+
hostDeviceId: this.input.hostDeviceId,
|
|
3256
|
+
payload: {
|
|
3257
|
+
conversationId: conversation.id,
|
|
3258
|
+
conversation: this.conversationSnapshot(conversation),
|
|
3259
|
+
items: [...itemsById.values()].map((item) => snapshotTimelineItem(item)),
|
|
3260
|
+
sinceRevision,
|
|
3261
|
+
revision: newestRevision,
|
|
3262
|
+
reset: false,
|
|
3263
|
+
hasMore: changed.length === limit && events.some((event) => event.revision > sinceRevision && event.revision < changed[0].revision),
|
|
3264
|
+
source: "device-live",
|
|
3265
|
+
canonical: true,
|
|
2673
3266
|
},
|
|
2674
3267
|
}));
|
|
2675
3268
|
}
|
|
@@ -2677,21 +3270,23 @@ export class AgentWorkspaceProxy {
|
|
|
2677
3270
|
if (!conversation.agentSessionId)
|
|
2678
3271
|
return;
|
|
2679
3272
|
const existing = this.timelines.get(conversation.id) ?? [];
|
|
2680
|
-
if (existing.length > 0)
|
|
3273
|
+
if (existing.length > 0) {
|
|
3274
|
+
this.setRevisionFloor(conversation.id, Math.max(existing.length, ...existing.map((item) => item.revision ?? 0)));
|
|
2681
3275
|
return;
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
if (result.items.length === 0)
|
|
3276
|
+
}
|
|
3277
|
+
const stored = this.storedTimeline(conversation, MAX_TIMELINE_ITEMS);
|
|
3278
|
+
conversation.historyComplete = stored.length <= MAX_SNAPSHOT_ITEMS;
|
|
3279
|
+
if (stored.length === 0) {
|
|
3280
|
+
this.setRevisionFloor(conversation.id, this.getRevision(conversation.id));
|
|
2688
3281
|
return;
|
|
2689
|
-
|
|
3282
|
+
}
|
|
3283
|
+
const items = stored
|
|
2690
3284
|
.sort((a, b) => a.createdAt - b.createdAt)
|
|
2691
3285
|
.slice(-MAX_TIMELINE_ITEMS);
|
|
2692
3286
|
this.timelines.set(conversation.id, items);
|
|
2693
3287
|
for (const item of items)
|
|
2694
3288
|
this.rememberItemConversationId(conversation.id, item);
|
|
3289
|
+
this.setRevisionFloor(conversation.id, Math.max(items.length, ...items.map((item) => item.revision ?? 0)));
|
|
2695
3290
|
const lastMessage = [...items].reverse().find((item) => item.text?.trim());
|
|
2696
3291
|
if (lastMessage?.text && !conversation.lastMessagePreview) {
|
|
2697
3292
|
conversation.lastMessagePreview = previewText(lastMessage.text);
|
|
@@ -2760,6 +3355,9 @@ export class AgentWorkspaceProxy {
|
|
|
2760
3355
|
rememberTurnConversationId(conversationId, turnId) {
|
|
2761
3356
|
this.currentTurnIds.set(conversationId, turnId);
|
|
2762
3357
|
this.turnConversationIds.set(turnId, conversationId);
|
|
3358
|
+
const conversation = this.conversations.get(conversationId);
|
|
3359
|
+
if (conversation)
|
|
3360
|
+
conversation.runningTurnId = turnId;
|
|
2763
3361
|
}
|
|
2764
3362
|
forgetCurrentTurn(conversationId, turnId) {
|
|
2765
3363
|
const currentTurnId = this.currentTurnIds.get(conversationId);
|
|
@@ -2768,6 +3366,9 @@ export class AgentWorkspaceProxy {
|
|
|
2768
3366
|
this.turnConversationIds.delete(turnId);
|
|
2769
3367
|
if (currentTurnId && currentTurnId !== turnId)
|
|
2770
3368
|
this.turnConversationIds.delete(currentTurnId);
|
|
3369
|
+
const conversation = this.conversations.get(conversationId);
|
|
3370
|
+
if (conversation)
|
|
3371
|
+
conversation.runningTurnId = undefined;
|
|
2771
3372
|
}
|
|
2772
3373
|
rememberItemConversationId(conversationId, item) {
|
|
2773
3374
|
const keys = [
|
|
@@ -2807,28 +3408,51 @@ export class AgentWorkspaceProxy {
|
|
|
2807
3408
|
}
|
|
2808
3409
|
cancelPendingPermissions(conversationId) {
|
|
2809
3410
|
for (const [requestId, waiter] of this.permissionWaiters) {
|
|
3411
|
+
const ownerConversationId = this.conversationIdForPermissionRequest(requestId);
|
|
3412
|
+
if (conversationId && ownerConversationId && ownerConversationId !== conversationId)
|
|
3413
|
+
continue;
|
|
2810
3414
|
clearTimeout(waiter.timer);
|
|
2811
3415
|
waiter.resolve(formatPermissionResponse(this.permissionSources.get(requestId), "cancelled", "cancelled"));
|
|
3416
|
+
if (ownerConversationId) {
|
|
3417
|
+
this.markPermission(ownerConversationId, requestId, {
|
|
3418
|
+
permissionOutcome: "cancelled",
|
|
3419
|
+
optionId: "cancelled",
|
|
3420
|
+
permissionLive: false,
|
|
3421
|
+
permissionPending: false,
|
|
3422
|
+
permissionExpired: false,
|
|
3423
|
+
permissionError: "已停止",
|
|
3424
|
+
});
|
|
3425
|
+
}
|
|
3426
|
+
this.permissionWaiters.delete(requestId);
|
|
2812
3427
|
this.pendingPermissions.delete(requestId);
|
|
2813
3428
|
this.permissionSources.delete(requestId);
|
|
2814
3429
|
}
|
|
2815
|
-
this.permissionWaiters.clear();
|
|
2816
3430
|
for (const [requestId, waiter] of this.structuredInputWaiters) {
|
|
3431
|
+
const pending = this.pendingStructuredInputs.get(requestId);
|
|
3432
|
+
if (conversationId && pending?.conversationId && pending.conversationId !== conversationId)
|
|
3433
|
+
continue;
|
|
2817
3434
|
clearTimeout(waiter.timer);
|
|
2818
3435
|
waiter.resolve(formatStructuredInputResponse({}));
|
|
2819
|
-
const pending = this.pendingStructuredInputs.get(requestId);
|
|
2820
3436
|
if (pending) {
|
|
2821
3437
|
this.markStructuredInput(pending.conversationId, requestId, {
|
|
2822
3438
|
inputPending: false,
|
|
2823
3439
|
inputError: "已停止",
|
|
2824
3440
|
});
|
|
2825
3441
|
}
|
|
3442
|
+
this.structuredInputWaiters.delete(requestId);
|
|
2826
3443
|
this.pendingStructuredInputs.delete(requestId);
|
|
2827
3444
|
}
|
|
2828
|
-
this.structuredInputWaiters.clear();
|
|
2829
3445
|
if (conversationId)
|
|
2830
3446
|
this.updateConversationStatus(conversationId, "idle");
|
|
2831
3447
|
}
|
|
3448
|
+
conversationIdForPermissionRequest(requestId) {
|
|
3449
|
+
for (const [conversationId, timeline] of this.timelines) {
|
|
3450
|
+
if (timeline.some((item) => item.type === "permission" && item.permission?.requestId === requestId)) {
|
|
3451
|
+
return conversationId;
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
return undefined;
|
|
3455
|
+
}
|
|
2832
3456
|
extractSessionId(value) {
|
|
2833
3457
|
const raw = asRecord(value);
|
|
2834
3458
|
if (!raw)
|