pi-advisor-flow 0.3.1 → 0.3.4
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/CHANGELOG.md +31 -0
- package/README.md +19 -3
- package/extensions/index.ts +4 -2
- package/package.json +46 -46
- package/src/commands.ts +50 -11
- package/src/config.ts +11 -0
- package/src/conversation.ts +1 -1
- package/src/model-stream.ts +106 -0
- package/src/outcomes.ts +11 -11
- package/src/scout-context.ts +526 -0
- package/src/scout.ts +360 -0
- package/src/tools.ts +431 -73
- package/src/ui.ts +11 -0
package/src/tools.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
type AssistantMessage,
|
|
4
|
-
type Message,
|
|
5
|
-
stream,
|
|
6
|
-
} from "@earendil-works/pi-ai/compat";
|
|
2
|
+
import type { Message } from "@earendil-works/pi-ai/compat";
|
|
7
3
|
import {
|
|
8
4
|
type AgentToolResult,
|
|
9
5
|
type ExtensionAPI,
|
|
@@ -33,13 +29,14 @@ import {
|
|
|
33
29
|
advisorPlanGateRef,
|
|
34
30
|
advisorRedactSecretsRef,
|
|
35
31
|
advisorRef,
|
|
32
|
+
advisorScoutEnabledRef,
|
|
36
33
|
advisorSessionSummaryRef,
|
|
37
34
|
advisorTrackedFileContentRef,
|
|
38
35
|
advisorUntrackedContentRef,
|
|
39
36
|
contextMaxCharsRef,
|
|
37
|
+
executorRef,
|
|
40
38
|
isSimpleMode,
|
|
41
39
|
loadConfig,
|
|
42
|
-
splitRef,
|
|
43
40
|
} from "./config.js";
|
|
44
41
|
import {
|
|
45
42
|
recentConversation,
|
|
@@ -60,8 +57,18 @@ import {
|
|
|
60
57
|
herdrAdvisorBlock,
|
|
61
58
|
notifyHerdrAdvisorFailure,
|
|
62
59
|
} from "./herdr.js";
|
|
60
|
+
import { collectTextStream, resolveConfiguredModel } from "./model-stream.js";
|
|
63
61
|
import { ADOPTIONS, appendOutcome, VALIDATIONS } from "./outcomes.js";
|
|
64
62
|
import { readProjectPreferences } from "./preferences.js";
|
|
63
|
+
import {
|
|
64
|
+
runAdvisorScout,
|
|
65
|
+
type ScoutLifecycleEvent,
|
|
66
|
+
type ScoutOutcome,
|
|
67
|
+
} from "./scout.js";
|
|
68
|
+
import {
|
|
69
|
+
buildScoutManifest,
|
|
70
|
+
reconstructScoutConversation,
|
|
71
|
+
} from "./scout-context.js";
|
|
65
72
|
import {
|
|
66
73
|
AdvisorSessionState,
|
|
67
74
|
type ConsultationTrigger,
|
|
@@ -301,6 +308,7 @@ export interface AdvisorConsultationResult {
|
|
|
301
308
|
markdown: string;
|
|
302
309
|
model: string;
|
|
303
310
|
preferenceBytes?: number;
|
|
311
|
+
scout?: Exclude<ScoutOutcome, { cancelled: true }>;
|
|
304
312
|
thinkingText: string;
|
|
305
313
|
trackedBytes?: number;
|
|
306
314
|
trigger: ConsultationTrigger;
|
|
@@ -414,6 +422,67 @@ export const parseAutomaticDecision = (
|
|
|
414
422
|
const adviceForText = (result: AdvisorGateResult) =>
|
|
415
423
|
`**Decision: ${result.decision}**\n\n${result.markdown}`;
|
|
416
424
|
|
|
425
|
+
export const curateAdvisorConversation = async (
|
|
426
|
+
ctx: ExtensionContext,
|
|
427
|
+
legacyConversation: string,
|
|
428
|
+
signal?: AbortSignal,
|
|
429
|
+
onScout?: (event: ScoutLifecycleEvent) => void,
|
|
430
|
+
enabled = advisorScoutEnabledRef,
|
|
431
|
+
runScout: typeof runAdvisorScout = runAdvisorScout,
|
|
432
|
+
currentInvocationId?: string,
|
|
433
|
+
maxChars?: number
|
|
434
|
+
): Promise<{
|
|
435
|
+
conversation: string;
|
|
436
|
+
scout?: Exclude<ScoutOutcome, { cancelled: true }>;
|
|
437
|
+
}> => {
|
|
438
|
+
if (!enabled) {
|
|
439
|
+
return { conversation: legacyConversation };
|
|
440
|
+
}
|
|
441
|
+
if (maxChars !== undefined && maxChars <= 0) {
|
|
442
|
+
return { conversation: "" };
|
|
443
|
+
}
|
|
444
|
+
const built = buildScoutManifest(ctx, {
|
|
445
|
+
currentInvocationId,
|
|
446
|
+
maxBytes: maxChars,
|
|
447
|
+
});
|
|
448
|
+
if (!built.ok) {
|
|
449
|
+
const scout: Exclude<ScoutOutcome, { cancelled: true }> = {
|
|
450
|
+
category: built.reason,
|
|
451
|
+
message: built.message,
|
|
452
|
+
metrics: {
|
|
453
|
+
availableCount: 0,
|
|
454
|
+
inputBytes: 0,
|
|
455
|
+
latencyMs: 0,
|
|
456
|
+
omittedBeforeScout: 0,
|
|
457
|
+
selectedCount: 0,
|
|
458
|
+
},
|
|
459
|
+
model: executorRef,
|
|
460
|
+
ok: false,
|
|
461
|
+
};
|
|
462
|
+
onScout?.({ outcome: scout, type: "fallback" });
|
|
463
|
+
return { conversation: legacyConversation, scout };
|
|
464
|
+
}
|
|
465
|
+
const outcome = await runScout(ctx, built.manifest, signal, onScout);
|
|
466
|
+
if (!outcome.ok && outcome.cancelled) {
|
|
467
|
+
throw signal?.reason instanceof Error
|
|
468
|
+
? signal.reason
|
|
469
|
+
: new Error("Advisor operation cancelled during Scout.");
|
|
470
|
+
}
|
|
471
|
+
let conversation = legacyConversation;
|
|
472
|
+
if (outcome.ok) {
|
|
473
|
+
conversation =
|
|
474
|
+
maxChars === undefined
|
|
475
|
+
? outcome.conversation
|
|
476
|
+
: reconstructScoutConversation(
|
|
477
|
+
built.manifest,
|
|
478
|
+
outcome.selection.selectedIds,
|
|
479
|
+
outcome.selection.synthesis,
|
|
480
|
+
maxChars
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
return { conversation, scout: outcome };
|
|
484
|
+
};
|
|
485
|
+
|
|
417
486
|
const collectAdvisorResponse = async (
|
|
418
487
|
ctx: ExtensionContext,
|
|
419
488
|
systemPrompt: string,
|
|
@@ -423,21 +492,12 @@ const collectAdvisorResponse = async (
|
|
|
423
492
|
gitContext?: GitContextLevel,
|
|
424
493
|
draft?: string,
|
|
425
494
|
includeUntracked?: string[],
|
|
426
|
-
includeTracked?: string[]
|
|
495
|
+
includeTracked?: string[],
|
|
496
|
+
onScout?: (event: ScoutLifecycleEvent) => void,
|
|
497
|
+
currentInvocationId?: string
|
|
427
498
|
) => {
|
|
428
499
|
loadConfig(ctx);
|
|
429
|
-
const
|
|
430
|
-
const model = ctx.modelRegistry.find(provider, modelId);
|
|
431
|
-
if (!model) {
|
|
432
|
-
throw new Error(`Advisor model not found: ${advisorRef}`);
|
|
433
|
-
}
|
|
434
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
435
|
-
if (!auth.ok) {
|
|
436
|
-
throw new Error((auth as { error: string }).error);
|
|
437
|
-
}
|
|
438
|
-
if (!auth.apiKey) {
|
|
439
|
-
throw new Error(`No API key for ${advisorRef}`);
|
|
440
|
-
}
|
|
500
|
+
const resolved = await resolveConfiguredModel(ctx, advisorRef, "Advisor");
|
|
441
501
|
|
|
442
502
|
// The user setting is the ceiling; the Executor may only narrow it.
|
|
443
503
|
const allowed = advisorGitContextRef;
|
|
@@ -464,10 +524,25 @@ const collectAdvisorResponse = async (
|
|
|
464
524
|
);
|
|
465
525
|
// Repository context spends part of the shared budget, so a large patch
|
|
466
526
|
// cannot silently push the conversation past the model's context window.
|
|
467
|
-
const
|
|
527
|
+
const conversationBudget = Math.max(
|
|
528
|
+
0,
|
|
529
|
+
contextMaxCharsRef - changeText.length
|
|
530
|
+
);
|
|
531
|
+
const legacyConversation = advisorRequestConversation(
|
|
468
532
|
ctx,
|
|
469
|
-
|
|
533
|
+
conversationBudget
|
|
470
534
|
);
|
|
535
|
+
const curated = await curateAdvisorConversation(
|
|
536
|
+
ctx,
|
|
537
|
+
legacyConversation,
|
|
538
|
+
signal,
|
|
539
|
+
onScout,
|
|
540
|
+
advisorScoutEnabledRef,
|
|
541
|
+
runAdvisorScout,
|
|
542
|
+
currentInvocationId,
|
|
543
|
+
conversationBudget
|
|
544
|
+
);
|
|
545
|
+
const { conversation, scout } = curated;
|
|
471
546
|
const preferences = await readProjectPreferences(
|
|
472
547
|
ctx,
|
|
473
548
|
8 * 1024,
|
|
@@ -519,41 +594,14 @@ const collectAdvisorResponse = async (
|
|
|
519
594
|
},
|
|
520
595
|
];
|
|
521
596
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
headers: auth.headers,
|
|
531
|
-
reasoning: advisorEffortRef as never,
|
|
532
|
-
signal,
|
|
533
|
-
}
|
|
534
|
-
);
|
|
535
|
-
|
|
536
|
-
for await (const event of eventStream) {
|
|
537
|
-
if (event.type === "thinking_delta") {
|
|
538
|
-
thinkingText += event.delta;
|
|
539
|
-
onChunk?.(thinkingText, responseText);
|
|
540
|
-
} else if (event.type === "text_delta") {
|
|
541
|
-
responseText += event.delta;
|
|
542
|
-
onChunk?.(thinkingText, responseText);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
const response = await eventStream.result();
|
|
547
|
-
const lastAssistant = [response].find(
|
|
548
|
-
(m): m is AssistantMessage => m.role === "assistant"
|
|
549
|
-
);
|
|
550
|
-
const markdown =
|
|
551
|
-
lastAssistant?.content
|
|
552
|
-
.filter(
|
|
553
|
-
(part): part is { type: "text"; text: string } => part.type === "text"
|
|
554
|
-
)
|
|
555
|
-
.map((part) => part.text)
|
|
556
|
-
.join("\n") || responseText;
|
|
597
|
+
const streamed = await collectTextStream(resolved, {
|
|
598
|
+
messages,
|
|
599
|
+
onChunk,
|
|
600
|
+
reasoning: advisorEffortRef,
|
|
601
|
+
signal,
|
|
602
|
+
systemPrompt,
|
|
603
|
+
});
|
|
604
|
+
const markdown = streamed.text;
|
|
557
605
|
if (!markdown.trim()) {
|
|
558
606
|
throw new Error("Advisor returned no advice.");
|
|
559
607
|
}
|
|
@@ -562,14 +610,13 @@ const collectAdvisorResponse = async (
|
|
|
562
610
|
markdown,
|
|
563
611
|
model: advisorRef,
|
|
564
612
|
preferenceBytes: preferences?.bytes,
|
|
565
|
-
thinkingText,
|
|
613
|
+
thinkingText: streamed.thinking,
|
|
566
614
|
trackedBytes:
|
|
567
615
|
tracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
|
|
568
616
|
untrackedBytes:
|
|
569
617
|
untracked.reduce((sum, item) => sum + item.bytes, 0) || undefined,
|
|
570
|
-
usage:
|
|
571
|
-
|
|
572
|
-
)?.usage,
|
|
618
|
+
usage: streamed.usage,
|
|
619
|
+
...(scout ? { scout } : {}),
|
|
573
620
|
};
|
|
574
621
|
};
|
|
575
622
|
|
|
@@ -582,7 +629,9 @@ export const consultAdvisor = async (
|
|
|
582
629
|
gitContext?: GitContextLevel,
|
|
583
630
|
draft?: string,
|
|
584
631
|
includeUntracked?: string[],
|
|
585
|
-
includeTracked?: string[]
|
|
632
|
+
includeTracked?: string[],
|
|
633
|
+
onScout?: (event: ScoutLifecycleEvent) => void,
|
|
634
|
+
currentInvocationId?: string
|
|
586
635
|
): Promise<AdvisorConsultationResult> => {
|
|
587
636
|
const result = await collectAdvisorResponse(
|
|
588
637
|
ctx,
|
|
@@ -593,7 +642,9 @@ export const consultAdvisor = async (
|
|
|
593
642
|
gitContext,
|
|
594
643
|
draft,
|
|
595
644
|
includeUntracked,
|
|
596
|
-
includeTracked
|
|
645
|
+
includeTracked,
|
|
646
|
+
onScout,
|
|
647
|
+
currentInvocationId
|
|
597
648
|
);
|
|
598
649
|
return { ...result, adviceId: randomUUID(), trigger };
|
|
599
650
|
};
|
|
@@ -603,7 +654,9 @@ export const runAdvisorGate = async (
|
|
|
603
654
|
question: string,
|
|
604
655
|
trigger: GateTrigger = "repeated-tool-call",
|
|
605
656
|
signal?: AbortSignal,
|
|
606
|
-
onChunk?: (thinking: string, text: string) => void
|
|
657
|
+
onChunk?: (thinking: string, text: string) => void,
|
|
658
|
+
onScout?: (event: ScoutLifecycleEvent) => void,
|
|
659
|
+
currentInvocationId?: string
|
|
607
660
|
): Promise<AdvisorGateOutcome> => {
|
|
608
661
|
try {
|
|
609
662
|
const result = await collectAdvisorResponse(
|
|
@@ -611,7 +664,13 @@ export const runAdvisorGate = async (
|
|
|
611
664
|
ADVISOR_DECISION_SYSTEM,
|
|
612
665
|
question,
|
|
613
666
|
signal,
|
|
614
|
-
onChunk
|
|
667
|
+
onChunk,
|
|
668
|
+
undefined,
|
|
669
|
+
undefined,
|
|
670
|
+
undefined,
|
|
671
|
+
undefined,
|
|
672
|
+
onScout,
|
|
673
|
+
currentInvocationId
|
|
615
674
|
);
|
|
616
675
|
const parsed = parseAutomaticDecision(result.markdown);
|
|
617
676
|
if (!parsed.ok) {
|
|
@@ -625,6 +684,9 @@ export const runAdvisorGate = async (
|
|
|
625
684
|
usage: result.usage,
|
|
626
685
|
};
|
|
627
686
|
} catch (error) {
|
|
687
|
+
if (signal?.aborted) {
|
|
688
|
+
throw error;
|
|
689
|
+
}
|
|
628
690
|
const message = error instanceof Error ? error.message : String(error);
|
|
629
691
|
return {
|
|
630
692
|
category:
|
|
@@ -763,7 +825,9 @@ const handleAutomaticGate = async (
|
|
|
763
825
|
pi: ExtensionAPI,
|
|
764
826
|
event: ToolCallEvent,
|
|
765
827
|
ctx: ExtensionContext,
|
|
766
|
-
session: AdvisorSessionState
|
|
828
|
+
session: AdvisorSessionState,
|
|
829
|
+
runGate: typeof runAdvisorGate,
|
|
830
|
+
scoutStatus: ScoutStatusManager
|
|
767
831
|
): Promise<ToolCallEventResult | undefined> => {
|
|
768
832
|
if (
|
|
769
833
|
isSimpleMode() ||
|
|
@@ -789,12 +853,36 @@ const handleAutomaticGate = async (
|
|
|
789
853
|
}
|
|
790
854
|
session.consumeCall();
|
|
791
855
|
herdrAdvisorActivity.start();
|
|
792
|
-
|
|
856
|
+
let scoutDetails: ScoutToolDetails | undefined;
|
|
857
|
+
const scoutStatusToken = Symbol("automatic-gate-scout");
|
|
858
|
+
scoutStatus.register(scoutStatusToken);
|
|
859
|
+
let gateCallSent = false;
|
|
860
|
+
const ensureGateCall = () => {
|
|
861
|
+
if (!gateCallSent) {
|
|
862
|
+
sendAutomaticGateCall(pi, event);
|
|
863
|
+
gateCallSent = true;
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
if (!advisorScoutEnabledRef) {
|
|
867
|
+
ensureGateCall();
|
|
868
|
+
}
|
|
793
869
|
try {
|
|
794
|
-
const result = await
|
|
870
|
+
const result = await runGate(
|
|
795
871
|
ctx,
|
|
796
|
-
`${reason} Review the repeated actions and recommend the smallest safe next step
|
|
872
|
+
`${reason} Review the repeated actions and recommend the smallest safe next step.`,
|
|
873
|
+
"repeated-tool-call",
|
|
874
|
+
ctx.signal,
|
|
875
|
+
undefined,
|
|
876
|
+
(scoutEvent) => {
|
|
877
|
+
scoutStatus.update(ctx, scoutStatusToken, scoutEvent);
|
|
878
|
+
scoutDetails = appendScoutLifecycleEntry(pi, scoutEvent, scoutDetails);
|
|
879
|
+
if (scoutEvent.type === "success" || scoutEvent.type === "fallback") {
|
|
880
|
+
ensureGateCall();
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
event.toolCallId
|
|
797
884
|
);
|
|
885
|
+
ensureGateCall();
|
|
798
886
|
if (!result.ok) {
|
|
799
887
|
session.recordInvocation({
|
|
800
888
|
executionEffect: gateFailureEffectForMode(advisorFailureModeRef),
|
|
@@ -841,22 +929,40 @@ const handleAutomaticGate = async (
|
|
|
841
929
|
}
|
|
842
930
|
return { block: true, reason: gateReason };
|
|
843
931
|
} finally {
|
|
932
|
+
scoutStatus.release(ctx, scoutStatusToken);
|
|
844
933
|
herdrAdvisorActivity.finish();
|
|
845
934
|
}
|
|
846
935
|
};
|
|
847
936
|
|
|
937
|
+
export interface ScoutToolDetails {
|
|
938
|
+
availableCount?: number;
|
|
939
|
+
fallbackReason?: string;
|
|
940
|
+
latencyMs?: number;
|
|
941
|
+
model: string;
|
|
942
|
+
omittedBeforeScout?: number;
|
|
943
|
+
selectedCount?: number;
|
|
944
|
+
selectedLabels?: string[];
|
|
945
|
+
status: "calling" | "streaming" | "curated" | "fallback" | "cancelled";
|
|
946
|
+
synthesis?: string;
|
|
947
|
+
text?: string;
|
|
948
|
+
thinking?: string;
|
|
949
|
+
usage?: unknown;
|
|
950
|
+
}
|
|
848
951
|
interface AdvisorToolDetails {
|
|
849
952
|
adviceId?: string;
|
|
850
953
|
advisor?: string;
|
|
851
954
|
draftBytes?: number;
|
|
852
955
|
preferenceBytes?: number;
|
|
853
956
|
question?: string;
|
|
957
|
+
scout?: ScoutToolDetails;
|
|
854
958
|
text?: string;
|
|
855
959
|
thinking?: string;
|
|
856
960
|
trackedBytes?: number;
|
|
857
961
|
untrackedBytes?: number;
|
|
858
962
|
}
|
|
859
963
|
interface AdvisorRenderState {
|
|
964
|
+
phase?: string;
|
|
965
|
+
scout?: ScoutToolDetails;
|
|
860
966
|
timerId?: ReturnType<typeof setInterval>;
|
|
861
967
|
}
|
|
862
968
|
interface AdvisorToolContext {
|
|
@@ -868,6 +974,196 @@ interface AdvisorToolContext {
|
|
|
868
974
|
const advisorResultDetails = (result: AgentToolResult<AdvisorToolDetails>) =>
|
|
869
975
|
result.details;
|
|
870
976
|
|
|
977
|
+
export const scoutDetailsFromEvent = (
|
|
978
|
+
event: ScoutLifecycleEvent,
|
|
979
|
+
previous?: ScoutToolDetails
|
|
980
|
+
): ScoutToolDetails => {
|
|
981
|
+
if (event.type === "call") {
|
|
982
|
+
return { model: event.model, status: "calling" };
|
|
983
|
+
}
|
|
984
|
+
if (event.type === "chunk") {
|
|
985
|
+
return {
|
|
986
|
+
...previous,
|
|
987
|
+
model: event.model,
|
|
988
|
+
status: "streaming",
|
|
989
|
+
text: event.text,
|
|
990
|
+
thinking: event.thinking,
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
if (event.type === "cancelled") {
|
|
994
|
+
return {
|
|
995
|
+
model: previous ? previous.model : executorRef,
|
|
996
|
+
status: "cancelled",
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
const { outcome } = event;
|
|
1000
|
+
return outcome.ok
|
|
1001
|
+
? {
|
|
1002
|
+
availableCount: outcome.metrics.availableCount,
|
|
1003
|
+
latencyMs: outcome.metrics.latencyMs,
|
|
1004
|
+
model: outcome.model,
|
|
1005
|
+
omittedBeforeScout: outcome.metrics.omittedBeforeScout,
|
|
1006
|
+
selectedCount: outcome.metrics.selectedCount,
|
|
1007
|
+
selectedLabels: outcome.selectedLabels,
|
|
1008
|
+
status: "curated",
|
|
1009
|
+
synthesis: outcome.selection.synthesis,
|
|
1010
|
+
usage: outcome.metrics.usage,
|
|
1011
|
+
}
|
|
1012
|
+
: {
|
|
1013
|
+
availableCount: outcome.metrics.availableCount,
|
|
1014
|
+
fallbackReason: `${outcome.category}: ${outcome.message}`,
|
|
1015
|
+
latencyMs: outcome.metrics.latencyMs,
|
|
1016
|
+
model: outcome.model,
|
|
1017
|
+
omittedBeforeScout: outcome.metrics.omittedBeforeScout,
|
|
1018
|
+
selectedCount: 0,
|
|
1019
|
+
status: "fallback",
|
|
1020
|
+
usage: outcome.metrics.usage,
|
|
1021
|
+
};
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
export const appendScoutLifecycleEntry = (
|
|
1025
|
+
pi: ExtensionAPI,
|
|
1026
|
+
event: ScoutLifecycleEvent,
|
|
1027
|
+
previous?: ScoutToolDetails
|
|
1028
|
+
) => {
|
|
1029
|
+
const scout = scoutDetailsFromEvent(event, previous);
|
|
1030
|
+
if (
|
|
1031
|
+
event.type === "success" ||
|
|
1032
|
+
event.type === "fallback" ||
|
|
1033
|
+
event.type === "cancelled"
|
|
1034
|
+
) {
|
|
1035
|
+
pi.appendEntry?.("advisor-scout-result", scout);
|
|
1036
|
+
}
|
|
1037
|
+
return scout;
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
export class ScoutStatusManager {
|
|
1041
|
+
readonly #active = new Set<symbol>();
|
|
1042
|
+
readonly #known = new Set<symbol>();
|
|
1043
|
+
readonly #retired = new Set<symbol>();
|
|
1044
|
+
|
|
1045
|
+
register(token: symbol) {
|
|
1046
|
+
if (!this.#retired.has(token)) {
|
|
1047
|
+
this.#known.add(token);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
update(ctx: ExtensionContext, token: symbol, event: ScoutLifecycleEvent) {
|
|
1052
|
+
if (this.#retired.has(token) || !ctx.hasUI) {
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
this.#known.add(token);
|
|
1056
|
+
if (event.type === "call" || event.type === "chunk") {
|
|
1057
|
+
this.#active.add(token);
|
|
1058
|
+
ctx.ui.setStatus("advisor-scout", "Scout curating…");
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
this.release(ctx, token);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
release(ctx: ExtensionContext, token: symbol) {
|
|
1065
|
+
this.#active.delete(token);
|
|
1066
|
+
this.#known.delete(token);
|
|
1067
|
+
this.#retired.add(token);
|
|
1068
|
+
if (!ctx.hasUI) {
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
ctx.ui.setStatus(
|
|
1072
|
+
"advisor-scout",
|
|
1073
|
+
this.#active.size > 0 ? "Scout curating…" : undefined
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
clear(ctx: ExtensionContext) {
|
|
1078
|
+
for (const token of this.#known) {
|
|
1079
|
+
this.#retired.add(token);
|
|
1080
|
+
}
|
|
1081
|
+
this.#known.clear();
|
|
1082
|
+
this.#active.clear();
|
|
1083
|
+
if (ctx.hasUI) {
|
|
1084
|
+
ctx.ui.setStatus("advisor-scout", undefined);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const scoutTitle = (scout: ScoutToolDetails, frame: string) => {
|
|
1090
|
+
if (scout.status === "calling" || scout.status === "streaming") {
|
|
1091
|
+
return `◆ SCOUT ${frame} · CURATING…`;
|
|
1092
|
+
}
|
|
1093
|
+
if (scout.status === "curated") {
|
|
1094
|
+
return "◆ SCOUT · CURATED";
|
|
1095
|
+
}
|
|
1096
|
+
if (scout.status === "cancelled") {
|
|
1097
|
+
return "◆ SCOUT · CANCELLED";
|
|
1098
|
+
}
|
|
1099
|
+
return "◆ SCOUT · FALLBACK";
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
export const renderScoutDetails = (
|
|
1103
|
+
box: Box,
|
|
1104
|
+
scout: ScoutToolDetails,
|
|
1105
|
+
expanded: boolean,
|
|
1106
|
+
theme: Theme
|
|
1107
|
+
) => {
|
|
1108
|
+
const active = scout.status === "calling" || scout.status === "streaming";
|
|
1109
|
+
const frame =
|
|
1110
|
+
SPINNER_FRAMES[Math.floor(Date.now() / 80) % SPINNER_FRAMES.length];
|
|
1111
|
+
const title = scoutTitle(scout, frame);
|
|
1112
|
+
const lines = [
|
|
1113
|
+
theme.fg(
|
|
1114
|
+
scout.status === "fallback" || scout.status === "cancelled"
|
|
1115
|
+
? "warning"
|
|
1116
|
+
: "accent",
|
|
1117
|
+
theme.bold(title)
|
|
1118
|
+
),
|
|
1119
|
+
theme.fg(
|
|
1120
|
+
"dim",
|
|
1121
|
+
` ${scout.model}${scout.selectedCount === undefined ? "" : ` · ${scout.selectedCount} kept / ${Math.max(0, (scout.availableCount ?? 0) - scout.selectedCount)} omitted`}${scout.latencyMs === undefined ? "" : ` · ${(scout.latencyMs / 1000).toFixed(1)}s`}`
|
|
1122
|
+
),
|
|
1123
|
+
];
|
|
1124
|
+
if (scout.fallbackReason) {
|
|
1125
|
+
lines.push(theme.fg("warning", ` ${scout.fallbackReason}`));
|
|
1126
|
+
}
|
|
1127
|
+
if (scout.thinking && active) {
|
|
1128
|
+
lines.push(
|
|
1129
|
+
theme.fg(
|
|
1130
|
+
"thinkingText",
|
|
1131
|
+
` 💭 ${scout.thinking.replace(/\n/g, " ").slice(-200)}`
|
|
1132
|
+
)
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
if (expanded && scout.selectedLabels?.length) {
|
|
1136
|
+
lines.push(
|
|
1137
|
+
theme.fg("dim", ` Selected: ${scout.selectedLabels.join("; ")}`)
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
if (expanded && scout.synthesis) {
|
|
1141
|
+
lines.push(
|
|
1142
|
+
theme.fg(
|
|
1143
|
+
"dim",
|
|
1144
|
+
` Scout synthesis (untrusted inference): ${scout.synthesis}`
|
|
1145
|
+
)
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
if (expanded && scout.omittedBeforeScout) {
|
|
1149
|
+
lines.push(
|
|
1150
|
+
theme.fg(
|
|
1151
|
+
"dim",
|
|
1152
|
+
` ${scout.omittedBeforeScout} group(s) omitted before Scout`
|
|
1153
|
+
)
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
const syncRenderPhase = (context: AdvisorToolContext, phase: string) => {
|
|
1160
|
+
if (context.state.phase !== phase && context.state.timerId) {
|
|
1161
|
+
clearInterval(context.state.timerId);
|
|
1162
|
+
context.state.timerId = undefined;
|
|
1163
|
+
}
|
|
1164
|
+
context.state.phase = phase;
|
|
1165
|
+
};
|
|
1166
|
+
|
|
871
1167
|
const renderPartialAdvisorResult = (
|
|
872
1168
|
box: Box,
|
|
873
1169
|
result: AgentToolResult<AdvisorToolDetails>,
|
|
@@ -875,12 +1171,25 @@ const renderPartialAdvisorResult = (
|
|
|
875
1171
|
theme: Theme,
|
|
876
1172
|
context: AdvisorToolContext
|
|
877
1173
|
) => {
|
|
1174
|
+
const details = advisorResultDetails(result);
|
|
1175
|
+
if (details?.scout) {
|
|
1176
|
+
context.state.scout = details.scout;
|
|
1177
|
+
}
|
|
1178
|
+
const scout = details?.scout ?? context.state.scout;
|
|
1179
|
+
const scoutActive =
|
|
1180
|
+
scout?.status === "calling" || scout?.status === "streaming";
|
|
1181
|
+
syncRenderPhase(context, scoutActive ? "scout" : "advisor");
|
|
878
1182
|
if (!context.state.timerId) {
|
|
879
1183
|
context.state.timerId = setInterval(() => context.invalidate(), 80);
|
|
880
1184
|
}
|
|
1185
|
+
if (scout) {
|
|
1186
|
+
renderScoutDetails(box, scout, expanded, theme);
|
|
1187
|
+
}
|
|
1188
|
+
if (scoutActive || scout?.status === "cancelled") {
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
881
1191
|
const frame =
|
|
882
1192
|
SPINNER_FRAMES[Math.floor(Date.now() / 80) % SPINNER_FRAMES.length];
|
|
883
|
-
const details = advisorResultDetails(result);
|
|
884
1193
|
const lines = [
|
|
885
1194
|
`${theme.fg("warning", theme.bold(`◆ ADVISOR ${frame}`))} ${theme.fg("dim", "· Working…")}`,
|
|
886
1195
|
];
|
|
@@ -911,11 +1220,22 @@ const renderFinalAdvisorResult = (
|
|
|
911
1220
|
theme: Theme,
|
|
912
1221
|
context: AdvisorToolContext
|
|
913
1222
|
) => {
|
|
1223
|
+
syncRenderPhase(context, "final");
|
|
914
1224
|
if (context.state.timerId) {
|
|
915
1225
|
clearInterval(context.state.timerId);
|
|
916
1226
|
context.state.timerId = undefined;
|
|
917
1227
|
}
|
|
918
1228
|
const details = advisorResultDetails(result);
|
|
1229
|
+
if (details?.scout) {
|
|
1230
|
+
context.state.scout = details.scout;
|
|
1231
|
+
}
|
|
1232
|
+
const scout = details?.scout ?? context.state.scout;
|
|
1233
|
+
if (scout) {
|
|
1234
|
+
renderScoutDetails(box, scout, expanded, theme);
|
|
1235
|
+
}
|
|
1236
|
+
if (scout?.status === "cancelled") {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
919
1239
|
const advice = details?.text || textFrom(result.content);
|
|
920
1240
|
const lines = [renderAdvisorResponseHeader(hasSoundVerdict(advice), theme)];
|
|
921
1241
|
if (details?.advisor) {
|
|
@@ -981,9 +1301,24 @@ const renderAdvisorResult = (
|
|
|
981
1301
|
|
|
982
1302
|
export const registerAdvisorTool = (
|
|
983
1303
|
pi: ExtensionAPI,
|
|
984
|
-
session: AdvisorSessionState = advisorSessionState
|
|
1304
|
+
session: AdvisorSessionState = advisorSessionState,
|
|
1305
|
+
dependencies: {
|
|
1306
|
+
runGate?: typeof runAdvisorGate;
|
|
1307
|
+
statusManager?: ScoutStatusManager;
|
|
1308
|
+
} = {}
|
|
985
1309
|
) => {
|
|
986
1310
|
const reservedCalls = new Set<string>();
|
|
1311
|
+
const scoutStatus = dependencies.statusManager ?? new ScoutStatusManager();
|
|
1312
|
+
|
|
1313
|
+
pi.registerEntryRenderer?.(
|
|
1314
|
+
"advisor-scout-result",
|
|
1315
|
+
(entry, { expanded }, theme) => {
|
|
1316
|
+
const scout = entry.data as ScoutToolDetails;
|
|
1317
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
1318
|
+
renderScoutDetails(box, scout, Boolean(expanded), theme);
|
|
1319
|
+
return box;
|
|
1320
|
+
}
|
|
1321
|
+
);
|
|
987
1322
|
|
|
988
1323
|
pi.registerMessageRenderer?.(
|
|
989
1324
|
"advisor-loop-call",
|
|
@@ -1082,7 +1417,14 @@ export const registerAdvisorTool = (
|
|
|
1082
1417
|
if (event.toolName === "ask_advisor") {
|
|
1083
1418
|
return reservation;
|
|
1084
1419
|
}
|
|
1085
|
-
return handleAutomaticGate(
|
|
1420
|
+
return handleAutomaticGate(
|
|
1421
|
+
pi,
|
|
1422
|
+
event,
|
|
1423
|
+
ctx,
|
|
1424
|
+
session,
|
|
1425
|
+
dependencies.runGate ?? runAdvisorGate,
|
|
1426
|
+
scoutStatus
|
|
1427
|
+
);
|
|
1086
1428
|
});
|
|
1087
1429
|
|
|
1088
1430
|
pi.on("agent_settled", (_event, ctx) => {
|
|
@@ -1098,8 +1440,9 @@ export const registerAdvisorTool = (
|
|
|
1098
1440
|
}
|
|
1099
1441
|
});
|
|
1100
1442
|
|
|
1101
|
-
pi.on("session_shutdown", () => {
|
|
1443
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
1102
1444
|
reservedCalls.clear();
|
|
1445
|
+
scoutStatus.clear(ctx);
|
|
1103
1446
|
herdrAdvisorBlock.clear();
|
|
1104
1447
|
});
|
|
1105
1448
|
|
|
@@ -1123,6 +1466,7 @@ export const registerAdvisorTool = (
|
|
|
1123
1466
|
session.consumeCall();
|
|
1124
1467
|
}
|
|
1125
1468
|
herdrAdvisorActivity.start();
|
|
1469
|
+
let scoutDetails: ScoutToolDetails | undefined;
|
|
1126
1470
|
try {
|
|
1127
1471
|
const result = await consultAdvisor(
|
|
1128
1472
|
ctx,
|
|
@@ -1134,6 +1478,7 @@ export const registerAdvisorTool = (
|
|
|
1134
1478
|
details: {
|
|
1135
1479
|
advisor: advisorRef,
|
|
1136
1480
|
question: resolveAdvisorRequest(params.question),
|
|
1481
|
+
scout: scoutDetails,
|
|
1137
1482
|
text: tx,
|
|
1138
1483
|
thinking: t,
|
|
1139
1484
|
},
|
|
@@ -1143,7 +1488,19 @@ export const registerAdvisorTool = (
|
|
|
1143
1488
|
params.gitContext === "none" ? "off" : params.gitContext,
|
|
1144
1489
|
params.draft,
|
|
1145
1490
|
params.includeUntracked,
|
|
1146
|
-
params.includeTrackedFiles
|
|
1491
|
+
params.includeTrackedFiles,
|
|
1492
|
+
(event) => {
|
|
1493
|
+
scoutDetails = scoutDetailsFromEvent(event, scoutDetails);
|
|
1494
|
+
onUpdate?.({
|
|
1495
|
+
content: [{ text: scoutDetails.text ?? "", type: "text" }],
|
|
1496
|
+
details: {
|
|
1497
|
+
advisor: advisorRef,
|
|
1498
|
+
question: resolveAdvisorRequest(params.question),
|
|
1499
|
+
scout: scoutDetails,
|
|
1500
|
+
},
|
|
1501
|
+
});
|
|
1502
|
+
},
|
|
1503
|
+
_id
|
|
1147
1504
|
);
|
|
1148
1505
|
session.issueAdvice(
|
|
1149
1506
|
result.adviceId,
|
|
@@ -1172,6 +1529,7 @@ export const registerAdvisorTool = (
|
|
|
1172
1529
|
draftBytes: result.draftBytes,
|
|
1173
1530
|
preferenceBytes: result.preferenceBytes,
|
|
1174
1531
|
question: resolveAdvisorRequest(params.question),
|
|
1532
|
+
scout: scoutDetails,
|
|
1175
1533
|
text: result.markdown,
|
|
1176
1534
|
thinking: result.thinkingText,
|
|
1177
1535
|
trackedBytes: result.trackedBytes,
|