linkshell-cli 0.3.28 → 0.4.0
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 +2 -27
- package/dist/cli/src/runtime/acp/agent-workspace.js +224 -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 +50 -82
- 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 +36 -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 +11978 -3423
- package/dist/shared-protocol/src/index.js +114 -163
- 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 +246 -1094
- package/src/runtime/acp/claude-sdk-client.ts +51 -86
- package/src/runtime/acp/claude-stream-json-client.ts +42 -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,24 +1471,42 @@ export class AgentWorkspaceProxy {
|
|
|
1829
1471
|
});
|
|
1830
1472
|
return;
|
|
1831
1473
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
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
|
+
const priorSettings = {
|
|
1475
|
+
model: conversation.model,
|
|
1476
|
+
reasoningEffort: conversation.reasoningEffort,
|
|
1477
|
+
permissionMode: conversation.permissionMode,
|
|
1478
|
+
};
|
|
1479
|
+
conversation.model = payload.model ?? conversation.model;
|
|
1480
|
+
conversation.reasoningEffort = payload.reasoningEffort ?? conversation.reasoningEffort;
|
|
1481
|
+
conversation.permissionMode = payload.permissionMode ?? conversation.permissionMode;
|
|
1482
|
+
conversation.collaborationMode = payload.collaborationMode ?? conversation.collaborationMode;
|
|
1847
1483
|
conversation.status = "running";
|
|
1848
1484
|
conversation.lastActivityAt = Date.now();
|
|
1849
1485
|
this.activeConversationId = conversation.id;
|
|
1486
|
+
if (payload.model && payload.model !== priorSettings.model) {
|
|
1487
|
+
const runtimeCapabilities = this.providerCapabilities.get(conversation.provider);
|
|
1488
|
+
const label = runtimeCapabilities?.models?.find((m) => m.id === payload.model)?.label ?? payload.model;
|
|
1489
|
+
this.emitNotice({
|
|
1490
|
+
conversationId: conversation.id,
|
|
1491
|
+
kind: "model_changed",
|
|
1492
|
+
title: `已切换模型 · ${label}`,
|
|
1493
|
+
detail: `${providerLabel(conversation.provider)} 下次回复将使用 ${payload.model}`,
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
if (payload.reasoningEffort && payload.reasoningEffort !== priorSettings.reasoningEffort) {
|
|
1497
|
+
this.emitNotice({
|
|
1498
|
+
conversationId: conversation.id,
|
|
1499
|
+
kind: "effort_changed",
|
|
1500
|
+
title: `思考强度 · ${payload.reasoningEffort}`,
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
if (payload.permissionMode && payload.permissionMode !== priorSettings.permissionMode) {
|
|
1504
|
+
this.emitNotice({
|
|
1505
|
+
conversationId: conversation.id,
|
|
1506
|
+
kind: "permission_changed",
|
|
1507
|
+
title: `权限模式 · ${payload.permissionMode}`,
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1850
1510
|
const userText = textFromBlocks(payload.contentBlocks);
|
|
1851
1511
|
this.addItem(conversation.id, {
|
|
1852
1512
|
id: payload.clientMessageId,
|
|
@@ -1863,11 +1523,10 @@ export class AgentWorkspaceProxy {
|
|
|
1863
1523
|
sessionId: conversation.agentSessionId,
|
|
1864
1524
|
content: payload.contentBlocks,
|
|
1865
1525
|
clientMessageId: payload.clientMessageId,
|
|
1866
|
-
model:
|
|
1867
|
-
reasoningEffort:
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
collaborationMode: conversation.collaborationMode,
|
|
1526
|
+
model: payload.model,
|
|
1527
|
+
reasoningEffort: payload.reasoningEffort,
|
|
1528
|
+
permissionMode: payload.permissionMode,
|
|
1529
|
+
collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
|
|
1871
1530
|
cwd: conversation.cwd,
|
|
1872
1531
|
});
|
|
1873
1532
|
const nextAgentSessionId = this.extractSessionId(result);
|
|
@@ -1947,7 +1606,6 @@ export class AgentWorkspaceProxy {
|
|
|
1947
1606
|
contentBlocks: [{ type: "text", text: rawText }],
|
|
1948
1607
|
model: conversation.model,
|
|
1949
1608
|
reasoningEffort: conversation.reasoningEffort,
|
|
1950
|
-
serviceTier: conversation.serviceTier,
|
|
1951
1609
|
permissionMode: conversation.permissionMode,
|
|
1952
1610
|
collaborationMode: conversation.collaborationMode,
|
|
1953
1611
|
});
|
|
@@ -1978,8 +1636,21 @@ export class AgentWorkspaceProxy {
|
|
|
1978
1636
|
this.emitStatus(conversation.id, conversation.status, `${providerLabel(conversation.provider)} · ${conversation.collaborationMode === "plan" ? "Plan mode" : "Default mode"} · ${conversation.cwd}`);
|
|
1979
1637
|
return;
|
|
1980
1638
|
}
|
|
1981
|
-
if (
|
|
1982
|
-
|
|
1639
|
+
if (conversation.provider !== "codex") {
|
|
1640
|
+
const message = `${command.title} 暂无 ${providerLabel(conversation.provider)} 原生实现。`;
|
|
1641
|
+
this.emitNotice({
|
|
1642
|
+
conversationId: conversation.id,
|
|
1643
|
+
kind: "native_unsupported",
|
|
1644
|
+
title: `${command.title} 不支持`,
|
|
1645
|
+
detail: `${providerLabel(conversation.provider)} 当前未实现这个命令。`,
|
|
1646
|
+
});
|
|
1647
|
+
this.addItem(conversation.id, {
|
|
1648
|
+
id: id("error"),
|
|
1649
|
+
conversationId: conversation.id,
|
|
1650
|
+
type: "error",
|
|
1651
|
+
error: message,
|
|
1652
|
+
createdAt: now,
|
|
1653
|
+
});
|
|
1983
1654
|
return;
|
|
1984
1655
|
}
|
|
1985
1656
|
if (command.name === "plan" || command.name === "exit-plan") {
|
|
@@ -1994,22 +1665,6 @@ export class AgentWorkspaceProxy {
|
|
|
1994
1665
|
: "已退出 Plan mode。");
|
|
1995
1666
|
return;
|
|
1996
1667
|
}
|
|
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
1668
|
if (command.name === "compact") {
|
|
2014
1669
|
if (!(client instanceof AcpClient))
|
|
2015
1670
|
throw new Error("当前 Codex runtime 不支持原生 compact。");
|
|
@@ -2030,7 +1685,7 @@ export class AgentWorkspaceProxy {
|
|
|
2030
1685
|
this.emitStatus(conversation.id, "idle", "上下文压缩完成。");
|
|
2031
1686
|
return;
|
|
2032
1687
|
}
|
|
2033
|
-
if (command.name === "clear"
|
|
1688
|
+
if (command.name === "clear") {
|
|
2034
1689
|
if (!client)
|
|
2035
1690
|
throw new Error("Agent provider 不在线。");
|
|
2036
1691
|
const result = await client.newSession({ cwd: conversation.cwd });
|
|
@@ -2048,6 +1703,21 @@ export class AgentWorkspaceProxy {
|
|
|
2048
1703
|
this.emitStatus(conversation.id, "idle", "上下文已重置,已创建新的 Codex thread。");
|
|
2049
1704
|
return;
|
|
2050
1705
|
}
|
|
1706
|
+
if (command.name === "review" || command.name === "subagents") {
|
|
1707
|
+
const prompt = command.name === "review"
|
|
1708
|
+
? args || "Review the current local changes."
|
|
1709
|
+
: args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
|
|
1710
|
+
await this.sendPrompt({
|
|
1711
|
+
conversationId: conversation.id,
|
|
1712
|
+
clientMessageId: id(command.name),
|
|
1713
|
+
contentBlocks: [{ type: "text", text: prompt }],
|
|
1714
|
+
model: conversation.model,
|
|
1715
|
+
reasoningEffort: conversation.reasoningEffort,
|
|
1716
|
+
permissionMode: conversation.permissionMode,
|
|
1717
|
+
collaborationMode: conversation.collaborationMode,
|
|
1718
|
+
});
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
2051
1721
|
throw new Error(`命令暂未实现:/${command.name}`);
|
|
2052
1722
|
}
|
|
2053
1723
|
catch (error) {
|
|
@@ -2062,41 +1732,6 @@ export class AgentWorkspaceProxy {
|
|
|
2062
1732
|
});
|
|
2063
1733
|
}
|
|
2064
1734
|
}
|
|
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
1735
|
handleRequest(method, params) {
|
|
2101
1736
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
2102
1737
|
return this.handleStructuredInput(params, true);
|
|
@@ -2130,11 +1765,19 @@ export class AgentWorkspaceProxy {
|
|
|
2130
1765
|
this.sendCapabilities();
|
|
2131
1766
|
}
|
|
2132
1767
|
}
|
|
1768
|
+
const initModel = firstString(asRecord(params), ["model", "currentModel"]);
|
|
1769
|
+
if (initModel && conversationId) {
|
|
1770
|
+
const conversation = this.conversations.get(conversationId);
|
|
1771
|
+
if (conversation && conversation.model !== initModel) {
|
|
1772
|
+
conversation.model = initModel;
|
|
1773
|
+
conversation.lastActivityAt = Date.now();
|
|
1774
|
+
this.emitConversation(conversation);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
2133
1777
|
return;
|
|
2134
1778
|
}
|
|
2135
1779
|
if (method.startsWith("account/") ||
|
|
2136
1780
|
method.startsWith("mcpServer/startupStatus/") ||
|
|
2137
|
-
method.startsWith("remoteControl/") ||
|
|
2138
1781
|
method === "thread/status/changed" ||
|
|
2139
1782
|
method === "thread/tokenUsage/updated" ||
|
|
2140
1783
|
method === "serverRequest/resolved" ||
|
|
@@ -2160,11 +1803,12 @@ export class AgentWorkspaceProxy {
|
|
|
2160
1803
|
this.conversationByAgentSessionId.set(agentSessionId, conversationId);
|
|
2161
1804
|
const conversation = this.conversations.get(conversationId);
|
|
2162
1805
|
if (conversation) {
|
|
2163
|
-
if (conversation.agentSessionId && conversation.agentSessionId !== agentSessionId) {
|
|
2164
|
-
this.conversationByAgentSessionId.delete(conversation.agentSessionId);
|
|
2165
|
-
}
|
|
2166
1806
|
conversation.agentSessionId = agentSessionId;
|
|
2167
1807
|
conversation.lastActivityAt = Date.now();
|
|
1808
|
+
const startedModel = firstString(asRecord(params), ["model", "currentModel"]);
|
|
1809
|
+
if (startedModel && conversation.model !== startedModel) {
|
|
1810
|
+
conversation.model = startedModel;
|
|
1811
|
+
}
|
|
2168
1812
|
this.emitConversation(conversation);
|
|
2169
1813
|
}
|
|
2170
1814
|
}
|
|
@@ -2741,7 +2385,7 @@ export class AgentWorkspaceProxy {
|
|
|
2741
2385
|
this.upsertItem(conversationId, item);
|
|
2742
2386
|
this.input.send(createEnvelope({
|
|
2743
2387
|
type: "agent.v2.permission.request",
|
|
2744
|
-
|
|
2388
|
+
sessionId: this.input.sessionId,
|
|
2745
2389
|
payload: { conversationId, ...permission, item },
|
|
2746
2390
|
}));
|
|
2747
2391
|
if (!waitForResponse)
|
|
@@ -2752,14 +2396,6 @@ export class AgentWorkspaceProxy {
|
|
|
2752
2396
|
this.permissionWaiters.delete(requestId);
|
|
2753
2397
|
this.permissionSources.delete(requestId);
|
|
2754
2398
|
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
2399
|
this.updateConversationStatus(conversationId, "idle");
|
|
2764
2400
|
}, PERMISSION_TIMEOUT_MS);
|
|
2765
2401
|
this.permissionWaiters.set(requestId, { resolve, timer });
|
|
@@ -2789,8 +2425,6 @@ export class AgentWorkspaceProxy {
|
|
|
2789
2425
|
this.markPermission(payload.conversationId, payload.requestId, {
|
|
2790
2426
|
permissionOutcome: payload.outcome,
|
|
2791
2427
|
optionId: selectedOptionId,
|
|
2792
|
-
permissionLive: false,
|
|
2793
|
-
permissionExpired: false,
|
|
2794
2428
|
permissionError: undefined,
|
|
2795
2429
|
permissionPending: false,
|
|
2796
2430
|
});
|
|
@@ -2837,209 +2471,6 @@ export class AgentWorkspaceProxy {
|
|
|
2837
2471
|
updatedAt: Date.now(),
|
|
2838
2472
|
});
|
|
2839
2473
|
}
|
|
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
2474
|
addItem(conversationId, item) {
|
|
3044
2475
|
this.rememberItemConversationId(conversationId, item);
|
|
3045
2476
|
const timeline = this.timelines.get(conversationId) ?? [];
|
|
@@ -3077,8 +2508,6 @@ export class AgentWorkspaceProxy {
|
|
|
3077
2508
|
...existing,
|
|
3078
2509
|
...toolCall,
|
|
3079
2510
|
id: targetToolId,
|
|
3080
|
-
input: toolCall.input ?? existing?.input,
|
|
3081
|
-
output: toolCall.output ?? existing?.output,
|
|
3082
2511
|
createdAt: existing?.createdAt ?? toolCall.createdAt ?? Date.now(),
|
|
3083
2512
|
};
|
|
3084
2513
|
this.toolConversationIds.set(toolCall.id, conversationId);
|
|
@@ -3147,7 +2576,6 @@ export class AgentWorkspaceProxy {
|
|
|
3147
2576
|
conversation.title = conversation.title ?? target.title;
|
|
3148
2577
|
conversation.model = conversation.model ?? target.model;
|
|
3149
2578
|
conversation.reasoningEffort = conversation.reasoningEffort ?? target.reasoningEffort;
|
|
3150
|
-
conversation.serviceTier = conversation.serviceTier ?? target.serviceTier;
|
|
3151
2579
|
conversation.permissionMode = conversation.permissionMode ?? target.permissionMode;
|
|
3152
2580
|
conversation.lastMessagePreview = conversation.lastMessagePreview ?? target.lastMessagePreview;
|
|
3153
2581
|
conversation.createdAt = Math.min(conversation.createdAt, target.createdAt);
|
|
@@ -3160,17 +2588,6 @@ export class AgentWorkspaceProxy {
|
|
|
3160
2588
|
this.timelines.set(newId, [...mergedTimeline.values()]
|
|
3161
2589
|
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3162
2590
|
.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
2591
|
for (const [agentSessionId, conversationId] of this.conversationByAgentSessionId) {
|
|
3175
2592
|
if (conversationId === oldId) {
|
|
3176
2593
|
this.conversationByAgentSessionId.set(agentSessionId, newId);
|
|
@@ -3204,47 +2621,17 @@ export class AgentWorkspaceProxy {
|
|
|
3204
2621
|
}
|
|
3205
2622
|
emitItem(conversationId, item) {
|
|
3206
2623
|
const conversation = this.conversations.get(conversationId);
|
|
3207
|
-
const itemSnapshot = snapshotTimelineItem(item, { stripImages: false });
|
|
3208
|
-
const event = this.recordRevisionEvent(conversationId, { conversation, item: itemSnapshot });
|
|
3209
2624
|
this.input.send(createEnvelope({
|
|
3210
2625
|
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
|
-
},
|
|
2626
|
+
sessionId: this.input.sessionId,
|
|
2627
|
+
payload: { conversationId, conversation, item },
|
|
3220
2628
|
}));
|
|
3221
2629
|
}
|
|
3222
2630
|
emitConversation(conversation) {
|
|
3223
|
-
const event = this.recordRevisionEvent(conversation.id, { conversation });
|
|
3224
2631
|
this.input.send(createEnvelope({
|
|
3225
2632
|
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
|
-
},
|
|
2633
|
+
sessionId: this.input.sessionId,
|
|
2634
|
+
payload: { conversationId: conversation.id, conversation },
|
|
3248
2635
|
}));
|
|
3249
2636
|
}
|
|
3250
2637
|
emitStatus(conversationId, status, text) {
|
|
@@ -3257,6 +2644,13 @@ export class AgentWorkspaceProxy {
|
|
|
3257
2644
|
createdAt: Date.now(),
|
|
3258
2645
|
});
|
|
3259
2646
|
}
|
|
2647
|
+
emitNotice(input) {
|
|
2648
|
+
this.input.send(createEnvelope({
|
|
2649
|
+
type: "agent.v2.notice",
|
|
2650
|
+
sessionId: this.input.sessionId,
|
|
2651
|
+
payload: input,
|
|
2652
|
+
}));
|
|
2653
|
+
}
|
|
3260
2654
|
updateConversationPreview(conversationId, text, status) {
|
|
3261
2655
|
const conversation = this.conversations.get(conversationId);
|
|
3262
2656
|
if (!conversation)
|
|
@@ -3278,169 +2672,20 @@ export class AgentWorkspaceProxy {
|
|
|
3278
2672
|
this.emitConversation(conversation);
|
|
3279
2673
|
}
|
|
3280
2674
|
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 };
|
|
2675
|
+
const conversations = [...this.conversations.values()];
|
|
2676
|
+
const items = conversationId
|
|
2677
|
+
? this.timelines.get(conversationId) ?? []
|
|
2678
|
+
: [...this.timelines.values()].flat();
|
|
3293
2679
|
this.input.send(createEnvelope({
|
|
3294
2680
|
type: "agent.v2.snapshot",
|
|
3295
|
-
|
|
2681
|
+
sessionId: this.input.sessionId,
|
|
3296
2682
|
payload: {
|
|
3297
2683
|
conversations,
|
|
3298
2684
|
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,
|
|
2685
|
+
items,
|
|
3305
2686
|
},
|
|
3306
2687
|
}));
|
|
3307
2688
|
}
|
|
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,
|
|
3411
|
-
},
|
|
3412
|
-
}));
|
|
3413
|
-
}
|
|
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
2689
|
conversationIdFromParams(params) {
|
|
3445
2690
|
const raw = asRecord(params);
|
|
3446
2691
|
if (!raw)
|
|
@@ -3500,9 +2745,6 @@ export class AgentWorkspaceProxy {
|
|
|
3500
2745
|
rememberTurnConversationId(conversationId, turnId) {
|
|
3501
2746
|
this.currentTurnIds.set(conversationId, turnId);
|
|
3502
2747
|
this.turnConversationIds.set(turnId, conversationId);
|
|
3503
|
-
const conversation = this.conversations.get(conversationId);
|
|
3504
|
-
if (conversation)
|
|
3505
|
-
conversation.runningTurnId = turnId;
|
|
3506
2748
|
}
|
|
3507
2749
|
forgetCurrentTurn(conversationId, turnId) {
|
|
3508
2750
|
const currentTurnId = this.currentTurnIds.get(conversationId);
|
|
@@ -3511,9 +2753,6 @@ export class AgentWorkspaceProxy {
|
|
|
3511
2753
|
this.turnConversationIds.delete(turnId);
|
|
3512
2754
|
if (currentTurnId && currentTurnId !== turnId)
|
|
3513
2755
|
this.turnConversationIds.delete(currentTurnId);
|
|
3514
|
-
const conversation = this.conversations.get(conversationId);
|
|
3515
|
-
if (conversation)
|
|
3516
|
-
conversation.runningTurnId = undefined;
|
|
3517
2756
|
}
|
|
3518
2757
|
rememberItemConversationId(conversationId, item) {
|
|
3519
2758
|
const keys = [
|
|
@@ -3553,51 +2792,28 @@ export class AgentWorkspaceProxy {
|
|
|
3553
2792
|
}
|
|
3554
2793
|
cancelPendingPermissions(conversationId) {
|
|
3555
2794
|
for (const [requestId, waiter] of this.permissionWaiters) {
|
|
3556
|
-
const ownerConversationId = this.conversationIdForPermissionRequest(requestId);
|
|
3557
|
-
if (conversationId && ownerConversationId && ownerConversationId !== conversationId)
|
|
3558
|
-
continue;
|
|
3559
2795
|
clearTimeout(waiter.timer);
|
|
3560
2796
|
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
2797
|
this.pendingPermissions.delete(requestId);
|
|
3573
2798
|
this.permissionSources.delete(requestId);
|
|
3574
2799
|
}
|
|
2800
|
+
this.permissionWaiters.clear();
|
|
3575
2801
|
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
2802
|
clearTimeout(waiter.timer);
|
|
3580
2803
|
waiter.resolve(formatStructuredInputResponse({}));
|
|
2804
|
+
const pending = this.pendingStructuredInputs.get(requestId);
|
|
3581
2805
|
if (pending) {
|
|
3582
2806
|
this.markStructuredInput(pending.conversationId, requestId, {
|
|
3583
2807
|
inputPending: false,
|
|
3584
2808
|
inputError: "已停止",
|
|
3585
2809
|
});
|
|
3586
2810
|
}
|
|
3587
|
-
this.structuredInputWaiters.delete(requestId);
|
|
3588
2811
|
this.pendingStructuredInputs.delete(requestId);
|
|
3589
2812
|
}
|
|
2813
|
+
this.structuredInputWaiters.clear();
|
|
3590
2814
|
if (conversationId)
|
|
3591
2815
|
this.updateConversationStatus(conversationId, "idle");
|
|
3592
2816
|
}
|
|
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
2817
|
extractSessionId(value) {
|
|
3602
2818
|
const raw = asRecord(value);
|
|
3603
2819
|
if (!raw)
|