linkshell-cli 0.3.28 → 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 +20 -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 -27
- package/dist/cli/src/runtime/acp/agent-workspace.js +168 -1008
- 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/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 +13 -13
- 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 +22 -159
- package/src/runtime/acp/agent-session.ts +1180 -0
- package/src/runtime/acp/agent-workspace.ts +176 -1094
- 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 -233
- 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,38 +997,16 @@ 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
|
-
initializePromise;
|
|
1240
|
-
providerStartPromises = new Map();
|
|
1241
1010
|
status = "unavailable";
|
|
1242
1011
|
error;
|
|
1243
1012
|
activeConversationId;
|
|
@@ -1246,8 +1015,6 @@ export class AgentWorkspaceProxy {
|
|
|
1246
1015
|
conversations = new Map();
|
|
1247
1016
|
conversationByAgentSessionId = new Map();
|
|
1248
1017
|
timelines = new Map();
|
|
1249
|
-
conversationRevisions = new Map();
|
|
1250
|
-
revisionEvents = new Map();
|
|
1251
1018
|
toolOutputBuffers = new Map();
|
|
1252
1019
|
pendingPermissions = new Map();
|
|
1253
1020
|
permissionWaiters = new Map();
|
|
@@ -1261,32 +1028,6 @@ export class AgentWorkspaceProxy {
|
|
|
1261
1028
|
}
|
|
1262
1029
|
async handleEnvelope(envelope) {
|
|
1263
1030
|
switch (envelope.type) {
|
|
1264
|
-
case "agent.codex.rpc": {
|
|
1265
|
-
const payload = parseTypedPayload("agent.codex.rpc", envelope.payload);
|
|
1266
|
-
try {
|
|
1267
|
-
this.codexRpc().send(payload);
|
|
1268
|
-
}
|
|
1269
|
-
catch (error) {
|
|
1270
|
-
const idValue = payload && typeof payload === "object" && "id" in payload
|
|
1271
|
-
? payload.id
|
|
1272
|
-
: undefined;
|
|
1273
|
-
if (typeof idValue === "string" || typeof idValue === "number") {
|
|
1274
|
-
this.input.send(createEnvelope({
|
|
1275
|
-
type: "agent.codex.rpc",
|
|
1276
|
-
hostDeviceId: this.input.hostDeviceId,
|
|
1277
|
-
payload: {
|
|
1278
|
-
jsonrpc: "2.0",
|
|
1279
|
-
id: idValue,
|
|
1280
|
-
error: {
|
|
1281
|
-
code: -32000,
|
|
1282
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1283
|
-
},
|
|
1284
|
-
},
|
|
1285
|
-
}));
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
break;
|
|
1289
|
-
}
|
|
1290
1031
|
case "agent.v2.capabilities.request":
|
|
1291
1032
|
await this.initialize();
|
|
1292
1033
|
this.sendCapabilities();
|
|
@@ -1302,8 +1043,8 @@ export class AgentWorkspaceProxy {
|
|
|
1302
1043
|
const conversations = [...this.conversations.values()].filter((conversation) => payload.includeArchived ? true : !conversation.archived);
|
|
1303
1044
|
this.input.send(createEnvelope({
|
|
1304
1045
|
type: "agent.v2.conversation.list.result",
|
|
1305
|
-
|
|
1306
|
-
payload: { conversations
|
|
1046
|
+
sessionId: this.input.sessionId,
|
|
1047
|
+
payload: { conversations },
|
|
1307
1048
|
}));
|
|
1308
1049
|
break;
|
|
1309
1050
|
}
|
|
@@ -1312,16 +1053,6 @@ export class AgentWorkspaceProxy {
|
|
|
1312
1053
|
this.sendSnapshot(payload.conversationId);
|
|
1313
1054
|
break;
|
|
1314
1055
|
}
|
|
1315
|
-
case "agent.v2.history.request": {
|
|
1316
|
-
const payload = parseTypedPayload("agent.v2.history.request", envelope.payload);
|
|
1317
|
-
await this.sendHistoryPage(payload);
|
|
1318
|
-
break;
|
|
1319
|
-
}
|
|
1320
|
-
case "agent.v2.delta.request": {
|
|
1321
|
-
const payload = parseTypedPayload("agent.v2.delta.request", envelope.payload);
|
|
1322
|
-
this.sendDelta(payload);
|
|
1323
|
-
break;
|
|
1324
|
-
}
|
|
1325
1056
|
case "agent.v2.prompt": {
|
|
1326
1057
|
const payload = parseTypedPayload("agent.v2.prompt", envelope.payload);
|
|
1327
1058
|
await this.sendPrompt(payload);
|
|
@@ -1359,65 +1090,32 @@ export class AgentWorkspaceProxy {
|
|
|
1359
1090
|
}
|
|
1360
1091
|
}
|
|
1361
1092
|
stop() {
|
|
1362
|
-
this.codexRpcBridge?.stop();
|
|
1363
|
-
this.codexRpcBridge = undefined;
|
|
1364
1093
|
for (const client of this.clients.values()) {
|
|
1365
1094
|
client.stop();
|
|
1366
1095
|
}
|
|
1367
1096
|
this.clients.clear();
|
|
1368
|
-
this.providerStartPromises.clear();
|
|
1369
|
-
this.initializePromise = undefined;
|
|
1370
1097
|
}
|
|
1371
1098
|
clientForProvider(provider) {
|
|
1372
1099
|
return this.clients.get(provider);
|
|
1373
1100
|
}
|
|
1374
|
-
codexRpc() {
|
|
1375
|
-
this.codexRpcBridge ??= new CodexRpcBridge({
|
|
1376
|
-
command: this.input.command,
|
|
1377
|
-
cwd: this.input.cwd,
|
|
1378
|
-
hostDeviceId: this.input.hostDeviceId,
|
|
1379
|
-
send: this.input.send,
|
|
1380
|
-
verbose: this.input.verbose,
|
|
1381
|
-
});
|
|
1382
|
-
return this.codexRpcBridge;
|
|
1383
|
-
}
|
|
1384
1101
|
protocolForProvider(provider) {
|
|
1385
1102
|
return this.agentProtocols.get(provider);
|
|
1386
1103
|
}
|
|
1387
1104
|
async initialize() {
|
|
1388
1105
|
if (this.initialized)
|
|
1389
1106
|
return;
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
await Promise.allSettled(startPromises);
|
|
1398
|
-
this.initialized = true;
|
|
1399
|
-
this.status = "idle";
|
|
1400
|
-
this.error = undefined;
|
|
1401
|
-
this.sendCapabilities();
|
|
1402
|
-
})().finally(() => {
|
|
1403
|
-
this.initializePromise = undefined;
|
|
1404
|
-
});
|
|
1405
|
-
return this.initializePromise;
|
|
1107
|
+
this.initialized = true;
|
|
1108
|
+
// Eagerly start all detected providers so capabilities report real status
|
|
1109
|
+
const startPromises = this.input.availableProviders.map((p) => this.ensureProviderClient(p));
|
|
1110
|
+
await Promise.allSettled(startPromises);
|
|
1111
|
+
this.status = "idle";
|
|
1112
|
+
this.error = undefined;
|
|
1113
|
+
this.sendCapabilities();
|
|
1406
1114
|
}
|
|
1407
1115
|
async ensureProviderClient(provider) {
|
|
1408
1116
|
const existing = this.clients.get(provider);
|
|
1409
1117
|
if (existing)
|
|
1410
1118
|
return existing;
|
|
1411
|
-
const starting = this.providerStartPromises.get(provider);
|
|
1412
|
-
if (starting)
|
|
1413
|
-
return starting;
|
|
1414
|
-
const startPromise = this.startProviderClient(provider).finally(() => {
|
|
1415
|
-
this.providerStartPromises.delete(provider);
|
|
1416
|
-
});
|
|
1417
|
-
this.providerStartPromises.set(provider, startPromise);
|
|
1418
|
-
return startPromise;
|
|
1419
|
-
}
|
|
1420
|
-
async startProviderClient(provider) {
|
|
1421
1119
|
const resolved = resolveAgentCommand({
|
|
1422
1120
|
provider,
|
|
1423
1121
|
command: this.input.command,
|
|
@@ -1514,13 +1212,10 @@ export class AgentWorkspaceProxy {
|
|
|
1514
1212
|
try {
|
|
1515
1213
|
const result = await listModels.call(client);
|
|
1516
1214
|
const runtimeCapabilities = parseModelListCapabilities(result);
|
|
1517
|
-
if (runtimeCapabilities)
|
|
1215
|
+
if (runtimeCapabilities)
|
|
1518
1216
|
this.providerCapabilities.set(provider, runtimeCapabilities);
|
|
1519
|
-
this.providerCapabilityErrors.delete(provider);
|
|
1520
|
-
}
|
|
1521
1217
|
}
|
|
1522
1218
|
catch (error) {
|
|
1523
|
-
this.providerCapabilityErrors.set(provider, error instanceof Error ? error.message : String(error));
|
|
1524
1219
|
if (this.input.verbose) {
|
|
1525
1220
|
process.stderr.write(`[agent:v2] model/list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1526
1221
|
}
|
|
@@ -1528,11 +1223,36 @@ export class AgentWorkspaceProxy {
|
|
|
1528
1223
|
}
|
|
1529
1224
|
async syncProviderSessions() {
|
|
1530
1225
|
await this.initialize();
|
|
1531
|
-
this.upsertProviderSessions("claude", listClaudeStoredSessions(this.input.cwd));
|
|
1532
1226
|
for (const [provider, client] of this.clients) {
|
|
1533
1227
|
try {
|
|
1534
1228
|
const result = await client.listSessions();
|
|
1535
|
-
|
|
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
|
+
}
|
|
1536
1256
|
}
|
|
1537
1257
|
catch (error) {
|
|
1538
1258
|
if (this.input.verbose) {
|
|
@@ -1541,55 +1261,16 @@ export class AgentWorkspaceProxy {
|
|
|
1541
1261
|
}
|
|
1542
1262
|
}
|
|
1543
1263
|
}
|
|
1544
|
-
upsertProviderSessions(provider, result) {
|
|
1545
|
-
for (const remote of parseRemoteSessions(result)) {
|
|
1546
|
-
const agentSessionId = remote.id;
|
|
1547
|
-
const existingId = this.conversationByAgentSessionId.get(agentSessionId);
|
|
1548
|
-
const now = Date.now();
|
|
1549
|
-
const conversationId = existingId ?? `agent:${agentSessionId}`;
|
|
1550
|
-
const existing = this.conversations.get(conversationId);
|
|
1551
|
-
const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
|
|
1552
|
-
const conversation = {
|
|
1553
|
-
id: conversationId,
|
|
1554
|
-
agentSessionId,
|
|
1555
|
-
provider,
|
|
1556
|
-
cwd,
|
|
1557
|
-
title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
|
|
1558
|
-
model: remote.model ?? existing?.model,
|
|
1559
|
-
reasoningEffort: existing?.reasoningEffort,
|
|
1560
|
-
serviceTier: existing?.serviceTier,
|
|
1561
|
-
permissionMode: existing?.permissionMode,
|
|
1562
|
-
collaborationMode: existing?.collaborationMode,
|
|
1563
|
-
status: remote.status ?? existing?.status ?? "idle",
|
|
1564
|
-
archived: remote.archived ?? existing?.archived ?? false,
|
|
1565
|
-
timelineRevision: existing?.timelineRevision ?? this.getRevision(conversationId),
|
|
1566
|
-
historyComplete: existing?.historyComplete ?? false,
|
|
1567
|
-
runningTurnId: remote.runningTurnId ?? this.currentTurnIds.get(conversationId),
|
|
1568
|
-
source: "device",
|
|
1569
|
-
canonical: true,
|
|
1570
|
-
lastMessagePreview: existing?.lastMessagePreview,
|
|
1571
|
-
lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
|
|
1572
|
-
createdAt: remote.createdAt ?? existing?.createdAt ?? now,
|
|
1573
|
-
};
|
|
1574
|
-
this.conversations.set(conversation.id, conversation);
|
|
1575
|
-
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
1576
|
-
if (remote.runningTurnId)
|
|
1577
|
-
this.rememberTurnConversationId(conversation.id, remote.runningTurnId);
|
|
1578
|
-
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
1264
|
sendCapabilities() {
|
|
1582
1265
|
const providers = this.input.availableProviders.map((provider) => {
|
|
1583
1266
|
const client = this.clients.get(provider);
|
|
1584
|
-
const protocol =
|
|
1267
|
+
const protocol = this.agentProtocols.get(provider);
|
|
1585
1268
|
const runtimeCapabilities = this.providerCapabilities.get(provider);
|
|
1586
|
-
const enabled =
|
|
1587
|
-
const hasRuntimeModels = Boolean(runtimeCapabilities?.models?.length);
|
|
1269
|
+
const enabled = Boolean(client);
|
|
1588
1270
|
const supportsImages = enabled && protocolSupportsImages(protocol);
|
|
1589
|
-
const isClaudeSdk = protocol === "claude-agent-sdk";
|
|
1590
1271
|
const isClaudeFallback = protocol === "claude-stream-json";
|
|
1591
|
-
const supportsPermission = enabled &&
|
|
1592
|
-
const supportsReasoningEffort = enabled;
|
|
1272
|
+
const supportsPermission = enabled && !isClaudeFallback;
|
|
1273
|
+
const supportsReasoningEffort = enabled && !isClaudeFallback;
|
|
1593
1274
|
const commands = mergeCommands(defaultProviderCommands(provider, this.input.cwd, enabled), runtimeCapabilities?.commands);
|
|
1594
1275
|
const currentMode = [...this.conversations.values()].find((conversation) => conversation.provider === provider)?.collaborationMode;
|
|
1595
1276
|
return {
|
|
@@ -1597,23 +1278,21 @@ export class AgentWorkspaceProxy {
|
|
|
1597
1278
|
label: providerLabel(provider),
|
|
1598
1279
|
enabled,
|
|
1599
1280
|
reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
|
|
1600
|
-
providerProtocol: protocol,
|
|
1601
1281
|
supportsImages,
|
|
1602
1282
|
supportsPermission,
|
|
1603
1283
|
supportsPlan: enabled,
|
|
1604
1284
|
supportsCancel: enabled,
|
|
1605
1285
|
models: runtimeCapabilities?.models ?? [{ id: "default", label: "默认模型" }],
|
|
1606
|
-
defaultModel: runtimeCapabilities?.defaultModel,
|
|
1607
|
-
modelsSource: hasRuntimeModels ? "runtime" : enabled ? "fallback" : "unavailable",
|
|
1608
|
-
modelListError: this.providerCapabilityErrors.get(provider),
|
|
1286
|
+
defaultModel: runtimeCapabilities?.defaultModel ?? "default",
|
|
1609
1287
|
reasoningEfforts: supportsReasoningEffort
|
|
1610
|
-
? runtimeCapabilities?.reasoningEfforts ??
|
|
1288
|
+
? runtimeCapabilities?.reasoningEfforts ?? [...ALL_REASONING_EFFORTS]
|
|
1611
1289
|
: [],
|
|
1612
|
-
|
|
1613
|
-
defaultServiceTier: provider === "codex" ? runtimeCapabilities?.defaultServiceTier : undefined,
|
|
1614
|
-
permissionModes: supportsPermission ? AGENT_PERMISSION_MODES : [],
|
|
1290
|
+
permissionModes: supportsPermission ? [...ALL_PERMISSION_MODES] : [],
|
|
1615
1291
|
commands,
|
|
1616
|
-
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
|
+
] : []),
|
|
1617
1296
|
currentMode,
|
|
1618
1297
|
features: {
|
|
1619
1298
|
images: supportsImages,
|
|
@@ -1621,8 +1300,6 @@ export class AgentWorkspaceProxy {
|
|
|
1621
1300
|
plan: enabled,
|
|
1622
1301
|
cancel: enabled,
|
|
1623
1302
|
reasoningEffort: supportsReasoningEffort,
|
|
1624
|
-
serviceTier: provider === "codex" && Boolean(runtimeCapabilities?.speedTiers?.includes("fast")),
|
|
1625
|
-
claudeSdk: isClaudeSdk,
|
|
1626
1303
|
streamJsonFallback: isClaudeFallback,
|
|
1627
1304
|
},
|
|
1628
1305
|
};
|
|
@@ -1631,14 +1308,13 @@ export class AgentWorkspaceProxy {
|
|
|
1631
1308
|
const anyPermission = providers.some((p) => p.supportsPermission);
|
|
1632
1309
|
this.input.send(createEnvelope({
|
|
1633
1310
|
type: "agent.v2.capabilities",
|
|
1634
|
-
|
|
1311
|
+
sessionId: this.input.sessionId,
|
|
1635
1312
|
payload: {
|
|
1636
1313
|
enabled: anyEnabled,
|
|
1637
1314
|
provider: this.input.availableProviders[0] ?? "codex",
|
|
1638
1315
|
providers,
|
|
1639
1316
|
protocolVersion: 1,
|
|
1640
1317
|
workspaceProtocolVersion: 2,
|
|
1641
|
-
capabilitiesRevision: ++this.capabilitiesRevision,
|
|
1642
1318
|
error: anyEnabled ? undefined : "没有可用的 Agent provider。请安装 Claude Code 或 Codex CLI。",
|
|
1643
1319
|
supportsSessionList: anyEnabled,
|
|
1644
1320
|
supportsSessionLoad: anyEnabled,
|
|
@@ -1652,6 +1328,16 @@ export class AgentWorkspaceProxy {
|
|
|
1652
1328
|
}
|
|
1653
1329
|
async openConversation(payload) {
|
|
1654
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
|
+
}
|
|
1655
1341
|
const cwd = payload.cwd ?? this.input.cwd;
|
|
1656
1342
|
let agentSessionId = payload.agentSessionId;
|
|
1657
1343
|
let existingConversation = (payload.conversationId ? this.conversations.get(payload.conversationId) : undefined) ??
|
|
@@ -1660,34 +1346,17 @@ export class AgentWorkspaceProxy {
|
|
|
1660
1346
|
if (payload.conversationId && existingConversation.id !== payload.conversationId) {
|
|
1661
1347
|
existingConversation = this.adoptConversationId(existingConversation.id, payload.conversationId);
|
|
1662
1348
|
}
|
|
1663
|
-
this.hydrateStoredTimeline(existingConversation);
|
|
1664
1349
|
this.activeConversationId = existingConversation.id;
|
|
1665
|
-
const snapshot = this.latestSnapshot(existingConversation.id);
|
|
1666
1350
|
this.input.send(createEnvelope({
|
|
1667
1351
|
type: "agent.v2.conversation.opened",
|
|
1668
|
-
|
|
1352
|
+
sessionId: this.input.sessionId,
|
|
1669
1353
|
payload: {
|
|
1670
|
-
conversation:
|
|
1671
|
-
snapshot:
|
|
1672
|
-
revision: this.getRevision(existingConversation.id),
|
|
1673
|
-
cursor: snapshot.cursor,
|
|
1674
|
-
hasMore: snapshot.hasMore,
|
|
1675
|
-
source: "device-history",
|
|
1676
|
-
canonical: true,
|
|
1354
|
+
conversation: existingConversation,
|
|
1355
|
+
snapshot: this.timelines.get(existingConversation.id) ?? [],
|
|
1677
1356
|
},
|
|
1678
1357
|
}));
|
|
1679
1358
|
return existingConversation;
|
|
1680
1359
|
}
|
|
1681
|
-
if (!provider) {
|
|
1682
|
-
return this.openFailure(payload, "没有可用的 Agent provider。");
|
|
1683
|
-
}
|
|
1684
|
-
if (!this.input.availableProviders.includes(provider)) {
|
|
1685
|
-
return this.openFailure(payload, `${providerLabel(provider)} 未安装或不可用。`);
|
|
1686
|
-
}
|
|
1687
|
-
const client = await this.ensureProviderClient(provider);
|
|
1688
|
-
if (!client) {
|
|
1689
|
-
return this.openFailure(payload, `${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`);
|
|
1690
|
-
}
|
|
1691
1360
|
try {
|
|
1692
1361
|
const result = agentSessionId
|
|
1693
1362
|
? await client.loadSession({ sessionId: agentSessionId, cwd })
|
|
@@ -1704,16 +1373,10 @@ export class AgentWorkspaceProxy {
|
|
|
1704
1373
|
title: payload.title ?? existingConversation?.title ?? titleFromCwd(cwd),
|
|
1705
1374
|
model: payload.model ?? existingConversation?.model,
|
|
1706
1375
|
reasoningEffort: payload.reasoningEffort ?? existingConversation?.reasoningEffort,
|
|
1707
|
-
serviceTier: payload.serviceTier ?? existingConversation?.serviceTier,
|
|
1708
1376
|
permissionMode: payload.permissionMode ?? existingConversation?.permissionMode,
|
|
1709
1377
|
collaborationMode: payload.collaborationMode ?? existingConversation?.collaborationMode,
|
|
1710
1378
|
status: "idle",
|
|
1711
1379
|
archived: existingConversation?.archived ?? false,
|
|
1712
|
-
timelineRevision: existingConversation?.timelineRevision ?? this.getRevision(conversationId),
|
|
1713
|
-
historyComplete: existingConversation?.historyComplete ?? false,
|
|
1714
|
-
runningTurnId: this.currentTurnIds.get(conversationId),
|
|
1715
|
-
source: "device",
|
|
1716
|
-
canonical: true,
|
|
1717
1380
|
lastMessagePreview: existingConversation?.status === "error" ? undefined : existingConversation?.lastMessagePreview,
|
|
1718
1381
|
lastActivityAt: now,
|
|
1719
1382
|
createdAt: existingConversation?.createdAt ?? now,
|
|
@@ -1722,20 +1385,10 @@ export class AgentWorkspaceProxy {
|
|
|
1722
1385
|
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
1723
1386
|
this.activeConversationId = conversation.id;
|
|
1724
1387
|
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
1725
|
-
this.hydrateStoredTimeline(conversation);
|
|
1726
|
-
const snapshot = this.latestSnapshot(conversation.id);
|
|
1727
1388
|
this.input.send(createEnvelope({
|
|
1728
1389
|
type: "agent.v2.conversation.opened",
|
|
1729
|
-
|
|
1730
|
-
payload: {
|
|
1731
|
-
conversation: this.conversationSnapshot(conversation),
|
|
1732
|
-
snapshot: snapshot.items,
|
|
1733
|
-
revision: this.getRevision(conversation.id),
|
|
1734
|
-
cursor: snapshot.cursor,
|
|
1735
|
-
hasMore: snapshot.hasMore,
|
|
1736
|
-
source: "device-history",
|
|
1737
|
-
canonical: true,
|
|
1738
|
-
},
|
|
1390
|
+
sessionId: this.input.sessionId,
|
|
1391
|
+
payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
|
|
1739
1392
|
}));
|
|
1740
1393
|
return conversation;
|
|
1741
1394
|
}
|
|
@@ -1758,10 +1411,6 @@ export class AgentWorkspaceProxy {
|
|
|
1758
1411
|
collaborationMode: payload.collaborationMode,
|
|
1759
1412
|
status: "error",
|
|
1760
1413
|
archived: false,
|
|
1761
|
-
timelineRevision: this.getRevision(fallbackId),
|
|
1762
|
-
historyComplete: false,
|
|
1763
|
-
source: "device",
|
|
1764
|
-
canonical: true,
|
|
1765
1414
|
lastMessagePreview: message,
|
|
1766
1415
|
lastActivityAt: now,
|
|
1767
1416
|
createdAt: now,
|
|
@@ -1777,15 +1426,8 @@ export class AgentWorkspaceProxy {
|
|
|
1777
1426
|
});
|
|
1778
1427
|
this.input.send(createEnvelope({
|
|
1779
1428
|
type: "agent.v2.conversation.opened",
|
|
1780
|
-
|
|
1781
|
-
payload: {
|
|
1782
|
-
conversation: this.conversationSnapshot(conversation),
|
|
1783
|
-
snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []),
|
|
1784
|
-
revision: this.getRevision(conversation.id),
|
|
1785
|
-
hasMore: false,
|
|
1786
|
-
source: "device",
|
|
1787
|
-
canonical: true,
|
|
1788
|
-
},
|
|
1429
|
+
sessionId: this.input.sessionId,
|
|
1430
|
+
payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
|
|
1789
1431
|
}));
|
|
1790
1432
|
return conversation;
|
|
1791
1433
|
}
|
|
@@ -1829,21 +1471,10 @@ export class AgentWorkspaceProxy {
|
|
|
1829
1471
|
});
|
|
1830
1472
|
return;
|
|
1831
1473
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
conversation.reasoningEffort = payload.reasoningEffort ?? undefined;
|
|
1837
|
-
}
|
|
1838
|
-
if (Object.prototype.hasOwnProperty.call(payload, "serviceTier")) {
|
|
1839
|
-
conversation.serviceTier = payload.serviceTier ?? undefined;
|
|
1840
|
-
}
|
|
1841
|
-
if (Object.prototype.hasOwnProperty.call(payload, "permissionMode")) {
|
|
1842
|
-
conversation.permissionMode = payload.permissionMode ?? undefined;
|
|
1843
|
-
}
|
|
1844
|
-
if (Object.prototype.hasOwnProperty.call(payload, "collaborationMode")) {
|
|
1845
|
-
conversation.collaborationMode = payload.collaborationMode ?? "default";
|
|
1846
|
-
}
|
|
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;
|
|
1847
1478
|
conversation.status = "running";
|
|
1848
1479
|
conversation.lastActivityAt = Date.now();
|
|
1849
1480
|
this.activeConversationId = conversation.id;
|
|
@@ -1863,11 +1494,10 @@ export class AgentWorkspaceProxy {
|
|
|
1863
1494
|
sessionId: conversation.agentSessionId,
|
|
1864
1495
|
content: payload.contentBlocks,
|
|
1865
1496
|
clientMessageId: payload.clientMessageId,
|
|
1866
|
-
model:
|
|
1867
|
-
reasoningEffort:
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
collaborationMode: conversation.collaborationMode,
|
|
1497
|
+
model: payload.model,
|
|
1498
|
+
reasoningEffort: payload.reasoningEffort,
|
|
1499
|
+
permissionMode: payload.permissionMode,
|
|
1500
|
+
collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
|
|
1871
1501
|
cwd: conversation.cwd,
|
|
1872
1502
|
});
|
|
1873
1503
|
const nextAgentSessionId = this.extractSessionId(result);
|
|
@@ -1947,7 +1577,6 @@ export class AgentWorkspaceProxy {
|
|
|
1947
1577
|
contentBlocks: [{ type: "text", text: rawText }],
|
|
1948
1578
|
model: conversation.model,
|
|
1949
1579
|
reasoningEffort: conversation.reasoningEffort,
|
|
1950
|
-
serviceTier: conversation.serviceTier,
|
|
1951
1580
|
permissionMode: conversation.permissionMode,
|
|
1952
1581
|
collaborationMode: conversation.collaborationMode,
|
|
1953
1582
|
});
|
|
@@ -1978,8 +1607,14 @@ export class AgentWorkspaceProxy {
|
|
|
1978
1607
|
this.emitStatus(conversation.id, conversation.status, `${providerLabel(conversation.provider)} · ${conversation.collaborationMode === "plan" ? "Plan mode" : "Default mode"} · ${conversation.cwd}`);
|
|
1979
1608
|
return;
|
|
1980
1609
|
}
|
|
1981
|
-
if (
|
|
1982
|
-
|
|
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
|
+
});
|
|
1983
1618
|
return;
|
|
1984
1619
|
}
|
|
1985
1620
|
if (command.name === "plan" || command.name === "exit-plan") {
|
|
@@ -1994,22 +1629,6 @@ export class AgentWorkspaceProxy {
|
|
|
1994
1629
|
: "已退出 Plan mode。");
|
|
1995
1630
|
return;
|
|
1996
1631
|
}
|
|
1997
|
-
if (command.name === "review" || command.name === "subagents") {
|
|
1998
|
-
const prompt = command.name === "review"
|
|
1999
|
-
? args || "Review the current local changes."
|
|
2000
|
-
: args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
|
|
2001
|
-
await this.sendPrompt({
|
|
2002
|
-
conversationId: conversation.id,
|
|
2003
|
-
clientMessageId: id(command.name),
|
|
2004
|
-
contentBlocks: [{ type: "text", text: prompt }],
|
|
2005
|
-
model: conversation.model,
|
|
2006
|
-
reasoningEffort: conversation.reasoningEffort,
|
|
2007
|
-
serviceTier: conversation.serviceTier,
|
|
2008
|
-
permissionMode: conversation.permissionMode,
|
|
2009
|
-
collaborationMode: conversation.collaborationMode,
|
|
2010
|
-
});
|
|
2011
|
-
return;
|
|
2012
|
-
}
|
|
2013
1632
|
if (command.name === "compact") {
|
|
2014
1633
|
if (!(client instanceof AcpClient))
|
|
2015
1634
|
throw new Error("当前 Codex runtime 不支持原生 compact。");
|
|
@@ -2030,7 +1649,7 @@ export class AgentWorkspaceProxy {
|
|
|
2030
1649
|
this.emitStatus(conversation.id, "idle", "上下文压缩完成。");
|
|
2031
1650
|
return;
|
|
2032
1651
|
}
|
|
2033
|
-
if (command.name === "clear"
|
|
1652
|
+
if (command.name === "clear") {
|
|
2034
1653
|
if (!client)
|
|
2035
1654
|
throw new Error("Agent provider 不在线。");
|
|
2036
1655
|
const result = await client.newSession({ cwd: conversation.cwd });
|
|
@@ -2048,6 +1667,21 @@ export class AgentWorkspaceProxy {
|
|
|
2048
1667
|
this.emitStatus(conversation.id, "idle", "上下文已重置,已创建新的 Codex thread。");
|
|
2049
1668
|
return;
|
|
2050
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
|
+
}
|
|
2051
1685
|
throw new Error(`命令暂未实现:/${command.name}`);
|
|
2052
1686
|
}
|
|
2053
1687
|
catch (error) {
|
|
@@ -2062,41 +1696,6 @@ export class AgentWorkspaceProxy {
|
|
|
2062
1696
|
});
|
|
2063
1697
|
}
|
|
2064
1698
|
}
|
|
2065
|
-
async executeGitNativeCommand(conversation, commandName, args) {
|
|
2066
|
-
const git = gitCommandArgs(commandName, args);
|
|
2067
|
-
const toolId = id(commandName);
|
|
2068
|
-
const now = Date.now();
|
|
2069
|
-
conversation.status = "running";
|
|
2070
|
-
conversation.lastMessagePreview = git.display;
|
|
2071
|
-
conversation.lastActivityAt = now;
|
|
2072
|
-
this.emitConversation(conversation);
|
|
2073
|
-
this.upsertTool(conversation.id, {
|
|
2074
|
-
id: toolId,
|
|
2075
|
-
name: "命令",
|
|
2076
|
-
input: `${git.display}\n\ncwd: ${conversation.cwd}`,
|
|
2077
|
-
createdAt: now,
|
|
2078
|
-
status: "running",
|
|
2079
|
-
});
|
|
2080
|
-
const result = await runProcess("git", git.argv, {
|
|
2081
|
-
cwd: conversation.cwd,
|
|
2082
|
-
maxBytes: COMMAND_OUTPUT_MAX_BYTES,
|
|
2083
|
-
});
|
|
2084
|
-
const ok = result.exitCode === 0;
|
|
2085
|
-
this.upsertTool(conversation.id, {
|
|
2086
|
-
id: toolId,
|
|
2087
|
-
name: "命令",
|
|
2088
|
-
input: `${git.display}\n\ncwd: ${conversation.cwd}`,
|
|
2089
|
-
output: result.output || (ok ? "完成" : `退出码 ${result.exitCode ?? "unknown"}`),
|
|
2090
|
-
createdAt: now,
|
|
2091
|
-
status: ok ? "completed" : "failed",
|
|
2092
|
-
});
|
|
2093
|
-
conversation.status = ok ? "idle" : "error";
|
|
2094
|
-
conversation.lastMessagePreview = ok
|
|
2095
|
-
? `${git.display} 完成`
|
|
2096
|
-
: `${git.display} 失败`;
|
|
2097
|
-
conversation.lastActivityAt = Date.now();
|
|
2098
|
-
this.emitConversation(conversation);
|
|
2099
|
-
}
|
|
2100
1699
|
handleRequest(method, params) {
|
|
2101
1700
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
2102
1701
|
return this.handleStructuredInput(params, true);
|
|
@@ -2134,7 +1733,6 @@ export class AgentWorkspaceProxy {
|
|
|
2134
1733
|
}
|
|
2135
1734
|
if (method.startsWith("account/") ||
|
|
2136
1735
|
method.startsWith("mcpServer/startupStatus/") ||
|
|
2137
|
-
method.startsWith("remoteControl/") ||
|
|
2138
1736
|
method === "thread/status/changed" ||
|
|
2139
1737
|
method === "thread/tokenUsage/updated" ||
|
|
2140
1738
|
method === "serverRequest/resolved" ||
|
|
@@ -2160,9 +1758,6 @@ export class AgentWorkspaceProxy {
|
|
|
2160
1758
|
this.conversationByAgentSessionId.set(agentSessionId, conversationId);
|
|
2161
1759
|
const conversation = this.conversations.get(conversationId);
|
|
2162
1760
|
if (conversation) {
|
|
2163
|
-
if (conversation.agentSessionId && conversation.agentSessionId !== agentSessionId) {
|
|
2164
|
-
this.conversationByAgentSessionId.delete(conversation.agentSessionId);
|
|
2165
|
-
}
|
|
2166
1761
|
conversation.agentSessionId = agentSessionId;
|
|
2167
1762
|
conversation.lastActivityAt = Date.now();
|
|
2168
1763
|
this.emitConversation(conversation);
|
|
@@ -2741,7 +2336,7 @@ export class AgentWorkspaceProxy {
|
|
|
2741
2336
|
this.upsertItem(conversationId, item);
|
|
2742
2337
|
this.input.send(createEnvelope({
|
|
2743
2338
|
type: "agent.v2.permission.request",
|
|
2744
|
-
|
|
2339
|
+
sessionId: this.input.sessionId,
|
|
2745
2340
|
payload: { conversationId, ...permission, item },
|
|
2746
2341
|
}));
|
|
2747
2342
|
if (!waitForResponse)
|
|
@@ -2752,14 +2347,6 @@ export class AgentWorkspaceProxy {
|
|
|
2752
2347
|
this.permissionWaiters.delete(requestId);
|
|
2753
2348
|
this.permissionSources.delete(requestId);
|
|
2754
2349
|
resolve(formatPermissionResponse(source, "cancelled", "cancelled"));
|
|
2755
|
-
this.markPermission(conversationId, requestId, {
|
|
2756
|
-
permissionOutcome: "cancelled",
|
|
2757
|
-
optionId: "cancelled",
|
|
2758
|
-
permissionLive: false,
|
|
2759
|
-
permissionPending: false,
|
|
2760
|
-
permissionExpired: true,
|
|
2761
|
-
permissionError: "等待授权超时",
|
|
2762
|
-
});
|
|
2763
2350
|
this.updateConversationStatus(conversationId, "idle");
|
|
2764
2351
|
}, PERMISSION_TIMEOUT_MS);
|
|
2765
2352
|
this.permissionWaiters.set(requestId, { resolve, timer });
|
|
@@ -2789,8 +2376,6 @@ export class AgentWorkspaceProxy {
|
|
|
2789
2376
|
this.markPermission(payload.conversationId, payload.requestId, {
|
|
2790
2377
|
permissionOutcome: payload.outcome,
|
|
2791
2378
|
optionId: selectedOptionId,
|
|
2792
|
-
permissionLive: false,
|
|
2793
|
-
permissionExpired: false,
|
|
2794
2379
|
permissionError: undefined,
|
|
2795
2380
|
permissionPending: false,
|
|
2796
2381
|
});
|
|
@@ -2837,209 +2422,6 @@ export class AgentWorkspaceProxy {
|
|
|
2837
2422
|
updatedAt: Date.now(),
|
|
2838
2423
|
});
|
|
2839
2424
|
}
|
|
2840
|
-
getRevision(conversationId) {
|
|
2841
|
-
return this.conversationRevisions.get(conversationId) ??
|
|
2842
|
-
this.conversations.get(conversationId)?.timelineRevision ??
|
|
2843
|
-
0;
|
|
2844
|
-
}
|
|
2845
|
-
setRevisionFloor(conversationId, revision) {
|
|
2846
|
-
const nextRevision = Math.max(this.getRevision(conversationId), revision);
|
|
2847
|
-
this.conversationRevisions.set(conversationId, nextRevision);
|
|
2848
|
-
const conversation = this.conversations.get(conversationId);
|
|
2849
|
-
if (conversation) {
|
|
2850
|
-
conversation.timelineRevision = nextRevision;
|
|
2851
|
-
conversation.runningTurnId = this.currentTurnIds.get(conversationId);
|
|
2852
|
-
}
|
|
2853
|
-
return nextRevision;
|
|
2854
|
-
}
|
|
2855
|
-
conversationSnapshot(conversation) {
|
|
2856
|
-
return {
|
|
2857
|
-
...conversation,
|
|
2858
|
-
timelineRevision: this.getRevision(conversation.id),
|
|
2859
|
-
historyComplete: conversation.historyComplete ?? false,
|
|
2860
|
-
runningTurnId: this.currentTurnIds.get(conversation.id),
|
|
2861
|
-
source: conversation.source ?? "device",
|
|
2862
|
-
canonical: conversation.canonical ?? true,
|
|
2863
|
-
};
|
|
2864
|
-
}
|
|
2865
|
-
annotateTimelineItem(item, revision, source) {
|
|
2866
|
-
return {
|
|
2867
|
-
...item,
|
|
2868
|
-
revision: revision ?? item.revision,
|
|
2869
|
-
source: item.source ?? source,
|
|
2870
|
-
canonical: item.canonical ?? true,
|
|
2871
|
-
};
|
|
2872
|
-
}
|
|
2873
|
-
recordRevisionEvent(conversationId, change) {
|
|
2874
|
-
const revision = this.getRevision(conversationId) + 1;
|
|
2875
|
-
this.conversationRevisions.set(conversationId, revision);
|
|
2876
|
-
const conversation = this.conversations.get(conversationId);
|
|
2877
|
-
if (conversation) {
|
|
2878
|
-
conversation.timelineRevision = revision;
|
|
2879
|
-
conversation.runningTurnId = this.currentTurnIds.get(conversationId);
|
|
2880
|
-
}
|
|
2881
|
-
const event = {
|
|
2882
|
-
revision,
|
|
2883
|
-
item: change.item ? this.annotateTimelineItem(change.item, revision, "device-live") : undefined,
|
|
2884
|
-
conversation: change.conversation ? this.conversationSnapshot(change.conversation) : undefined,
|
|
2885
|
-
};
|
|
2886
|
-
const events = this.revisionEvents.get(conversationId) ?? [];
|
|
2887
|
-
events.push(event);
|
|
2888
|
-
if (events.length > MAX_DELTA_EVENTS) {
|
|
2889
|
-
events.splice(0, events.length - MAX_DELTA_EVENTS);
|
|
2890
|
-
}
|
|
2891
|
-
this.revisionEvents.set(conversationId, events);
|
|
2892
|
-
return event;
|
|
2893
|
-
}
|
|
2894
|
-
storedTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
|
|
2895
|
-
if (!conversation.agentSessionId)
|
|
2896
|
-
return [];
|
|
2897
|
-
const result = conversation.provider === "codex"
|
|
2898
|
-
? loadCodexStoredTimeline(conversation.agentSessionId, conversation.id, conversation.cwd || this.input.cwd, { maxItems })
|
|
2899
|
-
: conversation.provider === "claude"
|
|
2900
|
-
? loadClaudeStoredTimeline(conversation.agentSessionId, conversation.id, { maxItems })
|
|
2901
|
-
: { items: [] };
|
|
2902
|
-
return result.items
|
|
2903
|
-
.sort((a, b) => a.createdAt - b.createdAt)
|
|
2904
|
-
.map((item, index) => this.annotateTimelineItem(item, index + 1, "device-history"));
|
|
2905
|
-
}
|
|
2906
|
-
canonicalTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
|
|
2907
|
-
const merged = new Map();
|
|
2908
|
-
for (const item of this.storedTimeline(conversation, maxItems)) {
|
|
2909
|
-
merged.set(item.id, item);
|
|
2910
|
-
}
|
|
2911
|
-
for (const item of this.timelines.get(conversation.id) ?? []) {
|
|
2912
|
-
merged.set(item.id, this.annotateTimelineItem(item, item.revision, item.source ?? "device-live"));
|
|
2913
|
-
}
|
|
2914
|
-
const items = [...merged.values()]
|
|
2915
|
-
.sort((a, b) => a.createdAt - b.createdAt)
|
|
2916
|
-
.slice(-maxItems);
|
|
2917
|
-
this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
|
|
2918
|
-
return items;
|
|
2919
|
-
}
|
|
2920
|
-
async appServerTimeline(conversation, maxItems = HISTORY_PAGE_MAX_ITEMS) {
|
|
2921
|
-
if (conversation.provider !== "codex" || !conversation.agentSessionId)
|
|
2922
|
-
return [];
|
|
2923
|
-
if (this.protocolForProvider(conversation.provider) !== "codex-app-server")
|
|
2924
|
-
return [];
|
|
2925
|
-
const client = this.clientForProvider(conversation.provider);
|
|
2926
|
-
const listTurns = client?.listTurns;
|
|
2927
|
-
if (typeof listTurns !== "function")
|
|
2928
|
-
return [];
|
|
2929
|
-
try {
|
|
2930
|
-
const result = await listTurns.call(client, { sessionId: conversation.agentSessionId, limit: maxItems });
|
|
2931
|
-
return this.timelineFromAppServerTurns(conversation.id, conversation.agentSessionId, result);
|
|
2932
|
-
}
|
|
2933
|
-
catch (error) {
|
|
2934
|
-
if (this.input.verbose) {
|
|
2935
|
-
process.stderr.write(`[agent:v2] thread/turns/list failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2936
|
-
}
|
|
2937
|
-
return [];
|
|
2938
|
-
}
|
|
2939
|
-
}
|
|
2940
|
-
timelineFromAppServerTurns(conversationId, agentSessionId, value) {
|
|
2941
|
-
const raw = asRecord(value);
|
|
2942
|
-
const turns = Array.isArray(value) ? value :
|
|
2943
|
-
Array.isArray(raw?.turns) ? raw.turns :
|
|
2944
|
-
Array.isArray(raw?.items) ? raw.items :
|
|
2945
|
-
Array.isArray(raw?.entries) ? raw.entries :
|
|
2946
|
-
[];
|
|
2947
|
-
const items = [];
|
|
2948
|
-
turns.forEach((entry, index) => {
|
|
2949
|
-
const turn = asRecord(entry);
|
|
2950
|
-
if (!turn)
|
|
2951
|
-
return;
|
|
2952
|
-
const turnId = firstString(turn, ["id", "turnId", "turn_id"]) ?? `turn-${index + 1}`;
|
|
2953
|
-
const createdAt = parseTimestamp(turn.createdAt ?? turn.created_at ?? turn.startedAt ?? turn.started_at) ?? Date.now() + index;
|
|
2954
|
-
const updatedAt = parseTimestamp(turn.updatedAt ?? turn.updated_at ?? turn.completedAt ?? turn.completed_at);
|
|
2955
|
-
const userText = appServerText(turn.input ?? turn.prompt ?? turn.user ?? turn.userMessage ?? turn.request);
|
|
2956
|
-
if (userText) {
|
|
2957
|
-
items.push({
|
|
2958
|
-
id: `app-server:${agentSessionId}:${turnId}:user`,
|
|
2959
|
-
conversationId,
|
|
2960
|
-
type: "message",
|
|
2961
|
-
kind: "chat",
|
|
2962
|
-
turnId,
|
|
2963
|
-
role: "user",
|
|
2964
|
-
content: [{ type: "text", text: userText }],
|
|
2965
|
-
text: userText,
|
|
2966
|
-
createdAt,
|
|
2967
|
-
updatedAt,
|
|
2968
|
-
metadata: { source: "app-server", provider: "codex" },
|
|
2969
|
-
});
|
|
2970
|
-
}
|
|
2971
|
-
const assistantText = appServerText(turn.output ?? turn.response ?? turn.assistant ?? turn.assistantMessage ?? turn.result);
|
|
2972
|
-
if (assistantText) {
|
|
2973
|
-
items.push({
|
|
2974
|
-
id: `app-server:${agentSessionId}:${turnId}:assistant`,
|
|
2975
|
-
conversationId,
|
|
2976
|
-
type: "message",
|
|
2977
|
-
kind: "chat",
|
|
2978
|
-
turnId,
|
|
2979
|
-
role: "assistant",
|
|
2980
|
-
content: [{ type: "text", text: assistantText }],
|
|
2981
|
-
text: assistantText,
|
|
2982
|
-
createdAt: updatedAt ?? createdAt + 1,
|
|
2983
|
-
updatedAt,
|
|
2984
|
-
metadata: { source: "app-server", provider: "codex" },
|
|
2985
|
-
});
|
|
2986
|
-
}
|
|
2987
|
-
const nestedItems = Array.isArray(turn.items) ? turn.items : Array.isArray(turn.messages) ? turn.messages : [];
|
|
2988
|
-
for (const nested of nestedItems) {
|
|
2989
|
-
const nestedRecord = asRecord(nested);
|
|
2990
|
-
if (!nestedRecord)
|
|
2991
|
-
continue;
|
|
2992
|
-
const text = appServerText(nestedRecord.content ?? nestedRecord.text ?? nestedRecord.message);
|
|
2993
|
-
const role = nestedRecord.role === "user" || nestedRecord.role === "assistant" || nestedRecord.role === "system"
|
|
2994
|
-
? nestedRecord.role
|
|
2995
|
-
: undefined;
|
|
2996
|
-
if (!text || !role)
|
|
2997
|
-
continue;
|
|
2998
|
-
const itemId = firstString(nestedRecord, ["id", "itemId", "messageId"]) ?? `${role}-${items.length + 1}`;
|
|
2999
|
-
items.push({
|
|
3000
|
-
id: `app-server:${agentSessionId}:${turnId}:${itemId}`,
|
|
3001
|
-
conversationId,
|
|
3002
|
-
type: "message",
|
|
3003
|
-
kind: "chat",
|
|
3004
|
-
turnId,
|
|
3005
|
-
itemId,
|
|
3006
|
-
role,
|
|
3007
|
-
content: [{ type: "text", text }],
|
|
3008
|
-
text,
|
|
3009
|
-
createdAt: parseTimestamp(nestedRecord.createdAt ?? nestedRecord.created_at) ?? createdAt + items.length,
|
|
3010
|
-
updatedAt: parseTimestamp(nestedRecord.updatedAt ?? nestedRecord.updated_at),
|
|
3011
|
-
metadata: { source: "app-server", provider: "codex" },
|
|
3012
|
-
});
|
|
3013
|
-
}
|
|
3014
|
-
});
|
|
3015
|
-
return items
|
|
3016
|
-
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3017
|
-
.map((item, index) => this.annotateTimelineItem(item, index + 1, "app-server"));
|
|
3018
|
-
}
|
|
3019
|
-
mergeCanonicalTimelineItems(items, maxItems) {
|
|
3020
|
-
const byKey = new Map();
|
|
3021
|
-
for (const item of items.sort((a, b) => a.createdAt - b.createdAt)) {
|
|
3022
|
-
const key = item.type === "message" && item.role && item.text
|
|
3023
|
-
? `${item.role}:${item.text.replace(/\s+/g, " ").trim().slice(0, 500)}`
|
|
3024
|
-
: item.id;
|
|
3025
|
-
const existing = byKey.get(key);
|
|
3026
|
-
if (!existing || item.source === "app-server" || (item.updatedAt ?? item.createdAt) >= (existing.updatedAt ?? existing.createdAt)) {
|
|
3027
|
-
byKey.set(key, item);
|
|
3028
|
-
}
|
|
3029
|
-
}
|
|
3030
|
-
return [...byKey.values()]
|
|
3031
|
-
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3032
|
-
.slice(-maxItems);
|
|
3033
|
-
}
|
|
3034
|
-
latestSnapshot(conversationId) {
|
|
3035
|
-
const timeline = this.timelines.get(conversationId) ?? [];
|
|
3036
|
-
const start = Math.max(0, timeline.length - MAX_SNAPSHOT_ITEMS);
|
|
3037
|
-
return {
|
|
3038
|
-
items: timeline.slice(start).map((item) => snapshotTimelineItem(item)),
|
|
3039
|
-
cursor: start > 0 ? String(start) : undefined,
|
|
3040
|
-
hasMore: start > 0,
|
|
3041
|
-
};
|
|
3042
|
-
}
|
|
3043
2425
|
addItem(conversationId, item) {
|
|
3044
2426
|
this.rememberItemConversationId(conversationId, item);
|
|
3045
2427
|
const timeline = this.timelines.get(conversationId) ?? [];
|
|
@@ -3077,8 +2459,6 @@ export class AgentWorkspaceProxy {
|
|
|
3077
2459
|
...existing,
|
|
3078
2460
|
...toolCall,
|
|
3079
2461
|
id: targetToolId,
|
|
3080
|
-
input: toolCall.input ?? existing?.input,
|
|
3081
|
-
output: toolCall.output ?? existing?.output,
|
|
3082
2462
|
createdAt: existing?.createdAt ?? toolCall.createdAt ?? Date.now(),
|
|
3083
2463
|
};
|
|
3084
2464
|
this.toolConversationIds.set(toolCall.id, conversationId);
|
|
@@ -3147,7 +2527,6 @@ export class AgentWorkspaceProxy {
|
|
|
3147
2527
|
conversation.title = conversation.title ?? target.title;
|
|
3148
2528
|
conversation.model = conversation.model ?? target.model;
|
|
3149
2529
|
conversation.reasoningEffort = conversation.reasoningEffort ?? target.reasoningEffort;
|
|
3150
|
-
conversation.serviceTier = conversation.serviceTier ?? target.serviceTier;
|
|
3151
2530
|
conversation.permissionMode = conversation.permissionMode ?? target.permissionMode;
|
|
3152
2531
|
conversation.lastMessagePreview = conversation.lastMessagePreview ?? target.lastMessagePreview;
|
|
3153
2532
|
conversation.createdAt = Math.min(conversation.createdAt, target.createdAt);
|
|
@@ -3160,17 +2539,6 @@ export class AgentWorkspaceProxy {
|
|
|
3160
2539
|
this.timelines.set(newId, [...mergedTimeline.values()]
|
|
3161
2540
|
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3162
2541
|
.slice(-MAX_TIMELINE_ITEMS));
|
|
3163
|
-
const oldRevision = this.conversationRevisions.get(oldId) ?? 0;
|
|
3164
|
-
if (oldRevision > 0) {
|
|
3165
|
-
this.conversationRevisions.delete(oldId);
|
|
3166
|
-
this.setRevisionFloor(newId, oldRevision);
|
|
3167
|
-
}
|
|
3168
|
-
const oldEvents = this.revisionEvents.get(oldId);
|
|
3169
|
-
if (oldEvents) {
|
|
3170
|
-
this.revisionEvents.delete(oldId);
|
|
3171
|
-
const existingEvents = this.revisionEvents.get(newId) ?? [];
|
|
3172
|
-
this.revisionEvents.set(newId, [...existingEvents, ...oldEvents].slice(-MAX_DELTA_EVENTS));
|
|
3173
|
-
}
|
|
3174
2542
|
for (const [agentSessionId, conversationId] of this.conversationByAgentSessionId) {
|
|
3175
2543
|
if (conversationId === oldId) {
|
|
3176
2544
|
this.conversationByAgentSessionId.set(agentSessionId, newId);
|
|
@@ -3204,47 +2572,17 @@ export class AgentWorkspaceProxy {
|
|
|
3204
2572
|
}
|
|
3205
2573
|
emitItem(conversationId, item) {
|
|
3206
2574
|
const conversation = this.conversations.get(conversationId);
|
|
3207
|
-
const itemSnapshot = snapshotTimelineItem(item, { stripImages: false });
|
|
3208
|
-
const event = this.recordRevisionEvent(conversationId, { conversation, item: itemSnapshot });
|
|
3209
2575
|
this.input.send(createEnvelope({
|
|
3210
2576
|
type: "agent.v2.event",
|
|
3211
|
-
|
|
3212
|
-
payload: {
|
|
3213
|
-
conversationId,
|
|
3214
|
-
conversation: event.conversation,
|
|
3215
|
-
item: event.item,
|
|
3216
|
-
revision: event.revision,
|
|
3217
|
-
source: "device-live",
|
|
3218
|
-
canonical: true,
|
|
3219
|
-
},
|
|
2577
|
+
sessionId: this.input.sessionId,
|
|
2578
|
+
payload: { conversationId, conversation, item },
|
|
3220
2579
|
}));
|
|
3221
2580
|
}
|
|
3222
2581
|
emitConversation(conversation) {
|
|
3223
|
-
const event = this.recordRevisionEvent(conversation.id, { conversation });
|
|
3224
2582
|
this.input.send(createEnvelope({
|
|
3225
2583
|
type: "agent.v2.event",
|
|
3226
|
-
|
|
3227
|
-
payload: {
|
|
3228
|
-
conversationId: conversation.id,
|
|
3229
|
-
conversation: event.conversation,
|
|
3230
|
-
revision: event.revision,
|
|
3231
|
-
source: "device-live",
|
|
3232
|
-
canonical: true,
|
|
3233
|
-
},
|
|
3234
|
-
}));
|
|
3235
|
-
this.input.send(createEnvelope({
|
|
3236
|
-
type: "agent.v2.running_state",
|
|
3237
|
-
hostDeviceId: this.input.hostDeviceId,
|
|
3238
|
-
payload: {
|
|
3239
|
-
conversationId: conversation.id,
|
|
3240
|
-
status: conversation.status,
|
|
3241
|
-
runningTurnId: this.currentTurnIds.get(conversation.id),
|
|
3242
|
-
revision: event.revision,
|
|
3243
|
-
error: conversation.status === "error" ? conversation.lastMessagePreview : undefined,
|
|
3244
|
-
updatedAt: conversation.lastActivityAt,
|
|
3245
|
-
source: "device-live",
|
|
3246
|
-
canonical: true,
|
|
3247
|
-
},
|
|
2584
|
+
sessionId: this.input.sessionId,
|
|
2585
|
+
payload: { conversationId: conversation.id, conversation },
|
|
3248
2586
|
}));
|
|
3249
2587
|
}
|
|
3250
2588
|
emitStatus(conversationId, status, text) {
|
|
@@ -3278,169 +2616,20 @@ export class AgentWorkspaceProxy {
|
|
|
3278
2616
|
this.emitConversation(conversation);
|
|
3279
2617
|
}
|
|
3280
2618
|
sendSnapshot(conversationId) {
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
}
|
|
3286
|
-
else if (this.activeConversationId) {
|
|
3287
|
-
const conversation = this.conversations.get(this.activeConversationId);
|
|
3288
|
-
if (conversation)
|
|
3289
|
-
this.hydrateStoredTimeline(conversation);
|
|
3290
|
-
}
|
|
3291
|
-
const conversations = [...this.conversations.values()].map((conversation) => this.conversationSnapshot(conversation));
|
|
3292
|
-
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();
|
|
3293
2623
|
this.input.send(createEnvelope({
|
|
3294
2624
|
type: "agent.v2.snapshot",
|
|
3295
|
-
|
|
2625
|
+
sessionId: this.input.sessionId,
|
|
3296
2626
|
payload: {
|
|
3297
2627
|
conversations,
|
|
3298
2628
|
activeConversationId: this.activeConversationId,
|
|
3299
|
-
items
|
|
3300
|
-
revision: conversationId ? this.getRevision(conversationId) : undefined,
|
|
3301
|
-
cursor: snapshot.cursor,
|
|
3302
|
-
hasMore: snapshot.hasMore,
|
|
3303
|
-
source: "device-history",
|
|
3304
|
-
canonical: true,
|
|
3305
|
-
},
|
|
3306
|
-
}));
|
|
3307
|
-
}
|
|
3308
|
-
async sendHistoryPage(payload) {
|
|
3309
|
-
const conversation = this.conversations.get(payload.conversationId);
|
|
3310
|
-
if (!conversation)
|
|
3311
|
-
return;
|
|
3312
|
-
const limit = Math.min(Math.max(payload.limit ?? MAX_SNAPSHOT_ITEMS, 1), 200);
|
|
3313
|
-
const items = this.mergeCanonicalTimelineItems([
|
|
3314
|
-
...this.canonicalTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
|
|
3315
|
-
...await this.appServerTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
|
|
3316
|
-
], HISTORY_PAGE_MAX_ITEMS);
|
|
3317
|
-
this.timelines.set(conversation.id, items.slice(-MAX_TIMELINE_ITEMS).map((item) => this.annotateTimelineItem(item, item.revision, item.source ?? "device-history")));
|
|
3318
|
-
for (const item of this.timelines.get(conversation.id) ?? []) {
|
|
3319
|
-
this.rememberItemConversationId(conversation.id, item);
|
|
3320
|
-
}
|
|
3321
|
-
const direction = payload.direction ?? "older";
|
|
3322
|
-
let page;
|
|
3323
|
-
let cursor;
|
|
3324
|
-
let hasMore = false;
|
|
3325
|
-
if (direction === "newer") {
|
|
3326
|
-
const start = clampHistoryCursor(payload.cursor, 0, items.length);
|
|
3327
|
-
const end = Math.min(items.length, start + limit);
|
|
3328
|
-
page = items.slice(start, end);
|
|
3329
|
-
hasMore = end < items.length;
|
|
3330
|
-
cursor = hasMore ? String(end) : undefined;
|
|
3331
|
-
}
|
|
3332
|
-
else {
|
|
3333
|
-
const end = clampHistoryCursor(payload.cursor, items.length, items.length);
|
|
3334
|
-
const start = Math.max(0, end - limit);
|
|
3335
|
-
page = items.slice(start, end);
|
|
3336
|
-
hasMore = start > 0;
|
|
3337
|
-
cursor = hasMore ? String(start) : undefined;
|
|
3338
|
-
}
|
|
3339
|
-
conversation.historyComplete = !hasMore;
|
|
3340
|
-
const revision = this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
|
|
3341
|
-
this.input.send(createEnvelope({
|
|
3342
|
-
type: "agent.v2.history.page",
|
|
3343
|
-
hostDeviceId: this.input.hostDeviceId,
|
|
3344
|
-
payload: {
|
|
3345
|
-
conversationId: conversation.id,
|
|
3346
|
-
conversation: this.conversationSnapshot(conversation),
|
|
3347
|
-
items: page.map((item) => snapshotTimelineItem(item)),
|
|
3348
|
-
revision,
|
|
3349
|
-
cursor,
|
|
3350
|
-
hasMore,
|
|
3351
|
-
source: "device-history",
|
|
3352
|
-
canonical: true,
|
|
3353
|
-
},
|
|
3354
|
-
}));
|
|
3355
|
-
}
|
|
3356
|
-
sendDelta(payload) {
|
|
3357
|
-
const conversation = this.conversations.get(payload.conversationId);
|
|
3358
|
-
if (!conversation)
|
|
3359
|
-
return;
|
|
3360
|
-
this.hydrateStoredTimeline(conversation);
|
|
3361
|
-
const sinceRevision = payload.sinceRevision ?? 0;
|
|
3362
|
-
const limit = Math.min(Math.max(payload.limit ?? 100, 1), 500);
|
|
3363
|
-
const events = this.revisionEvents.get(conversation.id) ?? [];
|
|
3364
|
-
const oldestAvailable = events[0]?.revision ?? this.getRevision(conversation.id);
|
|
3365
|
-
const newestRevision = this.getRevision(conversation.id);
|
|
3366
|
-
const reset = sinceRevision > 0 &&
|
|
3367
|
-
(sinceRevision > newestRevision ||
|
|
3368
|
-
(sinceRevision < newestRevision &&
|
|
3369
|
-
(events.length === 0 || sinceRevision < oldestAvailable - 1)));
|
|
3370
|
-
if (reset) {
|
|
3371
|
-
const snapshot = this.latestSnapshot(conversation.id);
|
|
3372
|
-
this.input.send(createEnvelope({
|
|
3373
|
-
type: "agent.v2.delta",
|
|
3374
|
-
hostDeviceId: this.input.hostDeviceId,
|
|
3375
|
-
payload: {
|
|
3376
|
-
conversationId: conversation.id,
|
|
3377
|
-
conversation: this.conversationSnapshot(conversation),
|
|
3378
|
-
items: snapshot.items,
|
|
3379
|
-
sinceRevision,
|
|
3380
|
-
revision: newestRevision,
|
|
3381
|
-
reset: true,
|
|
3382
|
-
cursor: snapshot.cursor,
|
|
3383
|
-
hasMore: snapshot.hasMore,
|
|
3384
|
-
source: "device-history",
|
|
3385
|
-
canonical: true,
|
|
3386
|
-
},
|
|
3387
|
-
}));
|
|
3388
|
-
return;
|
|
3389
|
-
}
|
|
3390
|
-
const changed = events
|
|
3391
|
-
.filter((event) => event.revision > sinceRevision)
|
|
3392
|
-
.slice(-limit);
|
|
3393
|
-
const itemsById = new Map();
|
|
3394
|
-
for (const event of changed) {
|
|
3395
|
-
if (event.item)
|
|
3396
|
-
itemsById.set(event.item.id, event.item);
|
|
3397
|
-
}
|
|
3398
|
-
this.input.send(createEnvelope({
|
|
3399
|
-
type: "agent.v2.delta",
|
|
3400
|
-
hostDeviceId: this.input.hostDeviceId,
|
|
3401
|
-
payload: {
|
|
3402
|
-
conversationId: conversation.id,
|
|
3403
|
-
conversation: this.conversationSnapshot(conversation),
|
|
3404
|
-
items: [...itemsById.values()].map((item) => snapshotTimelineItem(item)),
|
|
3405
|
-
sinceRevision,
|
|
3406
|
-
revision: newestRevision,
|
|
3407
|
-
reset: false,
|
|
3408
|
-
hasMore: changed.length === limit && events.some((event) => event.revision > sinceRevision && event.revision < changed[0].revision),
|
|
3409
|
-
source: "device-live",
|
|
3410
|
-
canonical: true,
|
|
2629
|
+
items,
|
|
3411
2630
|
},
|
|
3412
2631
|
}));
|
|
3413
2632
|
}
|
|
3414
|
-
hydrateStoredTimeline(conversation) {
|
|
3415
|
-
if (!conversation.agentSessionId)
|
|
3416
|
-
return;
|
|
3417
|
-
const existing = this.timelines.get(conversation.id) ?? [];
|
|
3418
|
-
if (existing.length > 0) {
|
|
3419
|
-
this.setRevisionFloor(conversation.id, Math.max(existing.length, ...existing.map((item) => item.revision ?? 0)));
|
|
3420
|
-
return;
|
|
3421
|
-
}
|
|
3422
|
-
const stored = this.storedTimeline(conversation, MAX_TIMELINE_ITEMS);
|
|
3423
|
-
conversation.historyComplete = stored.length <= MAX_SNAPSHOT_ITEMS;
|
|
3424
|
-
if (stored.length === 0) {
|
|
3425
|
-
this.setRevisionFloor(conversation.id, this.getRevision(conversation.id));
|
|
3426
|
-
return;
|
|
3427
|
-
}
|
|
3428
|
-
const items = stored
|
|
3429
|
-
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3430
|
-
.slice(-MAX_TIMELINE_ITEMS);
|
|
3431
|
-
this.timelines.set(conversation.id, items);
|
|
3432
|
-
for (const item of items)
|
|
3433
|
-
this.rememberItemConversationId(conversation.id, item);
|
|
3434
|
-
this.setRevisionFloor(conversation.id, Math.max(items.length, ...items.map((item) => item.revision ?? 0)));
|
|
3435
|
-
const lastMessage = [...items].reverse().find((item) => item.text?.trim());
|
|
3436
|
-
if (lastMessage?.text && !conversation.lastMessagePreview) {
|
|
3437
|
-
conversation.lastMessagePreview = previewText(lastMessage.text);
|
|
3438
|
-
}
|
|
3439
|
-
const lastActivityAt = items.at(-1)?.createdAt;
|
|
3440
|
-
if (lastActivityAt) {
|
|
3441
|
-
conversation.lastActivityAt = Math.max(conversation.lastActivityAt, lastActivityAt);
|
|
3442
|
-
}
|
|
3443
|
-
}
|
|
3444
2633
|
conversationIdFromParams(params) {
|
|
3445
2634
|
const raw = asRecord(params);
|
|
3446
2635
|
if (!raw)
|
|
@@ -3500,9 +2689,6 @@ export class AgentWorkspaceProxy {
|
|
|
3500
2689
|
rememberTurnConversationId(conversationId, turnId) {
|
|
3501
2690
|
this.currentTurnIds.set(conversationId, turnId);
|
|
3502
2691
|
this.turnConversationIds.set(turnId, conversationId);
|
|
3503
|
-
const conversation = this.conversations.get(conversationId);
|
|
3504
|
-
if (conversation)
|
|
3505
|
-
conversation.runningTurnId = turnId;
|
|
3506
2692
|
}
|
|
3507
2693
|
forgetCurrentTurn(conversationId, turnId) {
|
|
3508
2694
|
const currentTurnId = this.currentTurnIds.get(conversationId);
|
|
@@ -3511,9 +2697,6 @@ export class AgentWorkspaceProxy {
|
|
|
3511
2697
|
this.turnConversationIds.delete(turnId);
|
|
3512
2698
|
if (currentTurnId && currentTurnId !== turnId)
|
|
3513
2699
|
this.turnConversationIds.delete(currentTurnId);
|
|
3514
|
-
const conversation = this.conversations.get(conversationId);
|
|
3515
|
-
if (conversation)
|
|
3516
|
-
conversation.runningTurnId = undefined;
|
|
3517
2700
|
}
|
|
3518
2701
|
rememberItemConversationId(conversationId, item) {
|
|
3519
2702
|
const keys = [
|
|
@@ -3553,51 +2736,28 @@ export class AgentWorkspaceProxy {
|
|
|
3553
2736
|
}
|
|
3554
2737
|
cancelPendingPermissions(conversationId) {
|
|
3555
2738
|
for (const [requestId, waiter] of this.permissionWaiters) {
|
|
3556
|
-
const ownerConversationId = this.conversationIdForPermissionRequest(requestId);
|
|
3557
|
-
if (conversationId && ownerConversationId && ownerConversationId !== conversationId)
|
|
3558
|
-
continue;
|
|
3559
2739
|
clearTimeout(waiter.timer);
|
|
3560
2740
|
waiter.resolve(formatPermissionResponse(this.permissionSources.get(requestId), "cancelled", "cancelled"));
|
|
3561
|
-
if (ownerConversationId) {
|
|
3562
|
-
this.markPermission(ownerConversationId, requestId, {
|
|
3563
|
-
permissionOutcome: "cancelled",
|
|
3564
|
-
optionId: "cancelled",
|
|
3565
|
-
permissionLive: false,
|
|
3566
|
-
permissionPending: false,
|
|
3567
|
-
permissionExpired: false,
|
|
3568
|
-
permissionError: "已停止",
|
|
3569
|
-
});
|
|
3570
|
-
}
|
|
3571
|
-
this.permissionWaiters.delete(requestId);
|
|
3572
2741
|
this.pendingPermissions.delete(requestId);
|
|
3573
2742
|
this.permissionSources.delete(requestId);
|
|
3574
2743
|
}
|
|
2744
|
+
this.permissionWaiters.clear();
|
|
3575
2745
|
for (const [requestId, waiter] of this.structuredInputWaiters) {
|
|
3576
|
-
const pending = this.pendingStructuredInputs.get(requestId);
|
|
3577
|
-
if (conversationId && pending?.conversationId && pending.conversationId !== conversationId)
|
|
3578
|
-
continue;
|
|
3579
2746
|
clearTimeout(waiter.timer);
|
|
3580
2747
|
waiter.resolve(formatStructuredInputResponse({}));
|
|
2748
|
+
const pending = this.pendingStructuredInputs.get(requestId);
|
|
3581
2749
|
if (pending) {
|
|
3582
2750
|
this.markStructuredInput(pending.conversationId, requestId, {
|
|
3583
2751
|
inputPending: false,
|
|
3584
2752
|
inputError: "已停止",
|
|
3585
2753
|
});
|
|
3586
2754
|
}
|
|
3587
|
-
this.structuredInputWaiters.delete(requestId);
|
|
3588
2755
|
this.pendingStructuredInputs.delete(requestId);
|
|
3589
2756
|
}
|
|
2757
|
+
this.structuredInputWaiters.clear();
|
|
3590
2758
|
if (conversationId)
|
|
3591
2759
|
this.updateConversationStatus(conversationId, "idle");
|
|
3592
2760
|
}
|
|
3593
|
-
conversationIdForPermissionRequest(requestId) {
|
|
3594
|
-
for (const [conversationId, timeline] of this.timelines) {
|
|
3595
|
-
if (timeline.some((item) => item.type === "permission" && item.permission?.requestId === requestId)) {
|
|
3596
|
-
return conversationId;
|
|
3597
|
-
}
|
|
3598
|
-
}
|
|
3599
|
-
return undefined;
|
|
3600
|
-
}
|
|
3601
2761
|
extractSessionId(value) {
|
|
3602
2762
|
const raw = asRecord(value);
|
|
3603
2763
|
if (!raw)
|