linkshell-cli 0.2.99 → 0.2.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/src/runtime/acp/acp-client.d.ts +1 -0
- package/dist/cli/src/runtime/acp/acp-client.js +17 -3
- package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-session.d.ts +1 -0
- package/dist/cli/src/runtime/acp/agent-session.js +73 -19
- package/dist/cli/src/runtime/acp/agent-session.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +6 -1
- package/dist/cli/src/runtime/acp/agent-workspace.js +346 -57
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +51 -0
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +318 -0
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -0
- package/dist/cli/src/runtime/acp/provider-resolver.d.ts +1 -1
- package/dist/cli/src/runtime/acp/provider-resolver.js +22 -2
- package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
- package/dist/cli/src/runtime/bridge-session.js +1 -0
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +1885 -265
- package/dist/shared-protocol/src/index.js +36 -10
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +8 -5
- package/src/runtime/acp/acp-client.ts +18 -3
- package/src/runtime/acp/agent-session.ts +68 -14
- package/src/runtime/acp/agent-workspace.ts +376 -54
- package/src/runtime/acp/claude-sdk-client.ts +372 -0
- package/src/runtime/acp/provider-resolver.ts +24 -3
- package/src/runtime/bridge-session.ts +1 -0
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type Envelope,
|
|
6
6
|
} from "@linkshell/protocol";
|
|
7
7
|
import { AcpClient } from "./acp-client.js";
|
|
8
|
+
import { ClaudeSdkClient } from "./claude-sdk-client.js";
|
|
8
9
|
import { ClaudeStreamJsonClient } from "./claude-stream-json-client.js";
|
|
9
10
|
import type { AgentProtocol, AgentProvider } from "./provider-resolver.js";
|
|
10
11
|
import { resolveAgentCommand } from "./provider-resolver.js";
|
|
@@ -172,6 +173,7 @@ interface PendingPermissionWaiter {
|
|
|
172
173
|
interface PendingStructuredInputWaiter {
|
|
173
174
|
resolve: (value: unknown) => void;
|
|
174
175
|
timer: ReturnType<typeof setTimeout>;
|
|
176
|
+
source?: string;
|
|
175
177
|
}
|
|
176
178
|
|
|
177
179
|
const PERMISSION_TIMEOUT_MS = 5 * 60_000;
|
|
@@ -392,9 +394,18 @@ function commandExecutionFromTool(toolCall: AgentToolCall): AgentCommandExecutio
|
|
|
392
394
|
};
|
|
393
395
|
}
|
|
394
396
|
|
|
395
|
-
function
|
|
396
|
-
const
|
|
397
|
-
|
|
397
|
+
function fileChangeFromStructuredInput(input: string | undefined): AgentFileChangeEntry[] {
|
|
398
|
+
const raw = input?.trim();
|
|
399
|
+
if (!raw) return [];
|
|
400
|
+
try {
|
|
401
|
+
const parsed = JSON.parse(raw);
|
|
402
|
+
if (parsed && typeof parsed === "object") {
|
|
403
|
+
return fileChangeEntriesFromItem(parsed as Record<string, unknown>);
|
|
404
|
+
}
|
|
405
|
+
} catch {
|
|
406
|
+
// Fall through to line parser.
|
|
407
|
+
}
|
|
408
|
+
return raw
|
|
398
409
|
.split("\n")
|
|
399
410
|
.map((line) => line.trim())
|
|
400
411
|
.filter(Boolean)
|
|
@@ -406,6 +417,11 @@ function fileChangeFromTool(toolCall: AgentToolCall): AgentFileChange | undefine
|
|
|
406
417
|
return entry;
|
|
407
418
|
})
|
|
408
419
|
.filter((entry) => entry.path.length > 0);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function fileChangeFromTool(toolCall: AgentToolCall): AgentFileChange | undefined {
|
|
423
|
+
const diff = toolCall.output && looksLikeDiff(toolCall.output) ? toolCall.output : undefined;
|
|
424
|
+
const entries = fileChangeFromStructuredInput(toolCall.input);
|
|
409
425
|
if (entries.length === 0 && !diff && !toolCall.output) return undefined;
|
|
410
426
|
return {
|
|
411
427
|
entries,
|
|
@@ -662,35 +678,119 @@ interface AgentModelOption {
|
|
|
662
678
|
label: string;
|
|
663
679
|
}
|
|
664
680
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
681
|
+
interface ProviderRuntimeCapabilities {
|
|
682
|
+
models?: AgentModelOption[];
|
|
683
|
+
defaultModel?: string;
|
|
684
|
+
reasoningEfforts?: string[];
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
688
|
+
const ALL_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"] as const;
|
|
689
|
+
|
|
690
|
+
function parseModelListCapabilities(value: unknown): ProviderRuntimeCapabilities | undefined {
|
|
691
|
+
const raw = asRecord(value);
|
|
692
|
+
const modelsValue =
|
|
693
|
+
Array.isArray(value) ? value :
|
|
694
|
+
Array.isArray(raw?.models) ? raw.models :
|
|
695
|
+
Array.isArray(raw?.items) ? raw.items :
|
|
696
|
+
Array.isArray(raw?.modelOptions) ? raw.modelOptions :
|
|
697
|
+
[];
|
|
698
|
+
const models = modelsValue
|
|
699
|
+
.map((entry, index) => {
|
|
700
|
+
const model = asRecord(entry);
|
|
701
|
+
if (!model) {
|
|
702
|
+
return typeof entry === "string" && entry
|
|
703
|
+
? { id: entry, label: entry }
|
|
704
|
+
: undefined;
|
|
705
|
+
}
|
|
706
|
+
const modelId = firstString(model, ["id", "model", "name", "value"]) ?? `model-${index + 1}`;
|
|
707
|
+
const label = firstString(model, ["label", "title", "displayName", "name"]) ?? modelId;
|
|
708
|
+
return { id: modelId, label };
|
|
709
|
+
})
|
|
710
|
+
.filter((entry): entry is AgentModelOption => Boolean(entry));
|
|
711
|
+
const defaultModel =
|
|
712
|
+
firstString(raw, ["defaultModel", "default_model", "currentModel"]) ??
|
|
713
|
+
firstString(asRecord(raw?.defaults), ["model"]);
|
|
714
|
+
const effortsValue =
|
|
715
|
+
Array.isArray(raw?.reasoningEfforts) ? raw.reasoningEfforts :
|
|
716
|
+
Array.isArray(raw?.reasoning_efforts) ? raw.reasoning_efforts :
|
|
717
|
+
Array.isArray(raw?.efforts) ? raw.efforts :
|
|
718
|
+
undefined;
|
|
719
|
+
const reasoningEfforts = effortsValue
|
|
720
|
+
?.filter((entry): entry is string => typeof entry === "string" && ALL_REASONING_EFFORTS.includes(entry as typeof ALL_REASONING_EFFORTS[number]));
|
|
721
|
+
if (models.length === 0 && !defaultModel && !reasoningEfforts?.length) return undefined;
|
|
722
|
+
return {
|
|
723
|
+
...(models.length > 0 ? { models: [{ id: "default", label: "默认模型" }, ...models] } : {}),
|
|
724
|
+
...(defaultModel ? { defaultModel } : {}),
|
|
725
|
+
...(reasoningEfforts?.length ? { reasoningEfforts } : {}),
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function parseTimestamp(value: unknown): number | undefined {
|
|
730
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
731
|
+
return value > 10_000_000_000 ? value : value * 1000;
|
|
673
732
|
}
|
|
674
|
-
if (
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
733
|
+
if (typeof value === "string" && value.trim()) {
|
|
734
|
+
const parsed = Date.parse(value);
|
|
735
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
736
|
+
}
|
|
737
|
+
return undefined;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function parseRemoteSessions(value: unknown): Array<{
|
|
741
|
+
id: string;
|
|
742
|
+
cwd?: string;
|
|
743
|
+
title?: string;
|
|
744
|
+
model?: string;
|
|
745
|
+
createdAt?: number;
|
|
746
|
+
lastActivityAt?: number;
|
|
747
|
+
}> {
|
|
748
|
+
const raw = asRecord(value);
|
|
749
|
+
const sessionsValue =
|
|
750
|
+
Array.isArray(value) ? value :
|
|
751
|
+
Array.isArray(raw?.threads) ? raw.threads :
|
|
752
|
+
Array.isArray(raw?.sessions) ? raw.sessions :
|
|
753
|
+
Array.isArray(raw?.items) ? raw.items :
|
|
754
|
+
[];
|
|
755
|
+
const result: Array<{
|
|
756
|
+
id: string;
|
|
757
|
+
cwd?: string;
|
|
758
|
+
title?: string;
|
|
759
|
+
model?: string;
|
|
760
|
+
createdAt?: number;
|
|
761
|
+
lastActivityAt?: number;
|
|
762
|
+
}> = [];
|
|
763
|
+
for (const entry of sessionsValue) {
|
|
764
|
+
const session = asRecord(entry);
|
|
765
|
+
if (!session) {
|
|
766
|
+
if (typeof entry === "string" && entry) result.push({ id: entry });
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
const nestedThread = asRecord(session.thread);
|
|
770
|
+
const source = nestedThread ?? session;
|
|
771
|
+
const id = firstString(source, ["id", "threadId", "sessionId", "agentSessionId"]);
|
|
772
|
+
if (!id) continue;
|
|
773
|
+
result.push({
|
|
774
|
+
id,
|
|
775
|
+
cwd: firstString(source, ["cwd", "workingDirectory", "workspacePath"]),
|
|
776
|
+
title: firstString(source, ["title", "name", "summary"]),
|
|
777
|
+
model: firstString(source, ["model", "modelId"]),
|
|
778
|
+
createdAt: parseTimestamp(source.createdAt ?? source.created_at),
|
|
779
|
+
lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
|
|
780
|
+
});
|
|
682
781
|
}
|
|
683
|
-
return
|
|
782
|
+
return result;
|
|
684
783
|
}
|
|
685
784
|
|
|
686
785
|
export class AgentWorkspaceProxy {
|
|
687
|
-
private clients = new Map<AgentProvider, AcpClient | ClaudeStreamJsonClient>();
|
|
786
|
+
private clients = new Map<AgentProvider, AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient>();
|
|
688
787
|
private agentProtocols = new Map<AgentProvider, AgentProtocol>();
|
|
788
|
+
private providerCapabilities = new Map<AgentProvider, ProviderRuntimeCapabilities>();
|
|
689
789
|
private initialized = false;
|
|
690
790
|
private status: AgentStatus = "unavailable";
|
|
691
791
|
private error: string | undefined;
|
|
692
792
|
private activeConversationId: string | undefined;
|
|
693
|
-
private
|
|
793
|
+
private currentTurnIds = new Map<string, string>();
|
|
694
794
|
private conversations = new Map<string, AgentConversation>();
|
|
695
795
|
private conversationByAgentSessionId = new Map<string, string>();
|
|
696
796
|
private timelines = new Map<string, AgentTimelineItem[]>();
|
|
@@ -726,6 +826,7 @@ export class AgentWorkspaceProxy {
|
|
|
726
826
|
}
|
|
727
827
|
case "agent.v2.conversation.list": {
|
|
728
828
|
const payload = parseTypedPayload("agent.v2.conversation.list", envelope.payload);
|
|
829
|
+
await this.syncProviderSessions();
|
|
729
830
|
const conversations = [...this.conversations.values()].filter((conversation) =>
|
|
730
831
|
payload.includeArchived ? true : !conversation.archived,
|
|
731
832
|
);
|
|
@@ -753,9 +854,9 @@ export class AgentWorkspaceProxy {
|
|
|
753
854
|
const cancelClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
|
|
754
855
|
cancelClient?.cancel({
|
|
755
856
|
sessionId: conversation?.agentSessionId,
|
|
756
|
-
turnId: this.
|
|
857
|
+
turnId: this.currentTurnIds.get(payload.conversationId),
|
|
757
858
|
});
|
|
758
|
-
this.
|
|
859
|
+
this.currentTurnIds.delete(payload.conversationId);
|
|
759
860
|
this.updateConversationStatus(payload.conversationId, "idle");
|
|
760
861
|
this.emitStatus(payload.conversationId, "idle", "已停止");
|
|
761
862
|
break;
|
|
@@ -780,7 +881,7 @@ export class AgentWorkspaceProxy {
|
|
|
780
881
|
this.clients.clear();
|
|
781
882
|
}
|
|
782
883
|
|
|
783
|
-
private clientForProvider(provider: AgentProvider): AcpClient | ClaudeStreamJsonClient | undefined {
|
|
884
|
+
private clientForProvider(provider: AgentProvider): AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient | undefined {
|
|
784
885
|
return this.clients.get(provider);
|
|
785
886
|
}
|
|
786
887
|
|
|
@@ -799,7 +900,7 @@ export class AgentWorkspaceProxy {
|
|
|
799
900
|
this.sendCapabilities();
|
|
800
901
|
}
|
|
801
902
|
|
|
802
|
-
private async ensureProviderClient(provider: AgentProvider): Promise<AcpClient | ClaudeStreamJsonClient | undefined> {
|
|
903
|
+
private async ensureProviderClient(provider: AgentProvider): Promise<AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient | undefined> {
|
|
803
904
|
const existing = this.clients.get(provider);
|
|
804
905
|
if (existing) return existing;
|
|
805
906
|
|
|
@@ -814,35 +915,76 @@ export class AgentWorkspaceProxy {
|
|
|
814
915
|
return undefined;
|
|
815
916
|
}
|
|
816
917
|
|
|
817
|
-
|
|
818
|
-
this.agentProtocols.set(provider,
|
|
819
|
-
const
|
|
820
|
-
const
|
|
918
|
+
const tryCreateClient = async (config: typeof resolved): Promise<AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient> => {
|
|
919
|
+
this.agentProtocols.set(provider, config.protocol);
|
|
920
|
+
const isClaudeSdk = config.protocol === "claude-agent-sdk";
|
|
921
|
+
const isClaudeStreamJson = config.protocol === "claude-stream-json";
|
|
922
|
+
const client = isClaudeSdk
|
|
923
|
+
? new ClaudeSdkClient({
|
|
924
|
+
command: config.command,
|
|
925
|
+
protocol: config.protocol,
|
|
926
|
+
framing: config.framing,
|
|
927
|
+
cwd: this.input.cwd,
|
|
928
|
+
onNotification: (method, params) => this.handleNotification(method, params),
|
|
929
|
+
onRequest: (method, params) => this.handleRequest(method, params),
|
|
930
|
+
onExit: (message) => this.handleProviderExit(provider, message),
|
|
931
|
+
})
|
|
932
|
+
: isClaudeStreamJson
|
|
821
933
|
? new ClaudeStreamJsonClient({
|
|
822
|
-
command:
|
|
823
|
-
protocol:
|
|
824
|
-
framing:
|
|
934
|
+
command: config.command,
|
|
935
|
+
protocol: config.protocol,
|
|
936
|
+
framing: config.framing,
|
|
825
937
|
cwd: this.input.cwd,
|
|
826
938
|
onNotification: (method, params) => this.handleNotification(method, params),
|
|
827
939
|
onRequest: (method, params) => this.handleRequest(method, params),
|
|
828
940
|
onExit: (message) => this.handleProviderExit(provider, message),
|
|
829
941
|
})
|
|
830
942
|
: new AcpClient({
|
|
831
|
-
command:
|
|
832
|
-
protocol:
|
|
833
|
-
framing:
|
|
943
|
+
command: config.command,
|
|
944
|
+
protocol: config.protocol,
|
|
945
|
+
framing: config.framing,
|
|
834
946
|
cwd: this.input.cwd,
|
|
835
947
|
onNotification: (method, params) => this.handleNotification(method, params),
|
|
836
948
|
onRequest: (method, params) => this.handleRequest(method, params),
|
|
837
949
|
onExit: (message) => this.handleProviderExit(provider, message),
|
|
838
|
-
|
|
950
|
+
});
|
|
839
951
|
await client.initialize();
|
|
952
|
+
return client;
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
try {
|
|
956
|
+
const client = await tryCreateClient(resolved);
|
|
840
957
|
this.clients.set(provider, client);
|
|
958
|
+
await this.refreshProviderCapabilities(provider, client, resolved.protocol);
|
|
841
959
|
this.status = "idle";
|
|
842
960
|
this.error = undefined;
|
|
843
961
|
this.sendCapabilities();
|
|
844
962
|
return client;
|
|
845
963
|
} catch (error) {
|
|
964
|
+
if (provider === "claude" && resolved.protocol === "claude-agent-sdk") {
|
|
965
|
+
if (this.input.verbose) {
|
|
966
|
+
process.stderr.write(`[agent:v2] Claude SDK failed, falling back to stream-json: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
967
|
+
}
|
|
968
|
+
try {
|
|
969
|
+
const fallback = {
|
|
970
|
+
provider,
|
|
971
|
+
command: "claude --print --output-format stream-json --input-format stream-json --verbose --permission-mode bypassPermissions",
|
|
972
|
+
protocol: "claude-stream-json" as const,
|
|
973
|
+
framing: "newline" as const,
|
|
974
|
+
};
|
|
975
|
+
const client = await tryCreateClient(fallback);
|
|
976
|
+
this.clients.set(provider, client);
|
|
977
|
+
await this.refreshProviderCapabilities(provider, client, fallback.protocol);
|
|
978
|
+
this.status = "idle";
|
|
979
|
+
this.error = undefined;
|
|
980
|
+
this.sendCapabilities();
|
|
981
|
+
return client;
|
|
982
|
+
} catch (fallbackError) {
|
|
983
|
+
if (this.input.verbose) {
|
|
984
|
+
process.stderr.write(`[agent:v2] Claude stream-json fallback failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}\n`);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
846
988
|
if (this.input.verbose) {
|
|
847
989
|
process.stderr.write(`[agent:v2] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
848
990
|
}
|
|
@@ -850,25 +992,99 @@ export class AgentWorkspaceProxy {
|
|
|
850
992
|
}
|
|
851
993
|
}
|
|
852
994
|
|
|
995
|
+
private async refreshProviderCapabilities(
|
|
996
|
+
provider: AgentProvider,
|
|
997
|
+
client: AcpClient | ClaudeSdkClient | ClaudeStreamJsonClient,
|
|
998
|
+
protocol: AgentProtocol,
|
|
999
|
+
): Promise<void> {
|
|
1000
|
+
if (!(client instanceof AcpClient) || protocol !== "codex-app-server") return;
|
|
1001
|
+
try {
|
|
1002
|
+
const result = await client.listModels();
|
|
1003
|
+
const runtimeCapabilities = parseModelListCapabilities(result);
|
|
1004
|
+
if (runtimeCapabilities) this.providerCapabilities.set(provider, runtimeCapabilities);
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
if (this.input.verbose) {
|
|
1007
|
+
process.stderr.write(`[agent:v2] model/list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
private async syncProviderSessions(): Promise<void> {
|
|
1013
|
+
await this.initialize();
|
|
1014
|
+
for (const [provider, client] of this.clients) {
|
|
1015
|
+
try {
|
|
1016
|
+
const result = await client.listSessions();
|
|
1017
|
+
for (const remote of parseRemoteSessions(result)) {
|
|
1018
|
+
const agentSessionId = remote.id;
|
|
1019
|
+
const existingId = this.conversationByAgentSessionId.get(agentSessionId);
|
|
1020
|
+
const now = Date.now();
|
|
1021
|
+
const conversationId = existingId ?? `agent:${agentSessionId}`;
|
|
1022
|
+
const existing = this.conversations.get(conversationId);
|
|
1023
|
+
const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
|
|
1024
|
+
const conversation: AgentConversation = {
|
|
1025
|
+
id: conversationId,
|
|
1026
|
+
agentSessionId,
|
|
1027
|
+
provider,
|
|
1028
|
+
cwd,
|
|
1029
|
+
title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
|
|
1030
|
+
model: remote.model ?? existing?.model,
|
|
1031
|
+
reasoningEffort: existing?.reasoningEffort,
|
|
1032
|
+
permissionMode: existing?.permissionMode,
|
|
1033
|
+
status: existing?.status ?? "idle",
|
|
1034
|
+
archived: existing?.archived ?? false,
|
|
1035
|
+
lastMessagePreview: existing?.lastMessagePreview,
|
|
1036
|
+
lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
|
|
1037
|
+
createdAt: remote.createdAt ?? existing?.createdAt ?? now,
|
|
1038
|
+
};
|
|
1039
|
+
this.conversations.set(conversation.id, conversation);
|
|
1040
|
+
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
1041
|
+
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
1042
|
+
}
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
if (this.input.verbose) {
|
|
1045
|
+
process.stderr.write(`[agent:v2] session list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
853
1051
|
private sendCapabilities(): void {
|
|
854
1052
|
const providers = this.input.availableProviders.map((provider) => {
|
|
855
1053
|
const client = this.clients.get(provider);
|
|
856
1054
|
const protocol = this.agentProtocols.get(provider);
|
|
1055
|
+
const runtimeCapabilities = this.providerCapabilities.get(provider);
|
|
857
1056
|
const enabled = Boolean(client);
|
|
858
1057
|
const supportsImages = enabled && protocol === "codex-app-server";
|
|
1058
|
+
const isClaudeFallback = protocol === "claude-stream-json";
|
|
1059
|
+
const supportsPermission = enabled && !isClaudeFallback;
|
|
1060
|
+
const supportsReasoningEffort = enabled && !isClaudeFallback;
|
|
859
1061
|
return {
|
|
860
1062
|
id: provider,
|
|
861
1063
|
label: providerLabel(provider),
|
|
862
1064
|
enabled,
|
|
863
1065
|
reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
|
|
864
1066
|
supportsImages,
|
|
865
|
-
supportsPermission
|
|
1067
|
+
supportsPermission,
|
|
866
1068
|
supportsPlan: enabled,
|
|
867
1069
|
supportsCancel: enabled,
|
|
868
|
-
models:
|
|
1070
|
+
models: runtimeCapabilities?.models ?? [{ id: "default", label: "默认模型" }],
|
|
1071
|
+
defaultModel: runtimeCapabilities?.defaultModel ?? "default",
|
|
1072
|
+
reasoningEfforts: supportsReasoningEffort
|
|
1073
|
+
? runtimeCapabilities?.reasoningEfforts ?? [...ALL_REASONING_EFFORTS]
|
|
1074
|
+
: [],
|
|
1075
|
+
permissionModes: supportsPermission ? [...ALL_PERMISSION_MODES] : [],
|
|
1076
|
+
features: {
|
|
1077
|
+
images: supportsImages,
|
|
1078
|
+
permissions: supportsPermission,
|
|
1079
|
+
plan: enabled,
|
|
1080
|
+
cancel: enabled,
|
|
1081
|
+
reasoningEffort: supportsReasoningEffort,
|
|
1082
|
+
streamJsonFallback: isClaudeFallback,
|
|
1083
|
+
},
|
|
869
1084
|
};
|
|
870
1085
|
});
|
|
871
1086
|
const anyEnabled = providers.some((p) => p.enabled);
|
|
1087
|
+
const anyPermission = providers.some((p) => p.supportsPermission);
|
|
872
1088
|
this.input.send(createEnvelope({
|
|
873
1089
|
type: "agent.v2.capabilities",
|
|
874
1090
|
sessionId: this.input.sessionId,
|
|
@@ -883,7 +1099,7 @@ export class AgentWorkspaceProxy {
|
|
|
883
1099
|
supportsSessionLoad: anyEnabled,
|
|
884
1100
|
supportsImages: providers.some((p) => p.supportsImages),
|
|
885
1101
|
supportsAudio: false,
|
|
886
|
-
supportsPermission:
|
|
1102
|
+
supportsPermission: anyPermission,
|
|
887
1103
|
supportsPlan: anyEnabled,
|
|
888
1104
|
supportsCancel: anyEnabled,
|
|
889
1105
|
},
|
|
@@ -1085,8 +1301,15 @@ export class AgentWorkspaceProxy {
|
|
|
1085
1301
|
permissionMode: payload.permissionMode,
|
|
1086
1302
|
cwd: conversation.cwd,
|
|
1087
1303
|
});
|
|
1088
|
-
|
|
1089
|
-
if (conversation.
|
|
1304
|
+
const nextAgentSessionId = this.extractSessionId(result);
|
|
1305
|
+
if (nextAgentSessionId && nextAgentSessionId !== conversation.agentSessionId) {
|
|
1306
|
+
if (conversation.agentSessionId) this.conversationByAgentSessionId.delete(conversation.agentSessionId);
|
|
1307
|
+
conversation.agentSessionId = nextAgentSessionId;
|
|
1308
|
+
this.conversationByAgentSessionId.set(nextAgentSessionId, conversation.id);
|
|
1309
|
+
}
|
|
1310
|
+
const turnId = this.extractTurnId(result);
|
|
1311
|
+
if (turnId) this.currentTurnIds.set(conversation.id, turnId);
|
|
1312
|
+
if (conversation.status === "running" && protocol !== "codex-app-server") {
|
|
1090
1313
|
this.updateConversationStatus(conversation.id, "idle");
|
|
1091
1314
|
}
|
|
1092
1315
|
} catch (error) {
|
|
@@ -1106,6 +1329,9 @@ export class AgentWorkspaceProxy {
|
|
|
1106
1329
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
1107
1330
|
return this.handleStructuredInput(params, true);
|
|
1108
1331
|
}
|
|
1332
|
+
if (method === "mcpServer/elicitation/request") {
|
|
1333
|
+
return this.handleStructuredInput({ ...(asRecord(params) ?? {}), source: method }, true);
|
|
1334
|
+
}
|
|
1109
1335
|
if (isPermissionRequestMethod(method)) {
|
|
1110
1336
|
return this.handlePermission(params, true, method);
|
|
1111
1337
|
}
|
|
@@ -1136,6 +1362,10 @@ export class AgentWorkspaceProxy {
|
|
|
1136
1362
|
this.handleStructuredInput(params);
|
|
1137
1363
|
return;
|
|
1138
1364
|
}
|
|
1365
|
+
if (method === "mcpServer/elicitation/request") {
|
|
1366
|
+
this.handleStructuredInput({ ...(asRecord(params) ?? {}), source: method });
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1139
1369
|
if (isPermissionRequestMethod(method)) {
|
|
1140
1370
|
this.handlePermission(params, false, method);
|
|
1141
1371
|
return;
|
|
@@ -1150,13 +1380,18 @@ export class AgentWorkspaceProxy {
|
|
|
1150
1380
|
return;
|
|
1151
1381
|
}
|
|
1152
1382
|
if (method === "turn/started") {
|
|
1153
|
-
|
|
1154
|
-
|
|
1383
|
+
if (conversationId) {
|
|
1384
|
+
const turnId = this.extractTurnId(params);
|
|
1385
|
+
if (turnId) this.currentTurnIds.set(conversationId, turnId);
|
|
1386
|
+
this.updateConversationStatus(conversationId, "running");
|
|
1387
|
+
}
|
|
1155
1388
|
return;
|
|
1156
1389
|
}
|
|
1157
1390
|
if (method === "turn/completed") {
|
|
1158
|
-
|
|
1159
|
-
|
|
1391
|
+
if (conversationId) {
|
|
1392
|
+
this.currentTurnIds.delete(conversationId);
|
|
1393
|
+
this.updateConversationStatus(conversationId, "idle");
|
|
1394
|
+
}
|
|
1160
1395
|
return;
|
|
1161
1396
|
}
|
|
1162
1397
|
if (method === "session/request_permission") {
|
|
@@ -1195,7 +1430,11 @@ export class AgentWorkspaceProxy {
|
|
|
1195
1430
|
this.handleCommandExecDelta(params);
|
|
1196
1431
|
return;
|
|
1197
1432
|
case "item/autoApprovalReview/started":
|
|
1433
|
+
this.handleAutoApprovalReview(params, true);
|
|
1434
|
+
return;
|
|
1198
1435
|
case "item/autoApprovalReview/completed":
|
|
1436
|
+
this.handleAutoApprovalReview(params, false);
|
|
1437
|
+
return;
|
|
1199
1438
|
case "item/commandExecution/terminalInteraction":
|
|
1200
1439
|
return;
|
|
1201
1440
|
}
|
|
@@ -1427,6 +1666,37 @@ export class AgentWorkspaceProxy {
|
|
|
1427
1666
|
});
|
|
1428
1667
|
}
|
|
1429
1668
|
|
|
1669
|
+
private handleAutoApprovalReview(params: unknown, streaming: boolean): void {
|
|
1670
|
+
const raw = asRecord(params) ?? {};
|
|
1671
|
+
const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
|
|
1672
|
+
if (!conversationId) return;
|
|
1673
|
+
const itemId = firstString(raw, ["itemId", "id", "reviewId"]) ?? "auto-approval-review";
|
|
1674
|
+
const existing = this.findItem(conversationId, itemId);
|
|
1675
|
+
const decision = firstString(raw, ["decision", "result", "outcome", "status"]);
|
|
1676
|
+
const summary =
|
|
1677
|
+
firstString(raw, ["summary", "message", "text", "reason"]) ??
|
|
1678
|
+
stringifyDefined(raw.review ?? raw.details);
|
|
1679
|
+
this.upsertItem(conversationId, {
|
|
1680
|
+
id: itemId,
|
|
1681
|
+
conversationId,
|
|
1682
|
+
type: "status",
|
|
1683
|
+
kind: "review",
|
|
1684
|
+
role: "system",
|
|
1685
|
+
turnId: this.extractTurnId(raw) ?? this.currentTurnIds.get(conversationId),
|
|
1686
|
+
itemId,
|
|
1687
|
+
text: summary ?? (streaming ? "正在审查自动授权" : decision ? `自动授权审查:${decision}` : "已完成自动授权审查"),
|
|
1688
|
+
metadata: {
|
|
1689
|
+
...(existing?.metadata ?? {}),
|
|
1690
|
+
autoApprovalReview: true,
|
|
1691
|
+
...(decision ? { decision } : {}),
|
|
1692
|
+
},
|
|
1693
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
1694
|
+
updatedAt: Date.now(),
|
|
1695
|
+
isStreaming: streaming,
|
|
1696
|
+
});
|
|
1697
|
+
this.updateConversationPreview(conversationId, streaming ? "正在审查自动授权" : "已完成自动授权审查", streaming ? "running" : undefined);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1430
1700
|
private handleCompletedMessageItem(item: Record<string, unknown>, streaming: boolean): void {
|
|
1431
1701
|
const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
|
|
1432
1702
|
if (!conversationId) return;
|
|
@@ -1501,7 +1771,7 @@ export class AgentWorkspaceProxy {
|
|
|
1501
1771
|
conversationId,
|
|
1502
1772
|
type: "status" as const,
|
|
1503
1773
|
role: "system" as const,
|
|
1504
|
-
turnId: this.extractTurnId(item) ?? this.
|
|
1774
|
+
turnId: this.extractTurnId(item) ?? this.currentTurnIds.get(conversationId),
|
|
1505
1775
|
itemId,
|
|
1506
1776
|
createdAt: existing?.createdAt ?? Date.now(),
|
|
1507
1777
|
updatedAt: Date.now(),
|
|
@@ -1559,7 +1829,7 @@ export class AgentWorkspaceProxy {
|
|
|
1559
1829
|
type: "status",
|
|
1560
1830
|
kind: "subagent_action",
|
|
1561
1831
|
role: "system",
|
|
1562
|
-
turnId: this.extractTurnId(item) ?? this.
|
|
1832
|
+
turnId: this.extractTurnId(item) ?? this.currentTurnIds.get(conversationId),
|
|
1563
1833
|
itemId,
|
|
1564
1834
|
text,
|
|
1565
1835
|
subagent,
|
|
@@ -1573,9 +1843,13 @@ export class AgentWorkspaceProxy {
|
|
|
1573
1843
|
private handleStructuredInput(params: unknown, waitForResponse = false): Promise<unknown> | void {
|
|
1574
1844
|
const raw = asRecord(params) ?? {};
|
|
1575
1845
|
const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
|
|
1576
|
-
|
|
1846
|
+
const source = firstString(raw, ["method", "source", "requestMethod"]);
|
|
1847
|
+
const formatResponse = source === "mcpServer/elicitation/request"
|
|
1848
|
+
? formatMcpElicitationResponse
|
|
1849
|
+
: formatStructuredInputResponse;
|
|
1850
|
+
if (!conversationId) return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
|
|
1577
1851
|
const structuredInput = decodeStructuredInput(raw);
|
|
1578
|
-
if (!structuredInput) return waitForResponse ? Promise.resolve(
|
|
1852
|
+
if (!structuredInput) return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
|
|
1579
1853
|
const text = structuredInput.questions.map((question) => question.question).join("\n");
|
|
1580
1854
|
this.pendingStructuredInputs.set(structuredInput.requestId, { conversationId, input: structuredInput });
|
|
1581
1855
|
this.upsertItem(conversationId, {
|
|
@@ -1596,13 +1870,13 @@ export class AgentWorkspaceProxy {
|
|
|
1596
1870
|
const timer = setTimeout(() => {
|
|
1597
1871
|
this.pendingStructuredInputs.delete(structuredInput.requestId);
|
|
1598
1872
|
this.structuredInputWaiters.delete(structuredInput.requestId);
|
|
1599
|
-
resolve(
|
|
1873
|
+
resolve(formatResponse({}));
|
|
1600
1874
|
this.markStructuredInput(conversationId, structuredInput.requestId, {
|
|
1601
1875
|
inputPending: false,
|
|
1602
1876
|
inputError: "等待用户输入超时",
|
|
1603
1877
|
});
|
|
1604
1878
|
}, PERMISSION_TIMEOUT_MS);
|
|
1605
|
-
this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer });
|
|
1879
|
+
this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source });
|
|
1606
1880
|
});
|
|
1607
1881
|
}
|
|
1608
1882
|
|
|
@@ -1711,6 +1985,12 @@ export class AgentWorkspaceProxy {
|
|
|
1711
1985
|
optionId: selectedOptionId,
|
|
1712
1986
|
});
|
|
1713
1987
|
}
|
|
1988
|
+
this.markPermission(payload.conversationId, payload.requestId, {
|
|
1989
|
+
permissionOutcome: payload.outcome,
|
|
1990
|
+
optionId: selectedOptionId,
|
|
1991
|
+
permissionError: undefined,
|
|
1992
|
+
permissionPending: false,
|
|
1993
|
+
});
|
|
1714
1994
|
this.updateConversationStatus(payload.conversationId, "running");
|
|
1715
1995
|
}
|
|
1716
1996
|
|
|
@@ -1725,16 +2005,35 @@ export class AgentWorkspaceProxy {
|
|
|
1725
2005
|
if (waiter) {
|
|
1726
2006
|
clearTimeout(waiter.timer);
|
|
1727
2007
|
this.structuredInputWaiters.delete(payload.requestId);
|
|
1728
|
-
waiter.
|
|
2008
|
+
const formatResponse = waiter.source === "mcpServer/elicitation/request"
|
|
2009
|
+
? formatMcpElicitationResponse
|
|
2010
|
+
: formatStructuredInputResponse;
|
|
2011
|
+
waiter.resolve(formatResponse(payload.answers));
|
|
1729
2012
|
}
|
|
1730
2013
|
this.markStructuredInput(payload.conversationId, payload.requestId, {
|
|
1731
2014
|
inputPending: false,
|
|
1732
2015
|
inputSubmitted: true,
|
|
2016
|
+
inputSubmitting: false,
|
|
2017
|
+
inputError: undefined,
|
|
1733
2018
|
answers: payload.answers,
|
|
1734
2019
|
});
|
|
1735
2020
|
this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
|
|
1736
2021
|
}
|
|
1737
2022
|
|
|
2023
|
+
private markPermission(
|
|
2024
|
+
conversationId: string,
|
|
2025
|
+
requestId: string,
|
|
2026
|
+
metadata: Record<string, unknown>,
|
|
2027
|
+
): void {
|
|
2028
|
+
const item = this.findItem(conversationId, `permission:${requestId}`);
|
|
2029
|
+
if (!item) return;
|
|
2030
|
+
this.upsertItem(conversationId, {
|
|
2031
|
+
...item,
|
|
2032
|
+
metadata: { ...(item.metadata ?? {}), ...metadata },
|
|
2033
|
+
updatedAt: Date.now(),
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
|
|
1738
2037
|
private markStructuredInput(
|
|
1739
2038
|
conversationId: string,
|
|
1740
2039
|
requestId: string,
|
|
@@ -2090,7 +2389,8 @@ function isPermissionRequestMethod(method: string): boolean {
|
|
|
2090
2389
|
return (
|
|
2091
2390
|
method === "session/request_permission" ||
|
|
2092
2391
|
method.endsWith("/requestApproval") ||
|
|
2093
|
-
method === "mcpServer/elicitation/request"
|
|
2392
|
+
method === "mcpServer/elicitation/request" ||
|
|
2393
|
+
method === "claude/requestApproval"
|
|
2094
2394
|
);
|
|
2095
2395
|
}
|
|
2096
2396
|
|
|
@@ -2105,6 +2405,20 @@ function formatStructuredInputResponse(answers: Record<string, string[]>): unkno
|
|
|
2105
2405
|
};
|
|
2106
2406
|
}
|
|
2107
2407
|
|
|
2408
|
+
function formatMcpElicitationResponse(answers: Record<string, string[]>): unknown {
|
|
2409
|
+
const content = Object.fromEntries(
|
|
2410
|
+
Object.entries(answers).map(([questionId, values]) => [
|
|
2411
|
+
questionId,
|
|
2412
|
+
values.length <= 1 ? values[0] ?? "" : values,
|
|
2413
|
+
]),
|
|
2414
|
+
);
|
|
2415
|
+
return {
|
|
2416
|
+
action: Object.keys(content).length > 0 ? "accept" : "cancel",
|
|
2417
|
+
content,
|
|
2418
|
+
_meta: { source: "linkshell" },
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2108
2422
|
function formatPermissionResponse(
|
|
2109
2423
|
source: string | undefined,
|
|
2110
2424
|
outcome: "allow" | "deny" | "cancelled",
|
|
@@ -2122,6 +2436,14 @@ function formatPermissionResponse(
|
|
|
2122
2436
|
}
|
|
2123
2437
|
return { permissions: { type: "managed", network: { enabled: false }, fileSystem: { type: "readOnly" } } };
|
|
2124
2438
|
}
|
|
2439
|
+
if (source === "claude/requestApproval") {
|
|
2440
|
+
return { behavior: outcome === "allow" ? "allow" : "deny" };
|
|
2441
|
+
}
|
|
2442
|
+
if (source === "mcpServer/elicitation/request") {
|
|
2443
|
+
return outcome === "allow"
|
|
2444
|
+
? { action: "accept", content: { optionId }, _meta: { source: "linkshell" } }
|
|
2445
|
+
: { action: outcome === "cancelled" ? "cancel" : "decline", content: {}, _meta: { source: "linkshell" } };
|
|
2446
|
+
}
|
|
2125
2447
|
return {
|
|
2126
2448
|
outcome:
|
|
2127
2449
|
outcome === "cancelled"
|