linkshell-cli 0.3.27 → 0.3.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/src/commands/setup.js +20 -2
- package/dist/cli/src/commands/setup.js.map +1 -1
- package/dist/cli/src/commands/upgrade.js +9 -24
- package/dist/cli/src/commands/upgrade.js.map +1 -1
- package/dist/cli/src/index.js +28 -26
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/providers.d.ts +3 -7
- package/dist/cli/src/providers.js +76 -19
- package/dist/cli/src/providers.js.map +1 -1
- package/dist/cli/src/runtime/acp/acp-client.d.ts +0 -10
- package/dist/cli/src/runtime/acp/acp-client.js +19 -146
- package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-session.d.ts +62 -0
- package/dist/cli/src/runtime/acp/agent-session.js +1075 -0
- package/dist/cli/src/runtime/acp/agent-session.js.map +1 -0
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +1 -24
- package/dist/cli/src/runtime/acp/agent-workspace.js +162 -981
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +1 -3
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +35 -81
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +1 -2
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js +35 -54
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/codex-rpc-bridge.js +12 -1
- package/dist/cli/src/runtime/acp/codex-rpc-bridge.js.map +1 -1
- package/dist/cli/src/runtime/acp/json-rpc.d.ts +1 -34
- package/dist/cli/src/runtime/acp/json-rpc.js +8 -32
- package/dist/cli/src/runtime/acp/json-rpc.js.map +1 -1
- package/dist/cli/src/runtime/acp/provider-resolver.d.ts +0 -3
- package/dist/cli/src/runtime/acp/provider-resolver.js +6 -26
- package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
- package/dist/cli/src/runtime/bridge-session.d.ts +31 -1
- package/dist/cli/src/runtime/bridge-session.js +991 -81
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/src/runtime/screen-fallback.d.ts +1 -1
- package/dist/cli/src/runtime/screen-fallback.js +4 -4
- package/dist/cli/src/runtime/screen-fallback.js.map +1 -1
- package/dist/cli/src/runtime/screen-share.d.ts +1 -1
- package/dist/cli/src/runtime/screen-share.js +7 -7
- package/dist/cli/src/runtime/screen-share.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +11940 -3423
- package/dist/shared-protocol/src/index.js +98 -164
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +5 -5
- package/src/commands/setup.ts +31 -5
- package/src/commands/upgrade.ts +8 -28
- package/src/index.ts +34 -29
- package/src/providers.ts +108 -26
- package/src/runtime/acp/acp-client.ts +21 -159
- package/src/runtime/acp/agent-session.ts +1180 -0
- package/src/runtime/acp/agent-workspace.ts +170 -1068
- package/src/runtime/acp/claude-sdk-client.ts +37 -85
- package/src/runtime/acp/claude-stream-json-client.ts +41 -59
- package/src/runtime/acp/json-rpc.ts +14 -46
- package/src/runtime/acp/provider-resolver.ts +6 -29
- package/src/runtime/bridge-session.ts +1092 -84
- package/src/runtime/screen-fallback.ts +5 -5
- package/src/runtime/screen-share.ts +8 -8
- package/src/runtime/acp/claude-sessions.ts +0 -563
- package/src/runtime/acp/codex-rpc-bridge.ts +0 -221
- package/src/runtime/acp/codex-sessions.ts +0 -802
- package/src/types/linkshell-gateway.d.ts +0 -18
|
@@ -1,5 +1,4 @@
|
|
|
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 {
|
|
@@ -10,9 +9,6 @@ import {
|
|
|
10
9
|
import { AcpClient } from "./acp-client.js";
|
|
11
10
|
import { ClaudeSdkClient } from "./claude-sdk-client.js";
|
|
12
11
|
import { ClaudeStreamJsonClient } from "./claude-stream-json-client.js";
|
|
13
|
-
import { CodexRpcBridge } from "./codex-rpc-bridge.js";
|
|
14
|
-
import { listClaudeStoredSessions, loadClaudeStoredTimeline } from "./claude-sessions.js";
|
|
15
|
-
import { loadCodexStoredTimeline } from "./codex-sessions.js";
|
|
16
12
|
import type { AgentProtocol, AgentProvider } from "./provider-resolver.js";
|
|
17
13
|
import { resolveAgentCommand } from "./provider-resolver.js";
|
|
18
14
|
|
|
@@ -21,15 +17,12 @@ type AgentPermissionMode = "read_only" | "workspace_write" | "full_access";
|
|
|
21
17
|
type AgentCollaborationMode = "default" | "plan";
|
|
22
18
|
type AgentCommandExecutionKind = "prompt" | "native" | "local_ui";
|
|
23
19
|
type AgentCommandSource = "built_in" | "custom" | "project" | "user" | "linkshell";
|
|
24
|
-
type AgentSyncSource = "device" | "device-history" | "device-live" | "app-server" | "cache";
|
|
25
20
|
|
|
26
21
|
interface AgentContentBlock {
|
|
27
|
-
type: "text" | "image"
|
|
22
|
+
type: "text" | "image";
|
|
28
23
|
text?: string;
|
|
29
24
|
data?: string;
|
|
30
25
|
mimeType?: string;
|
|
31
|
-
path?: string;
|
|
32
|
-
name?: string;
|
|
33
26
|
}
|
|
34
27
|
|
|
35
28
|
interface AgentToolCall {
|
|
@@ -165,16 +158,10 @@ interface AgentConversation {
|
|
|
165
158
|
title?: string;
|
|
166
159
|
model?: string;
|
|
167
160
|
reasoningEffort?: string;
|
|
168
|
-
serviceTier?: string;
|
|
169
161
|
permissionMode?: AgentPermissionMode;
|
|
170
162
|
collaborationMode?: AgentCollaborationMode;
|
|
171
163
|
status: AgentStatus;
|
|
172
164
|
archived: boolean;
|
|
173
|
-
timelineRevision?: number;
|
|
174
|
-
historyComplete?: boolean;
|
|
175
|
-
runningTurnId?: string;
|
|
176
|
-
source?: AgentSyncSource;
|
|
177
|
-
canonical?: boolean;
|
|
178
165
|
lastMessagePreview?: string;
|
|
179
166
|
lastActivityAt: number;
|
|
180
167
|
createdAt: number;
|
|
@@ -185,9 +172,6 @@ interface AgentTimelineItem {
|
|
|
185
172
|
conversationId: string;
|
|
186
173
|
type: "message" | "tool_call" | "plan" | "permission" | "status" | "error";
|
|
187
174
|
kind?: AgentTimelineKind;
|
|
188
|
-
revision?: number;
|
|
189
|
-
source?: AgentSyncSource;
|
|
190
|
-
canonical?: boolean;
|
|
191
175
|
turnId?: string;
|
|
192
176
|
itemId?: string;
|
|
193
177
|
role?: "user" | "assistant" | "system";
|
|
@@ -219,51 +203,13 @@ interface PendingStructuredInputWaiter {
|
|
|
219
203
|
source?: string;
|
|
220
204
|
}
|
|
221
205
|
|
|
222
|
-
interface AgentRevisionEvent {
|
|
223
|
-
revision: number;
|
|
224
|
-
item?: AgentTimelineItem;
|
|
225
|
-
conversation?: AgentConversation;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
206
|
const PERMISSION_TIMEOUT_MS = 5 * 60_000;
|
|
229
207
|
const MAX_TIMELINE_ITEMS = 200;
|
|
230
|
-
const MAX_SNAPSHOT_ITEMS = 80;
|
|
231
|
-
const MAX_SNAPSHOT_TEXT_BYTES = 128 * 1024;
|
|
232
|
-
const MAX_DELTA_EVENTS = 500;
|
|
233
|
-
const HISTORY_PAGE_MAX_ITEMS = 500;
|
|
234
208
|
|
|
235
209
|
function id(prefix: string): string {
|
|
236
210
|
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
237
211
|
}
|
|
238
212
|
|
|
239
|
-
function clampHistoryCursor(value: string | undefined, fallback: number, max: number): number {
|
|
240
|
-
if (!value) return fallback;
|
|
241
|
-
const parsed = Number.parseInt(value, 10);
|
|
242
|
-
if (!Number.isFinite(parsed)) return fallback;
|
|
243
|
-
return Math.max(0, Math.min(max, parsed));
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function appServerText(value: unknown): string | undefined {
|
|
247
|
-
if (typeof value === "string") {
|
|
248
|
-
const text = value.trim();
|
|
249
|
-
return text || undefined;
|
|
250
|
-
}
|
|
251
|
-
if (Array.isArray(value)) {
|
|
252
|
-
const text = value
|
|
253
|
-
.map((part) => appServerText(part))
|
|
254
|
-
.filter(Boolean)
|
|
255
|
-
.join("\n")
|
|
256
|
-
.trim();
|
|
257
|
-
return text || undefined;
|
|
258
|
-
}
|
|
259
|
-
const record = asRecord(value);
|
|
260
|
-
if (!record) return undefined;
|
|
261
|
-
if (typeof record.text === "string") return appServerText(record.text);
|
|
262
|
-
if (typeof record.content === "string") return appServerText(record.content);
|
|
263
|
-
if (typeof record.message === "string") return appServerText(record.message);
|
|
264
|
-
return appServerText(record.content ?? record.message ?? record.parts);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
213
|
function stringify(value: unknown): string {
|
|
268
214
|
if (typeof value === "string") return value;
|
|
269
215
|
try {
|
|
@@ -273,71 +219,6 @@ function stringify(value: unknown): string {
|
|
|
273
219
|
}
|
|
274
220
|
}
|
|
275
221
|
|
|
276
|
-
function truncateUtf8(value: string | undefined, maxBytes = MAX_SNAPSHOT_TEXT_BYTES): string | undefined {
|
|
277
|
-
if (!value) return value;
|
|
278
|
-
if (Buffer.byteLength(value, "utf8") <= maxBytes) return value;
|
|
279
|
-
let end = Math.min(value.length, maxBytes);
|
|
280
|
-
while (end > 0 && Buffer.byteLength(value.slice(0, end), "utf8") > maxBytes) {
|
|
281
|
-
end = Math.floor(end * 0.9);
|
|
282
|
-
}
|
|
283
|
-
return `${value.slice(0, end)}\n\n[truncated by LinkShell: original ${Buffer.byteLength(value, "utf8")} bytes]`;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function snapshotContentBlocks(
|
|
287
|
-
blocks: AgentContentBlock[] | undefined,
|
|
288
|
-
options: { stripImages?: boolean } = {},
|
|
289
|
-
): AgentContentBlock[] | undefined {
|
|
290
|
-
if (!blocks) return undefined;
|
|
291
|
-
return blocks.map((block) =>
|
|
292
|
-
block.type === "image" && options.stripImages !== false
|
|
293
|
-
? { ...block, data: undefined, text: block.text || "图片附件" }
|
|
294
|
-
: { ...block, text: truncateUtf8(block.text) },
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function snapshotTimelineItem(
|
|
299
|
-
item: AgentTimelineItem,
|
|
300
|
-
options: { stripImages?: boolean } = {},
|
|
301
|
-
): AgentTimelineItem {
|
|
302
|
-
return {
|
|
303
|
-
...item,
|
|
304
|
-
content: snapshotContentBlocks(item.content, options),
|
|
305
|
-
text: truncateUtf8(item.text),
|
|
306
|
-
toolCall: item.toolCall
|
|
307
|
-
? {
|
|
308
|
-
...item.toolCall,
|
|
309
|
-
input: truncateUtf8(item.toolCall.input),
|
|
310
|
-
output: truncateUtf8(item.toolCall.output),
|
|
311
|
-
}
|
|
312
|
-
: undefined,
|
|
313
|
-
commandExecution: item.commandExecution
|
|
314
|
-
? {
|
|
315
|
-
...item.commandExecution,
|
|
316
|
-
command: truncateUtf8(item.commandExecution.command, 16 * 1024),
|
|
317
|
-
output: truncateUtf8(item.commandExecution.output),
|
|
318
|
-
}
|
|
319
|
-
: undefined,
|
|
320
|
-
fileChange: item.fileChange
|
|
321
|
-
? {
|
|
322
|
-
...item.fileChange,
|
|
323
|
-
diff: truncateUtf8(item.fileChange.diff),
|
|
324
|
-
summary: truncateUtf8(item.fileChange.summary),
|
|
325
|
-
}
|
|
326
|
-
: undefined,
|
|
327
|
-
permission: item.permission
|
|
328
|
-
? {
|
|
329
|
-
...item.permission,
|
|
330
|
-
toolInput: truncateUtf8(item.permission.toolInput),
|
|
331
|
-
context: truncateUtf8(item.permission.context),
|
|
332
|
-
}
|
|
333
|
-
: undefined,
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function snapshotTimelineItems(items: AgentTimelineItem[]): AgentTimelineItem[] {
|
|
338
|
-
return items.slice(-MAX_SNAPSHOT_ITEMS).map((item) => snapshotTimelineItem(item));
|
|
339
|
-
}
|
|
340
|
-
|
|
341
222
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
342
223
|
return typeof value === "object" && value ? value as Record<string, unknown> : undefined;
|
|
343
224
|
}
|
|
@@ -643,12 +524,7 @@ function titleFromCwd(cwd: string): string {
|
|
|
643
524
|
|
|
644
525
|
function textFromBlocks(blocks: AgentContentBlock[]): string {
|
|
645
526
|
return blocks
|
|
646
|
-
.map((block) => {
|
|
647
|
-
if (block.type === "text") return block.text ?? "";
|
|
648
|
-
if (block.type === "image") return `[${block.mimeType ?? "image"} attachment]`;
|
|
649
|
-
if (block.type === "file" && block.path) return `@${block.path}`;
|
|
650
|
-
return "";
|
|
651
|
-
})
|
|
527
|
+
.map((block) => block.type === "text" ? block.text ?? "" : `[${block.mimeType ?? "image"} attachment]`)
|
|
652
528
|
.filter(Boolean)
|
|
653
529
|
.join("\n");
|
|
654
530
|
}
|
|
@@ -681,11 +557,6 @@ function contentBlocksFromValue(value: unknown): AgentContentBlock[] {
|
|
|
681
557
|
const text = firstString(raw, ["text", "content", "message"]);
|
|
682
558
|
return text ? [{ type: "text", text }] : [];
|
|
683
559
|
}
|
|
684
|
-
if (normalizedType === "file" || normalizedType === "inputfile" || normalizedType === "filereference") {
|
|
685
|
-
const path = firstString(raw, ["path", "file", "filePath", "file_path", "absolutePath", "relativePath"]);
|
|
686
|
-
const text = firstString(raw, ["text", "name", "label"]);
|
|
687
|
-
return path ? [{ type: "file", path, name: text, text: text ?? `@${path}` }] : [];
|
|
688
|
-
}
|
|
689
560
|
const nested = raw.content ?? raw.contentItems ?? raw.parts;
|
|
690
561
|
if (Array.isArray(nested)) return contentBlocksFromValue(nested);
|
|
691
562
|
const text = firstString(raw, ["text", "message", "content"]);
|
|
@@ -881,54 +752,20 @@ function providerLabel(provider: AgentProvider): string {
|
|
|
881
752
|
interface AgentModelOption {
|
|
882
753
|
id: string;
|
|
883
754
|
label: string;
|
|
884
|
-
reasoningEfforts?: string[];
|
|
885
|
-
defaultReasoningEffort?: string;
|
|
886
|
-
speedTiers?: string[];
|
|
887
|
-
supportsImages?: boolean;
|
|
888
|
-
description?: string;
|
|
889
755
|
}
|
|
890
756
|
|
|
891
757
|
interface ProviderRuntimeCapabilities {
|
|
892
758
|
models?: AgentModelOption[];
|
|
893
759
|
defaultModel?: string;
|
|
894
760
|
reasoningEfforts?: string[];
|
|
895
|
-
speedTiers?: string[];
|
|
896
|
-
defaultServiceTier?: string;
|
|
897
761
|
commands?: AgentCommandDescriptor[];
|
|
898
762
|
modes?: AgentModeDescriptor[];
|
|
899
763
|
currentMode?: string;
|
|
900
764
|
}
|
|
901
765
|
|
|
902
766
|
const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
903
|
-
const
|
|
904
|
-
const
|
|
905
|
-
const AGENT_PERMISSION_MODES: AgentPermissionMode[] = ["read_only", "workspace_write", "full_access"];
|
|
906
|
-
const COMMAND_OUTPUT_MAX_BYTES = 96 * 1024;
|
|
907
|
-
const LINKSHELL_NATIVE_COMMANDS: Array<{
|
|
908
|
-
name: string;
|
|
909
|
-
description: string;
|
|
910
|
-
category: string;
|
|
911
|
-
argsMode?: AgentCommandDescriptor["argsMode"];
|
|
912
|
-
destructive?: boolean;
|
|
913
|
-
providers?: AgentProvider[];
|
|
914
|
-
}> = [
|
|
915
|
-
{ name: "status", description: "Show current Agent and workspace status", category: "LinkShell", argsMode: "none" },
|
|
916
|
-
{ name: "plan", description: "Enter Plan mode for the next turn", category: "Agent", argsMode: "none" },
|
|
917
|
-
{ name: "exit-plan", description: "Exit Plan mode", category: "Agent", argsMode: "none" },
|
|
918
|
-
{ name: "review", description: "Ask the Agent to review current local changes", category: "Agent", argsMode: "optional" },
|
|
919
|
-
{ name: "subagents", description: "Ask the Agent to split work across subagents when useful", category: "Agent", argsMode: "optional" },
|
|
920
|
-
{ name: "compact", description: "Compact the active Codex context", category: "Codex", argsMode: "none", providers: ["codex"] },
|
|
921
|
-
{ name: "new", description: "Start a fresh Codex thread", category: "Codex", argsMode: "none", providers: ["codex"], destructive: true },
|
|
922
|
-
{ name: "clear", description: "Start a fresh Agent context for this conversation", category: "Agent", argsMode: "none", destructive: true },
|
|
923
|
-
{ name: "git-status", description: "Show branch and working tree status", category: "Git", argsMode: "none" },
|
|
924
|
-
{ name: "git-diff", description: "Show a compact diffstat for current changes", category: "Git", argsMode: "none" },
|
|
925
|
-
{ name: "git-commit", description: "Commit staged changes with the given message", category: "Git", argsMode: "required" },
|
|
926
|
-
{ name: "git-pull", description: "Pull with fast-forward only", category: "Git", argsMode: "none" },
|
|
927
|
-
{ name: "git-push", description: "Push the current branch", category: "Git", argsMode: "none" },
|
|
928
|
-
{ name: "git-stash", description: "Stash current working tree changes", category: "Git", argsMode: "optional" },
|
|
929
|
-
{ name: "git-stash-pop", description: "Pop the latest stash", category: "Git", argsMode: "none" },
|
|
930
|
-
];
|
|
931
|
-
const CODEX_JSON_RPC_COMMANDS = new Set(["plan", "exit-plan", "review", "compact", "new"]);
|
|
767
|
+
const ALL_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"] as const;
|
|
768
|
+
const CODEX_COMMAND_NAMES = ["plan", "exit-plan", "compact", "clear", "status", "review", "subagents"] as const;
|
|
932
769
|
const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
|
|
933
770
|
"add-dir",
|
|
934
771
|
"agents",
|
|
@@ -1201,31 +1038,57 @@ function customClaudeCommands(cwd: string): AgentCommandDescriptor[] {
|
|
|
1201
1038
|
|
|
1202
1039
|
function defaultProviderCommands(provider: AgentProvider, cwd: string, enabled: boolean): AgentCommandDescriptor[] {
|
|
1203
1040
|
const disabledReason = enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`;
|
|
1204
|
-
|
|
1205
|
-
.
|
|
1206
|
-
.filter((command) => !command.providers || command.providers.includes(provider))
|
|
1207
|
-
.map((command) => makeCommand({
|
|
1041
|
+
if (provider === "codex") {
|
|
1042
|
+
return CODEX_COMMAND_NAMES.map((name) => makeCommand({
|
|
1208
1043
|
provider,
|
|
1209
|
-
name
|
|
1210
|
-
description: command.description,
|
|
1044
|
+
name,
|
|
1211
1045
|
source: "linkshell",
|
|
1212
|
-
category:
|
|
1213
|
-
|
|
1214
|
-
|
|
1046
|
+
category: name === "plan" || name === "exit-plan" ? "Modes" : "Codex",
|
|
1047
|
+
description: {
|
|
1048
|
+
"plan": "Enter Codex plan mode",
|
|
1049
|
+
"exit-plan": "Exit Codex plan mode",
|
|
1050
|
+
compact: "Compact the current thread",
|
|
1051
|
+
clear: "Start a fresh Codex thread",
|
|
1052
|
+
status: "Show LinkShell agent status",
|
|
1053
|
+
review: "Ask Codex to review local changes",
|
|
1054
|
+
subagents: "Insert a delegation prompt",
|
|
1055
|
+
}[name],
|
|
1056
|
+
argsMode: name === "review" || name === "subagents" ? "optional" : "none",
|
|
1057
|
+
destructive: name === "clear",
|
|
1215
1058
|
disabledReason,
|
|
1216
|
-
executionKind: "native",
|
|
1059
|
+
executionKind: name === "review" || name === "subagents" ? "prompt" : "native",
|
|
1217
1060
|
}));
|
|
1218
|
-
if (provider === "codex") {
|
|
1219
|
-
return linkshellCommands;
|
|
1220
1061
|
}
|
|
1221
1062
|
if (provider === "claude") {
|
|
1063
|
+
const builtIns = CLAUDE_BUILT_IN_COMMANDS
|
|
1064
|
+
.filter((entry) => isClaudeRemoteFriendlyCommand(entry.name))
|
|
1065
|
+
.map((entry) => makeCommand({
|
|
1066
|
+
provider,
|
|
1067
|
+
name: entry.name,
|
|
1068
|
+
description: entry.description,
|
|
1069
|
+
argsMode: entry.argsMode,
|
|
1070
|
+
destructive: entry.destructive,
|
|
1071
|
+
disabledReason,
|
|
1072
|
+
executionKind: "prompt",
|
|
1073
|
+
}));
|
|
1222
1074
|
const custom = customClaudeCommands(cwd).map((command) => ({
|
|
1223
1075
|
...command,
|
|
1224
1076
|
disabledReason: command.disabledReason ?? disabledReason,
|
|
1225
1077
|
}));
|
|
1226
|
-
return [...
|
|
1078
|
+
return [...builtIns, ...custom];
|
|
1227
1079
|
}
|
|
1228
|
-
return
|
|
1080
|
+
return [
|
|
1081
|
+
makeCommand({
|
|
1082
|
+
provider,
|
|
1083
|
+
name: "status",
|
|
1084
|
+
source: "linkshell",
|
|
1085
|
+
category: "LinkShell",
|
|
1086
|
+
description: "Show LinkShell agent status",
|
|
1087
|
+
argsMode: "none",
|
|
1088
|
+
disabledReason,
|
|
1089
|
+
executionKind: "native",
|
|
1090
|
+
}),
|
|
1091
|
+
];
|
|
1229
1092
|
}
|
|
1230
1093
|
|
|
1231
1094
|
function mergeCommands(...groups: Array<AgentCommandDescriptor[] | undefined>): AgentCommandDescriptor[] {
|
|
@@ -1244,81 +1107,6 @@ function mergeCommands(...groups: Array<AgentCommandDescriptor[] | undefined>):
|
|
|
1244
1107
|
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
1245
1108
|
}
|
|
1246
1109
|
|
|
1247
|
-
function isGitNativeCommand(name: string): boolean {
|
|
1248
|
-
return name === "git-status" ||
|
|
1249
|
-
name === "git-diff" ||
|
|
1250
|
-
name === "git-commit" ||
|
|
1251
|
-
name === "git-pull" ||
|
|
1252
|
-
name === "git-push" ||
|
|
1253
|
-
name === "git-stash" ||
|
|
1254
|
-
name === "git-stash-pop";
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
function gitCommandArgs(name: string, args?: string): { display: string; argv: string[] } {
|
|
1258
|
-
const message = args?.trim();
|
|
1259
|
-
switch (name) {
|
|
1260
|
-
case "git-status":
|
|
1261
|
-
return { display: "git status --short --branch", argv: ["status", "--short", "--branch"] };
|
|
1262
|
-
case "git-diff":
|
|
1263
|
-
return { display: "git diff --stat", argv: ["diff", "--stat"] };
|
|
1264
|
-
case "git-commit":
|
|
1265
|
-
if (!message) throw new Error("请先输入提交信息,例如 /git-commit fix mobile agent timeline");
|
|
1266
|
-
return { display: `git commit -m ${JSON.stringify(message)}`, argv: ["commit", "-m", message] };
|
|
1267
|
-
case "git-pull":
|
|
1268
|
-
return { display: "git pull --ff-only", argv: ["pull", "--ff-only"] };
|
|
1269
|
-
case "git-push":
|
|
1270
|
-
return { display: "git push", argv: ["push"] };
|
|
1271
|
-
case "git-stash":
|
|
1272
|
-
return {
|
|
1273
|
-
display: "git stash push -u",
|
|
1274
|
-
argv: ["stash", "push", "-u", "-m", message || "LinkShell mobile stash"],
|
|
1275
|
-
};
|
|
1276
|
-
case "git-stash-pop":
|
|
1277
|
-
return { display: "git stash pop", argv: ["stash", "pop"] };
|
|
1278
|
-
default:
|
|
1279
|
-
throw new Error(`未知 Git 命令:/${name}`);
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
function runProcess(
|
|
1284
|
-
command: string,
|
|
1285
|
-
args: string[],
|
|
1286
|
-
options: { cwd: string; maxBytes?: number },
|
|
1287
|
-
): Promise<{ output: string; exitCode: number | null; signal: NodeJS.Signals | null }> {
|
|
1288
|
-
return new Promise((resolve, reject) => {
|
|
1289
|
-
const child = spawn(command, args, {
|
|
1290
|
-
cwd: options.cwd,
|
|
1291
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1292
|
-
windowsHide: true,
|
|
1293
|
-
});
|
|
1294
|
-
const maxBytes = options.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES;
|
|
1295
|
-
let output = "";
|
|
1296
|
-
let bytes = 0;
|
|
1297
|
-
let truncated = false;
|
|
1298
|
-
const append = (chunk: Buffer) => {
|
|
1299
|
-
if (bytes >= maxBytes) {
|
|
1300
|
-
truncated = true;
|
|
1301
|
-
return;
|
|
1302
|
-
}
|
|
1303
|
-
const remaining = maxBytes - bytes;
|
|
1304
|
-
const slice = chunk.byteLength > remaining ? chunk.subarray(0, remaining) : chunk;
|
|
1305
|
-
output += slice.toString("utf8");
|
|
1306
|
-
bytes += slice.byteLength;
|
|
1307
|
-
if (slice.byteLength < chunk.byteLength) truncated = true;
|
|
1308
|
-
};
|
|
1309
|
-
child.stdout.on("data", append);
|
|
1310
|
-
child.stderr.on("data", append);
|
|
1311
|
-
child.once("error", reject);
|
|
1312
|
-
child.once("close", (exitCode, signal) => {
|
|
1313
|
-
resolve({
|
|
1314
|
-
output: `${output.trimEnd()}${truncated ? "\n\n[truncated by LinkShell]" : ""}`,
|
|
1315
|
-
exitCode,
|
|
1316
|
-
signal,
|
|
1317
|
-
});
|
|
1318
|
-
});
|
|
1319
|
-
});
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
1110
|
function runtimeCommands(provider: AgentProvider, value: unknown): AgentCommandDescriptor[] {
|
|
1323
1111
|
const raw = asRecord(value);
|
|
1324
1112
|
const commandsValue =
|
|
@@ -1362,14 +1150,10 @@ function parseModelListCapabilities(value: unknown): ProviderRuntimeCapabilities
|
|
|
1362
1150
|
const raw = asRecord(value);
|
|
1363
1151
|
const modelsValue =
|
|
1364
1152
|
Array.isArray(value) ? value :
|
|
1365
|
-
Array.isArray(raw?.data) ? raw.data :
|
|
1366
1153
|
Array.isArray(raw?.models) ? raw.models :
|
|
1367
1154
|
Array.isArray(raw?.items) ? raw.items :
|
|
1368
1155
|
Array.isArray(raw?.modelOptions) ? raw.modelOptions :
|
|
1369
1156
|
[];
|
|
1370
|
-
const effortSet = new Set<string>();
|
|
1371
|
-
const speedTierSet = new Set<string>(["standard"]);
|
|
1372
|
-
let defaultModelFromEntry: string | undefined;
|
|
1373
1157
|
const models = modelsValue
|
|
1374
1158
|
.map((entry, index) => {
|
|
1375
1159
|
const model = asRecord(entry);
|
|
@@ -1379,40 +1163,13 @@ function parseModelListCapabilities(value: unknown): ProviderRuntimeCapabilities
|
|
|
1379
1163
|
: undefined;
|
|
1380
1164
|
}
|
|
1381
1165
|
const modelId = firstString(model, ["id", "model", "name", "value"]) ?? `model-${index + 1}`;
|
|
1382
|
-
const label = firstString(model, ["
|
|
1383
|
-
|
|
1384
|
-
const modelEfforts = arrayFromKeys(model, ["supportedReasoningEfforts", "reasoningEfforts"])
|
|
1385
|
-
.map((effort) => {
|
|
1386
|
-
const rawEffort = asRecord(effort);
|
|
1387
|
-
return firstString(rawEffort, ["reasoningEffort", "id", "value"]) ?? (typeof effort === "string" ? effort : undefined);
|
|
1388
|
-
})
|
|
1389
|
-
.filter((effort): effort is string => Boolean(effort) && ALL_REASONING_EFFORTS.includes(effort as typeof ALL_REASONING_EFFORTS[number]));
|
|
1390
|
-
modelEfforts.forEach((effort) => effortSet.add(effort));
|
|
1391
|
-
const defaultReasoningEffort = firstString(model, ["defaultReasoningEffort", "default_reasoning_effort"]);
|
|
1392
|
-
if (defaultReasoningEffort && ALL_REASONING_EFFORTS.includes(defaultReasoningEffort as typeof ALL_REASONING_EFFORTS[number])) {
|
|
1393
|
-
effortSet.add(defaultReasoningEffort);
|
|
1394
|
-
}
|
|
1395
|
-
const modalities = arrayFromKeys(model, ["inputModalities", "modalities"]);
|
|
1396
|
-
const speedTiers = arrayFromKeys(model, ["additionalSpeedTiers", "additional_speed_tiers", "serviceTiers", "service_tiers"])
|
|
1397
|
-
.map((tier) => typeof tier === "string" ? tier : firstString(asRecord(tier), ["id", "tier", "value"]))
|
|
1398
|
-
.filter((tier): tier is string => Boolean(tier) && ALL_SERVICE_TIERS.includes(tier as typeof ALL_SERVICE_TIERS[number]));
|
|
1399
|
-
speedTiers.forEach((tier) => speedTierSet.add(tier));
|
|
1400
|
-
if (model.isDefault === true) defaultModelFromEntry = modelId;
|
|
1401
|
-
return {
|
|
1402
|
-
id: modelId,
|
|
1403
|
-
label,
|
|
1404
|
-
...(modelEfforts.length > 0 ? { reasoningEfforts: modelEfforts } : {}),
|
|
1405
|
-
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
1406
|
-
...(speedTiers.length > 0 ? { speedTiers: ["standard", ...speedTiers.filter((tier) => tier !== "standard")] } : {}),
|
|
1407
|
-
...(modalities.length > 0 ? { supportsImages: modalities.includes("image") } : {}),
|
|
1408
|
-
...(firstString(model, ["description"]) ? { description: firstString(model, ["description"]) } : {}),
|
|
1409
|
-
};
|
|
1166
|
+
const label = firstString(model, ["label", "title", "displayName", "name"]) ?? modelId;
|
|
1167
|
+
return { id: modelId, label };
|
|
1410
1168
|
})
|
|
1411
1169
|
.filter((entry): entry is AgentModelOption => Boolean(entry));
|
|
1412
1170
|
const defaultModel =
|
|
1413
1171
|
firstString(raw, ["defaultModel", "default_model", "currentModel"]) ??
|
|
1414
|
-
firstString(asRecord(raw?.defaults), ["model"])
|
|
1415
|
-
defaultModelFromEntry;
|
|
1172
|
+
firstString(asRecord(raw?.defaults), ["model"]);
|
|
1416
1173
|
const effortsValue =
|
|
1417
1174
|
Array.isArray(raw?.reasoningEfforts) ? raw.reasoningEfforts :
|
|
1418
1175
|
Array.isArray(raw?.reasoning_efforts) ? raw.reasoning_efforts :
|
|
@@ -1420,14 +1177,11 @@ function parseModelListCapabilities(value: unknown): ProviderRuntimeCapabilities
|
|
|
1420
1177
|
undefined;
|
|
1421
1178
|
const reasoningEfforts = effortsValue
|
|
1422
1179
|
?.filter((entry): entry is string => typeof entry === "string" && ALL_REASONING_EFFORTS.includes(entry as typeof ALL_REASONING_EFFORTS[number]));
|
|
1423
|
-
|
|
1424
|
-
const resolvedSpeedTiers = [...speedTierSet];
|
|
1425
|
-
if (models.length === 0 && !defaultModel && resolvedReasoningEfforts.length === 0 && resolvedSpeedTiers.length <= 1) return undefined;
|
|
1180
|
+
if (models.length === 0 && !defaultModel && !reasoningEfforts?.length) return undefined;
|
|
1426
1181
|
return {
|
|
1427
|
-
...(models.length > 0 ? { models } : {}),
|
|
1182
|
+
...(models.length > 0 ? { models: [{ id: "default", label: "默认模型" }, ...models] } : {}),
|
|
1428
1183
|
...(defaultModel ? { defaultModel } : {}),
|
|
1429
|
-
...(
|
|
1430
|
-
...(resolvedSpeedTiers.length > 0 ? { speedTiers: resolvedSpeedTiers } : {}),
|
|
1184
|
+
...(reasoningEfforts?.length ? { reasoningEfforts } : {}),
|
|
1431
1185
|
};
|
|
1432
1186
|
}
|
|
1433
1187
|
|
|
@@ -1449,9 +1203,6 @@ function parseRemoteSessions(value: unknown): Array<{
|
|
|
1449
1203
|
model?: string;
|
|
1450
1204
|
createdAt?: number;
|
|
1451
1205
|
lastActivityAt?: number;
|
|
1452
|
-
archived?: boolean;
|
|
1453
|
-
status?: AgentStatus;
|
|
1454
|
-
runningTurnId?: string;
|
|
1455
1206
|
}> {
|
|
1456
1207
|
const raw = asRecord(value);
|
|
1457
1208
|
const sessionsValue =
|
|
@@ -1467,9 +1218,6 @@ function parseRemoteSessions(value: unknown): Array<{
|
|
|
1467
1218
|
model?: string;
|
|
1468
1219
|
createdAt?: number;
|
|
1469
1220
|
lastActivityAt?: number;
|
|
1470
|
-
archived?: boolean;
|
|
1471
|
-
status?: AgentStatus;
|
|
1472
|
-
runningTurnId?: string;
|
|
1473
1221
|
}> = [];
|
|
1474
1222
|
for (const entry of sessionsValue) {
|
|
1475
1223
|
const session = asRecord(entry);
|
|
@@ -1488,31 +1236,15 @@ function parseRemoteSessions(value: unknown): Array<{
|
|
|
1488
1236
|
model: firstString(source, ["model", "modelId"]),
|
|
1489
1237
|
createdAt: parseTimestamp(source.createdAt ?? source.created_at),
|
|
1490
1238
|
lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
|
|
1491
|
-
archived: typeof source.archived === "boolean" ? source.archived : undefined,
|
|
1492
|
-
status: normalizeAgentStatus(firstString(source, ["status", "state", "phase"])),
|
|
1493
|
-
runningTurnId: firstString(source, ["runningTurnId", "running_turn_id", "turnId", "activeTurnId"]),
|
|
1494
1239
|
});
|
|
1495
1240
|
}
|
|
1496
1241
|
return result;
|
|
1497
1242
|
}
|
|
1498
1243
|
|
|
1499
|
-
function normalizeAgentStatus(value: string | undefined): AgentStatus | undefined {
|
|
1500
|
-
if (!value) return undefined;
|
|
1501
|
-
const normalized = value.toLowerCase();
|
|
1502
|
-
if (normalized === "running" || normalized === "in_progress" || normalized === "busy") return "running";
|
|
1503
|
-
if (normalized === "waiting_permission" || normalized === "waiting" || normalized === "blocked") return "waiting_permission";
|
|
1504
|
-
if (normalized === "error" || normalized === "failed") return "error";
|
|
1505
|
-
if (normalized === "idle" || normalized === "completed" || normalized === "done") return "idle";
|
|
1506
|
-
return undefined;
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
1244
|
export class AgentWorkspaceProxy {
|
|
1510
1245
|
private clients = new Map<AgentProvider, AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient>();
|
|
1511
|
-
private codexRpcBridge: CodexRpcBridge | undefined;
|
|
1512
1246
|
private agentProtocols = new Map<AgentProvider, AgentProtocol>();
|
|
1513
1247
|
private providerCapabilities = new Map<AgentProvider, ProviderRuntimeCapabilities>();
|
|
1514
|
-
private providerCapabilityErrors = new Map<AgentProvider, string>();
|
|
1515
|
-
private capabilitiesRevision = 0;
|
|
1516
1248
|
private initialized = false;
|
|
1517
1249
|
private status: AgentStatus = "unavailable";
|
|
1518
1250
|
private error: string | undefined;
|
|
@@ -1522,8 +1254,6 @@ export class AgentWorkspaceProxy {
|
|
|
1522
1254
|
private conversations = new Map<string, AgentConversation>();
|
|
1523
1255
|
private conversationByAgentSessionId = new Map<string, string>();
|
|
1524
1256
|
private timelines = new Map<string, AgentTimelineItem[]>();
|
|
1525
|
-
private conversationRevisions = new Map<string, number>();
|
|
1526
|
-
private revisionEvents = new Map<string, AgentRevisionEvent[]>();
|
|
1527
1257
|
private toolOutputBuffers = new Map<string, string>();
|
|
1528
1258
|
private pendingPermissions = new Map<string, AgentPermission>();
|
|
1529
1259
|
private permissionWaiters = new Map<string, PendingPermissionWaiter>();
|
|
@@ -1535,7 +1265,7 @@ export class AgentWorkspaceProxy {
|
|
|
1535
1265
|
|
|
1536
1266
|
constructor(
|
|
1537
1267
|
private readonly input: {
|
|
1538
|
-
|
|
1268
|
+
sessionId: string;
|
|
1539
1269
|
cwd: string;
|
|
1540
1270
|
availableProviders: AgentProvider[];
|
|
1541
1271
|
command?: string;
|
|
@@ -1546,31 +1276,6 @@ export class AgentWorkspaceProxy {
|
|
|
1546
1276
|
|
|
1547
1277
|
async handleEnvelope(envelope: Envelope): Promise<void> {
|
|
1548
1278
|
switch (envelope.type) {
|
|
1549
|
-
case "agent.codex.rpc": {
|
|
1550
|
-
const payload = parseTypedPayload("agent.codex.rpc", envelope.payload);
|
|
1551
|
-
try {
|
|
1552
|
-
this.codexRpc().send(payload as never);
|
|
1553
|
-
} catch (error) {
|
|
1554
|
-
const idValue = payload && typeof payload === "object" && "id" in payload
|
|
1555
|
-
? (payload as { id?: unknown }).id
|
|
1556
|
-
: undefined;
|
|
1557
|
-
if (typeof idValue === "string" || typeof idValue === "number") {
|
|
1558
|
-
this.input.send(createEnvelope({
|
|
1559
|
-
type: "agent.codex.rpc",
|
|
1560
|
-
hostDeviceId: this.input.hostDeviceId,
|
|
1561
|
-
payload: {
|
|
1562
|
-
jsonrpc: "2.0",
|
|
1563
|
-
id: idValue,
|
|
1564
|
-
error: {
|
|
1565
|
-
code: -32000,
|
|
1566
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1567
|
-
},
|
|
1568
|
-
},
|
|
1569
|
-
}));
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
break;
|
|
1573
|
-
}
|
|
1574
1279
|
case "agent.v2.capabilities.request":
|
|
1575
1280
|
await this.initialize();
|
|
1576
1281
|
this.sendCapabilities();
|
|
@@ -1588,8 +1293,8 @@ export class AgentWorkspaceProxy {
|
|
|
1588
1293
|
);
|
|
1589
1294
|
this.input.send(createEnvelope({
|
|
1590
1295
|
type: "agent.v2.conversation.list.result",
|
|
1591
|
-
|
|
1592
|
-
payload: { conversations
|
|
1296
|
+
sessionId: this.input.sessionId,
|
|
1297
|
+
payload: { conversations },
|
|
1593
1298
|
}));
|
|
1594
1299
|
break;
|
|
1595
1300
|
}
|
|
@@ -1598,16 +1303,6 @@ export class AgentWorkspaceProxy {
|
|
|
1598
1303
|
this.sendSnapshot(payload.conversationId);
|
|
1599
1304
|
break;
|
|
1600
1305
|
}
|
|
1601
|
-
case "agent.v2.history.request": {
|
|
1602
|
-
const payload = parseTypedPayload("agent.v2.history.request", envelope.payload);
|
|
1603
|
-
await this.sendHistoryPage(payload);
|
|
1604
|
-
break;
|
|
1605
|
-
}
|
|
1606
|
-
case "agent.v2.delta.request": {
|
|
1607
|
-
const payload = parseTypedPayload("agent.v2.delta.request", envelope.payload);
|
|
1608
|
-
this.sendDelta(payload);
|
|
1609
|
-
break;
|
|
1610
|
-
}
|
|
1611
1306
|
case "agent.v2.prompt": {
|
|
1612
1307
|
const payload = parseTypedPayload("agent.v2.prompt", envelope.payload);
|
|
1613
1308
|
await this.sendPrompt(payload);
|
|
@@ -1646,8 +1341,6 @@ export class AgentWorkspaceProxy {
|
|
|
1646
1341
|
}
|
|
1647
1342
|
|
|
1648
1343
|
stop(): void {
|
|
1649
|
-
this.codexRpcBridge?.stop();
|
|
1650
|
-
this.codexRpcBridge = undefined;
|
|
1651
1344
|
for (const client of this.clients.values()) {
|
|
1652
1345
|
client.stop();
|
|
1653
1346
|
}
|
|
@@ -1658,17 +1351,6 @@ export class AgentWorkspaceProxy {
|
|
|
1658
1351
|
return this.clients.get(provider);
|
|
1659
1352
|
}
|
|
1660
1353
|
|
|
1661
|
-
private codexRpc(): CodexRpcBridge {
|
|
1662
|
-
this.codexRpcBridge ??= new CodexRpcBridge({
|
|
1663
|
-
command: this.input.command,
|
|
1664
|
-
cwd: this.input.cwd,
|
|
1665
|
-
hostDeviceId: this.input.hostDeviceId,
|
|
1666
|
-
send: this.input.send,
|
|
1667
|
-
verbose: this.input.verbose,
|
|
1668
|
-
});
|
|
1669
|
-
return this.codexRpcBridge;
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
1354
|
private protocolForProvider(provider: AgentProvider): AgentProtocol | undefined {
|
|
1673
1355
|
return this.agentProtocols.get(provider);
|
|
1674
1356
|
}
|
|
@@ -1677,9 +1359,7 @@ export class AgentWorkspaceProxy {
|
|
|
1677
1359
|
if (this.initialized) return;
|
|
1678
1360
|
this.initialized = true;
|
|
1679
1361
|
// Eagerly start all detected providers so capabilities report real status
|
|
1680
|
-
const startPromises = this.input.availableProviders
|
|
1681
|
-
.filter((provider) => provider !== "codex")
|
|
1682
|
-
.map((p) => this.ensureProviderClient(p));
|
|
1362
|
+
const startPromises = this.input.availableProviders.map((p) => this.ensureProviderClient(p));
|
|
1683
1363
|
await Promise.allSettled(startPromises);
|
|
1684
1364
|
this.status = "idle";
|
|
1685
1365
|
this.error = undefined;
|
|
@@ -1789,12 +1469,8 @@ export class AgentWorkspaceProxy {
|
|
|
1789
1469
|
try {
|
|
1790
1470
|
const result = await listModels.call(client);
|
|
1791
1471
|
const runtimeCapabilities = parseModelListCapabilities(result);
|
|
1792
|
-
if (runtimeCapabilities)
|
|
1793
|
-
this.providerCapabilities.set(provider, runtimeCapabilities);
|
|
1794
|
-
this.providerCapabilityErrors.delete(provider);
|
|
1795
|
-
}
|
|
1472
|
+
if (runtimeCapabilities) this.providerCapabilities.set(provider, runtimeCapabilities);
|
|
1796
1473
|
} catch (error) {
|
|
1797
|
-
this.providerCapabilityErrors.set(provider, error instanceof Error ? error.message : String(error));
|
|
1798
1474
|
if (this.input.verbose) {
|
|
1799
1475
|
process.stderr.write(`[agent:v2] model/list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1800
1476
|
}
|
|
@@ -1803,11 +1479,36 @@ export class AgentWorkspaceProxy {
|
|
|
1803
1479
|
|
|
1804
1480
|
private async syncProviderSessions(): Promise<void> {
|
|
1805
1481
|
await this.initialize();
|
|
1806
|
-
this.upsertProviderSessions("claude", listClaudeStoredSessions(this.input.cwd));
|
|
1807
1482
|
for (const [provider, client] of this.clients) {
|
|
1808
1483
|
try {
|
|
1809
1484
|
const result = await client.listSessions();
|
|
1810
|
-
|
|
1485
|
+
for (const remote of parseRemoteSessions(result)) {
|
|
1486
|
+
const agentSessionId = remote.id;
|
|
1487
|
+
const existingId = this.conversationByAgentSessionId.get(agentSessionId);
|
|
1488
|
+
const now = Date.now();
|
|
1489
|
+
const conversationId = existingId ?? `agent:${agentSessionId}`;
|
|
1490
|
+
const existing = this.conversations.get(conversationId);
|
|
1491
|
+
const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
|
|
1492
|
+
const conversation: AgentConversation = {
|
|
1493
|
+
id: conversationId,
|
|
1494
|
+
agentSessionId,
|
|
1495
|
+
provider,
|
|
1496
|
+
cwd,
|
|
1497
|
+
title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
|
|
1498
|
+
model: remote.model ?? existing?.model,
|
|
1499
|
+
reasoningEffort: existing?.reasoningEffort,
|
|
1500
|
+
permissionMode: existing?.permissionMode,
|
|
1501
|
+
collaborationMode: existing?.collaborationMode,
|
|
1502
|
+
status: existing?.status ?? "idle",
|
|
1503
|
+
archived: existing?.archived ?? false,
|
|
1504
|
+
lastMessagePreview: existing?.lastMessagePreview,
|
|
1505
|
+
lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
|
|
1506
|
+
createdAt: remote.createdAt ?? existing?.createdAt ?? now,
|
|
1507
|
+
};
|
|
1508
|
+
this.conversations.set(conversation.id, conversation);
|
|
1509
|
+
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
1510
|
+
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
1511
|
+
}
|
|
1811
1512
|
} catch (error) {
|
|
1812
1513
|
if (this.input.verbose) {
|
|
1813
1514
|
process.stderr.write(`[agent:v2] session list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
@@ -1816,55 +1517,16 @@ export class AgentWorkspaceProxy {
|
|
|
1816
1517
|
}
|
|
1817
1518
|
}
|
|
1818
1519
|
|
|
1819
|
-
private upsertProviderSessions(provider: AgentProvider, result: unknown): void {
|
|
1820
|
-
for (const remote of parseRemoteSessions(result)) {
|
|
1821
|
-
const agentSessionId = remote.id;
|
|
1822
|
-
const existingId = this.conversationByAgentSessionId.get(agentSessionId);
|
|
1823
|
-
const now = Date.now();
|
|
1824
|
-
const conversationId = existingId ?? `agent:${agentSessionId}`;
|
|
1825
|
-
const existing = this.conversations.get(conversationId);
|
|
1826
|
-
const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
|
|
1827
|
-
const conversation: AgentConversation = {
|
|
1828
|
-
id: conversationId,
|
|
1829
|
-
agentSessionId,
|
|
1830
|
-
provider,
|
|
1831
|
-
cwd,
|
|
1832
|
-
title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
|
|
1833
|
-
model: remote.model ?? existing?.model,
|
|
1834
|
-
reasoningEffort: existing?.reasoningEffort,
|
|
1835
|
-
serviceTier: existing?.serviceTier,
|
|
1836
|
-
permissionMode: existing?.permissionMode,
|
|
1837
|
-
collaborationMode: existing?.collaborationMode,
|
|
1838
|
-
status: remote.status ?? existing?.status ?? "idle",
|
|
1839
|
-
archived: remote.archived ?? existing?.archived ?? false,
|
|
1840
|
-
timelineRevision: existing?.timelineRevision ?? this.getRevision(conversationId),
|
|
1841
|
-
historyComplete: existing?.historyComplete ?? false,
|
|
1842
|
-
runningTurnId: remote.runningTurnId ?? this.currentTurnIds.get(conversationId),
|
|
1843
|
-
source: "device",
|
|
1844
|
-
canonical: true,
|
|
1845
|
-
lastMessagePreview: existing?.lastMessagePreview,
|
|
1846
|
-
lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
|
|
1847
|
-
createdAt: remote.createdAt ?? existing?.createdAt ?? now,
|
|
1848
|
-
};
|
|
1849
|
-
this.conversations.set(conversation.id, conversation);
|
|
1850
|
-
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
1851
|
-
if (remote.runningTurnId) this.rememberTurnConversationId(conversation.id, remote.runningTurnId);
|
|
1852
|
-
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
1520
|
private sendCapabilities(): void {
|
|
1857
1521
|
const providers = this.input.availableProviders.map((provider) => {
|
|
1858
1522
|
const client = this.clients.get(provider);
|
|
1859
|
-
const protocol =
|
|
1523
|
+
const protocol = this.agentProtocols.get(provider);
|
|
1860
1524
|
const runtimeCapabilities = this.providerCapabilities.get(provider);
|
|
1861
|
-
const enabled =
|
|
1862
|
-
const hasRuntimeModels = Boolean(runtimeCapabilities?.models?.length);
|
|
1525
|
+
const enabled = Boolean(client);
|
|
1863
1526
|
const supportsImages = enabled && protocolSupportsImages(protocol);
|
|
1864
|
-
const isClaudeSdk = protocol === "claude-agent-sdk";
|
|
1865
1527
|
const isClaudeFallback = protocol === "claude-stream-json";
|
|
1866
|
-
const supportsPermission = enabled &&
|
|
1867
|
-
const supportsReasoningEffort = enabled;
|
|
1528
|
+
const supportsPermission = enabled && !isClaudeFallback;
|
|
1529
|
+
const supportsReasoningEffort = enabled && !isClaudeFallback;
|
|
1868
1530
|
const commands = mergeCommands(
|
|
1869
1531
|
defaultProviderCommands(provider, this.input.cwd, enabled),
|
|
1870
1532
|
runtimeCapabilities?.commands,
|
|
@@ -1875,23 +1537,21 @@ export class AgentWorkspaceProxy {
|
|
|
1875
1537
|
label: providerLabel(provider),
|
|
1876
1538
|
enabled,
|
|
1877
1539
|
reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
|
|
1878
|
-
providerProtocol: protocol,
|
|
1879
1540
|
supportsImages,
|
|
1880
1541
|
supportsPermission,
|
|
1881
1542
|
supportsPlan: enabled,
|
|
1882
1543
|
supportsCancel: enabled,
|
|
1883
1544
|
models: runtimeCapabilities?.models ?? [{ id: "default", label: "默认模型" }],
|
|
1884
|
-
defaultModel: runtimeCapabilities?.defaultModel,
|
|
1885
|
-
modelsSource: hasRuntimeModels ? "runtime" : enabled ? "fallback" : "unavailable",
|
|
1886
|
-
modelListError: this.providerCapabilityErrors.get(provider),
|
|
1545
|
+
defaultModel: runtimeCapabilities?.defaultModel ?? "default",
|
|
1887
1546
|
reasoningEfforts: supportsReasoningEffort
|
|
1888
|
-
? runtimeCapabilities?.reasoningEfforts ??
|
|
1547
|
+
? runtimeCapabilities?.reasoningEfforts ?? [...ALL_REASONING_EFFORTS]
|
|
1889
1548
|
: [],
|
|
1890
|
-
|
|
1891
|
-
defaultServiceTier: provider === "codex" ? runtimeCapabilities?.defaultServiceTier : undefined,
|
|
1892
|
-
permissionModes: supportsPermission ? AGENT_PERMISSION_MODES : [],
|
|
1549
|
+
permissionModes: supportsPermission ? [...ALL_PERMISSION_MODES] : [],
|
|
1893
1550
|
commands,
|
|
1894
|
-
modes: runtimeCapabilities?.modes ?? [
|
|
1551
|
+
modes: runtimeCapabilities?.modes ?? (provider === "codex" ? [
|
|
1552
|
+
{ id: "default", title: "Default", description: "Run normal implementation turns" },
|
|
1553
|
+
{ id: "plan", title: "Plan", description: "Discuss and produce an implementation plan first" },
|
|
1554
|
+
] : []),
|
|
1895
1555
|
currentMode,
|
|
1896
1556
|
features: {
|
|
1897
1557
|
images: supportsImages,
|
|
@@ -1899,8 +1559,6 @@ export class AgentWorkspaceProxy {
|
|
|
1899
1559
|
plan: enabled,
|
|
1900
1560
|
cancel: enabled,
|
|
1901
1561
|
reasoningEffort: supportsReasoningEffort,
|
|
1902
|
-
serviceTier: provider === "codex" && Boolean(runtimeCapabilities?.speedTiers?.includes("fast")),
|
|
1903
|
-
claudeSdk: isClaudeSdk,
|
|
1904
1562
|
streamJsonFallback: isClaudeFallback,
|
|
1905
1563
|
},
|
|
1906
1564
|
};
|
|
@@ -1909,14 +1567,13 @@ export class AgentWorkspaceProxy {
|
|
|
1909
1567
|
const anyPermission = providers.some((p) => p.supportsPermission);
|
|
1910
1568
|
this.input.send(createEnvelope({
|
|
1911
1569
|
type: "agent.v2.capabilities",
|
|
1912
|
-
|
|
1570
|
+
sessionId: this.input.sessionId,
|
|
1913
1571
|
payload: {
|
|
1914
1572
|
enabled: anyEnabled,
|
|
1915
1573
|
provider: this.input.availableProviders[0] ?? "codex",
|
|
1916
1574
|
providers,
|
|
1917
1575
|
protocolVersion: 1,
|
|
1918
1576
|
workspaceProtocolVersion: 2,
|
|
1919
|
-
capabilitiesRevision: ++this.capabilitiesRevision,
|
|
1920
1577
|
error: anyEnabled ? undefined : "没有可用的 Agent provider。请安装 Claude Code 或 Codex CLI。",
|
|
1921
1578
|
supportsSessionList: anyEnabled,
|
|
1922
1579
|
supportsSessionLoad: anyEnabled,
|
|
@@ -1936,12 +1593,29 @@ export class AgentWorkspaceProxy {
|
|
|
1936
1593
|
provider?: AgentProvider;
|
|
1937
1594
|
model?: string;
|
|
1938
1595
|
reasoningEffort?: string;
|
|
1939
|
-
serviceTier?: string;
|
|
1940
1596
|
permissionMode?: AgentPermissionMode;
|
|
1941
1597
|
collaborationMode?: AgentCollaborationMode;
|
|
1942
1598
|
title?: string;
|
|
1943
1599
|
}): Promise<AgentConversation | undefined> {
|
|
1944
1600
|
const provider = payload.provider ?? this.input.availableProviders[0];
|
|
1601
|
+
if (!provider) {
|
|
1602
|
+
return this.openFailure(payload, "没有可用的 Agent provider。");
|
|
1603
|
+
}
|
|
1604
|
+
if (!this.input.availableProviders.includes(provider)) {
|
|
1605
|
+
return this.openFailure(
|
|
1606
|
+
payload,
|
|
1607
|
+
`${providerLabel(provider)} 未安装或不可用。`,
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
const client = await this.ensureProviderClient(provider);
|
|
1612
|
+
if (!client) {
|
|
1613
|
+
return this.openFailure(
|
|
1614
|
+
payload,
|
|
1615
|
+
`${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`,
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1945
1619
|
const cwd = payload.cwd ?? this.input.cwd;
|
|
1946
1620
|
let agentSessionId = payload.agentSessionId;
|
|
1947
1621
|
let existingConversation =
|
|
@@ -1952,43 +1626,18 @@ export class AgentWorkspaceProxy {
|
|
|
1952
1626
|
if (payload.conversationId && existingConversation.id !== payload.conversationId) {
|
|
1953
1627
|
existingConversation = this.adoptConversationId(existingConversation.id, payload.conversationId);
|
|
1954
1628
|
}
|
|
1955
|
-
this.hydrateStoredTimeline(existingConversation);
|
|
1956
1629
|
this.activeConversationId = existingConversation.id;
|
|
1957
|
-
const snapshot = this.latestSnapshot(existingConversation.id);
|
|
1958
1630
|
this.input.send(createEnvelope({
|
|
1959
1631
|
type: "agent.v2.conversation.opened",
|
|
1960
|
-
|
|
1632
|
+
sessionId: this.input.sessionId,
|
|
1961
1633
|
payload: {
|
|
1962
|
-
conversation:
|
|
1963
|
-
snapshot:
|
|
1964
|
-
revision: this.getRevision(existingConversation.id),
|
|
1965
|
-
cursor: snapshot.cursor,
|
|
1966
|
-
hasMore: snapshot.hasMore,
|
|
1967
|
-
source: "device-history",
|
|
1968
|
-
canonical: true,
|
|
1634
|
+
conversation: existingConversation,
|
|
1635
|
+
snapshot: this.timelines.get(existingConversation.id) ?? [],
|
|
1969
1636
|
},
|
|
1970
1637
|
}));
|
|
1971
1638
|
return existingConversation;
|
|
1972
1639
|
}
|
|
1973
1640
|
|
|
1974
|
-
if (!provider) {
|
|
1975
|
-
return this.openFailure(payload, "没有可用的 Agent provider。");
|
|
1976
|
-
}
|
|
1977
|
-
if (!this.input.availableProviders.includes(provider)) {
|
|
1978
|
-
return this.openFailure(
|
|
1979
|
-
payload,
|
|
1980
|
-
`${providerLabel(provider)} 未安装或不可用。`,
|
|
1981
|
-
);
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
const client = await this.ensureProviderClient(provider);
|
|
1985
|
-
if (!client) {
|
|
1986
|
-
return this.openFailure(
|
|
1987
|
-
payload,
|
|
1988
|
-
`${providerLabel(provider)} 启动失败。请确认 CLI 已安装并可用。`,
|
|
1989
|
-
);
|
|
1990
|
-
}
|
|
1991
|
-
|
|
1992
1641
|
try {
|
|
1993
1642
|
const result = agentSessionId
|
|
1994
1643
|
? await client.loadSession({ sessionId: agentSessionId, cwd })
|
|
@@ -2005,16 +1654,10 @@ export class AgentWorkspaceProxy {
|
|
|
2005
1654
|
title: payload.title ?? existingConversation?.title ?? titleFromCwd(cwd),
|
|
2006
1655
|
model: payload.model ?? existingConversation?.model,
|
|
2007
1656
|
reasoningEffort: payload.reasoningEffort ?? existingConversation?.reasoningEffort,
|
|
2008
|
-
serviceTier: payload.serviceTier ?? existingConversation?.serviceTier,
|
|
2009
1657
|
permissionMode: payload.permissionMode ?? existingConversation?.permissionMode,
|
|
2010
1658
|
collaborationMode: payload.collaborationMode ?? existingConversation?.collaborationMode,
|
|
2011
1659
|
status: "idle",
|
|
2012
1660
|
archived: existingConversation?.archived ?? false,
|
|
2013
|
-
timelineRevision: existingConversation?.timelineRevision ?? this.getRevision(conversationId),
|
|
2014
|
-
historyComplete: existingConversation?.historyComplete ?? false,
|
|
2015
|
-
runningTurnId: this.currentTurnIds.get(conversationId),
|
|
2016
|
-
source: "device",
|
|
2017
|
-
canonical: true,
|
|
2018
1661
|
lastMessagePreview: existingConversation?.status === "error" ? undefined : existingConversation?.lastMessagePreview,
|
|
2019
1662
|
lastActivityAt: now,
|
|
2020
1663
|
createdAt: existingConversation?.createdAt ?? now,
|
|
@@ -2023,20 +1666,10 @@ export class AgentWorkspaceProxy {
|
|
|
2023
1666
|
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
2024
1667
|
this.activeConversationId = conversation.id;
|
|
2025
1668
|
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
2026
|
-
this.hydrateStoredTimeline(conversation);
|
|
2027
|
-
const snapshot = this.latestSnapshot(conversation.id);
|
|
2028
1669
|
this.input.send(createEnvelope({
|
|
2029
1670
|
type: "agent.v2.conversation.opened",
|
|
2030
|
-
|
|
2031
|
-
payload: {
|
|
2032
|
-
conversation: this.conversationSnapshot(conversation),
|
|
2033
|
-
snapshot: snapshot.items,
|
|
2034
|
-
revision: this.getRevision(conversation.id),
|
|
2035
|
-
cursor: snapshot.cursor,
|
|
2036
|
-
hasMore: snapshot.hasMore,
|
|
2037
|
-
source: "device-history",
|
|
2038
|
-
canonical: true,
|
|
2039
|
-
},
|
|
1671
|
+
sessionId: this.input.sessionId,
|
|
1672
|
+
payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
|
|
2040
1673
|
}));
|
|
2041
1674
|
return conversation;
|
|
2042
1675
|
} catch (error) {
|
|
@@ -2072,10 +1705,6 @@ export class AgentWorkspaceProxy {
|
|
|
2072
1705
|
collaborationMode: payload.collaborationMode,
|
|
2073
1706
|
status: "error",
|
|
2074
1707
|
archived: false,
|
|
2075
|
-
timelineRevision: this.getRevision(fallbackId),
|
|
2076
|
-
historyComplete: false,
|
|
2077
|
-
source: "device",
|
|
2078
|
-
canonical: true,
|
|
2079
1708
|
lastMessagePreview: message,
|
|
2080
1709
|
lastActivityAt: now,
|
|
2081
1710
|
createdAt: now,
|
|
@@ -2091,15 +1720,8 @@ export class AgentWorkspaceProxy {
|
|
|
2091
1720
|
});
|
|
2092
1721
|
this.input.send(createEnvelope({
|
|
2093
1722
|
type: "agent.v2.conversation.opened",
|
|
2094
|
-
|
|
2095
|
-
payload: {
|
|
2096
|
-
conversation: this.conversationSnapshot(conversation),
|
|
2097
|
-
snapshot: snapshotTimelineItems(this.timelines.get(conversation.id) ?? []),
|
|
2098
|
-
revision: this.getRevision(conversation.id),
|
|
2099
|
-
hasMore: false,
|
|
2100
|
-
source: "device",
|
|
2101
|
-
canonical: true,
|
|
2102
|
-
},
|
|
1723
|
+
sessionId: this.input.sessionId,
|
|
1724
|
+
payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
|
|
2103
1725
|
}));
|
|
2104
1726
|
return conversation;
|
|
2105
1727
|
}
|
|
@@ -2108,11 +1730,10 @@ export class AgentWorkspaceProxy {
|
|
|
2108
1730
|
conversationId: string;
|
|
2109
1731
|
clientMessageId: string;
|
|
2110
1732
|
contentBlocks: AgentContentBlock[];
|
|
2111
|
-
model?: string
|
|
2112
|
-
reasoningEffort?: string
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
collaborationMode?: AgentCollaborationMode | null;
|
|
1733
|
+
model?: string;
|
|
1734
|
+
reasoningEffort?: string;
|
|
1735
|
+
permissionMode?: AgentPermissionMode;
|
|
1736
|
+
collaborationMode?: AgentCollaborationMode;
|
|
2116
1737
|
}): Promise<void> {
|
|
2117
1738
|
const conversation =
|
|
2118
1739
|
this.conversations.get(payload.conversationId) ??
|
|
@@ -2155,21 +1776,10 @@ export class AgentWorkspaceProxy {
|
|
|
2155
1776
|
return;
|
|
2156
1777
|
}
|
|
2157
1778
|
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
conversation.reasoningEffort = payload.reasoningEffort ?? undefined;
|
|
2163
|
-
}
|
|
2164
|
-
if (Object.prototype.hasOwnProperty.call(payload, "serviceTier")) {
|
|
2165
|
-
conversation.serviceTier = payload.serviceTier ?? undefined;
|
|
2166
|
-
}
|
|
2167
|
-
if (Object.prototype.hasOwnProperty.call(payload, "permissionMode")) {
|
|
2168
|
-
conversation.permissionMode = payload.permissionMode ?? undefined;
|
|
2169
|
-
}
|
|
2170
|
-
if (Object.prototype.hasOwnProperty.call(payload, "collaborationMode")) {
|
|
2171
|
-
conversation.collaborationMode = payload.collaborationMode ?? "default";
|
|
2172
|
-
}
|
|
1779
|
+
conversation.model = payload.model ?? conversation.model;
|
|
1780
|
+
conversation.reasoningEffort = payload.reasoningEffort ?? conversation.reasoningEffort;
|
|
1781
|
+
conversation.permissionMode = payload.permissionMode ?? conversation.permissionMode;
|
|
1782
|
+
conversation.collaborationMode = payload.collaborationMode ?? conversation.collaborationMode;
|
|
2173
1783
|
conversation.status = "running";
|
|
2174
1784
|
conversation.lastActivityAt = Date.now();
|
|
2175
1785
|
this.activeConversationId = conversation.id;
|
|
@@ -2191,11 +1801,10 @@ export class AgentWorkspaceProxy {
|
|
|
2191
1801
|
sessionId: conversation.agentSessionId,
|
|
2192
1802
|
content: payload.contentBlocks,
|
|
2193
1803
|
clientMessageId: payload.clientMessageId,
|
|
2194
|
-
model:
|
|
2195
|
-
reasoningEffort:
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
collaborationMode: conversation.collaborationMode,
|
|
1804
|
+
model: payload.model,
|
|
1805
|
+
reasoningEffort: payload.reasoningEffort,
|
|
1806
|
+
permissionMode: payload.permissionMode,
|
|
1807
|
+
collaborationMode: payload.collaborationMode ?? conversation.collaborationMode,
|
|
2199
1808
|
cwd: conversation.cwd,
|
|
2200
1809
|
});
|
|
2201
1810
|
const nextAgentSessionId = this.extractSessionId(result);
|
|
@@ -2288,7 +1897,6 @@ export class AgentWorkspaceProxy {
|
|
|
2288
1897
|
contentBlocks: [{ type: "text", text: rawText }],
|
|
2289
1898
|
model: conversation.model,
|
|
2290
1899
|
reasoningEffort: conversation.reasoningEffort,
|
|
2291
|
-
serviceTier: conversation.serviceTier,
|
|
2292
1900
|
permissionMode: conversation.permissionMode,
|
|
2293
1901
|
collaborationMode: conversation.collaborationMode,
|
|
2294
1902
|
});
|
|
@@ -2332,8 +1940,14 @@ export class AgentWorkspaceProxy {
|
|
|
2332
1940
|
return;
|
|
2333
1941
|
}
|
|
2334
1942
|
|
|
2335
|
-
if (
|
|
2336
|
-
|
|
1943
|
+
if (conversation.provider !== "codex") {
|
|
1944
|
+
this.addItem(conversation.id, {
|
|
1945
|
+
id: id("error"),
|
|
1946
|
+
conversationId: conversation.id,
|
|
1947
|
+
type: "error",
|
|
1948
|
+
error: `${command.title} 暂无 ${providerLabel(conversation.provider)} 原生实现。`,
|
|
1949
|
+
createdAt: now,
|
|
1950
|
+
});
|
|
2337
1951
|
return;
|
|
2338
1952
|
}
|
|
2339
1953
|
|
|
@@ -2350,23 +1964,6 @@ export class AgentWorkspaceProxy {
|
|
|
2350
1964
|
return;
|
|
2351
1965
|
}
|
|
2352
1966
|
|
|
2353
|
-
if (command.name === "review" || command.name === "subagents") {
|
|
2354
|
-
const prompt = command.name === "review"
|
|
2355
|
-
? args || "Review the current local changes."
|
|
2356
|
-
: args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
|
|
2357
|
-
await this.sendPrompt({
|
|
2358
|
-
conversationId: conversation.id,
|
|
2359
|
-
clientMessageId: id(command.name),
|
|
2360
|
-
contentBlocks: [{ type: "text", text: prompt }],
|
|
2361
|
-
model: conversation.model,
|
|
2362
|
-
reasoningEffort: conversation.reasoningEffort,
|
|
2363
|
-
serviceTier: conversation.serviceTier,
|
|
2364
|
-
permissionMode: conversation.permissionMode,
|
|
2365
|
-
collaborationMode: conversation.collaborationMode,
|
|
2366
|
-
});
|
|
2367
|
-
return;
|
|
2368
|
-
}
|
|
2369
|
-
|
|
2370
1967
|
if (command.name === "compact") {
|
|
2371
1968
|
if (!(client instanceof AcpClient)) throw new Error("当前 Codex runtime 不支持原生 compact。");
|
|
2372
1969
|
conversation.status = "running";
|
|
@@ -2387,7 +1984,7 @@ export class AgentWorkspaceProxy {
|
|
|
2387
1984
|
return;
|
|
2388
1985
|
}
|
|
2389
1986
|
|
|
2390
|
-
if (command.name === "clear"
|
|
1987
|
+
if (command.name === "clear") {
|
|
2391
1988
|
if (!client) throw new Error("Agent provider 不在线。");
|
|
2392
1989
|
const result = await client.newSession({ cwd: conversation.cwd });
|
|
2393
1990
|
const nextAgentSessionId = this.extractSessionId(result) ?? id("agent-session");
|
|
@@ -2404,6 +2001,22 @@ export class AgentWorkspaceProxy {
|
|
|
2404
2001
|
return;
|
|
2405
2002
|
}
|
|
2406
2003
|
|
|
2004
|
+
if (command.name === "review" || command.name === "subagents") {
|
|
2005
|
+
const prompt = command.name === "review"
|
|
2006
|
+
? args || "Review the current local changes."
|
|
2007
|
+
: args || "Run subagents for distinct tasks in parallel when useful, then synthesize the results.";
|
|
2008
|
+
await this.sendPrompt({
|
|
2009
|
+
conversationId: conversation.id,
|
|
2010
|
+
clientMessageId: id(command.name),
|
|
2011
|
+
contentBlocks: [{ type: "text", text: prompt }],
|
|
2012
|
+
model: conversation.model,
|
|
2013
|
+
reasoningEffort: conversation.reasoningEffort,
|
|
2014
|
+
permissionMode: conversation.permissionMode,
|
|
2015
|
+
collaborationMode: conversation.collaborationMode,
|
|
2016
|
+
});
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2407
2020
|
throw new Error(`命令暂未实现:/${command.name}`);
|
|
2408
2021
|
} catch (error) {
|
|
2409
2022
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -2418,46 +2031,6 @@ export class AgentWorkspaceProxy {
|
|
|
2418
2031
|
}
|
|
2419
2032
|
}
|
|
2420
2033
|
|
|
2421
|
-
private async executeGitNativeCommand(
|
|
2422
|
-
conversation: AgentConversation,
|
|
2423
|
-
commandName: string,
|
|
2424
|
-
args?: string,
|
|
2425
|
-
): Promise<void> {
|
|
2426
|
-
const git = gitCommandArgs(commandName, args);
|
|
2427
|
-
const toolId = id(commandName);
|
|
2428
|
-
const now = Date.now();
|
|
2429
|
-
conversation.status = "running";
|
|
2430
|
-
conversation.lastMessagePreview = git.display;
|
|
2431
|
-
conversation.lastActivityAt = now;
|
|
2432
|
-
this.emitConversation(conversation);
|
|
2433
|
-
this.upsertTool(conversation.id, {
|
|
2434
|
-
id: toolId,
|
|
2435
|
-
name: "命令",
|
|
2436
|
-
input: `${git.display}\n\ncwd: ${conversation.cwd}`,
|
|
2437
|
-
createdAt: now,
|
|
2438
|
-
status: "running",
|
|
2439
|
-
});
|
|
2440
|
-
const result = await runProcess("git", git.argv, {
|
|
2441
|
-
cwd: conversation.cwd,
|
|
2442
|
-
maxBytes: COMMAND_OUTPUT_MAX_BYTES,
|
|
2443
|
-
});
|
|
2444
|
-
const ok = result.exitCode === 0;
|
|
2445
|
-
this.upsertTool(conversation.id, {
|
|
2446
|
-
id: toolId,
|
|
2447
|
-
name: "命令",
|
|
2448
|
-
input: `${git.display}\n\ncwd: ${conversation.cwd}`,
|
|
2449
|
-
output: result.output || (ok ? "完成" : `退出码 ${result.exitCode ?? "unknown"}`),
|
|
2450
|
-
createdAt: now,
|
|
2451
|
-
status: ok ? "completed" : "failed",
|
|
2452
|
-
});
|
|
2453
|
-
conversation.status = ok ? "idle" : "error";
|
|
2454
|
-
conversation.lastMessagePreview = ok
|
|
2455
|
-
? `${git.display} 完成`
|
|
2456
|
-
: `${git.display} 失败`;
|
|
2457
|
-
conversation.lastActivityAt = Date.now();
|
|
2458
|
-
this.emitConversation(conversation);
|
|
2459
|
-
}
|
|
2460
|
-
|
|
2461
2034
|
private handleRequest(method: string, params: unknown): Promise<unknown> | unknown {
|
|
2462
2035
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
2463
2036
|
return this.handleStructuredInput(params, true);
|
|
@@ -2497,7 +2070,6 @@ export class AgentWorkspaceProxy {
|
|
|
2497
2070
|
if (
|
|
2498
2071
|
method.startsWith("account/") ||
|
|
2499
2072
|
method.startsWith("mcpServer/startupStatus/") ||
|
|
2500
|
-
method.startsWith("remoteControl/") ||
|
|
2501
2073
|
method === "thread/status/changed" ||
|
|
2502
2074
|
method === "thread/tokenUsage/updated" ||
|
|
2503
2075
|
method === "serverRequest/resolved" ||
|
|
@@ -2525,9 +2097,6 @@ export class AgentWorkspaceProxy {
|
|
|
2525
2097
|
this.conversationByAgentSessionId.set(agentSessionId, conversationId);
|
|
2526
2098
|
const conversation = this.conversations.get(conversationId);
|
|
2527
2099
|
if (conversation) {
|
|
2528
|
-
if (conversation.agentSessionId && conversation.agentSessionId !== agentSessionId) {
|
|
2529
|
-
this.conversationByAgentSessionId.delete(conversation.agentSessionId);
|
|
2530
|
-
}
|
|
2531
2100
|
conversation.agentSessionId = agentSessionId;
|
|
2532
2101
|
conversation.lastActivityAt = Date.now();
|
|
2533
2102
|
this.emitConversation(conversation);
|
|
@@ -3109,7 +2678,7 @@ export class AgentWorkspaceProxy {
|
|
|
3109
2678
|
this.upsertItem(conversationId, item);
|
|
3110
2679
|
this.input.send(createEnvelope({
|
|
3111
2680
|
type: "agent.v2.permission.request",
|
|
3112
|
-
|
|
2681
|
+
sessionId: this.input.sessionId,
|
|
3113
2682
|
payload: { conversationId, ...permission, item },
|
|
3114
2683
|
}));
|
|
3115
2684
|
|
|
@@ -3120,14 +2689,6 @@ export class AgentWorkspaceProxy {
|
|
|
3120
2689
|
this.permissionWaiters.delete(requestId);
|
|
3121
2690
|
this.permissionSources.delete(requestId);
|
|
3122
2691
|
resolve(formatPermissionResponse(source, "cancelled", "cancelled"));
|
|
3123
|
-
this.markPermission(conversationId, requestId, {
|
|
3124
|
-
permissionOutcome: "cancelled",
|
|
3125
|
-
optionId: "cancelled",
|
|
3126
|
-
permissionLive: false,
|
|
3127
|
-
permissionPending: false,
|
|
3128
|
-
permissionExpired: true,
|
|
3129
|
-
permissionError: "等待授权超时",
|
|
3130
|
-
});
|
|
3131
2692
|
this.updateConversationStatus(conversationId, "idle");
|
|
3132
2693
|
}, PERMISSION_TIMEOUT_MS);
|
|
3133
2694
|
this.permissionWaiters.set(requestId, { resolve, timer });
|
|
@@ -3167,8 +2728,6 @@ export class AgentWorkspaceProxy {
|
|
|
3167
2728
|
this.markPermission(payload.conversationId, payload.requestId, {
|
|
3168
2729
|
permissionOutcome: payload.outcome,
|
|
3169
2730
|
optionId: selectedOptionId,
|
|
3170
|
-
permissionLive: false,
|
|
3171
|
-
permissionExpired: false,
|
|
3172
2731
|
permissionError: undefined,
|
|
3173
2732
|
permissionPending: false,
|
|
3174
2733
|
});
|
|
@@ -3229,228 +2788,6 @@ export class AgentWorkspaceProxy {
|
|
|
3229
2788
|
});
|
|
3230
2789
|
}
|
|
3231
2790
|
|
|
3232
|
-
private getRevision(conversationId: string): number {
|
|
3233
|
-
return this.conversationRevisions.get(conversationId) ??
|
|
3234
|
-
this.conversations.get(conversationId)?.timelineRevision ??
|
|
3235
|
-
0;
|
|
3236
|
-
}
|
|
3237
|
-
|
|
3238
|
-
private setRevisionFloor(conversationId: string, revision: number): number {
|
|
3239
|
-
const nextRevision = Math.max(this.getRevision(conversationId), revision);
|
|
3240
|
-
this.conversationRevisions.set(conversationId, nextRevision);
|
|
3241
|
-
const conversation = this.conversations.get(conversationId);
|
|
3242
|
-
if (conversation) {
|
|
3243
|
-
conversation.timelineRevision = nextRevision;
|
|
3244
|
-
conversation.runningTurnId = this.currentTurnIds.get(conversationId);
|
|
3245
|
-
}
|
|
3246
|
-
return nextRevision;
|
|
3247
|
-
}
|
|
3248
|
-
|
|
3249
|
-
private conversationSnapshot(conversation: AgentConversation): AgentConversation {
|
|
3250
|
-
return {
|
|
3251
|
-
...conversation,
|
|
3252
|
-
timelineRevision: this.getRevision(conversation.id),
|
|
3253
|
-
historyComplete: conversation.historyComplete ?? false,
|
|
3254
|
-
runningTurnId: this.currentTurnIds.get(conversation.id),
|
|
3255
|
-
source: conversation.source ?? "device",
|
|
3256
|
-
canonical: conversation.canonical ?? true,
|
|
3257
|
-
};
|
|
3258
|
-
}
|
|
3259
|
-
|
|
3260
|
-
private annotateTimelineItem(
|
|
3261
|
-
item: AgentTimelineItem,
|
|
3262
|
-
revision: number | undefined,
|
|
3263
|
-
source: AgentSyncSource,
|
|
3264
|
-
): AgentTimelineItem {
|
|
3265
|
-
return {
|
|
3266
|
-
...item,
|
|
3267
|
-
revision: revision ?? item.revision,
|
|
3268
|
-
source: item.source ?? source,
|
|
3269
|
-
canonical: item.canonical ?? true,
|
|
3270
|
-
};
|
|
3271
|
-
}
|
|
3272
|
-
|
|
3273
|
-
private recordRevisionEvent(
|
|
3274
|
-
conversationId: string,
|
|
3275
|
-
change: { item?: AgentTimelineItem; conversation?: AgentConversation },
|
|
3276
|
-
): AgentRevisionEvent {
|
|
3277
|
-
const revision = this.getRevision(conversationId) + 1;
|
|
3278
|
-
this.conversationRevisions.set(conversationId, revision);
|
|
3279
|
-
const conversation = this.conversations.get(conversationId);
|
|
3280
|
-
if (conversation) {
|
|
3281
|
-
conversation.timelineRevision = revision;
|
|
3282
|
-
conversation.runningTurnId = this.currentTurnIds.get(conversationId);
|
|
3283
|
-
}
|
|
3284
|
-
const event: AgentRevisionEvent = {
|
|
3285
|
-
revision,
|
|
3286
|
-
item: change.item ? this.annotateTimelineItem(change.item, revision, "device-live") : undefined,
|
|
3287
|
-
conversation: change.conversation ? this.conversationSnapshot(change.conversation) : undefined,
|
|
3288
|
-
};
|
|
3289
|
-
const events = this.revisionEvents.get(conversationId) ?? [];
|
|
3290
|
-
events.push(event);
|
|
3291
|
-
if (events.length > MAX_DELTA_EVENTS) {
|
|
3292
|
-
events.splice(0, events.length - MAX_DELTA_EVENTS);
|
|
3293
|
-
}
|
|
3294
|
-
this.revisionEvents.set(conversationId, events);
|
|
3295
|
-
return event;
|
|
3296
|
-
}
|
|
3297
|
-
|
|
3298
|
-
private storedTimeline(conversation: AgentConversation, maxItems = HISTORY_PAGE_MAX_ITEMS): AgentTimelineItem[] {
|
|
3299
|
-
if (!conversation.agentSessionId) return [];
|
|
3300
|
-
const result = conversation.provider === "codex"
|
|
3301
|
-
? loadCodexStoredTimeline(conversation.agentSessionId, conversation.id, conversation.cwd || this.input.cwd, { maxItems })
|
|
3302
|
-
: conversation.provider === "claude"
|
|
3303
|
-
? loadClaudeStoredTimeline(conversation.agentSessionId, conversation.id, { maxItems })
|
|
3304
|
-
: { items: [] };
|
|
3305
|
-
return result.items
|
|
3306
|
-
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3307
|
-
.map((item, index) => this.annotateTimelineItem(item as AgentTimelineItem, index + 1, "device-history"));
|
|
3308
|
-
}
|
|
3309
|
-
|
|
3310
|
-
private canonicalTimeline(conversation: AgentConversation, maxItems = HISTORY_PAGE_MAX_ITEMS): AgentTimelineItem[] {
|
|
3311
|
-
const merged = new Map<string, AgentTimelineItem>();
|
|
3312
|
-
for (const item of this.storedTimeline(conversation, maxItems)) {
|
|
3313
|
-
merged.set(item.id, item);
|
|
3314
|
-
}
|
|
3315
|
-
for (const item of this.timelines.get(conversation.id) ?? []) {
|
|
3316
|
-
merged.set(item.id, this.annotateTimelineItem(item, item.revision, item.source ?? "device-live"));
|
|
3317
|
-
}
|
|
3318
|
-
const items = [...merged.values()]
|
|
3319
|
-
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3320
|
-
.slice(-maxItems);
|
|
3321
|
-
this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
|
|
3322
|
-
return items;
|
|
3323
|
-
}
|
|
3324
|
-
|
|
3325
|
-
private async appServerTimeline(conversation: AgentConversation, maxItems = HISTORY_PAGE_MAX_ITEMS): Promise<AgentTimelineItem[]> {
|
|
3326
|
-
if (conversation.provider !== "codex" || !conversation.agentSessionId) return [];
|
|
3327
|
-
if (this.protocolForProvider(conversation.provider) !== "codex-app-server") return [];
|
|
3328
|
-
const client = this.clientForProvider(conversation.provider);
|
|
3329
|
-
const listTurns = (client as { listTurns?: (input: { sessionId: string; limit?: number }) => Promise<unknown> } | undefined)?.listTurns;
|
|
3330
|
-
if (typeof listTurns !== "function") return [];
|
|
3331
|
-
try {
|
|
3332
|
-
const result = await listTurns.call(client, { sessionId: conversation.agentSessionId, limit: maxItems });
|
|
3333
|
-
return this.timelineFromAppServerTurns(conversation.id, conversation.agentSessionId, result);
|
|
3334
|
-
} catch (error) {
|
|
3335
|
-
if (this.input.verbose) {
|
|
3336
|
-
process.stderr.write(`[agent:v2] thread/turns/list failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
3337
|
-
}
|
|
3338
|
-
return [];
|
|
3339
|
-
}
|
|
3340
|
-
}
|
|
3341
|
-
|
|
3342
|
-
private timelineFromAppServerTurns(
|
|
3343
|
-
conversationId: string,
|
|
3344
|
-
agentSessionId: string,
|
|
3345
|
-
value: unknown,
|
|
3346
|
-
): AgentTimelineItem[] {
|
|
3347
|
-
const raw = asRecord(value);
|
|
3348
|
-
const turns =
|
|
3349
|
-
Array.isArray(value) ? value :
|
|
3350
|
-
Array.isArray(raw?.turns) ? raw.turns :
|
|
3351
|
-
Array.isArray(raw?.items) ? raw.items :
|
|
3352
|
-
Array.isArray(raw?.entries) ? raw.entries :
|
|
3353
|
-
[];
|
|
3354
|
-
const items: AgentTimelineItem[] = [];
|
|
3355
|
-
turns.forEach((entry, index) => {
|
|
3356
|
-
const turn = asRecord(entry);
|
|
3357
|
-
if (!turn) return;
|
|
3358
|
-
const turnId = firstString(turn, ["id", "turnId", "turn_id"]) ?? `turn-${index + 1}`;
|
|
3359
|
-
const createdAt = parseTimestamp(turn.createdAt ?? turn.created_at ?? turn.startedAt ?? turn.started_at) ?? Date.now() + index;
|
|
3360
|
-
const updatedAt = parseTimestamp(turn.updatedAt ?? turn.updated_at ?? turn.completedAt ?? turn.completed_at);
|
|
3361
|
-
const userText = appServerText(turn.input ?? turn.prompt ?? turn.user ?? turn.userMessage ?? turn.request);
|
|
3362
|
-
if (userText) {
|
|
3363
|
-
items.push({
|
|
3364
|
-
id: `app-server:${agentSessionId}:${turnId}:user`,
|
|
3365
|
-
conversationId,
|
|
3366
|
-
type: "message",
|
|
3367
|
-
kind: "chat",
|
|
3368
|
-
turnId,
|
|
3369
|
-
role: "user",
|
|
3370
|
-
content: [{ type: "text", text: userText }],
|
|
3371
|
-
text: userText,
|
|
3372
|
-
createdAt,
|
|
3373
|
-
updatedAt,
|
|
3374
|
-
metadata: { source: "app-server", provider: "codex" },
|
|
3375
|
-
});
|
|
3376
|
-
}
|
|
3377
|
-
const assistantText = appServerText(turn.output ?? turn.response ?? turn.assistant ?? turn.assistantMessage ?? turn.result);
|
|
3378
|
-
if (assistantText) {
|
|
3379
|
-
items.push({
|
|
3380
|
-
id: `app-server:${agentSessionId}:${turnId}:assistant`,
|
|
3381
|
-
conversationId,
|
|
3382
|
-
type: "message",
|
|
3383
|
-
kind: "chat",
|
|
3384
|
-
turnId,
|
|
3385
|
-
role: "assistant",
|
|
3386
|
-
content: [{ type: "text", text: assistantText }],
|
|
3387
|
-
text: assistantText,
|
|
3388
|
-
createdAt: updatedAt ?? createdAt + 1,
|
|
3389
|
-
updatedAt,
|
|
3390
|
-
metadata: { source: "app-server", provider: "codex" },
|
|
3391
|
-
});
|
|
3392
|
-
}
|
|
3393
|
-
const nestedItems = Array.isArray(turn.items) ? turn.items : Array.isArray(turn.messages) ? turn.messages : [];
|
|
3394
|
-
for (const nested of nestedItems) {
|
|
3395
|
-
const nestedRecord = asRecord(nested);
|
|
3396
|
-
if (!nestedRecord) continue;
|
|
3397
|
-
const text = appServerText(nestedRecord.content ?? nestedRecord.text ?? nestedRecord.message);
|
|
3398
|
-
const role = nestedRecord.role === "user" || nestedRecord.role === "assistant" || nestedRecord.role === "system"
|
|
3399
|
-
? nestedRecord.role
|
|
3400
|
-
: undefined;
|
|
3401
|
-
if (!text || !role) continue;
|
|
3402
|
-
const itemId = firstString(nestedRecord, ["id", "itemId", "messageId"]) ?? `${role}-${items.length + 1}`;
|
|
3403
|
-
items.push({
|
|
3404
|
-
id: `app-server:${agentSessionId}:${turnId}:${itemId}`,
|
|
3405
|
-
conversationId,
|
|
3406
|
-
type: "message",
|
|
3407
|
-
kind: "chat",
|
|
3408
|
-
turnId,
|
|
3409
|
-
itemId,
|
|
3410
|
-
role,
|
|
3411
|
-
content: [{ type: "text", text }],
|
|
3412
|
-
text,
|
|
3413
|
-
createdAt: parseTimestamp(nestedRecord.createdAt ?? nestedRecord.created_at) ?? createdAt + items.length,
|
|
3414
|
-
updatedAt: parseTimestamp(nestedRecord.updatedAt ?? nestedRecord.updated_at),
|
|
3415
|
-
metadata: { source: "app-server", provider: "codex" },
|
|
3416
|
-
});
|
|
3417
|
-
}
|
|
3418
|
-
});
|
|
3419
|
-
return items
|
|
3420
|
-
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3421
|
-
.map((item, index) => this.annotateTimelineItem(item, index + 1, "app-server"));
|
|
3422
|
-
}
|
|
3423
|
-
|
|
3424
|
-
private mergeCanonicalTimelineItems(items: AgentTimelineItem[], maxItems: number): AgentTimelineItem[] {
|
|
3425
|
-
const byKey = new Map<string, AgentTimelineItem>();
|
|
3426
|
-
for (const item of items.sort((a, b) => a.createdAt - b.createdAt)) {
|
|
3427
|
-
const key = item.type === "message" && item.role && item.text
|
|
3428
|
-
? `${item.role}:${item.text.replace(/\s+/g, " ").trim().slice(0, 500)}`
|
|
3429
|
-
: item.id;
|
|
3430
|
-
const existing = byKey.get(key);
|
|
3431
|
-
if (!existing || item.source === "app-server" || (item.updatedAt ?? item.createdAt) >= (existing.updatedAt ?? existing.createdAt)) {
|
|
3432
|
-
byKey.set(key, item);
|
|
3433
|
-
}
|
|
3434
|
-
}
|
|
3435
|
-
return [...byKey.values()]
|
|
3436
|
-
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3437
|
-
.slice(-maxItems);
|
|
3438
|
-
}
|
|
3439
|
-
|
|
3440
|
-
private latestSnapshot(conversationId: string): {
|
|
3441
|
-
items: AgentTimelineItem[];
|
|
3442
|
-
cursor?: string;
|
|
3443
|
-
hasMore: boolean;
|
|
3444
|
-
} {
|
|
3445
|
-
const timeline = this.timelines.get(conversationId) ?? [];
|
|
3446
|
-
const start = Math.max(0, timeline.length - MAX_SNAPSHOT_ITEMS);
|
|
3447
|
-
return {
|
|
3448
|
-
items: timeline.slice(start).map((item) => snapshotTimelineItem(item)),
|
|
3449
|
-
cursor: start > 0 ? String(start) : undefined,
|
|
3450
|
-
hasMore: start > 0,
|
|
3451
|
-
};
|
|
3452
|
-
}
|
|
3453
|
-
|
|
3454
2791
|
private addItem(conversationId: string, item: AgentTimelineItem): void {
|
|
3455
2792
|
this.rememberItemConversationId(conversationId, item);
|
|
3456
2793
|
const timeline = this.timelines.get(conversationId) ?? [];
|
|
@@ -3488,8 +2825,6 @@ export class AgentWorkspaceProxy {
|
|
|
3488
2825
|
...existing,
|
|
3489
2826
|
...toolCall,
|
|
3490
2827
|
id: targetToolId,
|
|
3491
|
-
input: toolCall.input ?? existing?.input,
|
|
3492
|
-
output: toolCall.output ?? existing?.output,
|
|
3493
2828
|
createdAt: existing?.createdAt ?? toolCall.createdAt ?? Date.now(),
|
|
3494
2829
|
};
|
|
3495
2830
|
this.toolConversationIds.set(toolCall.id, conversationId);
|
|
@@ -3568,7 +2903,6 @@ export class AgentWorkspaceProxy {
|
|
|
3568
2903
|
conversation.title = conversation.title ?? target.title;
|
|
3569
2904
|
conversation.model = conversation.model ?? target.model;
|
|
3570
2905
|
conversation.reasoningEffort = conversation.reasoningEffort ?? target.reasoningEffort;
|
|
3571
|
-
conversation.serviceTier = conversation.serviceTier ?? target.serviceTier;
|
|
3572
2906
|
conversation.permissionMode = conversation.permissionMode ?? target.permissionMode;
|
|
3573
2907
|
conversation.lastMessagePreview = conversation.lastMessagePreview ?? target.lastMessagePreview;
|
|
3574
2908
|
conversation.createdAt = Math.min(conversation.createdAt, target.createdAt);
|
|
@@ -3585,17 +2919,6 @@ export class AgentWorkspaceProxy {
|
|
|
3585
2919
|
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3586
2920
|
.slice(-MAX_TIMELINE_ITEMS),
|
|
3587
2921
|
);
|
|
3588
|
-
const oldRevision = this.conversationRevisions.get(oldId) ?? 0;
|
|
3589
|
-
if (oldRevision > 0) {
|
|
3590
|
-
this.conversationRevisions.delete(oldId);
|
|
3591
|
-
this.setRevisionFloor(newId, oldRevision);
|
|
3592
|
-
}
|
|
3593
|
-
const oldEvents = this.revisionEvents.get(oldId);
|
|
3594
|
-
if (oldEvents) {
|
|
3595
|
-
this.revisionEvents.delete(oldId);
|
|
3596
|
-
const existingEvents = this.revisionEvents.get(newId) ?? [];
|
|
3597
|
-
this.revisionEvents.set(newId, [...existingEvents, ...oldEvents].slice(-MAX_DELTA_EVENTS));
|
|
3598
|
-
}
|
|
3599
2922
|
|
|
3600
2923
|
for (const [agentSessionId, conversationId] of this.conversationByAgentSessionId) {
|
|
3601
2924
|
if (conversationId === oldId) {
|
|
@@ -3631,48 +2954,18 @@ export class AgentWorkspaceProxy {
|
|
|
3631
2954
|
|
|
3632
2955
|
private emitItem(conversationId: string, item: AgentTimelineItem): void {
|
|
3633
2956
|
const conversation = this.conversations.get(conversationId);
|
|
3634
|
-
const itemSnapshot = snapshotTimelineItem(item, { stripImages: false });
|
|
3635
|
-
const event = this.recordRevisionEvent(conversationId, { conversation, item: itemSnapshot });
|
|
3636
2957
|
this.input.send(createEnvelope({
|
|
3637
2958
|
type: "agent.v2.event",
|
|
3638
|
-
|
|
3639
|
-
payload: {
|
|
3640
|
-
conversationId,
|
|
3641
|
-
conversation: event.conversation,
|
|
3642
|
-
item: event.item,
|
|
3643
|
-
revision: event.revision,
|
|
3644
|
-
source: "device-live",
|
|
3645
|
-
canonical: true,
|
|
3646
|
-
},
|
|
2959
|
+
sessionId: this.input.sessionId,
|
|
2960
|
+
payload: { conversationId, conversation, item },
|
|
3647
2961
|
}));
|
|
3648
2962
|
}
|
|
3649
2963
|
|
|
3650
2964
|
private emitConversation(conversation: AgentConversation): void {
|
|
3651
|
-
const event = this.recordRevisionEvent(conversation.id, { conversation });
|
|
3652
2965
|
this.input.send(createEnvelope({
|
|
3653
2966
|
type: "agent.v2.event",
|
|
3654
|
-
|
|
3655
|
-
payload: {
|
|
3656
|
-
conversationId: conversation.id,
|
|
3657
|
-
conversation: event.conversation,
|
|
3658
|
-
revision: event.revision,
|
|
3659
|
-
source: "device-live",
|
|
3660
|
-
canonical: true,
|
|
3661
|
-
},
|
|
3662
|
-
}));
|
|
3663
|
-
this.input.send(createEnvelope({
|
|
3664
|
-
type: "agent.v2.running_state",
|
|
3665
|
-
hostDeviceId: this.input.hostDeviceId,
|
|
3666
|
-
payload: {
|
|
3667
|
-
conversationId: conversation.id,
|
|
3668
|
-
status: conversation.status,
|
|
3669
|
-
runningTurnId: this.currentTurnIds.get(conversation.id),
|
|
3670
|
-
revision: event.revision,
|
|
3671
|
-
error: conversation.status === "error" ? conversation.lastMessagePreview : undefined,
|
|
3672
|
-
updatedAt: conversation.lastActivityAt,
|
|
3673
|
-
source: "device-live",
|
|
3674
|
-
canonical: true,
|
|
3675
|
-
},
|
|
2967
|
+
sessionId: this.input.sessionId,
|
|
2968
|
+
payload: { conversationId: conversation.id, conversation },
|
|
3676
2969
|
}));
|
|
3677
2970
|
}
|
|
3678
2971
|
|
|
@@ -3714,186 +3007,21 @@ export class AgentWorkspaceProxy {
|
|
|
3714
3007
|
}
|
|
3715
3008
|
|
|
3716
3009
|
private sendSnapshot(conversationId?: string): void {
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
const conversation = this.conversations.get(this.activeConversationId);
|
|
3722
|
-
if (conversation) this.hydrateStoredTimeline(conversation);
|
|
3723
|
-
}
|
|
3724
|
-
const conversations = [...this.conversations.values()].map((conversation) => this.conversationSnapshot(conversation));
|
|
3725
|
-
const snapshot = conversationId ? this.latestSnapshot(conversationId) : { items: [], cursor: undefined, hasMore: false };
|
|
3010
|
+
const conversations = [...this.conversations.values()];
|
|
3011
|
+
const items = conversationId
|
|
3012
|
+
? this.timelines.get(conversationId) ?? []
|
|
3013
|
+
: [...this.timelines.values()].flat();
|
|
3726
3014
|
this.input.send(createEnvelope({
|
|
3727
3015
|
type: "agent.v2.snapshot",
|
|
3728
|
-
|
|
3016
|
+
sessionId: this.input.sessionId,
|
|
3729
3017
|
payload: {
|
|
3730
3018
|
conversations,
|
|
3731
3019
|
activeConversationId: this.activeConversationId,
|
|
3732
|
-
items
|
|
3733
|
-
revision: conversationId ? this.getRevision(conversationId) : undefined,
|
|
3734
|
-
cursor: snapshot.cursor,
|
|
3735
|
-
hasMore: snapshot.hasMore,
|
|
3736
|
-
source: "device-history",
|
|
3737
|
-
canonical: true,
|
|
3738
|
-
},
|
|
3739
|
-
}));
|
|
3740
|
-
}
|
|
3741
|
-
|
|
3742
|
-
private async sendHistoryPage(payload: {
|
|
3743
|
-
conversationId: string;
|
|
3744
|
-
cursor?: string;
|
|
3745
|
-
limit?: number;
|
|
3746
|
-
direction?: "older" | "newer";
|
|
3747
|
-
}): Promise<void> {
|
|
3748
|
-
const conversation = this.conversations.get(payload.conversationId);
|
|
3749
|
-
if (!conversation) return;
|
|
3750
|
-
const limit = Math.min(Math.max(payload.limit ?? MAX_SNAPSHOT_ITEMS, 1), 200);
|
|
3751
|
-
const items = this.mergeCanonicalTimelineItems([
|
|
3752
|
-
...this.canonicalTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
|
|
3753
|
-
...await this.appServerTimeline(conversation, HISTORY_PAGE_MAX_ITEMS),
|
|
3754
|
-
], HISTORY_PAGE_MAX_ITEMS);
|
|
3755
|
-
this.timelines.set(
|
|
3756
|
-
conversation.id,
|
|
3757
|
-
items.slice(-MAX_TIMELINE_ITEMS).map((item) => this.annotateTimelineItem(item, item.revision, item.source ?? "device-history")),
|
|
3758
|
-
);
|
|
3759
|
-
for (const item of this.timelines.get(conversation.id) ?? []) {
|
|
3760
|
-
this.rememberItemConversationId(conversation.id, item);
|
|
3761
|
-
}
|
|
3762
|
-
|
|
3763
|
-
const direction = payload.direction ?? "older";
|
|
3764
|
-
let page: AgentTimelineItem[];
|
|
3765
|
-
let cursor: string | undefined;
|
|
3766
|
-
let hasMore = false;
|
|
3767
|
-
if (direction === "newer") {
|
|
3768
|
-
const start = clampHistoryCursor(payload.cursor, 0, items.length);
|
|
3769
|
-
const end = Math.min(items.length, start + limit);
|
|
3770
|
-
page = items.slice(start, end);
|
|
3771
|
-
hasMore = end < items.length;
|
|
3772
|
-
cursor = hasMore ? String(end) : undefined;
|
|
3773
|
-
} else {
|
|
3774
|
-
const end = clampHistoryCursor(payload.cursor, items.length, items.length);
|
|
3775
|
-
const start = Math.max(0, end - limit);
|
|
3776
|
-
page = items.slice(start, end);
|
|
3777
|
-
hasMore = start > 0;
|
|
3778
|
-
cursor = hasMore ? String(start) : undefined;
|
|
3779
|
-
}
|
|
3780
|
-
conversation.historyComplete = !hasMore;
|
|
3781
|
-
const revision = this.setRevisionFloor(conversation.id, Math.max(this.getRevision(conversation.id), items.length));
|
|
3782
|
-
this.input.send(createEnvelope({
|
|
3783
|
-
type: "agent.v2.history.page",
|
|
3784
|
-
hostDeviceId: this.input.hostDeviceId,
|
|
3785
|
-
payload: {
|
|
3786
|
-
conversationId: conversation.id,
|
|
3787
|
-
conversation: this.conversationSnapshot(conversation),
|
|
3788
|
-
items: page.map((item) => snapshotTimelineItem(item)),
|
|
3789
|
-
revision,
|
|
3790
|
-
cursor,
|
|
3791
|
-
hasMore,
|
|
3792
|
-
source: "device-history",
|
|
3793
|
-
canonical: true,
|
|
3020
|
+
items,
|
|
3794
3021
|
},
|
|
3795
3022
|
}));
|
|
3796
3023
|
}
|
|
3797
3024
|
|
|
3798
|
-
private sendDelta(payload: {
|
|
3799
|
-
conversationId: string;
|
|
3800
|
-
sinceRevision?: number;
|
|
3801
|
-
limit?: number;
|
|
3802
|
-
}): void {
|
|
3803
|
-
const conversation = this.conversations.get(payload.conversationId);
|
|
3804
|
-
if (!conversation) return;
|
|
3805
|
-
this.hydrateStoredTimeline(conversation);
|
|
3806
|
-
const sinceRevision = payload.sinceRevision ?? 0;
|
|
3807
|
-
const limit = Math.min(Math.max(payload.limit ?? 100, 1), 500);
|
|
3808
|
-
const events = this.revisionEvents.get(conversation.id) ?? [];
|
|
3809
|
-
const oldestAvailable = events[0]?.revision ?? this.getRevision(conversation.id);
|
|
3810
|
-
const newestRevision = this.getRevision(conversation.id);
|
|
3811
|
-
const reset = sinceRevision > 0 &&
|
|
3812
|
-
(
|
|
3813
|
-
sinceRevision > newestRevision ||
|
|
3814
|
-
(
|
|
3815
|
-
sinceRevision < newestRevision &&
|
|
3816
|
-
(events.length === 0 || sinceRevision < oldestAvailable - 1)
|
|
3817
|
-
)
|
|
3818
|
-
);
|
|
3819
|
-
|
|
3820
|
-
if (reset) {
|
|
3821
|
-
const snapshot = this.latestSnapshot(conversation.id);
|
|
3822
|
-
this.input.send(createEnvelope({
|
|
3823
|
-
type: "agent.v2.delta",
|
|
3824
|
-
hostDeviceId: this.input.hostDeviceId,
|
|
3825
|
-
payload: {
|
|
3826
|
-
conversationId: conversation.id,
|
|
3827
|
-
conversation: this.conversationSnapshot(conversation),
|
|
3828
|
-
items: snapshot.items,
|
|
3829
|
-
sinceRevision,
|
|
3830
|
-
revision: newestRevision,
|
|
3831
|
-
reset: true,
|
|
3832
|
-
cursor: snapshot.cursor,
|
|
3833
|
-
hasMore: snapshot.hasMore,
|
|
3834
|
-
source: "device-history",
|
|
3835
|
-
canonical: true,
|
|
3836
|
-
},
|
|
3837
|
-
}));
|
|
3838
|
-
return;
|
|
3839
|
-
}
|
|
3840
|
-
|
|
3841
|
-
const changed = events
|
|
3842
|
-
.filter((event) => event.revision > sinceRevision)
|
|
3843
|
-
.slice(-limit);
|
|
3844
|
-
const itemsById = new Map<string, AgentTimelineItem>();
|
|
3845
|
-
for (const event of changed) {
|
|
3846
|
-
if (event.item) itemsById.set(event.item.id, event.item);
|
|
3847
|
-
}
|
|
3848
|
-
this.input.send(createEnvelope({
|
|
3849
|
-
type: "agent.v2.delta",
|
|
3850
|
-
hostDeviceId: this.input.hostDeviceId,
|
|
3851
|
-
payload: {
|
|
3852
|
-
conversationId: conversation.id,
|
|
3853
|
-
conversation: this.conversationSnapshot(conversation),
|
|
3854
|
-
items: [...itemsById.values()].map((item) => snapshotTimelineItem(item)),
|
|
3855
|
-
sinceRevision,
|
|
3856
|
-
revision: newestRevision,
|
|
3857
|
-
reset: false,
|
|
3858
|
-
hasMore: changed.length === limit && events.some((event) => event.revision > sinceRevision && event.revision < changed[0]!.revision),
|
|
3859
|
-
source: "device-live",
|
|
3860
|
-
canonical: true,
|
|
3861
|
-
},
|
|
3862
|
-
}));
|
|
3863
|
-
}
|
|
3864
|
-
|
|
3865
|
-
private hydrateStoredTimeline(conversation: AgentConversation): void {
|
|
3866
|
-
if (!conversation.agentSessionId) return;
|
|
3867
|
-
const existing = this.timelines.get(conversation.id) ?? [];
|
|
3868
|
-
if (existing.length > 0) {
|
|
3869
|
-
this.setRevisionFloor(conversation.id, Math.max(
|
|
3870
|
-
existing.length,
|
|
3871
|
-
...existing.map((item) => item.revision ?? 0),
|
|
3872
|
-
));
|
|
3873
|
-
return;
|
|
3874
|
-
}
|
|
3875
|
-
const stored = this.storedTimeline(conversation, MAX_TIMELINE_ITEMS);
|
|
3876
|
-
conversation.historyComplete = stored.length <= MAX_SNAPSHOT_ITEMS;
|
|
3877
|
-
if (stored.length === 0) {
|
|
3878
|
-
this.setRevisionFloor(conversation.id, this.getRevision(conversation.id));
|
|
3879
|
-
return;
|
|
3880
|
-
}
|
|
3881
|
-
const items = stored
|
|
3882
|
-
.sort((a, b) => a.createdAt - b.createdAt)
|
|
3883
|
-
.slice(-MAX_TIMELINE_ITEMS);
|
|
3884
|
-
this.timelines.set(conversation.id, items);
|
|
3885
|
-
for (const item of items) this.rememberItemConversationId(conversation.id, item);
|
|
3886
|
-
this.setRevisionFloor(conversation.id, Math.max(items.length, ...items.map((item) => item.revision ?? 0)));
|
|
3887
|
-
const lastMessage = [...items].reverse().find((item) => item.text?.trim());
|
|
3888
|
-
if (lastMessage?.text && !conversation.lastMessagePreview) {
|
|
3889
|
-
conversation.lastMessagePreview = previewText(lastMessage.text);
|
|
3890
|
-
}
|
|
3891
|
-
const lastActivityAt = items.at(-1)?.createdAt;
|
|
3892
|
-
if (lastActivityAt) {
|
|
3893
|
-
conversation.lastActivityAt = Math.max(conversation.lastActivityAt, lastActivityAt);
|
|
3894
|
-
}
|
|
3895
|
-
}
|
|
3896
|
-
|
|
3897
3025
|
private conversationIdFromParams(params: unknown): string | undefined {
|
|
3898
3026
|
const raw = asRecord(params);
|
|
3899
3027
|
if (!raw) return undefined;
|
|
@@ -3951,8 +3079,6 @@ export class AgentWorkspaceProxy {
|
|
|
3951
3079
|
private rememberTurnConversationId(conversationId: string, turnId: string): void {
|
|
3952
3080
|
this.currentTurnIds.set(conversationId, turnId);
|
|
3953
3081
|
this.turnConversationIds.set(turnId, conversationId);
|
|
3954
|
-
const conversation = this.conversations.get(conversationId);
|
|
3955
|
-
if (conversation) conversation.runningTurnId = turnId;
|
|
3956
3082
|
}
|
|
3957
3083
|
|
|
3958
3084
|
private forgetCurrentTurn(conversationId: string, turnId?: string): void {
|
|
@@ -3960,8 +3086,6 @@ export class AgentWorkspaceProxy {
|
|
|
3960
3086
|
this.currentTurnIds.delete(conversationId);
|
|
3961
3087
|
if (turnId) this.turnConversationIds.delete(turnId);
|
|
3962
3088
|
if (currentTurnId && currentTurnId !== turnId) this.turnConversationIds.delete(currentTurnId);
|
|
3963
|
-
const conversation = this.conversations.get(conversationId);
|
|
3964
|
-
if (conversation) conversation.runningTurnId = undefined;
|
|
3965
3089
|
}
|
|
3966
3090
|
|
|
3967
3091
|
private rememberItemConversationId(conversationId: string, item: AgentTimelineItem): void {
|
|
@@ -4003,54 +3127,32 @@ export class AgentWorkspaceProxy {
|
|
|
4003
3127
|
|
|
4004
3128
|
private cancelPendingPermissions(conversationId?: string): void {
|
|
4005
3129
|
for (const [requestId, waiter] of this.permissionWaiters) {
|
|
4006
|
-
const ownerConversationId = this.conversationIdForPermissionRequest(requestId);
|
|
4007
|
-
if (conversationId && ownerConversationId && ownerConversationId !== conversationId) continue;
|
|
4008
3130
|
clearTimeout(waiter.timer);
|
|
4009
3131
|
waiter.resolve(formatPermissionResponse(
|
|
4010
3132
|
this.permissionSources.get(requestId),
|
|
4011
3133
|
"cancelled",
|
|
4012
3134
|
"cancelled",
|
|
4013
3135
|
));
|
|
4014
|
-
if (ownerConversationId) {
|
|
4015
|
-
this.markPermission(ownerConversationId, requestId, {
|
|
4016
|
-
permissionOutcome: "cancelled",
|
|
4017
|
-
optionId: "cancelled",
|
|
4018
|
-
permissionLive: false,
|
|
4019
|
-
permissionPending: false,
|
|
4020
|
-
permissionExpired: false,
|
|
4021
|
-
permissionError: "已停止",
|
|
4022
|
-
});
|
|
4023
|
-
}
|
|
4024
|
-
this.permissionWaiters.delete(requestId);
|
|
4025
3136
|
this.pendingPermissions.delete(requestId);
|
|
4026
3137
|
this.permissionSources.delete(requestId);
|
|
4027
3138
|
}
|
|
3139
|
+
this.permissionWaiters.clear();
|
|
4028
3140
|
for (const [requestId, waiter] of this.structuredInputWaiters) {
|
|
4029
|
-
const pending = this.pendingStructuredInputs.get(requestId);
|
|
4030
|
-
if (conversationId && pending?.conversationId && pending.conversationId !== conversationId) continue;
|
|
4031
3141
|
clearTimeout(waiter.timer);
|
|
4032
3142
|
waiter.resolve(formatStructuredInputResponse({}));
|
|
3143
|
+
const pending = this.pendingStructuredInputs.get(requestId);
|
|
4033
3144
|
if (pending) {
|
|
4034
3145
|
this.markStructuredInput(pending.conversationId, requestId, {
|
|
4035
3146
|
inputPending: false,
|
|
4036
3147
|
inputError: "已停止",
|
|
4037
3148
|
});
|
|
4038
3149
|
}
|
|
4039
|
-
this.structuredInputWaiters.delete(requestId);
|
|
4040
3150
|
this.pendingStructuredInputs.delete(requestId);
|
|
4041
3151
|
}
|
|
3152
|
+
this.structuredInputWaiters.clear();
|
|
4042
3153
|
if (conversationId) this.updateConversationStatus(conversationId, "idle");
|
|
4043
3154
|
}
|
|
4044
3155
|
|
|
4045
|
-
private conversationIdForPermissionRequest(requestId: string): string | undefined {
|
|
4046
|
-
for (const [conversationId, timeline] of this.timelines) {
|
|
4047
|
-
if (timeline.some((item) => item.type === "permission" && item.permission?.requestId === requestId)) {
|
|
4048
|
-
return conversationId;
|
|
4049
|
-
}
|
|
4050
|
-
}
|
|
4051
|
-
return undefined;
|
|
4052
|
-
}
|
|
4053
|
-
|
|
4054
3156
|
private extractSessionId(value: unknown): string | undefined {
|
|
4055
3157
|
const raw = asRecord(value);
|
|
4056
3158
|
if (!raw) return undefined;
|