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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
2
|
import { createEnvelope, parseTypedPayload, } from "@linkshell/protocol";
|
|
3
3
|
import { AcpClient } from "./acp-client.js";
|
|
4
|
+
import { ClaudeSdkClient } from "./claude-sdk-client.js";
|
|
4
5
|
import { ClaudeStreamJsonClient } from "./claude-stream-json-client.js";
|
|
5
6
|
import { resolveAgentCommand } from "./provider-resolver.js";
|
|
6
7
|
const PERMISSION_TIMEOUT_MS = 5 * 60_000;
|
|
@@ -219,9 +220,20 @@ function commandExecutionFromTool(toolCall) {
|
|
|
219
220
|
status: toolCall.status,
|
|
220
221
|
};
|
|
221
222
|
}
|
|
222
|
-
function
|
|
223
|
-
const
|
|
224
|
-
|
|
223
|
+
function fileChangeFromStructuredInput(input) {
|
|
224
|
+
const raw = input?.trim();
|
|
225
|
+
if (!raw)
|
|
226
|
+
return [];
|
|
227
|
+
try {
|
|
228
|
+
const parsed = JSON.parse(raw);
|
|
229
|
+
if (parsed && typeof parsed === "object") {
|
|
230
|
+
return fileChangeEntriesFromItem(parsed);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// Fall through to line parser.
|
|
235
|
+
}
|
|
236
|
+
return raw
|
|
225
237
|
.split("\n")
|
|
226
238
|
.map((line) => line.trim())
|
|
227
239
|
.filter(Boolean)
|
|
@@ -234,6 +246,10 @@ function fileChangeFromTool(toolCall) {
|
|
|
234
246
|
return entry;
|
|
235
247
|
})
|
|
236
248
|
.filter((entry) => entry.path.length > 0);
|
|
249
|
+
}
|
|
250
|
+
function fileChangeFromTool(toolCall) {
|
|
251
|
+
const diff = toolCall.output && looksLikeDiff(toolCall.output) ? toolCall.output : undefined;
|
|
252
|
+
const entries = fileChangeFromStructuredInput(toolCall.input);
|
|
237
253
|
if (entries.length === 0 && !diff && !toolCall.output)
|
|
238
254
|
return undefined;
|
|
239
255
|
return {
|
|
@@ -486,35 +502,95 @@ function providerLabel(provider) {
|
|
|
486
502
|
return "Claude";
|
|
487
503
|
return "Custom";
|
|
488
504
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
505
|
+
const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"];
|
|
506
|
+
const ALL_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"];
|
|
507
|
+
function parseModelListCapabilities(value) {
|
|
508
|
+
const raw = asRecord(value);
|
|
509
|
+
const modelsValue = Array.isArray(value) ? value :
|
|
510
|
+
Array.isArray(raw?.models) ? raw.models :
|
|
511
|
+
Array.isArray(raw?.items) ? raw.items :
|
|
512
|
+
Array.isArray(raw?.modelOptions) ? raw.modelOptions :
|
|
513
|
+
[];
|
|
514
|
+
const models = modelsValue
|
|
515
|
+
.map((entry, index) => {
|
|
516
|
+
const model = asRecord(entry);
|
|
517
|
+
if (!model) {
|
|
518
|
+
return typeof entry === "string" && entry
|
|
519
|
+
? { id: entry, label: entry }
|
|
520
|
+
: undefined;
|
|
521
|
+
}
|
|
522
|
+
const modelId = firstString(model, ["id", "model", "name", "value"]) ?? `model-${index + 1}`;
|
|
523
|
+
const label = firstString(model, ["label", "title", "displayName", "name"]) ?? modelId;
|
|
524
|
+
return { id: modelId, label };
|
|
525
|
+
})
|
|
526
|
+
.filter((entry) => Boolean(entry));
|
|
527
|
+
const defaultModel = firstString(raw, ["defaultModel", "default_model", "currentModel"]) ??
|
|
528
|
+
firstString(asRecord(raw?.defaults), ["model"]);
|
|
529
|
+
const effortsValue = Array.isArray(raw?.reasoningEfforts) ? raw.reasoningEfforts :
|
|
530
|
+
Array.isArray(raw?.reasoning_efforts) ? raw.reasoning_efforts :
|
|
531
|
+
Array.isArray(raw?.efforts) ? raw.efforts :
|
|
532
|
+
undefined;
|
|
533
|
+
const reasoningEfforts = effortsValue
|
|
534
|
+
?.filter((entry) => typeof entry === "string" && ALL_REASONING_EFFORTS.includes(entry));
|
|
535
|
+
if (models.length === 0 && !defaultModel && !reasoningEfforts?.length)
|
|
536
|
+
return undefined;
|
|
537
|
+
return {
|
|
538
|
+
...(models.length > 0 ? { models: [{ id: "default", label: "默认模型" }, ...models] } : {}),
|
|
539
|
+
...(defaultModel ? { defaultModel } : {}),
|
|
540
|
+
...(reasoningEfforts?.length ? { reasoningEfforts } : {}),
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function parseTimestamp(value) {
|
|
544
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
545
|
+
return value > 10_000_000_000 ? value : value * 1000;
|
|
497
546
|
}
|
|
498
|
-
if (
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
{ id: "gpt-5.5", label: "GPT-5.5" },
|
|
502
|
-
{ id: "gpt-5.4", label: "GPT-5.4" },
|
|
503
|
-
{ id: "gpt-5.4-mini", label: "GPT-5.4 Mini" },
|
|
504
|
-
{ id: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
|
|
505
|
-
];
|
|
547
|
+
if (typeof value === "string" && value.trim()) {
|
|
548
|
+
const parsed = Date.parse(value);
|
|
549
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
506
550
|
}
|
|
507
|
-
return
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
function parseRemoteSessions(value) {
|
|
554
|
+
const raw = asRecord(value);
|
|
555
|
+
const sessionsValue = Array.isArray(value) ? value :
|
|
556
|
+
Array.isArray(raw?.threads) ? raw.threads :
|
|
557
|
+
Array.isArray(raw?.sessions) ? raw.sessions :
|
|
558
|
+
Array.isArray(raw?.items) ? raw.items :
|
|
559
|
+
[];
|
|
560
|
+
const result = [];
|
|
561
|
+
for (const entry of sessionsValue) {
|
|
562
|
+
const session = asRecord(entry);
|
|
563
|
+
if (!session) {
|
|
564
|
+
if (typeof entry === "string" && entry)
|
|
565
|
+
result.push({ id: entry });
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const nestedThread = asRecord(session.thread);
|
|
569
|
+
const source = nestedThread ?? session;
|
|
570
|
+
const id = firstString(source, ["id", "threadId", "sessionId", "agentSessionId"]);
|
|
571
|
+
if (!id)
|
|
572
|
+
continue;
|
|
573
|
+
result.push({
|
|
574
|
+
id,
|
|
575
|
+
cwd: firstString(source, ["cwd", "workingDirectory", "workspacePath"]),
|
|
576
|
+
title: firstString(source, ["title", "name", "summary"]),
|
|
577
|
+
model: firstString(source, ["model", "modelId"]),
|
|
578
|
+
createdAt: parseTimestamp(source.createdAt ?? source.created_at),
|
|
579
|
+
lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
return result;
|
|
508
583
|
}
|
|
509
584
|
export class AgentWorkspaceProxy {
|
|
510
585
|
input;
|
|
511
586
|
clients = new Map();
|
|
512
587
|
agentProtocols = new Map();
|
|
588
|
+
providerCapabilities = new Map();
|
|
513
589
|
initialized = false;
|
|
514
590
|
status = "unavailable";
|
|
515
591
|
error;
|
|
516
592
|
activeConversationId;
|
|
517
|
-
|
|
593
|
+
currentTurnIds = new Map();
|
|
518
594
|
conversations = new Map();
|
|
519
595
|
conversationByAgentSessionId = new Map();
|
|
520
596
|
timelines = new Map();
|
|
@@ -541,6 +617,7 @@ export class AgentWorkspaceProxy {
|
|
|
541
617
|
}
|
|
542
618
|
case "agent.v2.conversation.list": {
|
|
543
619
|
const payload = parseTypedPayload("agent.v2.conversation.list", envelope.payload);
|
|
620
|
+
await this.syncProviderSessions();
|
|
544
621
|
const conversations = [...this.conversations.values()].filter((conversation) => payload.includeArchived ? true : !conversation.archived);
|
|
545
622
|
this.input.send(createEnvelope({
|
|
546
623
|
type: "agent.v2.conversation.list.result",
|
|
@@ -566,9 +643,9 @@ export class AgentWorkspaceProxy {
|
|
|
566
643
|
const cancelClient = conversation ? this.clientForProvider(conversation.provider) : undefined;
|
|
567
644
|
cancelClient?.cancel({
|
|
568
645
|
sessionId: conversation?.agentSessionId,
|
|
569
|
-
turnId: this.
|
|
646
|
+
turnId: this.currentTurnIds.get(payload.conversationId),
|
|
570
647
|
});
|
|
571
|
-
this.
|
|
648
|
+
this.currentTurnIds.delete(payload.conversationId);
|
|
572
649
|
this.updateConversationStatus(payload.conversationId, "idle");
|
|
573
650
|
this.emitStatus(payload.conversationId, "idle", "已停止");
|
|
574
651
|
break;
|
|
@@ -622,61 +699,174 @@ export class AgentWorkspaceProxy {
|
|
|
622
699
|
}
|
|
623
700
|
return undefined;
|
|
624
701
|
}
|
|
625
|
-
|
|
626
|
-
this.agentProtocols.set(provider,
|
|
627
|
-
const
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
702
|
+
const tryCreateClient = async (config) => {
|
|
703
|
+
this.agentProtocols.set(provider, config.protocol);
|
|
704
|
+
const isClaudeSdk = config.protocol === "claude-agent-sdk";
|
|
705
|
+
const isClaudeStreamJson = config.protocol === "claude-stream-json";
|
|
706
|
+
const client = isClaudeSdk
|
|
707
|
+
? new ClaudeSdkClient({
|
|
708
|
+
command: config.command,
|
|
709
|
+
protocol: config.protocol,
|
|
710
|
+
framing: config.framing,
|
|
633
711
|
cwd: this.input.cwd,
|
|
634
712
|
onNotification: (method, params) => this.handleNotification(method, params),
|
|
635
713
|
onRequest: (method, params) => this.handleRequest(method, params),
|
|
636
714
|
onExit: (message) => this.handleProviderExit(provider, message),
|
|
637
715
|
})
|
|
638
|
-
:
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
716
|
+
: isClaudeStreamJson
|
|
717
|
+
? new ClaudeStreamJsonClient({
|
|
718
|
+
command: config.command,
|
|
719
|
+
protocol: config.protocol,
|
|
720
|
+
framing: config.framing,
|
|
721
|
+
cwd: this.input.cwd,
|
|
722
|
+
onNotification: (method, params) => this.handleNotification(method, params),
|
|
723
|
+
onRequest: (method, params) => this.handleRequest(method, params),
|
|
724
|
+
onExit: (message) => this.handleProviderExit(provider, message),
|
|
725
|
+
})
|
|
726
|
+
: new AcpClient({
|
|
727
|
+
command: config.command,
|
|
728
|
+
protocol: config.protocol,
|
|
729
|
+
framing: config.framing,
|
|
730
|
+
cwd: this.input.cwd,
|
|
731
|
+
onNotification: (method, params) => this.handleNotification(method, params),
|
|
732
|
+
onRequest: (method, params) => this.handleRequest(method, params),
|
|
733
|
+
onExit: (message) => this.handleProviderExit(provider, message),
|
|
734
|
+
});
|
|
647
735
|
await client.initialize();
|
|
736
|
+
return client;
|
|
737
|
+
};
|
|
738
|
+
try {
|
|
739
|
+
const client = await tryCreateClient(resolved);
|
|
648
740
|
this.clients.set(provider, client);
|
|
741
|
+
await this.refreshProviderCapabilities(provider, client, resolved.protocol);
|
|
649
742
|
this.status = "idle";
|
|
650
743
|
this.error = undefined;
|
|
651
744
|
this.sendCapabilities();
|
|
652
745
|
return client;
|
|
653
746
|
}
|
|
654
747
|
catch (error) {
|
|
748
|
+
if (provider === "claude" && resolved.protocol === "claude-agent-sdk") {
|
|
749
|
+
if (this.input.verbose) {
|
|
750
|
+
process.stderr.write(`[agent:v2] Claude SDK failed, falling back to stream-json: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
751
|
+
}
|
|
752
|
+
try {
|
|
753
|
+
const fallback = {
|
|
754
|
+
provider,
|
|
755
|
+
command: "claude --print --output-format stream-json --input-format stream-json --verbose --permission-mode bypassPermissions",
|
|
756
|
+
protocol: "claude-stream-json",
|
|
757
|
+
framing: "newline",
|
|
758
|
+
};
|
|
759
|
+
const client = await tryCreateClient(fallback);
|
|
760
|
+
this.clients.set(provider, client);
|
|
761
|
+
await this.refreshProviderCapabilities(provider, client, fallback.protocol);
|
|
762
|
+
this.status = "idle";
|
|
763
|
+
this.error = undefined;
|
|
764
|
+
this.sendCapabilities();
|
|
765
|
+
return client;
|
|
766
|
+
}
|
|
767
|
+
catch (fallbackError) {
|
|
768
|
+
if (this.input.verbose) {
|
|
769
|
+
process.stderr.write(`[agent:v2] Claude stream-json fallback failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}\n`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
655
773
|
if (this.input.verbose) {
|
|
656
774
|
process.stderr.write(`[agent:v2] failed to start ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
657
775
|
}
|
|
658
776
|
return undefined;
|
|
659
777
|
}
|
|
660
778
|
}
|
|
779
|
+
async refreshProviderCapabilities(provider, client, protocol) {
|
|
780
|
+
if (!(client instanceof AcpClient) || protocol !== "codex-app-server")
|
|
781
|
+
return;
|
|
782
|
+
try {
|
|
783
|
+
const result = await client.listModels();
|
|
784
|
+
const runtimeCapabilities = parseModelListCapabilities(result);
|
|
785
|
+
if (runtimeCapabilities)
|
|
786
|
+
this.providerCapabilities.set(provider, runtimeCapabilities);
|
|
787
|
+
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
if (this.input.verbose) {
|
|
790
|
+
process.stderr.write(`[agent:v2] model/list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
async syncProviderSessions() {
|
|
795
|
+
await this.initialize();
|
|
796
|
+
for (const [provider, client] of this.clients) {
|
|
797
|
+
try {
|
|
798
|
+
const result = await client.listSessions();
|
|
799
|
+
for (const remote of parseRemoteSessions(result)) {
|
|
800
|
+
const agentSessionId = remote.id;
|
|
801
|
+
const existingId = this.conversationByAgentSessionId.get(agentSessionId);
|
|
802
|
+
const now = Date.now();
|
|
803
|
+
const conversationId = existingId ?? `agent:${agentSessionId}`;
|
|
804
|
+
const existing = this.conversations.get(conversationId);
|
|
805
|
+
const cwd = remote.cwd ?? existing?.cwd ?? this.input.cwd;
|
|
806
|
+
const conversation = {
|
|
807
|
+
id: conversationId,
|
|
808
|
+
agentSessionId,
|
|
809
|
+
provider,
|
|
810
|
+
cwd,
|
|
811
|
+
title: remote.title ?? existing?.title ?? titleFromCwd(cwd),
|
|
812
|
+
model: remote.model ?? existing?.model,
|
|
813
|
+
reasoningEffort: existing?.reasoningEffort,
|
|
814
|
+
permissionMode: existing?.permissionMode,
|
|
815
|
+
status: existing?.status ?? "idle",
|
|
816
|
+
archived: existing?.archived ?? false,
|
|
817
|
+
lastMessagePreview: existing?.lastMessagePreview,
|
|
818
|
+
lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
|
|
819
|
+
createdAt: remote.createdAt ?? existing?.createdAt ?? now,
|
|
820
|
+
};
|
|
821
|
+
this.conversations.set(conversation.id, conversation);
|
|
822
|
+
this.conversationByAgentSessionId.set(agentSessionId, conversation.id);
|
|
823
|
+
this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
if (this.input.verbose) {
|
|
828
|
+
process.stderr.write(`[agent:v2] session list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
661
833
|
sendCapabilities() {
|
|
662
834
|
const providers = this.input.availableProviders.map((provider) => {
|
|
663
835
|
const client = this.clients.get(provider);
|
|
664
836
|
const protocol = this.agentProtocols.get(provider);
|
|
837
|
+
const runtimeCapabilities = this.providerCapabilities.get(provider);
|
|
665
838
|
const enabled = Boolean(client);
|
|
666
839
|
const supportsImages = enabled && protocol === "codex-app-server";
|
|
840
|
+
const isClaudeFallback = protocol === "claude-stream-json";
|
|
841
|
+
const supportsPermission = enabled && !isClaudeFallback;
|
|
842
|
+
const supportsReasoningEffort = enabled && !isClaudeFallback;
|
|
667
843
|
return {
|
|
668
844
|
id: provider,
|
|
669
845
|
label: providerLabel(provider),
|
|
670
846
|
enabled,
|
|
671
847
|
reason: enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`,
|
|
672
848
|
supportsImages,
|
|
673
|
-
supportsPermission
|
|
849
|
+
supportsPermission,
|
|
674
850
|
supportsPlan: enabled,
|
|
675
851
|
supportsCancel: enabled,
|
|
676
|
-
models:
|
|
852
|
+
models: runtimeCapabilities?.models ?? [{ id: "default", label: "默认模型" }],
|
|
853
|
+
defaultModel: runtimeCapabilities?.defaultModel ?? "default",
|
|
854
|
+
reasoningEfforts: supportsReasoningEffort
|
|
855
|
+
? runtimeCapabilities?.reasoningEfforts ?? [...ALL_REASONING_EFFORTS]
|
|
856
|
+
: [],
|
|
857
|
+
permissionModes: supportsPermission ? [...ALL_PERMISSION_MODES] : [],
|
|
858
|
+
features: {
|
|
859
|
+
images: supportsImages,
|
|
860
|
+
permissions: supportsPermission,
|
|
861
|
+
plan: enabled,
|
|
862
|
+
cancel: enabled,
|
|
863
|
+
reasoningEffort: supportsReasoningEffort,
|
|
864
|
+
streamJsonFallback: isClaudeFallback,
|
|
865
|
+
},
|
|
677
866
|
};
|
|
678
867
|
});
|
|
679
868
|
const anyEnabled = providers.some((p) => p.enabled);
|
|
869
|
+
const anyPermission = providers.some((p) => p.supportsPermission);
|
|
680
870
|
this.input.send(createEnvelope({
|
|
681
871
|
type: "agent.v2.capabilities",
|
|
682
872
|
sessionId: this.input.sessionId,
|
|
@@ -691,7 +881,7 @@ export class AgentWorkspaceProxy {
|
|
|
691
881
|
supportsSessionLoad: anyEnabled,
|
|
692
882
|
supportsImages: providers.some((p) => p.supportsImages),
|
|
693
883
|
supportsAudio: false,
|
|
694
|
-
supportsPermission:
|
|
884
|
+
supportsPermission: anyPermission,
|
|
695
885
|
supportsPlan: anyEnabled,
|
|
696
886
|
supportsCancel: anyEnabled,
|
|
697
887
|
},
|
|
@@ -849,8 +1039,17 @@ export class AgentWorkspaceProxy {
|
|
|
849
1039
|
permissionMode: payload.permissionMode,
|
|
850
1040
|
cwd: conversation.cwd,
|
|
851
1041
|
});
|
|
852
|
-
|
|
853
|
-
if (conversation.
|
|
1042
|
+
const nextAgentSessionId = this.extractSessionId(result);
|
|
1043
|
+
if (nextAgentSessionId && nextAgentSessionId !== conversation.agentSessionId) {
|
|
1044
|
+
if (conversation.agentSessionId)
|
|
1045
|
+
this.conversationByAgentSessionId.delete(conversation.agentSessionId);
|
|
1046
|
+
conversation.agentSessionId = nextAgentSessionId;
|
|
1047
|
+
this.conversationByAgentSessionId.set(nextAgentSessionId, conversation.id);
|
|
1048
|
+
}
|
|
1049
|
+
const turnId = this.extractTurnId(result);
|
|
1050
|
+
if (turnId)
|
|
1051
|
+
this.currentTurnIds.set(conversation.id, turnId);
|
|
1052
|
+
if (conversation.status === "running" && protocol !== "codex-app-server") {
|
|
854
1053
|
this.updateConversationStatus(conversation.id, "idle");
|
|
855
1054
|
}
|
|
856
1055
|
}
|
|
@@ -870,6 +1069,9 @@ export class AgentWorkspaceProxy {
|
|
|
870
1069
|
if (method === "item/tool/requestUserInput" || method === "tool/requestUserInput") {
|
|
871
1070
|
return this.handleStructuredInput(params, true);
|
|
872
1071
|
}
|
|
1072
|
+
if (method === "mcpServer/elicitation/request") {
|
|
1073
|
+
return this.handleStructuredInput({ ...(asRecord(params) ?? {}), source: method }, true);
|
|
1074
|
+
}
|
|
873
1075
|
if (isPermissionRequestMethod(method)) {
|
|
874
1076
|
return this.handlePermission(params, true, method);
|
|
875
1077
|
}
|
|
@@ -896,6 +1098,10 @@ export class AgentWorkspaceProxy {
|
|
|
896
1098
|
this.handleStructuredInput(params);
|
|
897
1099
|
return;
|
|
898
1100
|
}
|
|
1101
|
+
if (method === "mcpServer/elicitation/request") {
|
|
1102
|
+
this.handleStructuredInput({ ...(asRecord(params) ?? {}), source: method });
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
899
1105
|
if (isPermissionRequestMethod(method)) {
|
|
900
1106
|
this.handlePermission(params, false, method);
|
|
901
1107
|
return;
|
|
@@ -911,15 +1117,19 @@ export class AgentWorkspaceProxy {
|
|
|
911
1117
|
return;
|
|
912
1118
|
}
|
|
913
1119
|
if (method === "turn/started") {
|
|
914
|
-
|
|
915
|
-
|
|
1120
|
+
if (conversationId) {
|
|
1121
|
+
const turnId = this.extractTurnId(params);
|
|
1122
|
+
if (turnId)
|
|
1123
|
+
this.currentTurnIds.set(conversationId, turnId);
|
|
916
1124
|
this.updateConversationStatus(conversationId, "running");
|
|
1125
|
+
}
|
|
917
1126
|
return;
|
|
918
1127
|
}
|
|
919
1128
|
if (method === "turn/completed") {
|
|
920
|
-
|
|
921
|
-
|
|
1129
|
+
if (conversationId) {
|
|
1130
|
+
this.currentTurnIds.delete(conversationId);
|
|
922
1131
|
this.updateConversationStatus(conversationId, "idle");
|
|
1132
|
+
}
|
|
923
1133
|
return;
|
|
924
1134
|
}
|
|
925
1135
|
if (method === "session/request_permission") {
|
|
@@ -957,7 +1167,11 @@ export class AgentWorkspaceProxy {
|
|
|
957
1167
|
this.handleCommandExecDelta(params);
|
|
958
1168
|
return;
|
|
959
1169
|
case "item/autoApprovalReview/started":
|
|
1170
|
+
this.handleAutoApprovalReview(params, true);
|
|
1171
|
+
return;
|
|
960
1172
|
case "item/autoApprovalReview/completed":
|
|
1173
|
+
this.handleAutoApprovalReview(params, false);
|
|
1174
|
+
return;
|
|
961
1175
|
case "item/commandExecution/terminalInteraction":
|
|
962
1176
|
return;
|
|
963
1177
|
}
|
|
@@ -1199,6 +1413,36 @@ export class AgentWorkspaceProxy {
|
|
|
1199
1413
|
status: "running",
|
|
1200
1414
|
});
|
|
1201
1415
|
}
|
|
1416
|
+
handleAutoApprovalReview(params, streaming) {
|
|
1417
|
+
const raw = asRecord(params) ?? {};
|
|
1418
|
+
const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
|
|
1419
|
+
if (!conversationId)
|
|
1420
|
+
return;
|
|
1421
|
+
const itemId = firstString(raw, ["itemId", "id", "reviewId"]) ?? "auto-approval-review";
|
|
1422
|
+
const existing = this.findItem(conversationId, itemId);
|
|
1423
|
+
const decision = firstString(raw, ["decision", "result", "outcome", "status"]);
|
|
1424
|
+
const summary = firstString(raw, ["summary", "message", "text", "reason"]) ??
|
|
1425
|
+
stringifyDefined(raw.review ?? raw.details);
|
|
1426
|
+
this.upsertItem(conversationId, {
|
|
1427
|
+
id: itemId,
|
|
1428
|
+
conversationId,
|
|
1429
|
+
type: "status",
|
|
1430
|
+
kind: "review",
|
|
1431
|
+
role: "system",
|
|
1432
|
+
turnId: this.extractTurnId(raw) ?? this.currentTurnIds.get(conversationId),
|
|
1433
|
+
itemId,
|
|
1434
|
+
text: summary ?? (streaming ? "正在审查自动授权" : decision ? `自动授权审查:${decision}` : "已完成自动授权审查"),
|
|
1435
|
+
metadata: {
|
|
1436
|
+
...(existing?.metadata ?? {}),
|
|
1437
|
+
autoApprovalReview: true,
|
|
1438
|
+
...(decision ? { decision } : {}),
|
|
1439
|
+
},
|
|
1440
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
1441
|
+
updatedAt: Date.now(),
|
|
1442
|
+
isStreaming: streaming,
|
|
1443
|
+
});
|
|
1444
|
+
this.updateConversationPreview(conversationId, streaming ? "正在审查自动授权" : "已完成自动授权审查", streaming ? "running" : undefined);
|
|
1445
|
+
}
|
|
1202
1446
|
handleCompletedMessageItem(item, streaming) {
|
|
1203
1447
|
const conversationId = this.conversationIdFromParams(item) ?? this.activeConversationId;
|
|
1204
1448
|
if (!conversationId)
|
|
@@ -1271,7 +1515,7 @@ export class AgentWorkspaceProxy {
|
|
|
1271
1515
|
conversationId,
|
|
1272
1516
|
type: "status",
|
|
1273
1517
|
role: "system",
|
|
1274
|
-
turnId: this.extractTurnId(item) ?? this.
|
|
1518
|
+
turnId: this.extractTurnId(item) ?? this.currentTurnIds.get(conversationId),
|
|
1275
1519
|
itemId,
|
|
1276
1520
|
createdAt: existing?.createdAt ?? Date.now(),
|
|
1277
1521
|
updatedAt: Date.now(),
|
|
@@ -1322,7 +1566,7 @@ export class AgentWorkspaceProxy {
|
|
|
1322
1566
|
type: "status",
|
|
1323
1567
|
kind: "subagent_action",
|
|
1324
1568
|
role: "system",
|
|
1325
|
-
turnId: this.extractTurnId(item) ?? this.
|
|
1569
|
+
turnId: this.extractTurnId(item) ?? this.currentTurnIds.get(conversationId),
|
|
1326
1570
|
itemId,
|
|
1327
1571
|
text,
|
|
1328
1572
|
subagent,
|
|
@@ -1335,11 +1579,15 @@ export class AgentWorkspaceProxy {
|
|
|
1335
1579
|
handleStructuredInput(params, waitForResponse = false) {
|
|
1336
1580
|
const raw = asRecord(params) ?? {};
|
|
1337
1581
|
const conversationId = this.conversationIdFromParams(raw) ?? this.activeConversationId;
|
|
1582
|
+
const source = firstString(raw, ["method", "source", "requestMethod"]);
|
|
1583
|
+
const formatResponse = source === "mcpServer/elicitation/request"
|
|
1584
|
+
? formatMcpElicitationResponse
|
|
1585
|
+
: formatStructuredInputResponse;
|
|
1338
1586
|
if (!conversationId)
|
|
1339
|
-
return waitForResponse ? Promise.resolve(
|
|
1587
|
+
return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
|
|
1340
1588
|
const structuredInput = decodeStructuredInput(raw);
|
|
1341
1589
|
if (!structuredInput)
|
|
1342
|
-
return waitForResponse ? Promise.resolve(
|
|
1590
|
+
return waitForResponse ? Promise.resolve(formatResponse({})) : undefined;
|
|
1343
1591
|
const text = structuredInput.questions.map((question) => question.question).join("\n");
|
|
1344
1592
|
this.pendingStructuredInputs.set(structuredInput.requestId, { conversationId, input: structuredInput });
|
|
1345
1593
|
this.upsertItem(conversationId, {
|
|
@@ -1361,13 +1609,13 @@ export class AgentWorkspaceProxy {
|
|
|
1361
1609
|
const timer = setTimeout(() => {
|
|
1362
1610
|
this.pendingStructuredInputs.delete(structuredInput.requestId);
|
|
1363
1611
|
this.structuredInputWaiters.delete(structuredInput.requestId);
|
|
1364
|
-
resolve(
|
|
1612
|
+
resolve(formatResponse({}));
|
|
1365
1613
|
this.markStructuredInput(conversationId, structuredInput.requestId, {
|
|
1366
1614
|
inputPending: false,
|
|
1367
1615
|
inputError: "等待用户输入超时",
|
|
1368
1616
|
});
|
|
1369
1617
|
}, PERMISSION_TIMEOUT_MS);
|
|
1370
|
-
this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer });
|
|
1618
|
+
this.structuredInputWaiters.set(structuredInput.requestId, { resolve, timer, source });
|
|
1371
1619
|
});
|
|
1372
1620
|
}
|
|
1373
1621
|
toolCallFromItem(item, fallbackStatus) {
|
|
@@ -1460,6 +1708,12 @@ export class AgentWorkspaceProxy {
|
|
|
1460
1708
|
optionId: selectedOptionId,
|
|
1461
1709
|
});
|
|
1462
1710
|
}
|
|
1711
|
+
this.markPermission(payload.conversationId, payload.requestId, {
|
|
1712
|
+
permissionOutcome: payload.outcome,
|
|
1713
|
+
optionId: selectedOptionId,
|
|
1714
|
+
permissionError: undefined,
|
|
1715
|
+
permissionPending: false,
|
|
1716
|
+
});
|
|
1463
1717
|
this.updateConversationStatus(payload.conversationId, "running");
|
|
1464
1718
|
}
|
|
1465
1719
|
respondStructuredInput(payload) {
|
|
@@ -1469,15 +1723,30 @@ export class AgentWorkspaceProxy {
|
|
|
1469
1723
|
if (waiter) {
|
|
1470
1724
|
clearTimeout(waiter.timer);
|
|
1471
1725
|
this.structuredInputWaiters.delete(payload.requestId);
|
|
1472
|
-
waiter.
|
|
1726
|
+
const formatResponse = waiter.source === "mcpServer/elicitation/request"
|
|
1727
|
+
? formatMcpElicitationResponse
|
|
1728
|
+
: formatStructuredInputResponse;
|
|
1729
|
+
waiter.resolve(formatResponse(payload.answers));
|
|
1473
1730
|
}
|
|
1474
1731
|
this.markStructuredInput(payload.conversationId, payload.requestId, {
|
|
1475
1732
|
inputPending: false,
|
|
1476
1733
|
inputSubmitted: true,
|
|
1734
|
+
inputSubmitting: false,
|
|
1735
|
+
inputError: undefined,
|
|
1477
1736
|
answers: payload.answers,
|
|
1478
1737
|
});
|
|
1479
1738
|
this.updateConversationStatus(pending?.conversationId ?? payload.conversationId, "running");
|
|
1480
1739
|
}
|
|
1740
|
+
markPermission(conversationId, requestId, metadata) {
|
|
1741
|
+
const item = this.findItem(conversationId, `permission:${requestId}`);
|
|
1742
|
+
if (!item)
|
|
1743
|
+
return;
|
|
1744
|
+
this.upsertItem(conversationId, {
|
|
1745
|
+
...item,
|
|
1746
|
+
metadata: { ...(item.metadata ?? {}), ...metadata },
|
|
1747
|
+
updatedAt: Date.now(),
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1481
1750
|
markStructuredInput(conversationId, requestId, metadata) {
|
|
1482
1751
|
const item = this.findItem(conversationId, `input:${requestId}`);
|
|
1483
1752
|
if (!item)
|
|
@@ -1795,7 +2064,8 @@ function selectPermissionOption(permission, outcome) {
|
|
|
1795
2064
|
function isPermissionRequestMethod(method) {
|
|
1796
2065
|
return (method === "session/request_permission" ||
|
|
1797
2066
|
method.endsWith("/requestApproval") ||
|
|
1798
|
-
method === "mcpServer/elicitation/request"
|
|
2067
|
+
method === "mcpServer/elicitation/request" ||
|
|
2068
|
+
method === "claude/requestApproval");
|
|
1799
2069
|
}
|
|
1800
2070
|
function formatStructuredInputResponse(answers) {
|
|
1801
2071
|
return {
|
|
@@ -1805,6 +2075,17 @@ function formatStructuredInputResponse(answers) {
|
|
|
1805
2075
|
])),
|
|
1806
2076
|
};
|
|
1807
2077
|
}
|
|
2078
|
+
function formatMcpElicitationResponse(answers) {
|
|
2079
|
+
const content = Object.fromEntries(Object.entries(answers).map(([questionId, values]) => [
|
|
2080
|
+
questionId,
|
|
2081
|
+
values.length <= 1 ? values[0] ?? "" : values,
|
|
2082
|
+
]));
|
|
2083
|
+
return {
|
|
2084
|
+
action: Object.keys(content).length > 0 ? "accept" : "cancel",
|
|
2085
|
+
content,
|
|
2086
|
+
_meta: { source: "linkshell" },
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
1808
2089
|
function formatPermissionResponse(source, outcome, optionId) {
|
|
1809
2090
|
if (source === "item/commandExecution/requestApproval" || source === "item/fileChange/requestApproval") {
|
|
1810
2091
|
return { decision: outcome === "allow" ? "accept" : outcome === "deny" ? "decline" : "cancel" };
|
|
@@ -1818,6 +2099,14 @@ function formatPermissionResponse(source, outcome, optionId) {
|
|
|
1818
2099
|
}
|
|
1819
2100
|
return { permissions: { type: "managed", network: { enabled: false }, fileSystem: { type: "readOnly" } } };
|
|
1820
2101
|
}
|
|
2102
|
+
if (source === "claude/requestApproval") {
|
|
2103
|
+
return { behavior: outcome === "allow" ? "allow" : "deny" };
|
|
2104
|
+
}
|
|
2105
|
+
if (source === "mcpServer/elicitation/request") {
|
|
2106
|
+
return outcome === "allow"
|
|
2107
|
+
? { action: "accept", content: { optionId }, _meta: { source: "linkshell" } }
|
|
2108
|
+
: { action: outcome === "cancelled" ? "cancel" : "decline", content: {}, _meta: { source: "linkshell" } };
|
|
2109
|
+
}
|
|
1821
2110
|
return {
|
|
1822
2111
|
outcome: outcome === "cancelled"
|
|
1823
2112
|
? { outcome: "cancelled" }
|