linkshell-cli 0.3.27 → 0.3.29
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/commands/setup.js +20 -2
- package/dist/cli/src/commands/setup.js.map +1 -1
- package/dist/cli/src/commands/upgrade.js +9 -24
- package/dist/cli/src/commands/upgrade.js.map +1 -1
- package/dist/cli/src/index.js +28 -26
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/providers.d.ts +3 -7
- package/dist/cli/src/providers.js +76 -19
- package/dist/cli/src/providers.js.map +1 -1
- package/dist/cli/src/runtime/acp/acp-client.d.ts +0 -10
- package/dist/cli/src/runtime/acp/acp-client.js +19 -146
- package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-session.d.ts +62 -0
- package/dist/cli/src/runtime/acp/agent-session.js +1075 -0
- package/dist/cli/src/runtime/acp/agent-session.js.map +1 -0
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +1 -24
- package/dist/cli/src/runtime/acp/agent-workspace.js +162 -981
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +1 -3
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +35 -81
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +1 -2
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js +35 -54
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/codex-rpc-bridge.js +12 -1
- package/dist/cli/src/runtime/acp/codex-rpc-bridge.js.map +1 -1
- package/dist/cli/src/runtime/acp/json-rpc.d.ts +1 -34
- package/dist/cli/src/runtime/acp/json-rpc.js +8 -32
- package/dist/cli/src/runtime/acp/json-rpc.js.map +1 -1
- package/dist/cli/src/runtime/acp/provider-resolver.d.ts +0 -3
- package/dist/cli/src/runtime/acp/provider-resolver.js +6 -26
- package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
- package/dist/cli/src/runtime/bridge-session.d.ts +31 -1
- package/dist/cli/src/runtime/bridge-session.js +991 -81
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/src/runtime/screen-fallback.d.ts +1 -1
- package/dist/cli/src/runtime/screen-fallback.js +4 -4
- package/dist/cli/src/runtime/screen-fallback.js.map +1 -1
- package/dist/cli/src/runtime/screen-share.d.ts +1 -1
- package/dist/cli/src/runtime/screen-share.js +7 -7
- package/dist/cli/src/runtime/screen-share.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +11940 -3423
- package/dist/shared-protocol/src/index.js +98 -164
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +5 -5
- package/src/commands/setup.ts +31 -5
- package/src/commands/upgrade.ts +8 -28
- package/src/index.ts +34 -29
- package/src/providers.ts +108 -26
- package/src/runtime/acp/acp-client.ts +21 -159
- package/src/runtime/acp/agent-session.ts +1180 -0
- package/src/runtime/acp/agent-workspace.ts +170 -1068
- package/src/runtime/acp/claude-sdk-client.ts +37 -85
- package/src/runtime/acp/claude-stream-json-client.ts +41 -59
- package/src/runtime/acp/json-rpc.ts +14 -46
- package/src/runtime/acp/provider-resolver.ts +6 -29
- package/src/runtime/bridge-session.ts +1092 -84
- package/src/runtime/screen-fallback.ts +5 -5
- package/src/runtime/screen-share.ts +8 -8
- package/src/runtime/acp/claude-sessions.ts +0 -563
- package/src/runtime/acp/codex-rpc-bridge.ts +0 -221
- package/src/runtime/acp/codex-sessions.ts +0 -802
- package/src/types/linkshell-gateway.d.ts +0 -18
|
@@ -1,56 +1,16 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
2
|
import { homedir } from "node:os";
|
|
4
3
|
import { basename, join, relative } from "node:path";
|
|
5
4
|
import { createEnvelope, parseTypedPayload, } from "@linkshell/protocol";
|
|
6
5
|
import { AcpClient } from "./acp-client.js";
|
|
7
6
|
import { ClaudeSdkClient } from "./claude-sdk-client.js";
|
|
8
7
|
import { ClaudeStreamJsonClient } from "./claude-stream-json-client.js";
|
|
9
|
-
import { CodexRpcBridge } from "./codex-rpc-bridge.js";
|
|
10
|
-
import { listClaudeStoredSessions, loadClaudeStoredTimeline } from "./claude-sessions.js";
|
|
11
|
-
import { loadCodexStoredTimeline } from "./codex-sessions.js";
|
|
12
8
|
import { resolveAgentCommand } from "./provider-resolver.js";
|
|
13
9
|
const PERMISSION_TIMEOUT_MS = 5 * 60_000;
|
|
14
10
|
const MAX_TIMELINE_ITEMS = 200;
|
|
15
|
-
const MAX_SNAPSHOT_ITEMS = 80;
|
|
16
|
-
const MAX_SNAPSHOT_TEXT_BYTES = 128 * 1024;
|
|
17
|
-
const MAX_DELTA_EVENTS = 500;
|
|
18
|
-
const HISTORY_PAGE_MAX_ITEMS = 500;
|
|
19
11
|
function id(prefix) {
|
|
20
12
|
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
21
13
|
}
|
|
22
|
-
function clampHistoryCursor(value, fallback, max) {
|
|
23
|
-
if (!value)
|
|
24
|
-
return fallback;
|
|
25
|
-
const parsed = Number.parseInt(value, 10);
|
|
26
|
-
if (!Number.isFinite(parsed))
|
|
27
|
-
return fallback;
|
|
28
|
-
return Math.max(0, Math.min(max, parsed));
|
|
29
|
-
}
|
|
30
|
-
function appServerText(value) {
|
|
31
|
-
if (typeof value === "string") {
|
|
32
|
-
const text = value.trim();
|
|
33
|
-
return text || undefined;
|
|
34
|
-
}
|
|
35
|
-
if (Array.isArray(value)) {
|
|
36
|
-
const text = value
|
|
37
|
-
.map((part) => appServerText(part))
|
|
38
|
-
.filter(Boolean)
|
|
39
|
-
.join("\n")
|
|
40
|
-
.trim();
|
|
41
|
-
return text || undefined;
|
|
42
|
-
}
|
|
43
|
-
const record = asRecord(value);
|
|
44
|
-
if (!record)
|
|
45
|
-
return undefined;
|
|
46
|
-
if (typeof record.text === "string")
|
|
47
|
-
return appServerText(record.text);
|
|
48
|
-
if (typeof record.content === "string")
|
|
49
|
-
return appServerText(record.content);
|
|
50
|
-
if (typeof record.message === "string")
|
|
51
|
-
return appServerText(record.message);
|
|
52
|
-
return appServerText(record.content ?? record.message ?? record.parts);
|
|
53
|
-
}
|
|
54
14
|
function stringify(value) {
|
|
55
15
|
if (typeof value === "string")
|
|
56
16
|
return value;
|
|
@@ -61,62 +21,6 @@ function stringify(value) {
|
|
|
61
21
|
return String(value);
|
|
62
22
|
}
|
|
63
23
|
}
|
|
64
|
-
function truncateUtf8(value, maxBytes = MAX_SNAPSHOT_TEXT_BYTES) {
|
|
65
|
-
if (!value)
|
|
66
|
-
return value;
|
|
67
|
-
if (Buffer.byteLength(value, "utf8") <= maxBytes)
|
|
68
|
-
return value;
|
|
69
|
-
let end = Math.min(value.length, maxBytes);
|
|
70
|
-
while (end > 0 && Buffer.byteLength(value.slice(0, end), "utf8") > maxBytes) {
|
|
71
|
-
end = Math.floor(end * 0.9);
|
|
72
|
-
}
|
|
73
|
-
return `${value.slice(0, end)}\n\n[truncated by LinkShell: original ${Buffer.byteLength(value, "utf8")} bytes]`;
|
|
74
|
-
}
|
|
75
|
-
function snapshotContentBlocks(blocks, options = {}) {
|
|
76
|
-
if (!blocks)
|
|
77
|
-
return undefined;
|
|
78
|
-
return blocks.map((block) => block.type === "image" && options.stripImages !== false
|
|
79
|
-
? { ...block, data: undefined, text: block.text || "图片附件" }
|
|
80
|
-
: { ...block, text: truncateUtf8(block.text) });
|
|
81
|
-
}
|
|
82
|
-
function snapshotTimelineItem(item, options = {}) {
|
|
83
|
-
return {
|
|
84
|
-
...item,
|
|
85
|
-
content: snapshotContentBlocks(item.content, options),
|
|
86
|
-
text: truncateUtf8(item.text),
|
|
87
|
-
toolCall: item.toolCall
|
|
88
|
-
? {
|
|
89
|
-
...item.toolCall,
|
|
90
|
-
input: truncateUtf8(item.toolCall.input),
|
|
91
|
-
output: truncateUtf8(item.toolCall.output),
|
|
92
|
-
}
|
|
93
|
-
: undefined,
|
|
94
|
-
commandExecution: item.commandExecution
|
|
95
|
-
? {
|
|
96
|
-
...item.commandExecution,
|
|
97
|
-
command: truncateUtf8(item.commandExecution.command, 16 * 1024),
|
|
98
|
-
output: truncateUtf8(item.commandExecution.output),
|
|
99
|
-
}
|
|
100
|
-
: undefined,
|
|
101
|
-
fileChange: item.fileChange
|
|
102
|
-
? {
|
|
103
|
-
...item.fileChange,
|
|
104
|
-
diff: truncateUtf8(item.fileChange.diff),
|
|
105
|
-
summary: truncateUtf8(item.fileChange.summary),
|
|
106
|
-
}
|
|
107
|
-
: undefined,
|
|
108
|
-
permission: item.permission
|
|
109
|
-
? {
|
|
110
|
-
...item.permission,
|
|
111
|
-
toolInput: truncateUtf8(item.permission.toolInput),
|
|
112
|
-
context: truncateUtf8(item.permission.context),
|
|
113
|
-
}
|
|
114
|
-
: undefined,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
function snapshotTimelineItems(items) {
|
|
118
|
-
return items.slice(-MAX_SNAPSHOT_ITEMS).map((item) => snapshotTimelineItem(item));
|
|
119
|
-
}
|
|
120
24
|
function asRecord(value) {
|
|
121
25
|
return typeof value === "object" && value ? value : undefined;
|
|
122
26
|
}
|
|
@@ -422,15 +326,7 @@ function titleFromCwd(cwd) {
|
|
|
422
326
|
}
|
|
423
327
|
function textFromBlocks(blocks) {
|
|
424
328
|
return blocks
|
|
425
|
-
.map((block) => {
|
|
426
|
-
if (block.type === "text")
|
|
427
|
-
return block.text ?? "";
|
|
428
|
-
if (block.type === "image")
|
|
429
|
-
return `[${block.mimeType ?? "image"} attachment]`;
|
|
430
|
-
if (block.type === "file" && block.path)
|
|
431
|
-
return `@${block.path}`;
|
|
432
|
-
return "";
|
|
433
|
-
})
|
|
329
|
+
.map((block) => block.type === "text" ? block.text ?? "" : `[${block.mimeType ?? "image"} attachment]`)
|
|
434
330
|
.filter(Boolean)
|
|
435
331
|
.join("\n");
|
|
436
332
|
}
|
|
@@ -463,11 +359,6 @@ function contentBlocksFromValue(value) {
|
|
|
463
359
|
const text = firstString(raw, ["text", "content", "message"]);
|
|
464
360
|
return text ? [{ type: "text", text }] : [];
|
|
465
361
|
}
|
|
466
|
-
if (normalizedType === "file" || normalizedType === "inputfile" || normalizedType === "filereference") {
|
|
467
|
-
const path = firstString(raw, ["path", "file", "filePath", "file_path", "absolutePath", "relativePath"]);
|
|
468
|
-
const text = firstString(raw, ["text", "name", "label"]);
|
|
469
|
-
return path ? [{ type: "file", path, name: text, text: text ?? `@${path}` }] : [];
|
|
470
|
-
}
|
|
471
362
|
const nested = raw.content ?? raw.contentItems ?? raw.parts;
|
|
472
363
|
if (Array.isArray(nested))
|
|
473
364
|
return contentBlocksFromValue(nested);
|
|
@@ -663,28 +554,8 @@ function providerLabel(provider) {
|
|
|
663
554
|
return "Custom";
|
|
664
555
|
}
|
|
665
556
|
const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"];
|
|
666
|
-
const
|
|
667
|
-
const
|
|
668
|
-
const AGENT_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"];
|
|
669
|
-
const COMMAND_OUTPUT_MAX_BYTES = 96 * 1024;
|
|
670
|
-
const LINKSHELL_NATIVE_COMMANDS = [
|
|
671
|
-
{ name: "status", description: "Show current Agent and workspace status", category: "LinkShell", argsMode: "none" },
|
|
672
|
-
{ name: "plan", description: "Enter Plan mode for the next turn", category: "Agent", argsMode: "none" },
|
|
673
|
-
{ name: "exit-plan", description: "Exit Plan mode", category: "Agent", argsMode: "none" },
|
|
674
|
-
{ name: "review", description: "Ask the Agent to review current local changes", category: "Agent", argsMode: "optional" },
|
|
675
|
-
{ name: "subagents", description: "Ask the Agent to split work across subagents when useful", category: "Agent", argsMode: "optional" },
|
|
676
|
-
{ name: "compact", description: "Compact the active Codex context", category: "Codex", argsMode: "none", providers: ["codex"] },
|
|
677
|
-
{ name: "new", description: "Start a fresh Codex thread", category: "Codex", argsMode: "none", providers: ["codex"], destructive: true },
|
|
678
|
-
{ name: "clear", description: "Start a fresh Agent context for this conversation", category: "Agent", argsMode: "none", destructive: true },
|
|
679
|
-
{ name: "git-status", description: "Show branch and working tree status", category: "Git", argsMode: "none" },
|
|
680
|
-
{ name: "git-diff", description: "Show a compact diffstat for current changes", category: "Git", argsMode: "none" },
|
|
681
|
-
{ name: "git-commit", description: "Commit staged changes with the given message", category: "Git", argsMode: "required" },
|
|
682
|
-
{ name: "git-pull", description: "Pull with fast-forward only", category: "Git", argsMode: "none" },
|
|
683
|
-
{ name: "git-push", description: "Push the current branch", category: "Git", argsMode: "none" },
|
|
684
|
-
{ name: "git-stash", description: "Stash current working tree changes", category: "Git", argsMode: "optional" },
|
|
685
|
-
{ name: "git-stash-pop", description: "Pop the latest stash", category: "Git", argsMode: "none" },
|
|
686
|
-
];
|
|
687
|
-
const CODEX_JSON_RPC_COMMANDS = new Set(["plan", "exit-plan", "review", "compact", "new"]);
|
|
557
|
+
const ALL_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"];
|
|
558
|
+
const CODEX_COMMAND_NAMES = ["plan", "exit-plan", "compact", "clear", "status", "review", "subagents"];
|
|
688
559
|
const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
|
|
689
560
|
"add-dir",
|
|
690
561
|
"agents",
|
|
@@ -946,31 +817,57 @@ function customClaudeCommands(cwd) {
|
|
|
946
817
|
}
|
|
947
818
|
function defaultProviderCommands(provider, cwd, enabled) {
|
|
948
819
|
const disabledReason = enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`;
|
|
949
|
-
const linkshellCommands = LINKSHELL_NATIVE_COMMANDS
|
|
950
|
-
.filter((command) => provider !== "codex" || CODEX_JSON_RPC_COMMANDS.has(command.name))
|
|
951
|
-
.filter((command) => !command.providers || command.providers.includes(provider))
|
|
952
|
-
.map((command) => makeCommand({
|
|
953
|
-
provider,
|
|
954
|
-
name: command.name,
|
|
955
|
-
description: command.description,
|
|
956
|
-
source: "linkshell",
|
|
957
|
-
category: command.category,
|
|
958
|
-
argsMode: command.argsMode ?? "optional",
|
|
959
|
-
destructive: command.destructive,
|
|
960
|
-
disabledReason,
|
|
961
|
-
executionKind: "native",
|
|
962
|
-
}));
|
|
963
820
|
if (provider === "codex") {
|
|
964
|
-
return
|
|
821
|
+
return CODEX_COMMAND_NAMES.map((name) => makeCommand({
|
|
822
|
+
provider,
|
|
823
|
+
name,
|
|
824
|
+
source: "linkshell",
|
|
825
|
+
category: name === "plan" || name === "exit-plan" ? "Modes" : "Codex",
|
|
826
|
+
description: {
|
|
827
|
+
"plan": "Enter Codex plan mode",
|
|
828
|
+
"exit-plan": "Exit Codex plan mode",
|
|
829
|
+
compact: "Compact the current thread",
|
|
830
|
+
clear: "Start a fresh Codex thread",
|
|
831
|
+
status: "Show LinkShell agent status",
|
|
832
|
+
review: "Ask Codex to review local changes",
|
|
833
|
+
subagents: "Insert a delegation prompt",
|
|
834
|
+
}[name],
|
|
835
|
+
argsMode: name === "review" || name === "subagents" ? "optional" : "none",
|
|
836
|
+
destructive: name === "clear",
|
|
837
|
+
disabledReason,
|
|
838
|
+
executionKind: name === "review" || name === "subagents" ? "prompt" : "native",
|
|
839
|
+
}));
|
|
965
840
|
}
|
|
966
841
|
if (provider === "claude") {
|
|
842
|
+
const builtIns = CLAUDE_BUILT_IN_COMMANDS
|
|
843
|
+
.filter((entry) => isClaudeRemoteFriendlyCommand(entry.name))
|
|
844
|
+
.map((entry) => makeCommand({
|
|
845
|
+
provider,
|
|
846
|
+
name: entry.name,
|
|
847
|
+
description: entry.description,
|
|
848
|
+
argsMode: entry.argsMode,
|
|
849
|
+
destructive: entry.destructive,
|
|
850
|
+
disabledReason,
|
|
851
|
+
executionKind: "prompt",
|
|
852
|
+
}));
|
|
967
853
|
const custom = customClaudeCommands(cwd).map((command) => ({
|
|
968
854
|
...command,
|
|
969
855
|
disabledReason: command.disabledReason ?? disabledReason,
|
|
970
856
|
}));
|
|
971
|
-
return [...
|
|
857
|
+
return [...builtIns, ...custom];
|
|
972
858
|
}
|
|
973
|
-
return
|
|
859
|
+
return [
|
|
860
|
+
makeCommand({
|
|
861
|
+
provider,
|
|
862
|
+
name: "status",
|
|
863
|
+
source: "linkshell",
|
|
864
|
+
category: "LinkShell",
|
|
865
|
+
description: "Show LinkShell agent status",
|
|
866
|
+
argsMode: "none",
|
|
867
|
+
disabledReason,
|
|
868
|
+
executionKind: "native",
|
|
869
|
+
}),
|
|
870
|
+
];
|
|
974
871
|
}
|
|
975
872
|
function mergeCommands(...groups) {
|
|
976
873
|
const map = new Map();
|
|
@@ -987,76 +884,6 @@ function mergeCommands(...groups) {
|
|
|
987
884
|
}
|
|
988
885
|
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
989
886
|
}
|
|
990
|
-
function isGitNativeCommand(name) {
|
|
991
|
-
return name === "git-status" ||
|
|
992
|
-
name === "git-diff" ||
|
|
993
|
-
name === "git-commit" ||
|
|
994
|
-
name === "git-pull" ||
|
|
995
|
-
name === "git-push" ||
|
|
996
|
-
name === "git-stash" ||
|
|
997
|
-
name === "git-stash-pop";
|
|
998
|
-
}
|
|
999
|
-
function gitCommandArgs(name, args) {
|
|
1000
|
-
const message = args?.trim();
|
|
1001
|
-
switch (name) {
|
|
1002
|
-
case "git-status":
|
|
1003
|
-
return { display: "git status --short --branch", argv: ["status", "--short", "--branch"] };
|
|
1004
|
-
case "git-diff":
|
|
1005
|
-
return { display: "git diff --stat", argv: ["diff", "--stat"] };
|
|
1006
|
-
case "git-commit":
|
|
1007
|
-
if (!message)
|
|
1008
|
-
throw new Error("请先输入提交信息,例如 /git-commit fix mobile agent timeline");
|
|
1009
|
-
return { display: `git commit -m ${JSON.stringify(message)}`, argv: ["commit", "-m", message] };
|
|
1010
|
-
case "git-pull":
|
|
1011
|
-
return { display: "git pull --ff-only", argv: ["pull", "--ff-only"] };
|
|
1012
|
-
case "git-push":
|
|
1013
|
-
return { display: "git push", argv: ["push"] };
|
|
1014
|
-
case "git-stash":
|
|
1015
|
-
return {
|
|
1016
|
-
display: "git stash push -u",
|
|
1017
|
-
argv: ["stash", "push", "-u", "-m", message || "LinkShell mobile stash"],
|
|
1018
|
-
};
|
|
1019
|
-
case "git-stash-pop":
|
|
1020
|
-
return { display: "git stash pop", argv: ["stash", "pop"] };
|
|
1021
|
-
default:
|
|
1022
|
-
throw new Error(`未知 Git 命令:/${name}`);
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
function runProcess(command, args, options) {
|
|
1026
|
-
return new Promise((resolve, reject) => {
|
|
1027
|
-
const child = spawn(command, args, {
|
|
1028
|
-
cwd: options.cwd,
|
|
1029
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1030
|
-
windowsHide: true,
|
|
1031
|
-
});
|
|
1032
|
-
const maxBytes = options.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES;
|
|
1033
|
-
let output = "";
|
|
1034
|
-
let bytes = 0;
|
|
1035
|
-
let truncated = false;
|
|
1036
|
-
const append = (chunk) => {
|
|
1037
|
-
if (bytes >= maxBytes) {
|
|
1038
|
-
truncated = true;
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
const remaining = maxBytes - bytes;
|
|
1042
|
-
const slice = chunk.byteLength > remaining ? chunk.subarray(0, remaining) : chunk;
|
|
1043
|
-
output += slice.toString("utf8");
|
|
1044
|
-
bytes += slice.byteLength;
|
|
1045
|
-
if (slice.byteLength < chunk.byteLength)
|
|
1046
|
-
truncated = true;
|
|
1047
|
-
};
|
|
1048
|
-
child.stdout.on("data", append);
|
|
1049
|
-
child.stderr.on("data", append);
|
|
1050
|
-
child.once("error", reject);
|
|
1051
|
-
child.once("close", (exitCode, signal) => {
|
|
1052
|
-
resolve({
|
|
1053
|
-
output: `${output.trimEnd()}${truncated ? "\n\n[truncated by LinkShell]" : ""}`,
|
|
1054
|
-
exitCode,
|
|
1055
|
-
signal,
|
|
1056
|
-
});
|
|
1057
|
-
});
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
887
|
function runtimeCommands(provider, value) {
|
|
1061
888
|
const raw = asRecord(value);
|
|
1062
889
|
const commandsValue = Array.isArray(value) ? value :
|
|
@@ -1100,14 +927,10 @@ function runtimeCommands(provider, value) {
|
|
|
1100
927
|
function parseModelListCapabilities(value) {
|
|
1101
928
|
const raw = asRecord(value);
|
|
1102
929
|
const modelsValue = Array.isArray(value) ? value :
|
|
1103
|
-
Array.isArray(raw?.
|
|
1104
|
-
Array.isArray(raw?.
|
|
1105
|
-
Array.isArray(raw?.
|
|
1106
|
-
|
|
1107
|
-
[];
|
|
1108
|
-
const effortSet = new Set();
|
|
1109
|
-
const speedTierSet = new Set(["standard"]);
|
|
1110
|
-
let defaultModelFromEntry;
|
|
930
|
+
Array.isArray(raw?.models) ? raw.models :
|
|
931
|
+
Array.isArray(raw?.items) ? raw.items :
|
|
932
|
+
Array.isArray(raw?.modelOptions) ? raw.modelOptions :
|
|
933
|
+
[];
|
|
1111
934
|
const models = modelsValue
|
|
1112
935
|
.map((entry, index) => {
|
|
1113
936
|
const model = asRecord(entry);
|
|
@@ -1117,56 +940,24 @@ function parseModelListCapabilities(value) {
|
|
|
1117
940
|
: undefined;
|
|
1118
941
|
}
|
|
1119
942
|
const modelId = firstString(model, ["id", "model", "name", "value"]) ?? `model-${index + 1}`;
|
|
1120
|
-
const label = firstString(model, ["
|
|
1121
|
-
|
|
1122
|
-
return undefined;
|
|
1123
|
-
const modelEfforts = arrayFromKeys(model, ["supportedReasoningEfforts", "reasoningEfforts"])
|
|
1124
|
-
.map((effort) => {
|
|
1125
|
-
const rawEffort = asRecord(effort);
|
|
1126
|
-
return firstString(rawEffort, ["reasoningEffort", "id", "value"]) ?? (typeof effort === "string" ? effort : undefined);
|
|
1127
|
-
})
|
|
1128
|
-
.filter((effort) => Boolean(effort) && ALL_REASONING_EFFORTS.includes(effort));
|
|
1129
|
-
modelEfforts.forEach((effort) => effortSet.add(effort));
|
|
1130
|
-
const defaultReasoningEffort = firstString(model, ["defaultReasoningEffort", "default_reasoning_effort"]);
|
|
1131
|
-
if (defaultReasoningEffort && ALL_REASONING_EFFORTS.includes(defaultReasoningEffort)) {
|
|
1132
|
-
effortSet.add(defaultReasoningEffort);
|
|
1133
|
-
}
|
|
1134
|
-
const modalities = arrayFromKeys(model, ["inputModalities", "modalities"]);
|
|
1135
|
-
const speedTiers = arrayFromKeys(model, ["additionalSpeedTiers", "additional_speed_tiers", "serviceTiers", "service_tiers"])
|
|
1136
|
-
.map((tier) => typeof tier === "string" ? tier : firstString(asRecord(tier), ["id", "tier", "value"]))
|
|
1137
|
-
.filter((tier) => Boolean(tier) && ALL_SERVICE_TIERS.includes(tier));
|
|
1138
|
-
speedTiers.forEach((tier) => speedTierSet.add(tier));
|
|
1139
|
-
if (model.isDefault === true)
|
|
1140
|
-
defaultModelFromEntry = modelId;
|
|
1141
|
-
return {
|
|
1142
|
-
id: modelId,
|
|
1143
|
-
label,
|
|
1144
|
-
...(modelEfforts.length > 0 ? { reasoningEfforts: modelEfforts } : {}),
|
|
1145
|
-
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
1146
|
-
...(speedTiers.length > 0 ? { speedTiers: ["standard", ...speedTiers.filter((tier) => tier !== "standard")] } : {}),
|
|
1147
|
-
...(modalities.length > 0 ? { supportsImages: modalities.includes("image") } : {}),
|
|
1148
|
-
...(firstString(model, ["description"]) ? { description: firstString(model, ["description"]) } : {}),
|
|
1149
|
-
};
|
|
943
|
+
const label = firstString(model, ["label", "title", "displayName", "name"]) ?? modelId;
|
|
944
|
+
return { id: modelId, label };
|
|
1150
945
|
})
|
|
1151
946
|
.filter((entry) => Boolean(entry));
|
|
1152
947
|
const defaultModel = firstString(raw, ["defaultModel", "default_model", "currentModel"]) ??
|
|
1153
|
-
firstString(asRecord(raw?.defaults), ["model"])
|
|
1154
|
-
defaultModelFromEntry;
|
|
948
|
+
firstString(asRecord(raw?.defaults), ["model"]);
|
|
1155
949
|
const effortsValue = Array.isArray(raw?.reasoningEfforts) ? raw.reasoningEfforts :
|
|
1156
950
|
Array.isArray(raw?.reasoning_efforts) ? raw.reasoning_efforts :
|
|
1157
951
|
Array.isArray(raw?.efforts) ? raw.efforts :
|
|
1158
952
|
undefined;
|
|
1159
953
|
const reasoningEfforts = effortsValue
|
|
1160
954
|
?.filter((entry) => typeof entry === "string" && ALL_REASONING_EFFORTS.includes(entry));
|
|
1161
|
-
|
|
1162
|
-
const resolvedSpeedTiers = [...speedTierSet];
|
|
1163
|
-
if (models.length === 0 && !defaultModel && resolvedReasoningEfforts.length === 0 && resolvedSpeedTiers.length <= 1)
|
|
955
|
+
if (models.length === 0 && !defaultModel && !reasoningEfforts?.length)
|
|
1164
956
|
return undefined;
|
|
1165
957
|
return {
|
|
1166
|
-
...(models.length > 0 ? { models } : {}),
|
|
958
|
+
...(models.length > 0 ? { models: [{ id: "default", label: "默认模型" }, ...models] } : {}),
|
|
1167
959
|
...(defaultModel ? { defaultModel } : {}),
|
|
1168
|
-
...(
|
|
1169
|
-
...(resolvedSpeedTiers.length > 0 ? { speedTiers: resolvedSpeedTiers } : {}),
|
|
960
|
+
...(reasoningEfforts?.length ? { reasoningEfforts } : {}),
|
|
1170
961
|
};
|
|
1171
962
|
}
|
|
1172
963
|
function parseTimestamp(value) {
|
|
@@ -1206,35 +997,15 @@ function parseRemoteSessions(value) {
|
|
|
1206
997
|
model: firstString(source, ["model", "modelId"]),
|
|
1207
998
|
createdAt: parseTimestamp(source.createdAt ?? source.created_at),
|
|
1208
999
|
lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
|
|
1209
|
-
archived: typeof source.archived === "boolean" ? source.archived : undefined,
|
|
1210
|
-
status: normalizeAgentStatus(firstString(source, ["status", "state", "phase"])),
|
|
1211
|
-
runningTurnId: firstString(source, ["runningTurnId", "running_turn_id", "turnId", "activeTurnId"]),
|
|
1212
1000
|
});
|
|
1213
1001
|
}
|
|
1214
1002
|
return result;
|
|
1215
1003
|
}
|
|
1216
|
-
function normalizeAgentStatus(value) {
|
|
1217
|
-
if (!value)
|
|
1218
|
-
return undefined;
|
|
1219
|
-
const normalized = value.toLowerCase();
|
|
1220
|
-
if (normalized === "running" || normalized === "in_progress" || normalized === "busy")
|
|
1221
|
-
return "running";
|
|
1222
|
-
if (normalized === "waiting_permission" || normalized === "waiting" || normalized === "blocked")
|
|
1223
|
-
return "waiting_permission";
|
|
1224
|
-
if (normalized === "error" || normalized === "failed")
|
|
1225
|
-
return "error";
|
|
1226
|
-
if (normalized === "idle" || normalized === "completed" || normalized === "done")
|
|
1227
|
-
return "idle";
|
|
1228
|
-
return undefined;
|
|
1229
|
-
}
|
|
1230
1004
|
export class AgentWorkspaceProxy {
|
|
1231
1005
|
input;
|
|
1232
1006
|
clients = new Map();
|
|
1233
|
-
codexRpcBridge;
|
|
1234
1007
|
agentProtocols = new Map();
|
|
1235
1008
|
providerCapabilities = new Map();
|
|
1236
|
-
providerCapabilityErrors = new Map();
|
|
1237
|
-
capabilitiesRevision = 0;
|
|
1238
1009
|
initialized = false;
|
|
1239
1010
|
status = "unavailable";
|
|
1240
1011
|
error;
|
|
@@ -1244,8 +1015,6 @@ export class AgentWorkspaceProxy {
|
|
|
1244
1015
|
conversations = new Map();
|
|
1245
1016
|
conversationByAgentSessionId = new Map();
|
|
1246
1017
|
timelines = new Map();
|
|
1247
|
-
conversationRevisions = new Map();
|
|
1248
|
-
revisionEvents = new Map();
|
|
1249
1018
|
toolOutputBuffers = new Map();
|
|
1250
1019
|
pendingPermissions = new Map();
|
|
1251
1020
|
permissionWaiters = new Map();
|
|
@@ -1259,32 +1028,6 @@ export class AgentWorkspaceProxy {
|
|
|
1259
1028
|
}
|
|
1260
1029
|
async handleEnvelope(envelope) {
|
|
1261
1030
|
switch (envelope.type) {
|
|
1262
|
-
case "agent.codex.rpc": {
|
|
1263
|
-
const payload = parseTypedPayload("agent.codex.rpc", envelope.payload);
|
|
1264
|
-
try {
|
|
1265
|
-
this.codexRpc().send(payload);
|
|
1266
|
-
}
|
|
1267
|
-
catch (error) {
|
|
1268
|
-
const idValue = payload && typeof payload === "object" && "id" in payload
|
|
1269
|
-
? payload.id
|
|
1270
|
-
: undefined;
|
|
1271
|
-
if (typeof idValue === "string" || typeof idValue === "number") {
|
|
1272
|
-
this.input.send(createEnvelope({
|
|
1273
|
-
type: "agent.codex.rpc",
|
|
1274
|
-
hostDeviceId: this.input.hostDeviceId,
|
|
1275
|
-
payload: {
|
|
1276
|
-
jsonrpc: "2.0",
|
|
1277
|
-
id: idValue,
|
|
1278
|
-
error: {
|
|
1279
|
-
code: -32000,
|
|
1280
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1281
|
-
},
|
|
1282
|
-
},
|
|
1283
|
-
}));
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
break;
|
|
1287
|
-
}
|
|
1288
1031
|
case "agent.v2.capabilities.request":
|
|
1289
1032
|
await this.initialize();
|
|
1290
1033
|
this.sendCapabilities();
|
|
@@ -1300,8 +1043,8 @@ export class AgentWorkspaceProxy {
|
|
|
1300
1043
|
const conversations = [...this.conversations.values()].filter((conversation) => payload.includeArchived ? true : !conversation.archived);
|
|
1301
1044
|
this.input.send(createEnvelope({
|
|
1302
1045
|
type: "agent.v2.conversation.list.result",
|
|
1303
|
-
|
|
1304
|
-
payload: { conversations
|
|
1046
|
+
sessionId: this.input.sessionId,
|
|
1047
|
+
payload: { conversations },
|
|
1305
1048
|
}));
|
|
1306
1049
|
break;
|
|
1307
1050
|
}
|
|
@@ -1310,16 +1053,6 @@ export class AgentWorkspaceProxy {
|
|
|
1310
1053
|
this.sendSnapshot(payload.conversationId);
|
|
1311
1054
|
break;
|
|
1312
1055
|
}
|
|
1313
|
-
case "agent.v2.history.request": {
|
|
1314
|
-
const payload = parseTypedPayload("agent.v2.history.request", envelope.payload);
|
|
1315
|
-
await this.sendHistoryPage(payload);
|
|
1316
|
-
break;
|
|
1317
|
-
}
|
|
1318
|
-
case "agent.v2.delta.request": {
|
|
1319
|
-
const payload = parseTypedPayload("agent.v2.delta.request", envelope.payload);
|
|
1320
|
-
this.sendDelta(payload);
|
|
1321
|
-
break;
|
|
1322
|
-
}
|
|
1323
1056
|
case "agent.v2.prompt": {
|
|
1324
1057
|
const payload = parseTypedPayload("agent.v2.prompt", envelope.payload);
|
|
1325
1058
|
await this.sendPrompt(payload);
|
|
@@ -1357,8 +1090,6 @@ export class AgentWorkspaceProxy {
|
|
|
1357
1090
|
}
|
|
1358
1091
|
}
|
|
1359
1092
|
stop() {
|
|
1360
|
-
this.codexRpcBridge?.stop();
|
|
1361
|
-
this.codexRpcBridge = undefined;
|
|
1362
1093
|
for (const client of this.clients.values()) {
|
|
1363
1094
|
client.stop();
|
|
1364
1095
|
}
|
|
@@ -1367,16 +1098,6 @@ export class AgentWorkspaceProxy {
|
|
|
1367
1098
|
clientForProvider(provider) {
|
|
1368
1099
|
return this.clients.get(provider);
|
|
1369
1100
|
}
|
|
1370
|
-
codexRpc() {
|
|
1371
|
-
this.codexRpcBridge ??= new CodexRpcBridge({
|
|
1372
|
-
command: this.input.command,
|
|
1373
|
-
cwd: this.input.cwd,
|
|
1374
|
-
hostDeviceId: this.input.hostDeviceId,
|
|
1375
|
-
send: this.input.send,
|
|
1376
|
-
verbose: this.input.verbose,
|
|
1377
|
-
});
|
|
1378
|
-
return this.codexRpcBridge;
|
|
1379
|
-
}
|
|
1380
1101
|
protocolForProvider(provider) {
|
|
1381
1102
|
return this.agentProtocols.get(provider);
|
|
1382
1103
|
}
|
|
@@ -1385,9 +1106,7 @@ export class AgentWorkspaceProxy {
|
|
|
1385
1106
|
return;
|
|
1386
1107
|
this.initialized = true;
|
|
1387
1108
|
// Eagerly start all detected providers so capabilities report real status
|
|
1388
|
-
const startPromises = this.input.availableProviders
|
|
1389
|
-
.filter((provider) => provider !== "codex")
|
|
1390
|
-
.map((p) => this.ensureProviderClient(p));
|
|
1109
|
+
const startPromises = this.input.availableProviders.map((p) => this.ensureProviderClient(p));
|
|
1391
1110
|
await Promise.allSettled(startPromises);
|
|
1392
1111
|
this.status = "idle";
|
|
1393
1112
|
this.error = undefined;
|
|
@@ -1493,13 +1212,10 @@ export class AgentWorkspaceProxy {
|
|
|
1493
1212
|
try {
|
|
1494
1213
|
const result = await listModels.call(client);
|
|
1495
1214
|
const runtimeCapabilities = parseModelListCapabilities(result);
|
|
1496
|
-
if (runtimeCapabilities)
|
|
1215
|
+
if (runtimeCapabilities)
|
|
1497
1216
|
this.providerCapabilities.set(provider, runtimeCapabilities);
|
|
1498
|
-
this.providerCapabilityErrors.delete(provider);
|
|
1499
|
-
}
|
|
1500
1217
|
}
|
|
1501
1218
|
catch (error) {
|
|
1502
|
-
this.providerCapabilityErrors.set(provider, error instanceof Error ? error.message : String(error));
|
|
1503
1219
|
if (this.input.verbose) {
|
|
1504
1220
|
process.stderr.write(`[agent:v2] model/list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1505
1221
|
}
|
|
@@ -1507,11 +1223,36 @@ export class AgentWorkspaceProxy {
|
|
|
1507
1223
|
}
|
|
1508
1224
|
async syncProviderSessions() {
|
|
1509
1225
|
await this.initialize();
|
|
1510
|
-
this.upsertProviderSessions("claude", listClaudeStoredSessions(this.input.cwd));
|
|
1511
1226
|
for (const [provider, client] of this.clients) {
|
|
1512
1227
|
try {
|
|
1513
1228
|
const result = await client.listSessions();
|
|
1514
|
-
|
|
1229
|
+
for (const remote of parseRemoteSessions(result)) {
|
|
1230
|
+
const agentSessionId = remote.id;
|
|
1231
|
+
const existingId = this.conversationByAgentSessionId.get(agentSessionId);
|
|
1232
|
+
const now = Date.now();
|
|
1233
|
+
const conversationId = existingId ?? `agent:${agentSessionId}`;
|
|
1234
|
+
const existing = this.conversations.get(conversationId);
|
|
1235
|
+
const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
|
|
1236
|
+
const conversation = {
|
|
1237
|
+
id: conversationId,
|
|
1238
|
+
agentSessionId,
|
|
1239
|
+
provider,
|
|
1240
|
+
cwd,
|
|
1241
|
+
title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
|
|
1242
|
+
model: remote.model ?? existing?.model,
|
|
1243
|
+
reasoningEffort: existing?.reasoningEffort,
|
|
1244
|
+
permissionMode: existing?.permissionMode,
|
|
1245
|
+
collaborationMode: existing?.collaborationMode,
|
|
1246
|
+
status: existing?.status ?? "idle",
|
|
1247
|
+
archived: existing?.archived ?? false,
|
|
1248
|
+
lastMessagePreview: existing?.lastMessagePreview,
|
|
1249
|
+
lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
|
|
1250
|
+
createdAt: remote.createdAt ?? existing?.createdAt ?? now,
|
|
1251
|
+
};
|
|
1252
|
+
this.conversations.set(conversation.id, conversation);
|
|
1253
|
+
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
1254
|
+
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
1255
|
+
}
|
|
1515
1256
|
}
|
|
1516
1257
|
catch (error) {
|
|
1517
1258
|
if (this.input.verbose) {
|
|
@@ -1520,55 +1261,16 @@ export class AgentWorkspaceProxy {
|
|
|
1520
1261
|
}
|
|
1521
1262
|
}
|
|
1522
1263
|
}
|
|
1523
|
-
upsertProviderSessions(provider, result) {
|
|
1524
|
-
for (const remote of parseRemoteSessions(result)) {
|
|
1525
|
-
const agentSessionId = remote.id;
|
|
1526
|
-
const existingId = this.conversationByAgentSessionId.get(agentSessionId);
|
|
1527
|
-
const now = Date.now();
|
|
1528
|
-
const conversationId = existingId ?? `agent:${agentSessionId}`;
|
|
1529
|
-
const existing = this.conversations.get(conversationId);
|
|
1530
|
-
const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
|
|
1531
|
-
const conversation = {
|
|
1532
|
-
id: conversationId,
|
|
1533
|
-
agentSessionId,
|
|
1534
|
-
provider,
|
|
1535
|
-
cwd,
|
|
1536
|
-
title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
|
|
1537
|
-
model: remote.model ?? existing?.model,
|
|
1538
|
-
reasoningEffort: existing?.reasoningEffort,
|
|
1539
|
-
serviceTier: existing?.serviceTier,
|
|
1540
|
-
permissionMode: existing?.permissionMode,
|
|
1541
|
-
collaborationMode: existing?.collaborationMode,
|
|
1542
|
-
status: remote.status ?? existing?.status ?? "idle",
|
|
1543
|
-
archived: remote.archived ?? existing?.archived ?? false,
|
|
1544
|
-
timelineRevision: existing?.timelineRevision ?? this.getRevision(conversationId),
|
|
1545
|
-
historyComplete: existing?.historyComplete ?? false,
|
|
1546
|
-
runningTurnId: remote.runningTurnId ?? this.currentTurnIds.get(conversationId),
|
|
1547
|
-
source: "device",
|
|
1548
|
-
canonical: true,
|
|
1549
|
-
lastMessagePreview: existing?.lastMessagePreview,
|
|
1550
|
-
lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
|
|
1551
|
-
createdAt: remote.createdAt ?? existing?.createdAt ?? now,
|
|
1552
|
-
};
|
|
1553
|
-
this.conversations.set(conversation.id, conversation);
|
|
1554
|
-
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
1555
|
-
if (remote.runningTurnId)
|
|
1556
|
-
this.rememberTurnConversationId(conversation.id, remote.runningTurnId);
|
|
1557
|
-
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
1264
|
sendCapabilities() {
|
|
1561
1265
|
const providers = this.input.availableProviders.map((provider) => {
|
|
1562
1266
|
const client = this.clients.get(provider);
|
|
1563
|
-
const protocol =
|
|
1267
|
+
const protocol = this.agentProtocols.get(provider);
|
|
1564
1268
|
const runtimeCapabilities = this.providerCapabilities.get(provider);
|
|
1565
|
-
const enabled =
|
|
1566
|
-
const hasRuntimeModels = Boolean(runtimeCapabilities?.models?.length);
|
|
1269
|
+
const enabled = Boolean(client);
|
|
1567
1270
|
const supportsImages = enabled && protocolSupportsImages(protocol);
|
|
1568
|
-
const isClaudeSdk = protocol === "claude-agent-sdk";
|
|
1569
1271
|
const isClaudeFallback = protocol === "claude-stream-json";
|
|
1570
|
-
const supportsPermission = enabled &&
|
|
1571
|
-
const supportsReasoningEffort = enabled;
|
|
1272
|
+
const supportsPermission = enabled && !isClaudeFallback;
|
|
1273
|
+
const supportsReasoningEffort = enabled && !isClaudeFallback;
|
|
1572
1274
|
const commands = mergeCommands(defaultProviderCommands(provider, this.input.cwd, enabled), runtimeCapabilities?.commands);
|
|
1573
1275
|
const currentMode = [...this.conversations.values()].find((conversation) => conversation.provider === provider)?.collaborationMode;
|
|
1574
1276
|
return {
|
|
@@ -1576,23 +1278,21 @@ export class AgentWorkspaceProxy {
|
|
|
1576
1278
|
label: providerLabel(provider),
|
|
1577
1279
|
enabled,
|
|
1578
1280
|
reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
|
|
1579
|
-
providerProtocol: protocol,
|
|
1580
1281
|
supportsImages,
|
|
1581
1282
|
supportsPermission,
|
|
1582
1283
|
supportsPlan: enabled,
|
|
1583
1284
|
supportsCancel: enabled,
|
|
1584
1285
|
models: runtimeCapabilities?.models ?? [{ id: "default", label: "默认模型" }],
|
|
1585
|
-
defaultModel: runtimeCapabilities?.defaultModel,
|
|
1586
|
-
modelsSource: hasRuntimeModels ? "runtime" : enabled ? "fallback" : "unavailable",
|
|
1587
|
-
modelListError: this.providerCapabilityErrors.get(provider),
|
|
1286
|
+
defaultModel: runtimeCapabilities?.defaultModel ?? "default",
|
|
1588
1287
|
reasoningEfforts: supportsReasoningEffort
|
|
1589
|
-
? runtimeCapabilities?.reasoningEfforts ??
|
|
1288
|
+
? runtimeCapabilities?.reasoningEfforts ?? [...ALL_REASONING_EFFORTS]
|
|
1590
1289
|
: [],
|
|
1591
|
-
|
|
1592
|
-
defaultServiceTier: provider === "codex" ? runtimeCapabilities?.defaultServiceTier : undefined,
|
|
1593
|
-
permissionModes: supportsPermission ? AGENT_PERMISSION_MODES : [],
|
|
1290
|
+
permissionModes: supportsPermission ? [...ALL_PERMISSION_MODES] : [],
|
|
1594
1291
|
commands,
|
|
1595
|
-
modes: runtimeCapabilities?.modes ?? [
|
|
1292
|
+
modes: runtimeCapabilities?.modes ?? (provider === "codex" ? [
|
|
1293
|
+
{ id: "default", title: "Default", description: "Run normal implementation turns" },
|
|
1294
|
+
{ id: "plan", title: "Plan", description: "Discuss and produce an implementation plan first" },
|
|
1295
|
+
] : []),
|
|
1596
1296
|
currentMode,
|
|
1597
1297
|
features: {
|
|
1598
1298
|
images: supportsImages,
|
|
@@ -1600,8 +1300,6 @@ export class AgentWorkspaceProxy {
|
|
|
1600
1300
|
plan: enabled,
|
|
1601
1301
|
cancel: enabled,
|
|
1602
1302
|
reasoningEffort: supportsReasoningEffort,
|
|
1603
|
-
serviceTier: provider === "codex" && Boolean(runtimeCapabilities?.speedTiers?.includes("fast")),
|
|
1604
|
-
claudeSdk: isClaudeSdk,
|
|
1605
1303
|
streamJsonFallback: isClaudeFallback,
|
|
1606
1304
|
},
|
|
1607
1305
|
};
|
|
@@ -1610,14 +1308,13 @@ export class AgentWorkspaceProxy {
|
|
|
1610
1308
|
const anyPermission = providers.some((p) => p.supportsPermission);
|
|
1611
1309
|
this.input.send(createEnvelope({
|
|
1612
1310
|
type: "agent.v2.capabilities",
|
|
1613
|
-
|
|
1311
|
+
sessionId: this.input.sessionId,
|
|
1614
1312
|
payload: {
|
|
1615
1313
|
enabled: anyEnabled,
|
|
1616
1314
|
provider: this.input.availableProviders[0] ?? "codex",
|
|
1617
1315
|
providers,
|
|
1618
1316
|
protocolVersion: 1,
|
|
1619
1317
|
workspaceProtocolVersion: 2,
|
|
1620
|
-
capabilitiesRevision: ++this.capabilitiesRevision,
|
|
1621
1318
|
error: anyEnabled ? undefined : "没有可用的 Agent provider。请安装 Claude Code 或 Codex CLI。",
|
|
1622
1319
|
supportsSessionList: anyEnabled,
|
|
1623
1320
|
supportsSessionLoad: anyEnabled,
|
|
@@ -1631,6 +1328,16 @@ export class AgentWorkspaceProxy {
|
|
|
1631
1328
|
}
|
|
1632
1329
|
async openConversation(payload) {
|
|
1633
1330
|
const provider = payload.provider ?? this.input.availableProviders[0];
|
|
1331
|
+
if (!provider) {
|
|
1332
|
+
return this.openFailure(payload, "没有可用的 Agent provider。");
|
|
1333
|
+
}
|
|
1334
|
+
if (!this.input.availableProviders.includes(provider)) {
|
|
1335
|
+
return this.openFailure(payload, `${providerLabel(provider)} 未安装或不可用。`);
|
|
1336
|
+
}
|
|
1337
|
+
const client = await this.ensureProviderClient(provider);
|
|
1338
|
+
if (!client) {
|
|
1339
|
+
return this.openFailure(payload, `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`);
|
|
1340
|
+
}
|
|
1634
1341
|
const cwd = payload.cwd ?? this.input.cwd;
|
|
1635
1342
|
let agentSessionId = payload.agentSessionId;
|
|
1636
1343
|
let existingConversation = (payload.conversationId ? this.conversations.get(payload.conversationId) : undefined) ??
|
|
@@ -1639,34 +1346,17 @@ export class AgentWorkspaceProxy {
|
|
|
1639
1346
|
if (payload.conversationId && existingConversation.id !== payload.conversationId) {
|
|
1640
1347
|
existingConversation = this.adoptConversationId(existingConversation.id, payload.conversationId);
|
|
1641
1348
|
}
|
|
1642
|
-
this.hydrateStoredTimeline(existingConversation);
|
|
1643
1349
|
this.activeConversationId = existingConversation.id;
|
|
1644
|
-
const snapshot = this.latestSnapshot(existingConversation.id);
|
|
1645
1350
|
this.input.send(createEnvelope({
|
|
1646
1351
|
type: "agent.v2.conversation.opened",
|
|
1647
|
-
|
|
1352
|
+
sessionId: this.input.sessionId,
|
|
1648
1353
|
payload: {
|
|
1649
|
-
conversation:
|
|
1650
|
-
snapshot:
|
|
1651
|
-
revision: this.getRevision(existingConversation.id),
|
|
1652
|
-
cursor: snapshot.cursor,
|
|
1653
|
-
hasMore: snapshot.hasMore,
|
|
1654
|
-
source: "device-history",
|
|
1655
|
-
canonical: true,
|
|
1354
|
+
conversation: existingConversation,
|
|
1355
|
+
snapshot: this.timelines.get(existingConversation.id) ?? [],
|
|
1656
1356
|
},
|
|
1657
1357
|
}));
|
|
1658
1358
|
return existingConversation;
|
|
1659
1359
|
}
|
|
1660
|
-
if (!provider) {
|
|
1661
|
-
return this.openFailure(payload, "没有可用的 Agent provider。");
|
|
1662
|
-
}
|
|
1663
|
-
if (!this.input.availableProviders.includes(provider)) {
|
|
1664
|
-
return this.openFailure(payload, `${providerLabel(provider)} 未安装或不可用。`);
|
|
1665
|
-
}
|
|
1666
|
-
const client = await this.ensureProviderClient(provider);
|
|
1667
|
-
if (!client) {
|
|
1668
|
-
return this.openFailure(payload, `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`);
|
|
1669
|
-
}
|
|
1670
1360
|
try {
|
|
1671
1361
|
const result = agentSessionId
|
|
1672
1362
|
? await client.loadSession({ sessionId: agentSessionId, cwd })
|
|
@@ -1683,16 +1373,10 @@ export class AgentWorkspaceProxy {
|
|
|
1683
1373
|
title: payload.title ?? existingConversation?.title ?? titleFromCwd(cwd),
|
|
1684
1374
|
model: payload.model ?? existingConversation?.model,
|
|
1685
1375
|
reasoningEffort: payload.reasoningEffort ?? existingConversation?.reasoningEffort,
|
|
1686
|
-
serviceTier: payload.serviceTier ?? existingConversation?.serviceTier,
|
|
1687
1376
|
permissionMode: payload.permissionMode ?? existingConversation?.permissionMode,
|
|
1688
1377
|
collaborationMode: payload.collaborationMode ?? existingConversation?.collaborationMode,
|
|
1689
1378
|
status: "idle",
|
|
1690
1379
|
archived: existingConversation?.archived ?? false,
|
|
1691
|
-
timelineRevision: existingConversation?.timelineRevision ?? this.getRevision(conversationId),
|
|
1692
|
-
historyComplete: existingConversation?.historyComplete ?? false,
|
|
1693
|
-
runningTurnId: this.currentTurnIds.get(conversationId),
|
|
1694
|
-
source: "device",
|
|
1695
|
-
canonical: true,
|
|
1696
1380
|
lastMessagePreview: existingConversation?.status === "error" ? undefined : existingConversation?.lastMessagePreview,
|
|
1697
1381
|
lastActivityAt: now,
|
|
1698
1382
|
createdAt: existingConversation?.createdAt ?? now,
|
|
@@ -1701,20 +1385,10 @@ export class AgentWorkspaceProxy {
|
|
|
1701
1385
|
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
1702
1386
|
this.activeConversationId = conversation.id;
|
|
1703
1387
|
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
1704
|
-
this.hydrateStoredTimeline(conversation);
|
|
1705
|
-
const snapshot = this.latestSnapshot(conversation.id);
|
|
1706
1388
|
this.input.send(createEnvelope({
|
|
1707
1389
|
type: "agent.v2.conversation.opened",
|
|
1708
|
-
|
|
1709
|
-
payload: {
|
|
1710
|
-
conversation: this.conversationSnapshot(conversation),
|
|
1711
|
-
snapshot: snapshot.items,
|
|
1712
|
-
revision: this.getRevision(conversation.id),
|
|
1713
|
-
cursor: snapshot.cursor,
|
|
1714
|
-
hasMore: snapshot.hasMore,
|
|
1715
|
-
source: "device-history",
|
|
1716
|
-
canonical: true,
|
|
1717
|
-
},
|
|
1390
|
+
sessionId: this.input.sessionId,
|
|
1391
|
+
payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
|
|
1718
1392
|
}));
|
|
1719
1393
|
return conversation;
|
|
1720
1394
|
}
|
|
@@ -1737,10 +1411,6 @@ export class AgentWorkspaceProxy {
|
|
|
1737
1411
|
collaborationMode: payload.collaborationMode,
|
|
1738
1412
|
status: "error",
|
|
1739
1413
|
archived: false,
|
|
1740
|
-
timelineRevision: this.getRevision(fallbackId),
|
|
1741
|
-
historyComplete: false,
|
|
1742
|
-
source: "device",
|
|
1743
|
-
canonical: true,
|
|
1744
1414
|
lastMessagePreview: message,
|
|
1745
1415
|
lastActivityAt: now,
|
|
1746
1416
|
createdAt: now,
|
|
@@ -1756,15 +1426,8 @@ export class AgentWorkspaceProxy {
|
|
|
1756
1426
|
});
|
|
1757
1427
|
this.input.send(createEnvelope({
|
|
1758
1428
|
type: "agent.v2.conversation.opened",
|
|
1759
|
-
|
|
1760
|
-
payload: {
|
|
1761
|
-
conversation: this.conversationSnapshot(conversation),
|
|
1762
|
-
snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []),
|
|
1763
|
-
revision: this.getRevision(conversation.id),
|
|
1764
|
-
hasMore: false,
|
|
1765
|
-
source: "device",
|
|
1766
|
-
canonical: true,
|
|
1767
|
-
},
|
|
1429
|
+
sessionId: this.input.sessionId,
|
|
1430
|
+
payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
|
|
1768
1431
|
}));
|
|
1769
1432
|
return conversation;
|
|
1770
1433
|
}
|
|
@@ -1808,21 +1471,10 @@ export class AgentWorkspaceProxy {
|
|
|
1808
1471
|
});
|
|
1809
1472
|
return;
|
|
1810
1473
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
conversation.reasoningEffort = payload.reasoningEffort ?? undefined;
|
|
1816
|
-
}
|
|
1817
|
-
if (Object.prototype.hasOwnProperty.call(payload, "serviceTier")) {
|
|
1818
|
-
conversation.serviceTier = payload.serviceTier ?? undefined;
|
|
1819
|
-
}
|
|
1820
|
-
if (Object.prototype.hasOwnProperty.call(payload, "permissionMode")) {
|
|
1821
|
-
conversation.permissionMode = payload.permissionMode ?? undefined;
|
|
1822
|
-
}
|
|
1823
|
-
if (Object.prototype.hasOwnProperty.call(payload, "collaborationMode")) {
|
|
1824
|
-
conversation.collaborationMode = payload.collaborationMode ?? "default";
|
|
1825
|
-
}
|
|
1474
|
+
conversation.model = payload.model ?? conversation.model;
|
|
1475
|
+
conversation.reasoningEffort = payload.reasoningEffort ?? conversation.reasoningEffort;
|
|
1476
|
+
conversation.permissionMode = payload.permissionMode ?? conversation.permissionMode;
|
|
1477
|
+
conversation.collaborationMode = payload.collaborationMode ?? conversation.collaborationMode;
|
|
1826
1478
|
conversation.status = "running";
|
|
1827
1479
|
conversation.lastActivityAt = Date.now();
|
|
1828
1480
|
this.activeConversationId = conversation.id;
|
|
@@ -1842,11 +1494,10 @@ export class AgentWorkspaceProxy {
|
|
|
1842
1494
|
sessionId: conversation.agentSessionId,
|
|
1843
1495
|
content: payload.contentBlocks,
|
|
1844
1496
|
clientMessageId: payload.clientMessageId,
|
|
1845
|
-
model:
|
|
1846
|
-
reasoningEffort:
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
collaborationMode: conversation.collaborationMode,
|
|
1497
|
+
model: payload.model,
|
|
1498
|
+
reasoningEffort: payload.reasoningEffort,
|
|
1499
|
+
permissionMode: payload.permissionMode,
|
|
1500
|
+
collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
|
|
1850
1501
|
cwd: conversation.cwd,
|
|
1851
1502
|
});
|
|
1852
1503
|
const nextAgentSessionId = this.extractSessionId(result);
|
|
@@ -1926,7 +1577,6 @@ export class AgentWorkspaceProxy {
|
|
|
1926
1577
|
contentBlocks: [{ type: "text", text: rawText }],
|
|
1927
1578
|
model: conversation.model,
|
|
1928
1579
|
reasoningEffort: conversation.reasoningEffort,
|
|
1929
|
-
serviceTier: conversation.serviceTier,
|
|
1930
1580
|
permissionMode: conversation.permissionMode,
|
|
1931
1581
|
collaborationMode: conversation.collaborationMode,
|
|
1932
1582
|
});
|
|
@@ -1957,8 +1607,14 @@ export class AgentWorkspaceProxy {
|
|
|
1957
1607
|
this.emitStatus(conversation.id, conversation.status, `${providerLabel(conversation.provider)} · ${conversation.collaborationMode === "plan" ? "Plan mode" : "Default mode"} · ${conversation.cwd}`);
|
|
1958
1608
|
return;
|
|
1959
1609
|
}
|
|
1960
|
-
if (
|
|
1961
|
-
|
|
1610
|
+
if (conversation.provider !== "codex") {
|
|
1611
|
+
this.addItem(conversation.id, {
|
|
1612
|
+
id: id("error"),
|
|
1613
|
+
conversationId: conversation.id,
|
|
1614
|
+
type: "error",
|
|
1615
|
+
error: `${command.title} 暂无 ${providerLabel(conversation.provider)} 原生实现。`,
|
|
1616
|
+
createdAt: now,
|
|
1617
|
+
});
|
|
1962
1618
|
return;
|
|
1963
1619
|
}
|
|
1964
1620
|
if (command.name === "plan" || command.name === "exit-plan") {
|
|
@@ -1973,22 +1629,6 @@ export class AgentWorkspaceProxy {
|
|
|
1973
1629
|
: "已退出 Plan mode。");
|
|
1974
1630
|
return;
|
|
1975
1631
|
}
|
|
1976
|
-
if (command.name === "review" || command.name === "subagents") {
|
|
1977
|
-
const prompt = command.name === "review"
|
|
1978
|
-
? args || "Review the current local changes."
|
|
1979
|
-
: args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
|
|
1980
|
-
await this.sendPrompt({
|
|
1981
|
-
conversationId: conversation.id,
|
|
1982
|
-
clientMessageId: id(command.name),
|
|
1983
|
-
contentBlocks: [{ type: "text", text: prompt }],
|
|
1984
|
-
model: conversation.model,
|
|
1985
|
-
reasoningEffort: conversation.reasoningEffort,
|
|
1986
|
-
serviceTier: conversation.serviceTier,
|
|
1987
|
-
permissionMode: conversation.permissionMode,
|
|
1988
|
-
collaborationMode: conversation.collaborationMode,
|
|
1989
|
-
});
|
|
1990
|
-
return;
|
|
1991
|
-
}
|
|
1992
1632
|
if (command.name === "compact") {
|
|
1993
1633
|
if (!(client instanceof AcpClient))
|
|
1994
1634
|
throw new Error("当前 Codex runtime 不支持原生 compact。");
|
|
@@ -2009,7 +1649,7 @@ export class AgentWorkspaceProxy {
|
|
|
2009
1649
|
this.emitStatus(conversation.id, "idle", "上下文压缩完成。");
|
|
2010
1650
|
return;
|
|
2011
1651
|
}
|
|
2012
|
-
if (command.name === "clear"
|
|
1652
|
+
if (command.name === "clear") {
|
|
2013
1653
|
if (!client)
|
|
2014
1654
|
throw new Error("Agent provider 不在线。");
|
|
2015
1655
|
const result = await client.newSession({ cwd: conversation.cwd });
|
|
@@ -2027,6 +1667,21 @@ export class AgentWorkspaceProxy {
|
|
|
2027
1667
|
this.emitStatus(conversation.id, "idle", "上下文已重置,已创建新的 Codex thread。");
|
|
2028
1668
|
return;
|
|
2029
1669
|
}
|
|
1670
|
+
if (command.name === "review" || command.name === "subagents") {
|
|
1671
|
+
const prompt = command.name === "review"
|
|
1672
|
+
? args || "Review the current local changes."
|
|
1673
|
+
: args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
|
|
1674
|
+
await this.sendPrompt({
|
|
1675
|
+
conversationId: conversation.id,
|
|
1676
|
+
clientMessageId: id(command.name),
|
|
1677
|
+
contentBlocks: [{ type: "text", text: prompt }],
|
|
1678
|
+
model: conversation.model,
|
|
1679
|
+
reasoningEffort: conversation.reasoningEffort,
|
|
1680
|
+
permissionMode: conversation.permissionMode,
|
|
1681
|
+
collaborationMode: conversation.collaborationMode,
|
|
1682
|
+
});
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
2030
1685
|
throw new Error(`命令暂未实现:/${command.name}`);
|
|
2031
1686
|
}
|
|
2032
1687
|
catch (error) {
|
|
@@ -2041,41 +1696,6 @@ export class AgentWorkspaceProxy {
|
|
|
2041
1696
|
});
|
|
2042
1697
|
}
|
|
2043
1698
|
}
|
|
2044
|
-
async executeGitNativeCommand(conversation, commandName, args) {
|
|
2045
|
-
const git = gitCommandArgs(commandName, args);
|
|
2046
|
-
const toolId = id(commandName);
|
|
2047
|
-
const now = Date.now();
|
|
2048
|
-
conversation.status = "running";
|
|
2049
|
-
conversation.lastMessagePreview = git.display;
|
|
2050
|
-
conversation.lastActivityAt = now;
|
|
2051
|
-
this.emitConversation(conversation);
|
|
2052
|
-
this.upsertTool(conversation.id, {
|
|
2053
|
-
id: toolId,
|
|
2054
|
-
name: "命令",
|
|
2055
|
-
input: `${git.display}\n\ncwd: ${conversation.cwd}`,
|
|
2056
|
-
createdAt: now,
|
|
2057
|
-
status: "running",
|
|
2058
|
-
});
|
|
2059
|
-
const result = await runProcess("git", git.argv, {
|
|
2060
|
-
cwd: conversation.cwd,
|
|
2061
|
-
maxBytes: COMMAND_OUTPUT_MAX_BYTES,
|
|
2062
|
-
});
|
|
2063
|
-
const ok = result.exitCode === 0;
|
|
2064
|
-
this.upsertTool(conversation.id, {
|
|
2065
|
-
id: toolId,
|
|
2066
|
-
name: "命令",
|
|
2067
|
-
input: `${git.display}\n\ncwd: ${conversation.cwd}`,
|
|
2068
|
-
output: result.output || (ok ? "完成" : `退出码 ${result.exitCode ?? "unknown"}`),
|
|
2069
|
-
createdAt: now,
|
|
2070
|
-
status: ok ? "completed" : "failed",
|
|
2071
|
-
});
|
|
2072
|
-
conversation.status = ok ? "idle" : "error";
|
|
2073
|
-
conversation.lastMessagePreview = ok
|
|
2074
|
-
? `${git.display} 完成`
|
|
2075
|
-
: `${git.display} 失败`;
|
|
2076
|
-
conversation.lastActivityAt = Date.now();
|
|
2077
|
-
this.emitConversation(conversation);
|
|
2078
|
-
}
|
|
2079
1699
|
handleRequest(method, params) {
|
|
2080
1700
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
2081
1701
|
return this.handleStructuredInput(params, true);
|
|
@@ -2113,7 +1733,6 @@ export class AgentWorkspaceProxy {
|
|
|
2113
1733
|
}
|
|
2114
1734
|
if (method.startsWith("account/") ||
|
|
2115
1735
|
method.startsWith("mcpServer/startupStatus/") ||
|
|
2116
|
-
method.startsWith("remoteControl/") ||
|
|
2117
1736
|
method === "thread/status/changed" ||
|
|
2118
1737
|
method === "thread/tokenUsage/updated" ||
|
|
2119
1738
|
method === "serverRequest/resolved" ||
|
|
@@ -2139,9 +1758,6 @@ export class AgentWorkspaceProxy {
|
|
|
2139
1758
|
this.conversationByAgentSessionId.set(agentSessionId, conversationId);
|
|
2140
1759
|
const conversation = this.conversations.get(conversationId);
|
|
2141
1760
|
if (conversation) {
|
|
2142
|
-
if (conversation.agentSessionId && conversation.agentSessionId !== agentSessionId) {
|
|
2143
|
-
this.conversationByAgentSessionId.delete(conversation.agentSessionId);
|
|
2144
|
-
}
|
|
2145
1761
|
conversation.agentSessionId = agentSessionId;
|
|
2146
1762
|
conversation.lastActivityAt = Date.now();
|
|
2147
1763
|
this.emitConversation(conversation);
|
|
@@ -2720,7 +2336,7 @@ export class AgentWorkspaceProxy {
|
|
|
2720
2336
|
this.upsertItem(conversationId, item);
|
|
2721
2337
|
this.input.send(createEnvelope({
|
|
2722
2338
|
type: "agent.v2.permission.request",
|
|
2723
|
-
|
|
2339
|
+
sessionId: this.input.sessionId,
|
|
2724
2340
|
payload: { conversationId, ...permission, item },
|
|
2725
2341
|
}));
|
|
2726
2342
|
if (!waitForResponse)
|
|
@@ -2731,14 +2347,6 @@ export class AgentWorkspaceProxy {
|
|
|
2731
2347
|
this.permissionWaiters.delete(requestId);
|
|
2732
2348
|
this.permissionSources.delete(requestId);
|
|
2733
2349
|
resolve(formatPermissionResponse(source, "cancelled", "cancelled"));
|
|
2734
|
-
this.markPermission(conversationId, requestId, {
|
|
2735
|
-
permissionOutcome: "cancelled",
|
|
2736
|
-
optionId: "cancelled",
|
|
2737
|
-
permissionLive: false,
|
|
2738
|
-
permissionPending: false,
|
|
2739
|
-
permissionExpired: true,
|
|
2740
|
-
permissionError: "等待授权超时",
|
|
2741
|
-
});
|
|
2742
2350
|
this.updateConversationStatus(conversationId, "idle");
|
|
2743
2351
|
}, PERMISSION_TIMEOUT_MS);
|
|
2744
2352
|
this.permissionWaiters.set(requestId, { resolve, timer });
|
|
@@ -2768,8 +2376,6 @@ export class AgentWorkspaceProxy {
|
|
|
2768
2376
|
this.markPermission(payload.conversationId, payload.requestId, {
|
|
2769
2377
|
permissionOutcome: payload.outcome,
|
|
2770
2378
|
optionId: selectedOptionId,
|
|
2771
|
-
permissionLive: false,
|
|
2772
|
-
permissionExpired: false,
|
|
2773
2379
|
permissionError: undefined,
|
|
2774
2380
|
permissionPending: false,
|
|
2775
2381
|
});
|
|
@@ -2816,209 +2422,6 @@ export class AgentWorkspaceProxy {
|
|
|
2816
2422
|
updatedAt: Date.now(),
|
|
2817
2423
|
});
|
|
2818
2424
|
}
|
|
2819
|
-
getRevision(conversationId) {
|
|
2820
|
-
return this.conversationRevisions.get(conversationId) ??
|
|
2821
|
-
this.conversations.get(conversationId)?.timelineRevision ??
|
|
2822
|
-
0;
|
|
2823
|
-
}
|
|
2824
|
-
setRevisionFloor(conversationId, revision) {
|
|
2825
|
-
const nextRevision = Math.max(this.getRevision(conversationId), revision);
|
|
2826
|
-
this.conversationRevisions.set(conversationId, nextRevision);
|
|
2827
|
-
const conversation = this.conversations.get(conversationId);
|
|
2828
|
-
if (conversation) {
|
|
2829
|
-
conversation.timelineRevision = nextRevision;
|
|
2830
|
-
conversation.runningTurnId = this.currentTurnIds.get(conversationId);
|
|
2831
|
-
}
|
|
2832
|
-
return nextRevision;
|
|
2833
|
-
}
|
|
2834
|
-
conversationSnapshot(conversation) {
|
|
2835
|
-
return {
|
|
2836
|
-
...conversation,
|
|
2837
|
-
timelineRevision: this.getRevision(conversation.id),
|
|
2838
|
-
historyComplete: conversation.historyComplete ?? false,
|
|
2839
|
-
runningTurnId: this.currentTurnIds.get(conversation.id),
|
|
2840
|
-
source: conversation.source ?? "device",
|
|
2841
|
-
canonical: conversation.canonical ?? true,
|
|
2842
|
-
};
|
|
2843
|
-
}
|
|
2844
|
-
annotateTimelineItem(item, revision, source) {
|
|
2845
|
-
return {
|
|
2846
|
-
...item,
|
|
2847
|
-
revision: revision ?? item.revision,
|
|
2848
|
-
source: item.source ?? source,
|
|
2849
|
-
canonical: item.canonical ?? true,
|
|
2850
|
-
};
|
|
2851
|
-
}
|
|
2852
|
-
recordRevisionEvent(conversationId, change) {
|
|
2853
|
-
const revision = this.getRevision(conversationId) + 1;
|
|
2854
|
-
this.conversationRevisions.set(conversationId, revision);
|
|
2855
|
-
const conversation = this.conversations.get(conversationId);
|
|
2856
|
-
if (conversation) {
|
|
2857
|
-
conversation.timelineRevision = revision;
|
|
2858
|
-
conversation.runningTurnId = this.currentTurnIds.get(conversationId);
|
|
2859
|
-
}
|
|
2860
|
-
const event = {
|
|
2861
|
-
revision,
|
|
2862
|
-
item: change.item ? this.annotateTimelineItem(change.item, revision, "device-live") : undefined,
|
|
2863
|
-
conversation: change.conversation ? this.conversationSnapshot(change.conversation) : undefined,
|
|
2864
|
-
};
|
|
2865
|
-
const events = this.revisionEvents.get(conversationId) ?? [];
|
|
2866
|
-
events.push(event);
|
|
2867
|
-
if (events.length > MAX_DELTA_EVENTS) {
|
|
2868
|
-
events.splice(0, events.length - MAX_DELTA_EVENTS);
|
|
2869
|
-
}
|
|
2870
|
-
this.revisionEvents.set(conversationId, events);
|
|
2871
|
-
return event;
|
|
2872
|
-
}
|
|
2873
|
-
storedTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
|
|
2874
|
-
if (!conversation.agentSessionId)
|
|
2875
|
-
return [];
|
|
2876
|
-
const result = conversation.provider === "codex"
|
|
2877
|
-
? loadCodexStoredTimeline(conversation.agentSessionId, conversation.id, conversation.cwd || this.input.cwd, { maxItems })
|
|
2878
|
-
: conversation.provider === "claude"
|
|
2879
|
-
? loadClaudeStoredTimeline(conversation.agentSessionId, conversation.id, { maxItems })
|
|
2880
|
-
: { items: [] };
|
|
2881
|
-
return result.items
|
|
2882
|
-
.sort((a, b) => a.createdAt - b.createdAt)
|
|
2883
|
-
.map((item, index) => this.annotateTimelineItem(item, index + 1, "device-history"));
|
|
2884
|
-
}
|
|
2885
|
-
canonicalTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
|
|
2886
|
-
const merged = new Map();
|
|
2887
|
-
for (const item of this.storedTimeline(conversation, maxItems)) {
|
|
2888
|
-
merged.set(item.id, item);
|
|
2889
|
-
}
|
|
2890
|
-
for (const item of this.timelines.get(conversation.id) ?? []) {
|
|
2891
|
-
merged.set(item.id, this.annotateTimelineItem(item, item.revision, item.source ?? "device-live"));
|
|
2892
|
-
}
|
|
2893
|
-
const items = [...merged.values()]
|
|
2894
|
-
.sort((a, b) => a.createdAt - b.createdAt)
|
|
2895
|
-
.slice(-maxItems);
|
|
2896
|
-
this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
|
|
2897
|
-
return items;
|
|
2898
|
-
}
|
|
2899
|
-
async appServerTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
|
|
2900
|
-
if (conversation.provider !== "codex" || !conversation.agentSessionId)
|
|
2901
|
-
return [];
|
|
2902
|
-
if (this.protocolForProvider(conversation.provider) !== "codex-app-server")
|
|
2903
|
-
return [];
|
|
2904
|
-
const client = this.clientForProvider(conversation.provider);
|
|
2905
|
-
const listTurns = client?.listTurns;
|
|
2906
|
-
if (typeof listTurns !== "function")
|
|
2907
|
-
return [];
|
|
2908
|
-
try {
|
|
2909
|
-
const result = await listTurns.call(client, { sessionId: conversation.agentSessionId, limit: maxItems });
|
|
2910
|
-
return this.timelineFromAppServerTurns(conversation.id, conversation.agentSessionId, result);
|
|
2911
|
-
}
|
|
2912
|
-
catch (error) {
|
|
2913
|
-
if (this.input.verbose) {
|
|
2914
|
-
process.stderr.write(`[agent:v2] thread/turns/list failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2915
|
-
}
|
|
2916
|
-
return [];
|
|
2917
|
-
}
|
|
2918
|
-
}
|
|
2919
|
-
timelineFromAppServerTurns(conversationId, agentSessionId, value) {
|
|
2920
|
-
const raw = asRecord(value);
|
|
2921
|
-
const turns = Array.isArray(value) ? value :
|
|
2922
|
-
Array.isArray(raw?.turns) ? raw.turns :
|
|
2923
|
-
Array.isArray(raw?.items) ? raw.items :
|
|
2924
|
-
Array.isArray(raw?.entries) ? raw.entries :
|
|
2925
|
-
[];
|
|
2926
|
-
const items = [];
|
|
2927
|
-
turns.forEach((entry, index) => {
|
|
2928
|
-
const turn = asRecord(entry);
|
|
2929
|
-
if (!turn)
|
|
2930
|
-
return;
|
|
2931
|
-
const turnId = firstString(turn, ["id", "turnId", "turn_id"]) ?? `turn-${index + 1}`;
|
|
2932
|
-
const createdAt = parseTimestamp(turn.createdAt ?? turn.created_at ?? turn.startedAt ?? turn.started_at) ?? Date.now() + index;
|
|
2933
|
-
const updatedAt = parseTimestamp(turn.updatedAt ?? turn.updated_at ?? turn.completedAt ?? turn.completed_at);
|
|
2934
|
-
const userText = appServerText(turn.input ?? turn.prompt ?? turn.user ?? turn.userMessage ?? turn.request);
|
|
2935
|
-
if (userText) {
|
|
2936
|
-
items.push({
|
|
2937
|
-
id: `app-server:${agentSessionId}:${turnId}:user`,
|
|
2938
|
-
conversationId,
|
|
2939
|
-
type: "message",
|
|
2940
|
-
kind: "chat",
|
|
2941
|
-
turnId,
|
|
2942
|
-
role: "user",
|
|
2943
|
-
content: [{ type: "text", text: userText }],
|
|
2944
|
-
text: userText,
|
|
2945
|
-
createdAt,
|
|
2946
|
-
updatedAt,
|
|
2947
|
-
metadata: { source: "app-server", provider: "codex" },
|
|
2948
|
-
});
|
|
2949
|
-
}
|
|
2950
|
-
const assistantText = appServerText(turn.output ?? turn.response ?? turn.assistant ?? turn.assistantMessage ?? turn.result);
|
|
2951
|
-
if (assistantText) {
|
|
2952
|
-
items.push({
|
|
2953
|
-
id: `app-server:${agentSessionId}:${turnId}:assistant`,
|
|
2954
|
-
conversationId,
|
|
2955
|
-
type: "message",
|
|
2956
|
-
kind: "chat",
|
|
2957
|
-
turnId,
|
|
2958
|
-
role: "assistant",
|
|
2959
|
-
content: [{ type: "text", text: assistantText }],
|
|
2960
|
-
text: assistantText,
|
|
2961
|
-
createdAt: updatedAt ?? createdAt + 1,
|
|
2962
|
-
updatedAt,
|
|
2963
|
-
metadata: { source: "app-server", provider: "codex" },
|
|
2964
|
-
});
|
|
2965
|
-
}
|
|
2966
|
-
const nestedItems = Array.isArray(turn.items) ? turn.items : Array.isArray(turn.messages) ? turn.messages : [];
|
|
2967
|
-
for (const nested of nestedItems) {
|
|
2968
|
-
const nestedRecord = asRecord(nested);
|
|
2969
|
-
if (!nestedRecord)
|
|
2970
|
-
continue;
|
|
2971
|
-
const text = appServerText(nestedRecord.content ?? nestedRecord.text ?? nestedRecord.message);
|
|
2972
|
-
const role = nestedRecord.role === "user" || nestedRecord.role === "assistant" || nestedRecord.role === "system"
|
|
2973
|
-
? nestedRecord.role
|
|
2974
|
-
: undefined;
|
|
2975
|
-
if (!text || !role)
|
|
2976
|
-
continue;
|
|
2977
|
-
const itemId = firstString(nestedRecord, ["id", "itemId", "messageId"]) ?? `${role}-${items.length + 1}`;
|
|
2978
|
-
items.push({
|
|
2979
|
-
id: `app-server:${agentSessionId}:${turnId}:${itemId}`,
|
|
2980
|
-
conversationId,
|
|
2981
|
-
type: "message",
|
|
2982
|
-
kind: "chat",
|
|
2983
|
-
turnId,
|
|
2984
|
-
itemId,
|
|
2985
|
-
role,
|
|
2986
|
-
content: [{ type: "text", text }],
|
|
2987
|
-
text,
|
|
2988
|
-
createdAt: parseTimestamp(nestedRecord.createdAt ?? nestedRecord.created_at) ?? createdAt + items.length,
|
|
2989
|
-
updatedAt: parseTimestamp(nestedRecord.updatedAt ?? nestedRecord.updated_at),
|
|
2990
|
-
metadata: { source: "app-server", provider: "codex" },
|
|
2991
|
-
});
|
|
2992
|
-
}
|
|
2993
|
-
});
|
|
2994
|
-
return items
|
|
2995
|
-
.sort((a, b) => a.createdAt - b.createdAt)
|
|
2996
|
-
.map((item, index) => this.annotateTimelineItem(item, index + 1, "app-server"));
|
|
2997
|
-
}
|
|
2998
|
-
mergeCanonicalTimelineItems(items, maxItems) {
|
|
2999
|
-
const byKey = new Map();
|
|
3000
|
-
for (const item of items.sort((a, b) => a.createdAt - b.createdAt)) {
|
|
3001
|
-
const key = item.type === "message" && item.role && item.text
|
|
3002
|
-
? `${item.role}:${item.text.replace(/\s+/g, " ").trim().slice(0, 500)}`
|
|
3003
|
-
: item.id;
|
|
3004
|
-
const existing = byKey.get(key);
|
|
3005
|
-
if (!existing || item.source === "app-server" || (item.updatedAt ?? item.createdAt) >= (existing.updatedAt ?? existing.createdAt)) {
|
|
3006
|
-
byKey.set(key, item);
|
|
3007
|
-
}
|
|
3008
|
-
}
|
|
3009
|
-
return [...byKey.values()]
|
|
3010
|
-
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3011
|
-
.slice(-maxItems);
|
|
3012
|
-
}
|
|
3013
|
-
latestSnapshot(conversationId) {
|
|
3014
|
-
const timeline = this.timelines.get(conversationId) ?? [];
|
|
3015
|
-
const start = Math.max(0, timeline.length - MAX_SNAPSHOT_ITEMS);
|
|
3016
|
-
return {
|
|
3017
|
-
items: timeline.slice(start).map((item) => snapshotTimelineItem(item)),
|
|
3018
|
-
cursor: start > 0 ? String(start) : undefined,
|
|
3019
|
-
hasMore: start > 0,
|
|
3020
|
-
};
|
|
3021
|
-
}
|
|
3022
2425
|
addItem(conversationId, item) {
|
|
3023
2426
|
this.rememberItemConversationId(conversationId, item);
|
|
3024
2427
|
const timeline = this.timelines.get(conversationId) ?? [];
|
|
@@ -3056,8 +2459,6 @@ export class AgentWorkspaceProxy {
|
|
|
3056
2459
|
...existing,
|
|
3057
2460
|
...toolCall,
|
|
3058
2461
|
id: targetToolId,
|
|
3059
|
-
input: toolCall.input ?? existing?.input,
|
|
3060
|
-
output: toolCall.output ?? existing?.output,
|
|
3061
2462
|
createdAt: existing?.createdAt ?? toolCall.createdAt ?? Date.now(),
|
|
3062
2463
|
};
|
|
3063
2464
|
this.toolConversationIds.set(toolCall.id, conversationId);
|
|
@@ -3126,7 +2527,6 @@ export class AgentWorkspaceProxy {
|
|
|
3126
2527
|
conversation.title = conversation.title ?? target.title;
|
|
3127
2528
|
conversation.model = conversation.model ?? target.model;
|
|
3128
2529
|
conversation.reasoningEffort = conversation.reasoningEffort ?? target.reasoningEffort;
|
|
3129
|
-
conversation.serviceTier = conversation.serviceTier ?? target.serviceTier;
|
|
3130
2530
|
conversation.permissionMode = conversation.permissionMode ?? target.permissionMode;
|
|
3131
2531
|
conversation.lastMessagePreview = conversation.lastMessagePreview ?? target.lastMessagePreview;
|
|
3132
2532
|
conversation.createdAt = Math.min(conversation.createdAt, target.createdAt);
|
|
@@ -3139,17 +2539,6 @@ export class AgentWorkspaceProxy {
|
|
|
3139
2539
|
this.timelines.set(newId, [...mergedTimeline.values()]
|
|
3140
2540
|
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3141
2541
|
.slice(-MAX_TIMELINE_ITEMS));
|
|
3142
|
-
const oldRevision = this.conversationRevisions.get(oldId) ?? 0;
|
|
3143
|
-
if (oldRevision > 0) {
|
|
3144
|
-
this.conversationRevisions.delete(oldId);
|
|
3145
|
-
this.setRevisionFloor(newId, oldRevision);
|
|
3146
|
-
}
|
|
3147
|
-
const oldEvents = this.revisionEvents.get(oldId);
|
|
3148
|
-
if (oldEvents) {
|
|
3149
|
-
this.revisionEvents.delete(oldId);
|
|
3150
|
-
const existingEvents = this.revisionEvents.get(newId) ?? [];
|
|
3151
|
-
this.revisionEvents.set(newId, [...existingEvents, ...oldEvents].slice(-MAX_DELTA_EVENTS));
|
|
3152
|
-
}
|
|
3153
2542
|
for (const [agentSessionId, conversationId] of this.conversationByAgentSessionId) {
|
|
3154
2543
|
if (conversationId === oldId) {
|
|
3155
2544
|
this.conversationByAgentSessionId.set(agentSessionId, newId);
|
|
@@ -3183,47 +2572,17 @@ export class AgentWorkspaceProxy {
|
|
|
3183
2572
|
}
|
|
3184
2573
|
emitItem(conversationId, item) {
|
|
3185
2574
|
const conversation = this.conversations.get(conversationId);
|
|
3186
|
-
const itemSnapshot = snapshotTimelineItem(item, { stripImages: false });
|
|
3187
|
-
const event = this.recordRevisionEvent(conversationId, { conversation, item: itemSnapshot });
|
|
3188
2575
|
this.input.send(createEnvelope({
|
|
3189
2576
|
type: "agent.v2.event",
|
|
3190
|
-
|
|
3191
|
-
payload: {
|
|
3192
|
-
conversationId,
|
|
3193
|
-
conversation: event.conversation,
|
|
3194
|
-
item: event.item,
|
|
3195
|
-
revision: event.revision,
|
|
3196
|
-
source: "device-live",
|
|
3197
|
-
canonical: true,
|
|
3198
|
-
},
|
|
2577
|
+
sessionId: this.input.sessionId,
|
|
2578
|
+
payload: { conversationId, conversation, item },
|
|
3199
2579
|
}));
|
|
3200
2580
|
}
|
|
3201
2581
|
emitConversation(conversation) {
|
|
3202
|
-
const event = this.recordRevisionEvent(conversation.id, { conversation });
|
|
3203
2582
|
this.input.send(createEnvelope({
|
|
3204
2583
|
type: "agent.v2.event",
|
|
3205
|
-
|
|
3206
|
-
payload: {
|
|
3207
|
-
conversationId: conversation.id,
|
|
3208
|
-
conversation: event.conversation,
|
|
3209
|
-
revision: event.revision,
|
|
3210
|
-
source: "device-live",
|
|
3211
|
-
canonical: true,
|
|
3212
|
-
},
|
|
3213
|
-
}));
|
|
3214
|
-
this.input.send(createEnvelope({
|
|
3215
|
-
type: "agent.v2.running_state",
|
|
3216
|
-
hostDeviceId: this.input.hostDeviceId,
|
|
3217
|
-
payload: {
|
|
3218
|
-
conversationId: conversation.id,
|
|
3219
|
-
status: conversation.status,
|
|
3220
|
-
runningTurnId: this.currentTurnIds.get(conversation.id),
|
|
3221
|
-
revision: event.revision,
|
|
3222
|
-
error: conversation.status === "error" ? conversation.lastMessagePreview : undefined,
|
|
3223
|
-
updatedAt: conversation.lastActivityAt,
|
|
3224
|
-
source: "device-live",
|
|
3225
|
-
canonical: true,
|
|
3226
|
-
},
|
|
2584
|
+
sessionId: this.input.sessionId,
|
|
2585
|
+
payload: { conversationId: conversation.id, conversation },
|
|
3227
2586
|
}));
|
|
3228
2587
|
}
|
|
3229
2588
|
emitStatus(conversationId, status, text) {
|
|
@@ -3257,169 +2616,20 @@ export class AgentWorkspaceProxy {
|
|
|
3257
2616
|
this.emitConversation(conversation);
|
|
3258
2617
|
}
|
|
3259
2618
|
sendSnapshot(conversationId) {
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
}
|
|
3265
|
-
else if (this.activeConversationId) {
|
|
3266
|
-
const conversation = this.conversations.get(this.activeConversationId);
|
|
3267
|
-
if (conversation)
|
|
3268
|
-
this.hydrateStoredTimeline(conversation);
|
|
3269
|
-
}
|
|
3270
|
-
const conversations = [...this.conversations.values()].map((conversation) => this.conversationSnapshot(conversation));
|
|
3271
|
-
const snapshot = conversationId ? this.latestSnapshot(conversationId) : { items: [], cursor: undefined, hasMore: false };
|
|
2619
|
+
const conversations = [...this.conversations.values()];
|
|
2620
|
+
const items = conversationId
|
|
2621
|
+
? this.timelines.get(conversationId) ?? []
|
|
2622
|
+
: [...this.timelines.values()].flat();
|
|
3272
2623
|
this.input.send(createEnvelope({
|
|
3273
2624
|
type: "agent.v2.snapshot",
|
|
3274
|
-
|
|
2625
|
+
sessionId: this.input.sessionId,
|
|
3275
2626
|
payload: {
|
|
3276
2627
|
conversations,
|
|
3277
2628
|
activeConversationId: this.activeConversationId,
|
|
3278
|
-
items
|
|
3279
|
-
revision: conversationId ? this.getRevision(conversationId) : undefined,
|
|
3280
|
-
cursor: snapshot.cursor,
|
|
3281
|
-
hasMore: snapshot.hasMore,
|
|
3282
|
-
source: "device-history",
|
|
3283
|
-
canonical: true,
|
|
3284
|
-
},
|
|
3285
|
-
}));
|
|
3286
|
-
}
|
|
3287
|
-
async sendHistoryPage(payload) {
|
|
3288
|
-
const conversation = this.conversations.get(payload.conversationId);
|
|
3289
|
-
if (!conversation)
|
|
3290
|
-
return;
|
|
3291
|
-
const limit = Math.min(Math.max(payload.limit ?? MAX_SNAPSHOT_ITEMS, 1), 200);
|
|
3292
|
-
const items = this.mergeCanonicalTimelineItems([
|
|
3293
|
-
...this.canonicalTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
|
|
3294
|
-
...await this.appServerTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
|
|
3295
|
-
], HISTORY_PAGE_MAX_ITEMS);
|
|
3296
|
-
this.timelines.set(conversation.id, items.slice(-MAX_TIMELINE_ITEMS).map((item) => this.annotateTimelineItem(item, item.revision, item.source ?? "device-history")));
|
|
3297
|
-
for (const item of this.timelines.get(conversation.id) ?? []) {
|
|
3298
|
-
this.rememberItemConversationId(conversation.id, item);
|
|
3299
|
-
}
|
|
3300
|
-
const direction = payload.direction ?? "older";
|
|
3301
|
-
let page;
|
|
3302
|
-
let cursor;
|
|
3303
|
-
let hasMore = false;
|
|
3304
|
-
if (direction === "newer") {
|
|
3305
|
-
const start = clampHistoryCursor(payload.cursor, 0, items.length);
|
|
3306
|
-
const end = Math.min(items.length, start + limit);
|
|
3307
|
-
page = items.slice(start, end);
|
|
3308
|
-
hasMore = end < items.length;
|
|
3309
|
-
cursor = hasMore ? String(end) : undefined;
|
|
3310
|
-
}
|
|
3311
|
-
else {
|
|
3312
|
-
const end = clampHistoryCursor(payload.cursor, items.length, items.length);
|
|
3313
|
-
const start = Math.max(0, end - limit);
|
|
3314
|
-
page = items.slice(start, end);
|
|
3315
|
-
hasMore = start > 0;
|
|
3316
|
-
cursor = hasMore ? String(start) : undefined;
|
|
3317
|
-
}
|
|
3318
|
-
conversation.historyComplete = !hasMore;
|
|
3319
|
-
const revision = this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
|
|
3320
|
-
this.input.send(createEnvelope({
|
|
3321
|
-
type: "agent.v2.history.page",
|
|
3322
|
-
hostDeviceId: this.input.hostDeviceId,
|
|
3323
|
-
payload: {
|
|
3324
|
-
conversationId: conversation.id,
|
|
3325
|
-
conversation: this.conversationSnapshot(conversation),
|
|
3326
|
-
items: page.map((item) => snapshotTimelineItem(item)),
|
|
3327
|
-
revision,
|
|
3328
|
-
cursor,
|
|
3329
|
-
hasMore,
|
|
3330
|
-
source: "device-history",
|
|
3331
|
-
canonical: true,
|
|
3332
|
-
},
|
|
3333
|
-
}));
|
|
3334
|
-
}
|
|
3335
|
-
sendDelta(payload) {
|
|
3336
|
-
const conversation = this.conversations.get(payload.conversationId);
|
|
3337
|
-
if (!conversation)
|
|
3338
|
-
return;
|
|
3339
|
-
this.hydrateStoredTimeline(conversation);
|
|
3340
|
-
const sinceRevision = payload.sinceRevision ?? 0;
|
|
3341
|
-
const limit = Math.min(Math.max(payload.limit ?? 100, 1), 500);
|
|
3342
|
-
const events = this.revisionEvents.get(conversation.id) ?? [];
|
|
3343
|
-
const oldestAvailable = events[0]?.revision ?? this.getRevision(conversation.id);
|
|
3344
|
-
const newestRevision = this.getRevision(conversation.id);
|
|
3345
|
-
const reset = sinceRevision > 0 &&
|
|
3346
|
-
(sinceRevision > newestRevision ||
|
|
3347
|
-
(sinceRevision < newestRevision &&
|
|
3348
|
-
(events.length === 0 || sinceRevision < oldestAvailable - 1)));
|
|
3349
|
-
if (reset) {
|
|
3350
|
-
const snapshot = this.latestSnapshot(conversation.id);
|
|
3351
|
-
this.input.send(createEnvelope({
|
|
3352
|
-
type: "agent.v2.delta",
|
|
3353
|
-
hostDeviceId: this.input.hostDeviceId,
|
|
3354
|
-
payload: {
|
|
3355
|
-
conversationId: conversation.id,
|
|
3356
|
-
conversation: this.conversationSnapshot(conversation),
|
|
3357
|
-
items: snapshot.items,
|
|
3358
|
-
sinceRevision,
|
|
3359
|
-
revision: newestRevision,
|
|
3360
|
-
reset: true,
|
|
3361
|
-
cursor: snapshot.cursor,
|
|
3362
|
-
hasMore: snapshot.hasMore,
|
|
3363
|
-
source: "device-history",
|
|
3364
|
-
canonical: true,
|
|
3365
|
-
},
|
|
3366
|
-
}));
|
|
3367
|
-
return;
|
|
3368
|
-
}
|
|
3369
|
-
const changed = events
|
|
3370
|
-
.filter((event) => event.revision > sinceRevision)
|
|
3371
|
-
.slice(-limit);
|
|
3372
|
-
const itemsById = new Map();
|
|
3373
|
-
for (const event of changed) {
|
|
3374
|
-
if (event.item)
|
|
3375
|
-
itemsById.set(event.item.id, event.item);
|
|
3376
|
-
}
|
|
3377
|
-
this.input.send(createEnvelope({
|
|
3378
|
-
type: "agent.v2.delta",
|
|
3379
|
-
hostDeviceId: this.input.hostDeviceId,
|
|
3380
|
-
payload: {
|
|
3381
|
-
conversationId: conversation.id,
|
|
3382
|
-
conversation: this.conversationSnapshot(conversation),
|
|
3383
|
-
items: [...itemsById.values()].map((item) => snapshotTimelineItem(item)),
|
|
3384
|
-
sinceRevision,
|
|
3385
|
-
revision: newestRevision,
|
|
3386
|
-
reset: false,
|
|
3387
|
-
hasMore: changed.length === limit && events.some((event) => event.revision > sinceRevision && event.revision < changed[0].revision),
|
|
3388
|
-
source: "device-live",
|
|
3389
|
-
canonical: true,
|
|
2629
|
+
items,
|
|
3390
2630
|
},
|
|
3391
2631
|
}));
|
|
3392
2632
|
}
|
|
3393
|
-
hydrateStoredTimeline(conversation) {
|
|
3394
|
-
if (!conversation.agentSessionId)
|
|
3395
|
-
return;
|
|
3396
|
-
const existing = this.timelines.get(conversation.id) ?? [];
|
|
3397
|
-
if (existing.length > 0) {
|
|
3398
|
-
this.setRevisionFloor(conversation.id, Math.max(existing.length, ...existing.map((item) => item.revision ?? 0)));
|
|
3399
|
-
return;
|
|
3400
|
-
}
|
|
3401
|
-
const stored = this.storedTimeline(conversation, MAX_TIMELINE_ITEMS);
|
|
3402
|
-
conversation.historyComplete = stored.length <= MAX_SNAPSHOT_ITEMS;
|
|
3403
|
-
if (stored.length === 0) {
|
|
3404
|
-
this.setRevisionFloor(conversation.id, this.getRevision(conversation.id));
|
|
3405
|
-
return;
|
|
3406
|
-
}
|
|
3407
|
-
const items = stored
|
|
3408
|
-
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3409
|
-
.slice(-MAX_TIMELINE_ITEMS);
|
|
3410
|
-
this.timelines.set(conversation.id, items);
|
|
3411
|
-
for (const item of items)
|
|
3412
|
-
this.rememberItemConversationId(conversation.id, item);
|
|
3413
|
-
this.setRevisionFloor(conversation.id, Math.max(items.length, ...items.map((item) => item.revision ?? 0)));
|
|
3414
|
-
const lastMessage = [...items].reverse().find((item) => item.text?.trim());
|
|
3415
|
-
if (lastMessage?.text && !conversation.lastMessagePreview) {
|
|
3416
|
-
conversation.lastMessagePreview = previewText(lastMessage.text);
|
|
3417
|
-
}
|
|
3418
|
-
const lastActivityAt = items.at(-1)?.createdAt;
|
|
3419
|
-
if (lastActivityAt) {
|
|
3420
|
-
conversation.lastActivityAt = Math.max(conversation.lastActivityAt, lastActivityAt);
|
|
3421
|
-
}
|
|
3422
|
-
}
|
|
3423
2633
|
conversationIdFromParams(params) {
|
|
3424
2634
|
const raw = asRecord(params);
|
|
3425
2635
|
if (!raw)
|
|
@@ -3479,9 +2689,6 @@ export class AgentWorkspaceProxy {
|
|
|
3479
2689
|
rememberTurnConversationId(conversationId, turnId) {
|
|
3480
2690
|
this.currentTurnIds.set(conversationId, turnId);
|
|
3481
2691
|
this.turnConversationIds.set(turnId, conversationId);
|
|
3482
|
-
const conversation = this.conversations.get(conversationId);
|
|
3483
|
-
if (conversation)
|
|
3484
|
-
conversation.runningTurnId = turnId;
|
|
3485
2692
|
}
|
|
3486
2693
|
forgetCurrentTurn(conversationId, turnId) {
|
|
3487
2694
|
const currentTurnId = this.currentTurnIds.get(conversationId);
|
|
@@ -3490,9 +2697,6 @@ export class AgentWorkspaceProxy {
|
|
|
3490
2697
|
this.turnConversationIds.delete(turnId);
|
|
3491
2698
|
if (currentTurnId && currentTurnId !== turnId)
|
|
3492
2699
|
this.turnConversationIds.delete(currentTurnId);
|
|
3493
|
-
const conversation = this.conversations.get(conversationId);
|
|
3494
|
-
if (conversation)
|
|
3495
|
-
conversation.runningTurnId = undefined;
|
|
3496
2700
|
}
|
|
3497
2701
|
rememberItemConversationId(conversationId, item) {
|
|
3498
2702
|
const keys = [
|
|
@@ -3532,51 +2736,28 @@ export class AgentWorkspaceProxy {
|
|
|
3532
2736
|
}
|
|
3533
2737
|
cancelPendingPermissions(conversationId) {
|
|
3534
2738
|
for (const [requestId, waiter] of this.permissionWaiters) {
|
|
3535
|
-
const ownerConversationId = this.conversationIdForPermissionRequest(requestId);
|
|
3536
|
-
if (conversationId && ownerConversationId && ownerConversationId !== conversationId)
|
|
3537
|
-
continue;
|
|
3538
2739
|
clearTimeout(waiter.timer);
|
|
3539
2740
|
waiter.resolve(formatPermissionResponse(this.permissionSources.get(requestId), "cancelled", "cancelled"));
|
|
3540
|
-
if (ownerConversationId) {
|
|
3541
|
-
this.markPermission(ownerConversationId, requestId, {
|
|
3542
|
-
permissionOutcome: "cancelled",
|
|
3543
|
-
optionId: "cancelled",
|
|
3544
|
-
permissionLive: false,
|
|
3545
|
-
permissionPending: false,
|
|
3546
|
-
permissionExpired: false,
|
|
3547
|
-
permissionError: "已停止",
|
|
3548
|
-
});
|
|
3549
|
-
}
|
|
3550
|
-
this.permissionWaiters.delete(requestId);
|
|
3551
2741
|
this.pendingPermissions.delete(requestId);
|
|
3552
2742
|
this.permissionSources.delete(requestId);
|
|
3553
2743
|
}
|
|
2744
|
+
this.permissionWaiters.clear();
|
|
3554
2745
|
for (const [requestId, waiter] of this.structuredInputWaiters) {
|
|
3555
|
-
const pending = this.pendingStructuredInputs.get(requestId);
|
|
3556
|
-
if (conversationId && pending?.conversationId && pending.conversationId !== conversationId)
|
|
3557
|
-
continue;
|
|
3558
2746
|
clearTimeout(waiter.timer);
|
|
3559
2747
|
waiter.resolve(formatStructuredInputResponse({}));
|
|
2748
|
+
const pending = this.pendingStructuredInputs.get(requestId);
|
|
3560
2749
|
if (pending) {
|
|
3561
2750
|
this.markStructuredInput(pending.conversationId, requestId, {
|
|
3562
2751
|
inputPending: false,
|
|
3563
2752
|
inputError: "已停止",
|
|
3564
2753
|
});
|
|
3565
2754
|
}
|
|
3566
|
-
this.structuredInputWaiters.delete(requestId);
|
|
3567
2755
|
this.pendingStructuredInputs.delete(requestId);
|
|
3568
2756
|
}
|
|
2757
|
+
this.structuredInputWaiters.clear();
|
|
3569
2758
|
if (conversationId)
|
|
3570
2759
|
this.updateConversationStatus(conversationId, "idle");
|
|
3571
2760
|
}
|
|
3572
|
-
conversationIdForPermissionRequest(requestId) {
|
|
3573
|
-
for (const [conversationId, timeline] of this.timelines) {
|
|
3574
|
-
if (timeline.some((item) => item.type === "permission" && item.permission?.requestId === requestId)) {
|
|
3575
|
-
return conversationId;
|
|
3576
|
-
}
|
|
3577
|
-
}
|
|
3578
|
-
return undefined;
|
|
3579
|
-
}
|
|
3580
2761
|
extractSessionId(value) {
|
|
3581
2762
|
const raw = asRecord(value);
|
|
3582
2763
|
if (!raw)
|