@co0ontty/wand 4.82.0 → 4.83.1
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/README.md +47 -9
- package/dist/agent-dispatch.d.ts +3 -2
- package/dist/agent-dispatch.js +19 -3
- package/dist/ai-team-delivery-types.d.ts +40 -0
- package/dist/ai-team-delivery-types.js +1 -0
- package/dist/ai-team-delivery.d.ts +13 -0
- package/dist/ai-team-delivery.js +89 -0
- package/dist/ai-team-employee-binding.d.ts +16 -0
- package/dist/ai-team-employee-binding.js +77 -0
- package/dist/ai-team-prompts.js +5 -0
- package/dist/ai-team-runner.d.ts +9 -2
- package/dist/ai-team-runner.js +52 -7
- package/dist/ai-team-types.d.ts +17 -3
- package/dist/ai-team-types.js +43 -3
- package/dist/build-info.json +3 -3
- package/dist/cli.js +36 -0
- package/dist/config.d.ts +18 -10
- package/dist/config.js +63 -105
- package/dist/decision-client.d.ts +4 -0
- package/dist/decision-client.js +91 -0
- package/dist/decision-runner.d.ts +9 -0
- package/dist/decision-runner.js +36 -0
- package/dist/decision-service.d.ts +46 -0
- package/dist/decision-service.js +230 -0
- package/dist/decision-skill.d.ts +6 -0
- package/dist/decision-skill.js +70 -0
- package/dist/decision-tool.d.ts +21 -0
- package/dist/decision-tool.js +346 -0
- package/dist/decision-types.d.ts +36 -0
- package/dist/decision-types.js +123 -0
- package/dist/decision-worker.py +86 -0
- package/dist/default-employee.d.ts +4 -1
- package/dist/default-employee.js +11 -1
- package/dist/env-utils.js +3 -0
- package/dist/git-quick-commit.js +7 -1
- package/dist/message-truncator.d.ts +1 -1
- package/dist/message-truncator.js +29 -22
- package/dist/models.d.ts +1 -0
- package/dist/models.js +31 -4
- package/dist/process-manager.d.ts +39 -1
- package/dist/process-manager.js +112 -8
- package/dist/provider-catalog.d.ts +47 -0
- package/dist/provider-catalog.js +138 -0
- package/dist/provider-cli-updater.d.ts +1 -1
- package/dist/pty-foreground.d.ts +37 -0
- package/dist/pty-foreground.js +56 -0
- package/dist/pty-turn-activity.d.ts +6 -5
- package/dist/pty-turn-activity.js +6 -5
- package/dist/retention.d.ts +29 -0
- package/dist/retention.js +142 -0
- package/dist/server-ai-team-routes.d.ts +2 -1
- package/dist/server-ai-team-routes.js +27 -9
- package/dist/server-decision-routes.d.ts +10 -0
- package/dist/server-decision-routes.js +52 -0
- package/dist/server-employee-routes.js +9 -1
- package/dist/server-session-routes.d.ts +2 -2
- package/dist/server-session-routes.js +97 -1
- package/dist/server-settings-routes.js +10 -36
- package/dist/server-update-routes.js +2 -1
- package/dist/server-workspace-routes.js +8 -0
- package/dist/server.js +37 -5
- package/dist/session-completion-state.d.ts +26 -0
- package/dist/session-completion-state.js +30 -0
- package/dist/session-completion.d.ts +12 -0
- package/dist/session-completion.js +62 -0
- package/dist/session-provider.d.ts +1 -15
- package/dist/session-provider.js +4 -54
- package/dist/session-registry.d.ts +5 -0
- package/dist/session-registry.js +19 -1
- package/dist/session-transport.js +2 -0
- package/dist/storage.d.ts +12 -0
- package/dist/storage.js +119 -16
- package/dist/structured-client-protocol.js +36 -1
- package/dist/structured-provider-common.js +3 -3
- package/dist/structured-session-manager.d.ts +13 -1
- package/dist/structured-session-manager.js +196 -27
- package/dist/system-employee.js +1 -0
- package/dist/task-types.d.ts +4 -2
- package/dist/tool-preview.d.ts +8 -0
- package/dist/tool-preview.js +158 -0
- package/dist/types.d.ts +35 -2
- package/dist/web-ui/content/ai-teams.js +13 -13
- package/dist/web-ui/content/scripts.js +66 -64
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +3 -0
- package/dist/web-ui/embedded-assets.js +3 -3
- package/dist/web-ui/provider-identity.d.ts +7 -13
- package/dist/web-ui/provider-identity.js +8 -71
- package/package.json +3 -2
- package/skills/wand-decision/SKILL.md +61 -0
- package/skills/wand-decision/scripts/decide.mjs +16 -0
|
@@ -7,6 +7,7 @@ import { getErrorMessage } from "./error-utils.js";
|
|
|
7
7
|
import { getDefaultModelForProvider } from "./config.js";
|
|
8
8
|
import { recordIterationPrompt } from "./iteration-log.js";
|
|
9
9
|
import { startEmployeeKnowledgeRunner } from "./employee-knowledge.js";
|
|
10
|
+
import { withDecisionAccess } from "./decision-runner.js";
|
|
10
11
|
import { signalNameFromNumber } from "./signal-utils.js";
|
|
11
12
|
import { provisionalSessionTopic, SessionNativeTitleTracker, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, shouldGenerateSessionTopicFromInput, } from "./session-topic.js";
|
|
12
13
|
import { resolveSessionCwd } from "./session-cwd.js";
|
|
@@ -26,6 +27,7 @@ import { GeminiRunner, applyGeminiEvent, isMissingGeminiSession } from "./struct
|
|
|
26
27
|
import { structuredRunId, } from "./structured-exec-host.js";
|
|
27
28
|
import { defaultStructuredRunner, defaultStructuredState, isStructuredRunnerForProvider, normalizeThinkingEffort, resolveStructuredRunner, } from "./structured-provider-common.js";
|
|
28
29
|
import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
|
|
30
|
+
import { RETENTION_IDLE_MS } from "./retention.js";
|
|
29
31
|
class PersistedStructuredRunnerError extends Error {
|
|
30
32
|
constructor(message) {
|
|
31
33
|
super(message);
|
|
@@ -158,7 +160,7 @@ const STREAM_EMIT_DEBOUNCE_MS = 16;
|
|
|
158
160
|
// second on the event loop.
|
|
159
161
|
const STREAM_SAVE_THROTTLE_MS = 1_000;
|
|
160
162
|
const DETACHED_RECOVERY_RETRY_MS = 500;
|
|
161
|
-
const ARCHIVE_AFTER_MS =
|
|
163
|
+
const ARCHIVE_AFTER_MS = RETENTION_IDLE_MS;
|
|
162
164
|
/**
|
|
163
165
|
* 找出最后一条 assistant turn 中尚未配对 tool_result 的 AskUserQuestion tool_use。
|
|
164
166
|
* 用来识别"刚被 SIGTERM 中断、正在等用户提交答案"的状态。
|
|
@@ -400,7 +402,11 @@ export class StructuredSessionManager {
|
|
|
400
402
|
logger;
|
|
401
403
|
execHost;
|
|
402
404
|
sessions = new Map();
|
|
405
|
+
// Both freshly started runners and adopted daemon runs own the same input gate.
|
|
403
406
|
pendingRunnerExecutions = new Map();
|
|
407
|
+
// Server-only, one-shot execution UUIDs. Actual request lifecycle clears them; metadata updates do not.
|
|
408
|
+
// Never persisted or included in session DTOs.
|
|
409
|
+
unacceptedTeamStarts = new Map();
|
|
404
410
|
interruptedWith = new Map();
|
|
405
411
|
preserveQueueOnInterrupt = new Set();
|
|
406
412
|
/** Last wall-clock time (ms) a streaming checkpoint reached SQLite. */
|
|
@@ -434,18 +440,19 @@ export class StructuredSessionManager {
|
|
|
434
440
|
detachedRecoveryRetryTimer = null;
|
|
435
441
|
detachedRecoveryFailureLogged = false;
|
|
436
442
|
disposed = false;
|
|
437
|
-
constructor(storage, config, logger = null, runners = {}, execHost) {
|
|
443
|
+
constructor(storage, config, logger = null, runners = {}, execHost, decisionRuntime = () => null) {
|
|
438
444
|
this.storage = storage;
|
|
439
445
|
this.config = config;
|
|
440
446
|
this.logger = logger;
|
|
441
447
|
this.execHost = execHost;
|
|
442
|
-
|
|
443
|
-
this.
|
|
444
|
-
this.
|
|
445
|
-
this.
|
|
446
|
-
this.
|
|
447
|
-
this.
|
|
448
|
-
this.
|
|
448
|
+
const wrap = (runner) => withDecisionAccess(runner, storage, decisionRuntime);
|
|
449
|
+
this.claudeCliRunner = wrap(runners.claudeCli ?? new ClaudeCliRunner({ language: () => this.config.language }, this.execHost));
|
|
450
|
+
this.codexRunner = wrap(runners.codex ?? new CodexRunner(undefined, this.execHost));
|
|
451
|
+
this.openCodeRunner = wrap(runners.opencode ?? new OpenCodeRunner(undefined, this.execHost));
|
|
452
|
+
this.grokRunner = wrap(runners.grok ?? new GrokRunner(undefined, this.execHost));
|
|
453
|
+
this.qoderRunner = wrap(runners.qoder ?? new QoderRunner(undefined, this.execHost));
|
|
454
|
+
this.piRunner = wrap(runners.pi ?? new PiRunner(undefined, this.execHost));
|
|
455
|
+
this.geminiRunner = wrap(runners.gemini ?? new GeminiRunner(undefined, this.execHost));
|
|
449
456
|
for (const snapshot of this.storage.loadSessions()) {
|
|
450
457
|
if ((snapshot.sessionKind ?? "pty") !== "structured")
|
|
451
458
|
continue;
|
|
@@ -532,6 +539,7 @@ export class StructuredSessionManager {
|
|
|
532
539
|
if (this.disposed)
|
|
533
540
|
return;
|
|
534
541
|
this.disposed = true;
|
|
542
|
+
this.unacceptedTeamStarts.clear();
|
|
535
543
|
if (this.archiveTimer) {
|
|
536
544
|
clearInterval(this.archiveTimer);
|
|
537
545
|
this.archiveTimer = null;
|
|
@@ -635,13 +643,16 @@ export class StructuredSessionManager {
|
|
|
635
643
|
this.scheduleDetachedRecovery();
|
|
636
644
|
return;
|
|
637
645
|
}
|
|
638
|
-
|
|
639
|
-
|
|
646
|
+
if (this.disposed)
|
|
647
|
+
return;
|
|
648
|
+
const ids = [...this.pendingRecoveryIds];
|
|
640
649
|
const byRunId = new Map(runs.map((run) => [run.runId, run]));
|
|
641
650
|
for (const sessionId of ids) {
|
|
642
651
|
const session = this.sessions.get(sessionId);
|
|
643
|
-
if (!session)
|
|
652
|
+
if (!session) {
|
|
653
|
+
this.pendingRecoveryIds = this.pendingRecoveryIds.filter((id) => id !== sessionId);
|
|
644
654
|
continue;
|
|
655
|
+
}
|
|
645
656
|
const state = byRunId.get(structuredRunId(sessionId));
|
|
646
657
|
if (!state) {
|
|
647
658
|
// Inventory was read successfully, so this is a genuinely lost run,
|
|
@@ -661,16 +672,20 @@ export class StructuredSessionManager {
|
|
|
661
672
|
this.sessions.set(sessionId, interrupted);
|
|
662
673
|
this.saveAuthoritativeSession(interrupted);
|
|
663
674
|
this.emitStructuredSnapshot(interrupted);
|
|
675
|
+
this.pendingRecoveryIds = this.pendingRecoveryIds.filter((id) => id !== sessionId);
|
|
664
676
|
continue;
|
|
665
677
|
}
|
|
666
678
|
try {
|
|
667
679
|
// listRuns is metadata-only on Render v2. Fetch the authoritative
|
|
668
680
|
// paged replay from the owner before feeding any provider reducer.
|
|
669
681
|
const attached = await this.execHost.attachRun(state.runId);
|
|
682
|
+
if (this.disposed)
|
|
683
|
+
return;
|
|
670
684
|
if (!attached || attached.incarnationId !== state.incarnationId) {
|
|
671
685
|
throw new Error("Structured run changed owner between inventory and replay");
|
|
672
686
|
}
|
|
673
|
-
await this.resumeDetachedRun(
|
|
687
|
+
await this.resumeDetachedRun(this.requireSession(sessionId), attached);
|
|
688
|
+
this.pendingRecoveryIds = this.pendingRecoveryIds.filter((id) => id !== sessionId);
|
|
674
689
|
}
|
|
675
690
|
catch (error) {
|
|
676
691
|
console.error(`[WAND] structured run recovery failed for ${sessionId}:`, error);
|
|
@@ -692,9 +707,20 @@ export class StructuredSessionManager {
|
|
|
692
707
|
}
|
|
693
708
|
async resumeDetachedRun(snapshot, initialState) {
|
|
694
709
|
const sessionId = snapshot.id;
|
|
695
|
-
if (!this.execHost || !snapshot.structuredState)
|
|
710
|
+
if (this.disposed || !this.execHost || !snapshot.structuredState)
|
|
696
711
|
return;
|
|
697
712
|
const requestId = `recover-${initialState.incarnationId}`;
|
|
713
|
+
let runningHandle = null;
|
|
714
|
+
let interruptRequested = false;
|
|
715
|
+
const execution = {
|
|
716
|
+
interrupt: () => {
|
|
717
|
+
interruptRequested = true;
|
|
718
|
+
runningHandle?.interrupt();
|
|
719
|
+
},
|
|
720
|
+
};
|
|
721
|
+
// Reserve ownership before publishing inFlight or awaiting adoption. Inputs
|
|
722
|
+
// arriving during replay/attach must queue just like inputs to a fresh runner.
|
|
723
|
+
this.pendingRunnerExecutions.set(sessionId, execution);
|
|
698
724
|
// Re-arm the in-flight marker so UI and request guards treat the resumed
|
|
699
725
|
// turn like any other streaming turn.
|
|
700
726
|
const resumed = {
|
|
@@ -800,7 +826,6 @@ export class StructuredSessionManager {
|
|
|
800
826
|
runningHandle.interrupt();
|
|
801
827
|
}
|
|
802
828
|
};
|
|
803
|
-
let runningHandle = null;
|
|
804
829
|
let lastStdoutSeq = initialState.stdoutSeq;
|
|
805
830
|
let lastStderrSeq = initialState.stderrSeq;
|
|
806
831
|
const feedLine = (line) => {
|
|
@@ -843,7 +868,22 @@ export class StructuredSessionManager {
|
|
|
843
868
|
for (const line of lines)
|
|
844
869
|
feedLine(line);
|
|
845
870
|
}
|
|
846
|
-
|
|
871
|
+
try {
|
|
872
|
+
runningHandle = await this.execHost.adoptRun(structuredRunId(sessionId));
|
|
873
|
+
}
|
|
874
|
+
catch (error) {
|
|
875
|
+
this.releasePendingRunnerExecution(sessionId, execution, false);
|
|
876
|
+
throw error;
|
|
877
|
+
}
|
|
878
|
+
// Server shutdown detaches persistent runs; only an explicit stop/delete
|
|
879
|
+
// should cancel a handle that arrived after its request was invalidated.
|
|
880
|
+
if (this.disposed)
|
|
881
|
+
return;
|
|
882
|
+
if (!this.isCurrentRequest(sessionId, requestId)) {
|
|
883
|
+
runningHandle?.interrupt();
|
|
884
|
+
this.releasePendingRunnerExecution(sessionId, execution);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
847
887
|
if (!runningHandle) {
|
|
848
888
|
// Run vanished between listing and adoption; fall back to failure notes.
|
|
849
889
|
this.finalizeRecoveredRun(sessionId, requestId, processor, replayView, {
|
|
@@ -891,11 +931,18 @@ export class StructuredSessionManager {
|
|
|
891
931
|
});
|
|
892
932
|
flushEmit();
|
|
893
933
|
});
|
|
934
|
+
// Install exit listeners before delivering an interrupt that arrived during adopt.
|
|
935
|
+
if (interruptRequested)
|
|
936
|
+
runningHandle.interrupt();
|
|
894
937
|
}
|
|
895
938
|
finalizeRecoveredRun(sessionId, requestId, processor, replayView, outcome) {
|
|
896
|
-
this.execHost?.forgetRun(structuredRunId(sessionId));
|
|
897
939
|
if (!this.isCurrentRequest(sessionId, requestId))
|
|
898
940
|
return;
|
|
941
|
+
const execution = this.pendingRunnerExecutions.get(sessionId);
|
|
942
|
+
if (execution)
|
|
943
|
+
this.releasePendingRunnerExecution(sessionId, execution);
|
|
944
|
+
else
|
|
945
|
+
this.execHost?.forgetRun(structuredRunId(sessionId));
|
|
899
946
|
const current = this.sessions.get(sessionId);
|
|
900
947
|
if (!current)
|
|
901
948
|
return;
|
|
@@ -907,7 +954,8 @@ export class StructuredSessionManager {
|
|
|
907
954
|
const failedExit = outcome.lost
|
|
908
955
|
|| (outcome.exitCode !== null && outcome.exitCode !== 0)
|
|
909
956
|
|| outcome.signal !== null;
|
|
910
|
-
|
|
957
|
+
const interruptPrompt = this.interruptedWith.get(sessionId);
|
|
958
|
+
if ((processor.primaryError || failedExit) && !interruptedForQuestion && !interruptPrompt) {
|
|
911
959
|
const errorText = outcome.lost
|
|
912
960
|
? "服务重启后运行进程已丢失,本轮未能完成。"
|
|
913
961
|
: this.formatStructuredExitError(commandLabel, outcome.exitCode, outcome.signal, {
|
|
@@ -923,7 +971,7 @@ export class StructuredSessionManager {
|
|
|
923
971
|
this.emitStructuredSnapshot(failed, "ended");
|
|
924
972
|
return;
|
|
925
973
|
}
|
|
926
|
-
const keepRunning = interruptedForQuestion;
|
|
974
|
+
const keepRunning = interruptedForQuestion || !!interruptPrompt;
|
|
927
975
|
const messages = replayTruncated
|
|
928
976
|
? this.mergeTruncatedReplayTurn(current, processor, replayView, !keepRunning)
|
|
929
977
|
: this.buildCompletedAssistantMessages(current, processor.state, false);
|
|
@@ -936,6 +984,7 @@ export class StructuredSessionManager {
|
|
|
936
984
|
output: replayTruncated ? current.output || processor.state.result : processor.state.result,
|
|
937
985
|
claudeSessionId: processor.state.sessionId ?? current.claudeSessionId,
|
|
938
986
|
messages,
|
|
987
|
+
queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
|
|
939
988
|
pendingEscalation: null,
|
|
940
989
|
permissionBlocked: false,
|
|
941
990
|
structuredState: {
|
|
@@ -952,7 +1001,16 @@ export class StructuredSessionManager {
|
|
|
952
1001
|
this.emitStructuredSnapshot(finished);
|
|
953
1002
|
if (!keepRunning)
|
|
954
1003
|
this.emitStructuredSnapshot(finished, "ended");
|
|
955
|
-
if (
|
|
1004
|
+
if (interruptPrompt) {
|
|
1005
|
+
this.interruptedWith.delete(sessionId);
|
|
1006
|
+
this.preserveQueueOnInterrupt.delete(sessionId);
|
|
1007
|
+
setImmediate(() => {
|
|
1008
|
+
this.sendMessage(sessionId, interruptPrompt).catch((error) => {
|
|
1009
|
+
console.error("[WAND] recovered interrupt-and-send failed:", error);
|
|
1010
|
+
});
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
else if ((finished.queuedMessages?.length ?? 0) > 0) {
|
|
956
1014
|
setImmediate(() => { void this.flushNextQueuedMessage(sessionId); });
|
|
957
1015
|
}
|
|
958
1016
|
}
|
|
@@ -1067,6 +1125,20 @@ export class StructuredSessionManager {
|
|
|
1067
1125
|
this.emitStructuredSnapshot(updated);
|
|
1068
1126
|
return updated;
|
|
1069
1127
|
}
|
|
1128
|
+
/** 归档 / 取消归档:只写标记并广播,不中断运行中的 run,也不删历史。 */
|
|
1129
|
+
setSessionArchived(id, archived) {
|
|
1130
|
+
const current = this.requireSession(id);
|
|
1131
|
+
const archivedAt = archived ? new Date().toISOString() : null;
|
|
1132
|
+
const updated = { ...current, archived, archivedAt };
|
|
1133
|
+
this.sessions.set(id, updated);
|
|
1134
|
+
this.storage.updateSessionRuntimeMetadata(updated);
|
|
1135
|
+
this.emit({
|
|
1136
|
+
type: "status",
|
|
1137
|
+
sessionId: id,
|
|
1138
|
+
data: { archived, archivedAt, sessionKind: "structured" },
|
|
1139
|
+
});
|
|
1140
|
+
return updated;
|
|
1141
|
+
}
|
|
1070
1142
|
setSessionTopicGenerating(id, titleGenerating) {
|
|
1071
1143
|
const current = this.sessions.get(id);
|
|
1072
1144
|
if (!current || current.titleGenerating === titleGenerating)
|
|
@@ -1256,12 +1328,39 @@ export class StructuredSessionManager {
|
|
|
1256
1328
|
this.emitStructuredSnapshot(titled);
|
|
1257
1329
|
return titled;
|
|
1258
1330
|
}
|
|
1331
|
+
/** Server-only generation anchor, including a failed unaccepted turn whose activeRequestId is cleared. */
|
|
1332
|
+
teamRequestId(id) {
|
|
1333
|
+
if (this.disposed)
|
|
1334
|
+
return null;
|
|
1335
|
+
const session = this.sessions.get(id);
|
|
1336
|
+
if (!session?.automationId?.startsWith("ai-team:"))
|
|
1337
|
+
return null;
|
|
1338
|
+
const failed = this.unacceptedTeamStarts.get(id);
|
|
1339
|
+
return session.structuredState?.activeRequestId
|
|
1340
|
+
?? (session.status === "failed" && !session.structuredState?.inFlight ? failed?.requestId ?? null : null);
|
|
1341
|
+
}
|
|
1342
|
+
/** Consume only the exact failed request observed by the team's dispatch, never error-text inference. */
|
|
1343
|
+
consumeUnacceptedTeamStartup(id, requestId) {
|
|
1344
|
+
const fact = this.unacceptedTeamStarts.get(id);
|
|
1345
|
+
if (!fact || fact.requestId !== requestId)
|
|
1346
|
+
return false;
|
|
1347
|
+
this.unacceptedTeamStarts.delete(id);
|
|
1348
|
+
const session = this.sessions.get(id);
|
|
1349
|
+
return !this.disposed && session?.status === "failed"
|
|
1350
|
+
&& !session.structuredState?.activeRequestId && !session.structuredState?.inFlight;
|
|
1351
|
+
}
|
|
1259
1352
|
/** Only an unstarted employee conversation may change CLI after a failed first turn. */
|
|
1260
1353
|
retryEmployeeCandidate(id, original, prompt, error) {
|
|
1354
|
+
// Team runs own candidate scheduling; keep the snapshot without a second retry loop.
|
|
1355
|
+
if (original.automationId?.startsWith("ai-team:"))
|
|
1356
|
+
return null;
|
|
1261
1357
|
const candidates = original.employeeCandidates ?? [];
|
|
1262
1358
|
const nextIndex = (original.employeeCandidateIndex ?? 0) + 1;
|
|
1263
1359
|
if (!original.employeeId || (original.messages?.length ?? 0) !== 0 || nextIndex >= candidates.length)
|
|
1264
1360
|
return null;
|
|
1361
|
+
// 手动选择候选链外的工具时,不把明确选择又换回员工的另一条候选。
|
|
1362
|
+
if (original.provider !== candidates[original.employeeCandidateIndex ?? 0]?.provider)
|
|
1363
|
+
return null;
|
|
1265
1364
|
const current = this.sessions.get(id);
|
|
1266
1365
|
if (!current || (current.status !== "running" && current.status !== "failed"))
|
|
1267
1366
|
return null;
|
|
@@ -1359,10 +1458,8 @@ export class StructuredSessionManager {
|
|
|
1359
1458
|
const runnerExecution = this.pendingRunnerExecutions.get(id);
|
|
1360
1459
|
// interrupt() only requests cancellation; completion can settle later.
|
|
1361
1460
|
// Treat runner-map ownership as the authoritative in-flight state.
|
|
1362
|
-
const childActive = Boolean(runnerExecution);
|
|
1461
|
+
const childActive = Boolean(runnerExecution) || this.pendingRecoveryIds.includes(id);
|
|
1363
1462
|
if (!childActive) {
|
|
1364
|
-
if (runnerExecution)
|
|
1365
|
-
this.releasePendingRunnerExecution(id, runnerExecution);
|
|
1366
1463
|
const recovered = {
|
|
1367
1464
|
...session,
|
|
1368
1465
|
status: "idle",
|
|
@@ -1378,6 +1475,7 @@ export class StructuredSessionManager {
|
|
|
1378
1475
|
session = recovered;
|
|
1379
1476
|
}
|
|
1380
1477
|
else if (opts?.interrupt) {
|
|
1478
|
+
this.requireRecoveryControl(id);
|
|
1381
1479
|
this.interruptedWith.set(id, prompt);
|
|
1382
1480
|
if (opts.preserveQueue) {
|
|
1383
1481
|
this.preserveQueueOnInterrupt.add(id);
|
|
@@ -1448,6 +1546,7 @@ export class StructuredSessionManager {
|
|
|
1448
1546
|
content: [{ type: "text", text: prompt }],
|
|
1449
1547
|
};
|
|
1450
1548
|
const requestId = randomUUID();
|
|
1549
|
+
this.unacceptedTeamStarts.delete(id);
|
|
1451
1550
|
if ((session.provider === "pi" && isMissingPiSession(session.structuredState?.lastError, session.claudeSessionId))
|
|
1452
1551
|
|| (session.provider === "gemini" && isMissingGeminiSession(session.structuredState?.lastError))) {
|
|
1453
1552
|
session = { ...session, claudeSessionId: null };
|
|
@@ -1466,6 +1565,8 @@ export class StructuredSessionManager {
|
|
|
1466
1565
|
},
|
|
1467
1566
|
};
|
|
1468
1567
|
this.sessions.set(id, updated);
|
|
1568
|
+
if (updated.automationId?.startsWith("ai-team:"))
|
|
1569
|
+
opts?.teamRequestStarted?.(id, requestId);
|
|
1469
1570
|
this.checkpointSessionMessages(updated);
|
|
1470
1571
|
this.emitStructuredSnapshot(updated);
|
|
1471
1572
|
this.emit({
|
|
@@ -1569,6 +1670,14 @@ export class StructuredSessionManager {
|
|
|
1569
1670
|
};
|
|
1570
1671
|
this.sessions.set(id, failed);
|
|
1571
1672
|
this.saveAuthoritativeSession(failed);
|
|
1673
|
+
if (!this.disposed && error instanceof UnacceptedStructuredSpawnError
|
|
1674
|
+
&& current.automationId?.startsWith("ai-team:")) {
|
|
1675
|
+
// Available before status/ended listeners run. A later turn/stop/delete invalidates this fact.
|
|
1676
|
+
if (this.unacceptedTeamStarts.size >= 1024) {
|
|
1677
|
+
this.unacceptedTeamStarts.delete(this.unacceptedTeamStarts.keys().next().value);
|
|
1678
|
+
}
|
|
1679
|
+
this.unacceptedTeamStarts.set(id, { requestId });
|
|
1680
|
+
}
|
|
1572
1681
|
this.emit({
|
|
1573
1682
|
type: "status",
|
|
1574
1683
|
sessionId: id,
|
|
@@ -1654,6 +1763,7 @@ export class StructuredSessionManager {
|
|
|
1654
1763
|
*/
|
|
1655
1764
|
async promoteQueuedMessage(sessionId, index, expectedText, idempotencyKey) {
|
|
1656
1765
|
const session = this.requireSession(sessionId);
|
|
1766
|
+
this.requireRecoveryControl(sessionId);
|
|
1657
1767
|
if (idempotencyKey && this.seenIdempotencyKeys.has(`${sessionId}:${idempotencyKey}`)) {
|
|
1658
1768
|
return session;
|
|
1659
1769
|
}
|
|
@@ -1700,6 +1810,56 @@ export class StructuredSessionManager {
|
|
|
1700
1810
|
this.emitStructuredSnapshot(updated);
|
|
1701
1811
|
return updated;
|
|
1702
1812
|
}
|
|
1813
|
+
/** 新建空白对话可原位换 CLI;接受过输入、恢复会话和自动化不跨 provider 搬历史。 */
|
|
1814
|
+
setSessionProvider(sessionId, provider) {
|
|
1815
|
+
const session = this.requireSession(sessionId);
|
|
1816
|
+
if (!isSessionProvider(provider))
|
|
1817
|
+
throw new Error("请选择有效的 CLI 工具。");
|
|
1818
|
+
if (session.status !== "idle" || session.archived || session.structuredState?.inFlight ||
|
|
1819
|
+
this.pendingRunnerExecutions.has(sessionId) || session.claudeSessionId ||
|
|
1820
|
+
(session.messages?.length ?? 0) > 0 || (session.queuedMessages?.length ?? 0) > 0 ||
|
|
1821
|
+
session.automationId || (session.sessionSource && session.sessionSource !== "interactive") ||
|
|
1822
|
+
this.relayHandlerFor(session)) {
|
|
1823
|
+
throw new Error("只有尚未发送消息的新建空白对话可以更换工具。");
|
|
1824
|
+
}
|
|
1825
|
+
if (session.provider === provider)
|
|
1826
|
+
return session;
|
|
1827
|
+
const candidates = session.employeeCandidates ?? [];
|
|
1828
|
+
const candidateIndex = candidates.findIndex((candidate) => candidate.provider === provider);
|
|
1829
|
+
const candidate = candidateIndex >= 0 ? candidates[candidateIndex] : undefined;
|
|
1830
|
+
const runner = defaultStructuredRunner(provider);
|
|
1831
|
+
const model = candidate && candidate.model !== "default" ? candidate.model
|
|
1832
|
+
: getDefaultModelForProvider(this.config, provider) || null;
|
|
1833
|
+
const effort = normalizeThinkingEffort(candidate?.thinkingEffort ?? this.config.defaultThinkingEffort);
|
|
1834
|
+
const thinkingEffort = effort?.includes(":") && !effort.startsWith(`${provider}:`) ? "off" : effort;
|
|
1835
|
+
const requestedMode = candidate?.mode ?? session.mode;
|
|
1836
|
+
const unsupportedMode = (requestedMode === "native" && provider !== "claude") ||
|
|
1837
|
+
(requestedMode === "auto-edit" && ["opencode", "grok", "pi"].includes(provider));
|
|
1838
|
+
const mode = provider === "codex" ? "full-access"
|
|
1839
|
+
: unsupportedMode ? "default" : requestedMode;
|
|
1840
|
+
const updated = {
|
|
1841
|
+
...session,
|
|
1842
|
+
provider,
|
|
1843
|
+
runner,
|
|
1844
|
+
command: recoveredCommandLabel(runner),
|
|
1845
|
+
mode,
|
|
1846
|
+
autoApprovePermissions: shouldAutoApproveForMode(mode),
|
|
1847
|
+
selectedModel: model,
|
|
1848
|
+
thinkingEffort,
|
|
1849
|
+
employeeCandidateIndex: candidateIndex >= 0 ? candidateIndex : undefined,
|
|
1850
|
+
structuredState: { ...defaultStructuredState(provider, runner), model: model ?? undefined },
|
|
1851
|
+
};
|
|
1852
|
+
// 身份、知识归属、任务/目录和候选快照不变;只更新本会话的执行参数。
|
|
1853
|
+
this.storage.saveSession(updated);
|
|
1854
|
+
this.sessions.set(sessionId, updated);
|
|
1855
|
+
this.emit({
|
|
1856
|
+
type: "status",
|
|
1857
|
+
sessionId,
|
|
1858
|
+
data: { ...buildStructuredOutputPayload(updated), command: updated.command,
|
|
1859
|
+
mode, autoApprovePermissions: updated.autoApprovePermissions },
|
|
1860
|
+
});
|
|
1861
|
+
return updated;
|
|
1862
|
+
}
|
|
1703
1863
|
/** Update the selected model for a structured session. Takes effect on the next spawn. */
|
|
1704
1864
|
setSessionModel(sessionId, model) {
|
|
1705
1865
|
const session = this.requireSession(sessionId);
|
|
@@ -1840,8 +2000,15 @@ export class StructuredSessionManager {
|
|
|
1840
2000
|
});
|
|
1841
2001
|
return updated;
|
|
1842
2002
|
}
|
|
2003
|
+
requireRecoveryControl(id) {
|
|
2004
|
+
if (this.pendingRecoveryIds.includes(id) && !this.pendingRunnerExecutions.has(id)) {
|
|
2005
|
+
throw new Error("正在恢复运行中的任务,请稍后重试控制操作;新消息仍可排队。");
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
1843
2008
|
stop(id) {
|
|
2009
|
+
this.unacceptedTeamStarts.delete(id);
|
|
1844
2010
|
const session = this.requireSession(id);
|
|
2011
|
+
this.requireRecoveryControl(id);
|
|
1845
2012
|
this.interruptedWith.delete(id);
|
|
1846
2013
|
this.preserveQueueOnInterrupt.delete(id);
|
|
1847
2014
|
// Clearing activeRequestId is the generation barrier: late data/close callbacks
|
|
@@ -1874,6 +2041,8 @@ export class StructuredSessionManager {
|
|
|
1874
2041
|
return cancelled;
|
|
1875
2042
|
}
|
|
1876
2043
|
delete(id) {
|
|
2044
|
+
this.unacceptedTeamStarts.delete(id);
|
|
2045
|
+
this.requireRecoveryControl(id);
|
|
1877
2046
|
const runnerExecution = this.pendingRunnerExecutions.get(id);
|
|
1878
2047
|
// Invalidate callback ownership before signalling the runner. Cancellation
|
|
1879
2048
|
// can synchronously wake listeners in some adapter implementations.
|
|
@@ -1908,13 +2077,13 @@ export class StructuredSessionManager {
|
|
|
1908
2077
|
return this.sessions.get(sessionId) ?? null;
|
|
1909
2078
|
}
|
|
1910
2079
|
/** Delete a handle only if it still belongs to the execution doing cleanup. */
|
|
1911
|
-
releasePendingRunnerExecution(sessionId, execution) {
|
|
2080
|
+
releasePendingRunnerExecution(sessionId, execution, forgetRun = true) {
|
|
1912
2081
|
if (this.pendingRunnerExecutions.get(sessionId) !== execution)
|
|
1913
2082
|
return false;
|
|
1914
2083
|
this.pendingRunnerExecutions.delete(sessionId);
|
|
1915
|
-
//
|
|
1916
|
-
|
|
1917
|
-
|
|
2084
|
+
// A failed recovery attach must retain the daemon record for the next retry.
|
|
2085
|
+
if (forgetRun)
|
|
2086
|
+
this.execHost?.forgetRun(structuredRunId(sessionId));
|
|
1918
2087
|
return true;
|
|
1919
2088
|
}
|
|
1920
2089
|
emitStructuredSnapshot(session, eventType = "output") {
|
package/dist/system-employee.js
CHANGED
|
@@ -52,6 +52,7 @@ export function systemEmployeeDefinition(agents, now, existing) {
|
|
|
52
52
|
avatar: "",
|
|
53
53
|
agents: agents.length ? agents : systemEmployeeSeedAgents(),
|
|
54
54
|
systemKey: SYSTEM_EMPLOYEE_KEY,
|
|
55
|
+
tags: [SYSTEM_EMPLOYEE_TAG],
|
|
55
56
|
createdAt: existing?.createdAt ?? now,
|
|
56
57
|
updatedAt: now,
|
|
57
58
|
};
|
package/dist/task-types.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ export declare function isClosedWandTaskStatus(status: WandTaskStatus): boolean;
|
|
|
4
4
|
export type WandTaskPriority = "none" | "low" | "medium" | "high" | "urgent";
|
|
5
5
|
/** 新建任务的默认优先级:用户没挑就是「低」,不再落成「无优先级」。 */
|
|
6
6
|
export declare const DEFAULT_WAND_TASK_PRIORITY: WandTaskPriority;
|
|
7
|
-
/** 任务派发时选择的 CLI
|
|
8
|
-
export type WandTaskAgentProvider = "
|
|
7
|
+
/** 任务派发时选择的 CLI 工具;空串表示尚未指定。沿用 provider 的唯一真源。 */
|
|
8
|
+
export type WandTaskAgentProvider = import("./provider-catalog.js").SessionProvider;
|
|
9
9
|
export type WandTaskAgentModel = string;
|
|
10
10
|
/** 与会话思考档位同一套:旧四档,或 `provider:level` 原生档。 */
|
|
11
11
|
export type WandTaskAgentEffort = "off" | "standard" | "deep" | "max" | `${WandTaskAgentProvider}:${string}`;
|
|
@@ -147,6 +147,8 @@ export interface WandTask {
|
|
|
147
147
|
/** 该任务绑定的执行 Agent;null 表示还没指定。 */
|
|
148
148
|
agent: WandTaskAgent | null;
|
|
149
149
|
executionSubject?: TaskExecutionSubject | null;
|
|
150
|
+
/** 进入归档的时间;未归档为 null。保留期清理按它起算。 */
|
|
151
|
+
archivedAt: string | null;
|
|
150
152
|
createdAt: string;
|
|
151
153
|
updatedAt: string;
|
|
152
154
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ContentBlock, ToolResultBlock, ToolUseBlock } from "./types.js";
|
|
2
|
+
export declare const TOOL_PREVIEW_LIMIT = 180;
|
|
3
|
+
/** A small, literal excerpt for a collapsed card, never generated by a model. */
|
|
4
|
+
export declare function compactPreviewText(value: string): string;
|
|
5
|
+
export declare function toolInputPreview(use: Pick<ToolUseBlock, "name" | "input" | "description" | "preview">): string;
|
|
6
|
+
export declare function toolResultPreview(result: Pick<ToolResultBlock, "content" | "is_error" | "preview" | "_truncated">): string;
|
|
7
|
+
/** Called at transport boundaries; provider history and full detail remain untouched. */
|
|
8
|
+
export declare function withToolPreview(block: ContentBlock): ContentBlock;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
export const TOOL_PREVIEW_LIMIT = 180;
|
|
2
|
+
/** A small, literal excerpt for a collapsed card, never generated by a model. */
|
|
3
|
+
export function compactPreviewText(value) {
|
|
4
|
+
const text = value.slice(0, 8_192)
|
|
5
|
+
.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "")
|
|
6
|
+
.replace(/(?:Bearer\s+)[\w.+/=-]+/gi, "Bearer [隐藏]")
|
|
7
|
+
.replace(/((?:api[_-]?key|access[_-]?token|token|password|secret)\s*[=:]\s*)[^\s,;]+/gi, "$1[隐藏]")
|
|
8
|
+
.replace(/\s+/g, " ").trim();
|
|
9
|
+
return text.length > TOOL_PREVIEW_LIMIT ? text.slice(0, TOOL_PREVIEW_LIMIT - 1) + "…" : text;
|
|
10
|
+
}
|
|
11
|
+
function stringField(input, ...keys) {
|
|
12
|
+
for (const key of keys) {
|
|
13
|
+
if (typeof input[key] === "string" && input[key].trim())
|
|
14
|
+
return input[key];
|
|
15
|
+
}
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
function shortPath(path) {
|
|
19
|
+
return path.replace(/\\/g, "/").split("/").filter(Boolean).slice(-2).join("/");
|
|
20
|
+
}
|
|
21
|
+
function lineCount(text) {
|
|
22
|
+
return text ? text.split("\n").length - (text.endsWith("\n") ? 1 : 0) : 0;
|
|
23
|
+
}
|
|
24
|
+
export function toolInputPreview(use) {
|
|
25
|
+
if (use.preview)
|
|
26
|
+
return compactPreviewText(use.preview);
|
|
27
|
+
const input = use.input ?? {};
|
|
28
|
+
const name = use.name.toLowerCase().split(/\/|__/).pop() ?? "";
|
|
29
|
+
const path = shortPath(stringField(input, "file_path", "path", "filename", "file", "notebook_path"));
|
|
30
|
+
const command = stringField(input, "command", "cmd", "script");
|
|
31
|
+
let parts = [];
|
|
32
|
+
if (command) {
|
|
33
|
+
parts = [command];
|
|
34
|
+
}
|
|
35
|
+
else if (Array.isArray(input.tool_uses)) {
|
|
36
|
+
const calls = input.tool_uses.filter((value) => !!value && typeof value === "object");
|
|
37
|
+
parts = [`并行 ${calls.length} 个工具`, calls.slice(0, 3).map(call => stringField(call, "recipient_name", "name")).filter(Boolean).join("、")];
|
|
38
|
+
}
|
|
39
|
+
else if (typeof input.code === "string") {
|
|
40
|
+
const calls = [...new Set(Array.from(input.code.slice(0, 16_384).matchAll(/tools\.(\w+)\s*\(/g), match => match[1]))];
|
|
41
|
+
parts = [calls.length ? `脚本调用 ${calls.slice(0, 4).join("、")}` : "执行代码", use.description || ""];
|
|
42
|
+
}
|
|
43
|
+
else if (Array.isArray(input.urls)) {
|
|
44
|
+
const urls = input.urls.filter((value) => typeof value === "string");
|
|
45
|
+
parts = [`读取 ${urls.length} 个网址`, ...urls.slice(0, 2).map(value => {
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL(value);
|
|
48
|
+
return url.hostname + url.pathname;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return "网址";
|
|
52
|
+
}
|
|
53
|
+
})];
|
|
54
|
+
}
|
|
55
|
+
else if (/edit|write|replace|patch|file_change/.test(name)) {
|
|
56
|
+
parts = [path];
|
|
57
|
+
if (Array.isArray(input.edits))
|
|
58
|
+
parts.push(`${input.edits.length} 处替换`);
|
|
59
|
+
else if (typeof input.old_string === "string" && typeof input.new_string === "string") {
|
|
60
|
+
parts.push(`替换 ${lineCount(input.old_string)} 行 → ${lineCount(input.new_string)} 行`);
|
|
61
|
+
}
|
|
62
|
+
else if (typeof input.content === "string")
|
|
63
|
+
parts.push(`写入 ${lineCount(input.content)} 行`);
|
|
64
|
+
const patch = stringField(input, "patch", "unified_diff");
|
|
65
|
+
if (patch) {
|
|
66
|
+
const paths = Array.from(patch.matchAll(/^(?:\*\*\* (?:Update|Add|Delete) File: |\+\+\+ b\/)(.+)$/gm), match => shortPath(match[1]));
|
|
67
|
+
if (paths.length)
|
|
68
|
+
parts.push([...new Set(paths)].slice(0, 3).join("、"));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (/grep|glob|search|find/.test(name)) {
|
|
72
|
+
const query = stringField(input, "pattern", "query", "search", "regex");
|
|
73
|
+
const queries = Array.isArray(input.queries) ? input.queries.filter((v) => typeof v === "string") : [];
|
|
74
|
+
parts = [query || queries.slice(0, 2).join(";"), path ? `范围 ${path}` : "", stringField(input, "glob", "include")];
|
|
75
|
+
}
|
|
76
|
+
else if (path) {
|
|
77
|
+
parts = [path];
|
|
78
|
+
const offset = input.offset ?? input.start_line;
|
|
79
|
+
const limit = input.limit ?? input.end_line;
|
|
80
|
+
if (typeof offset === "number")
|
|
81
|
+
parts.push(`起始行 ${offset}`);
|
|
82
|
+
if (typeof limit === "number")
|
|
83
|
+
parts.push(`${input.end_line !== undefined ? "结束行" : "最多"} ${limit}${input.end_line !== undefined ? "" : " 行"}`);
|
|
84
|
+
}
|
|
85
|
+
else if (typeof input.url === "string") {
|
|
86
|
+
try {
|
|
87
|
+
const url = new URL(input.url);
|
|
88
|
+
parts = [url.hostname + url.pathname];
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
parts = [input.url];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (Array.isArray(input.todos)) {
|
|
95
|
+
const todos = input.todos.filter((v) => !!v && typeof v === "object");
|
|
96
|
+
const done = todos.filter(todo => todo.status === "completed").length;
|
|
97
|
+
const active = todos.find(todo => todo.status === "in_progress");
|
|
98
|
+
parts = [`${done}/${todos.length} 项完成`, active ? stringField(active, "activeForm", "content", "subject") : ""];
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
parts = [stringField(input, "action", "agent"), stringField(input, "subject", "title", "description", "task", "prompt", "query", "name") || use.description || ""];
|
|
102
|
+
if (!parts.some(Boolean)) {
|
|
103
|
+
parts = Object.entries(input).filter(([key, value]) => !/token|password|secret|key|content|body|data|code/i.test(key) && ["string", "number", "boolean"].includes(typeof value))
|
|
104
|
+
.slice(0, 2).map(([key, value]) => `${key}: ${String(value)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return compactPreviewText(parts.filter(Boolean).join(" · "));
|
|
108
|
+
}
|
|
109
|
+
export function toolResultPreview(result) {
|
|
110
|
+
if (result.preview)
|
|
111
|
+
return compactPreviewText(result.preview);
|
|
112
|
+
const raw = typeof result.content === "string" ? result.content : result.content
|
|
113
|
+
.filter(part => part.type === "text" && typeof part.text === "string")
|
|
114
|
+
.slice(0, 3).map(part => part.text).join("\n");
|
|
115
|
+
if (!raw.trim())
|
|
116
|
+
return "";
|
|
117
|
+
let text = raw;
|
|
118
|
+
let exit = "";
|
|
119
|
+
if (raw.length <= 65_536 && /^\s*[{[]/.test(raw)) {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(raw);
|
|
122
|
+
if (Array.isArray(parsed))
|
|
123
|
+
return `返回 ${parsed.length} 项`;
|
|
124
|
+
if (parsed && typeof parsed === "object") {
|
|
125
|
+
const code = parsed.exit_code ?? parsed.exitCode;
|
|
126
|
+
if (typeof code === "number")
|
|
127
|
+
exit = `退出码 ${code}`;
|
|
128
|
+
text = stringField(parsed, "error", "message", "summary", "output", "stdout", "result");
|
|
129
|
+
if (!text) {
|
|
130
|
+
const rows = parsed.results ?? parsed.matches ?? parsed.items;
|
|
131
|
+
if (Array.isArray(rows))
|
|
132
|
+
text = `返回 ${rows.length} 项`;
|
|
133
|
+
else
|
|
134
|
+
text = Object.entries(parsed).filter(([key, value]) => /^(?:status|count|total|success|passed|failed|duration|wall_time_seconds)$/.test(key) &&
|
|
135
|
+
["string", "number", "boolean"].includes(typeof value)).map(([key, value]) => `${key}: ${value}`).join(" · ");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch { /* Plain tool output may begin with a bracket. */ }
|
|
140
|
+
}
|
|
141
|
+
const sample = text.length > 8_192 ? text.slice(0, 2_048) + "\n…\n" + text.slice(-6_144) : text;
|
|
142
|
+
const lines = sample.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
143
|
+
const facts = lines.filter(line => /(?:error|failed|failure|passed|\b(?:pass|fail|tests?)\b|exit(?:ed)?(?: code)?|BUILD (?:SUCCESS|FAIL)|错误|失败|通过|命中|找到)/i.test(line));
|
|
144
|
+
const selected = (facts.length ? facts.slice(-2) : result.is_error ? lines.slice(-2) : lines.slice(0, 2)).join(" · ");
|
|
145
|
+
return compactPreviewText([exit, selected].filter(Boolean).join(" · "));
|
|
146
|
+
}
|
|
147
|
+
/** Called at transport boundaries; provider history and full detail remain untouched. */
|
|
148
|
+
export function withToolPreview(block) {
|
|
149
|
+
if (block.type === "tool_use") {
|
|
150
|
+
const preview = toolInputPreview(block);
|
|
151
|
+
return preview ? { ...block, preview } : block;
|
|
152
|
+
}
|
|
153
|
+
if (block.type === "tool_result") {
|
|
154
|
+
const preview = toolResultPreview(block);
|
|
155
|
+
return preview ? { ...block, preview } : block;
|
|
156
|
+
}
|
|
157
|
+
return block;
|
|
158
|
+
}
|