@llblab/pi-kit 0.9.0 → 0.10.0
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 +10 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -0
- package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-grow-loop/README.md +2 -0
- package/node_modules/@llblab/pi-grow-loop/index.ts +115 -8
- package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +4 -4
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +7 -0
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +10 -0
- package/node_modules/@llblab/pi-state-flow/README.md +4 -2
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +5 -3
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +2 -2
- package/node_modules/@llblab/pi-state-flow/index.ts +22 -1
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +248 -144
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +267 -0
- package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +8 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +4 -4
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/config.ts +6 -4
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +2 -1
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +20 -0
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +4 -4
|
@@ -7,6 +7,7 @@ import { assistantToolCallCount, finalizedAssistantResponse, stateFlowProtocol }
|
|
|
7
7
|
import { createPassiveContinuation, currentRunTrajectory, passiveContinuationMessages, runtimeContextMessage, VALIDATION_MESSAGE_TYPE, withoutPrivateValidation, type PassiveContinuation } from "./context.ts";
|
|
8
8
|
import { ArtifactReadTracker } from "./acquisition.ts";
|
|
9
9
|
import { loadStateFlowConfig } from "./config.ts";
|
|
10
|
+
import { createStateFlowTelegramAdapter, type StateFlowTelegramControlResult, type StateFlowTelegramLoader } from "./telegram.ts";
|
|
10
11
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
11
12
|
import { SkillReadTracker } from "./skills.ts";
|
|
12
13
|
import { emptySnapshot, migrationFailure, persistableSnapshot, type Snapshot } from "./snapshot.ts";
|
|
@@ -44,11 +45,12 @@ export interface StateFlowExtensionOptions {
|
|
|
44
45
|
repositoryRoot?: string;
|
|
45
46
|
knowledgeRoot?: string;
|
|
46
47
|
onRuntime?: (accessor: { read(offset?: number, scope?: StateScope): MaterializedState }) => void;
|
|
48
|
+
telegram?: { load?: StateFlowTelegramLoader };
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export const PATCH_STATE_TOOL_NAME = "patch_state";
|
|
50
52
|
export const READ_STATE_TOOL_NAME = "read_state";
|
|
51
|
-
export const
|
|
53
|
+
export const MAX_FALLBACK_ATTEMPTS: number = 2;
|
|
52
54
|
const PASSIVE_STOP_ENTRY_TYPE = "state-flow-passive-stop";
|
|
53
55
|
|
|
54
56
|
/** Keep a failed tool invocation visually separated from its rendered error without changing error semantics. */
|
|
@@ -65,9 +67,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
65
67
|
let branchHasSnapshot = false;
|
|
66
68
|
let branchStartsWithoutRuntime = false;
|
|
67
69
|
let terminalEligible = false;
|
|
68
|
-
let
|
|
69
|
-
let
|
|
70
|
-
let
|
|
70
|
+
let resolutionPending = false;
|
|
71
|
+
let fallbackAttempts = 0;
|
|
72
|
+
let fallbackFailureReported = false;
|
|
71
73
|
let responseAwaitingReconciliation = false;
|
|
72
74
|
let passiveContinuation: PassiveContinuation | undefined;
|
|
73
75
|
let bootstrapContinuation: PassiveContinuation | undefined;
|
|
@@ -85,6 +87,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
85
87
|
const globalMarkdown = new GlobalMarkdownDiscovery(options.knowledgeRoot ?? getKnowledgeRoot(agentDir));
|
|
86
88
|
let artifactInvalidations: ArtifactInvalidationRequest[] = [];
|
|
87
89
|
let loggingWarningReported = false;
|
|
90
|
+
let telegramStartPending = false;
|
|
88
91
|
|
|
89
92
|
function sessionAddress(ctx: ExtensionContext): SessionAddress {
|
|
90
93
|
return resolveSessionAddress(ctx.sessionManager.getSessionFile(), ctx.sessionManager.getSessionId(), ctx.sessionManager.getHeader()?.timestamp);
|
|
@@ -124,9 +127,9 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
124
127
|
|
|
125
128
|
function clearRunTransient(): void {
|
|
126
129
|
terminalEligible = false;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
resolutionPending = false;
|
|
131
|
+
fallbackAttempts = 0;
|
|
132
|
+
fallbackFailureReported = false;
|
|
130
133
|
responseAwaitingReconciliation = false;
|
|
131
134
|
runAnchorTimestamp = undefined;
|
|
132
135
|
skillReads.clear();
|
|
@@ -273,10 +276,14 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
273
276
|
}, runtime!.causalBasis(), { finalizeRun });
|
|
274
277
|
if (!committed) return false;
|
|
275
278
|
installScopeStates();
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
279
|
+
// A preserved primary response commits mid-run; pending acquisition obligations must
|
|
280
|
+
// still block final eligibility until the fallback turns resolve or expire.
|
|
281
|
+
if (!(finalizeRun && resolutionPending)) {
|
|
282
|
+
artifactInvalidations = artifactInvalidations.filter(({ path }) => !acquiredArtifactPaths.has(path));
|
|
283
|
+
artifactReads.setCandidates(artifactInvalidations);
|
|
284
|
+
skillReads.clear();
|
|
285
|
+
artifactReads.clear();
|
|
286
|
+
}
|
|
280
287
|
persist();
|
|
281
288
|
return true;
|
|
282
289
|
}
|
|
@@ -451,6 +458,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
451
458
|
artifactInvalidations = [];
|
|
452
459
|
artifactReads.setCandidates([]);
|
|
453
460
|
artifactRefreshPending = false;
|
|
461
|
+
telegramStartPending = false;
|
|
454
462
|
activeContext = ctx;
|
|
455
463
|
const session = sessionAddress(ctx);
|
|
456
464
|
runtime = new TemporalRuntime(ctx.cwd, session, repositoryRoot);
|
|
@@ -579,15 +587,77 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
579
587
|
}
|
|
580
588
|
}
|
|
581
589
|
|
|
582
|
-
function
|
|
583
|
-
|
|
590
|
+
function continueFallbackResolution(lastWarning: boolean): void {
|
|
591
|
+
resolutionPending = true;
|
|
584
592
|
pi.sendMessage({
|
|
585
593
|
customType: VALIDATION_MESSAGE_TYPE,
|
|
586
|
-
content:
|
|
594
|
+
content: lastWarning
|
|
595
|
+
? "This is the last State Flow fallback turn. The iteration's answer is preserved and will not change. Apply the final:true patch now: call patch_state with any remaining durable scope changes and final:true, or with {\"final\":true} when nothing remains. Do not restate the answer."
|
|
596
|
+
: "The iteration's answer is preserved as the final response; later turns cannot replace it. Apply the final:true patch: call patch_state with any durable scope changes from this iteration (including required artifact or Skill compilation) and final:true, or with {\"final\":true} when nothing remains to persist. Do not restate the answer.",
|
|
587
597
|
display: false,
|
|
588
598
|
}, { deliverAs: "steer", triggerTurn: true });
|
|
589
599
|
}
|
|
590
600
|
|
|
601
|
+
/** Preserve the primary answer as this iteration's response and steer bounded fallback turns whose only purpose is the final:true patch. */
|
|
602
|
+
function beginFallbackResolution(ctx: ExtensionContext, message: { content?: unknown }, reason: string): void {
|
|
603
|
+
responseAwaitingReconciliation = true;
|
|
604
|
+
fallbackAttempts = 0;
|
|
605
|
+
fallbackFailureReported = false;
|
|
606
|
+
recordDiagnostic(`Preserved the terminal draft as the iteration response; fallback resolution started: ${reason}`, "terminal-pending", ctx, {
|
|
607
|
+
content: message.content,
|
|
608
|
+
resolutionAttempt: 0,
|
|
609
|
+
terminalEligible,
|
|
610
|
+
});
|
|
611
|
+
continueFallbackResolution(MAX_FALLBACK_ATTEMPTS === 1);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** Fallback turns never become the response; they exist only to supply the final:true patch. */
|
|
615
|
+
function resolveFallbackTurn(ctx: ExtensionContext, message: { content?: unknown }): any {
|
|
616
|
+
let resolved = terminalEligible;
|
|
617
|
+
if (resolved) {
|
|
618
|
+
try {
|
|
619
|
+
validateFinalEligibility(scopeStates, skillReads.successful.values(), runtime!.causalBasis(), artifactReads.successful.values());
|
|
620
|
+
} catch (error) {
|
|
621
|
+
resolved = false;
|
|
622
|
+
recordDiagnostic(error instanceof Error ? error.message : String(error), "terminal-pending", ctx, {
|
|
623
|
+
content: message.content,
|
|
624
|
+
resolutionAttempt: fallbackAttempts,
|
|
625
|
+
terminalEligible,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (resolved) {
|
|
630
|
+
resolutionPending = false;
|
|
631
|
+
recordDiagnostic(`Fallback resolution obtained final:true after ${fallbackAttempts} fallback turn${fallbackAttempts === 1 ? "" : "s"}; the preserved answer stands`, "finalization", ctx, {
|
|
632
|
+
content: message.content,
|
|
633
|
+
resolutionAttempt: fallbackAttempts,
|
|
634
|
+
terminalEligible,
|
|
635
|
+
});
|
|
636
|
+
return { message: { ...message, role: "assistant" as const, content: [] } };
|
|
637
|
+
}
|
|
638
|
+
fallbackAttempts = Math.min(MAX_FALLBACK_ATTEMPTS, fallbackAttempts + 1);
|
|
639
|
+
if (fallbackAttempts < MAX_FALLBACK_ATTEMPTS) {
|
|
640
|
+
recordDiagnostic(`Fallback turn ended without final:true (${fallbackAttempts}/${MAX_FALLBACK_ATTEMPTS})`, "terminal-pending", ctx, {
|
|
641
|
+
content: message.content,
|
|
642
|
+
resolutionAttempt: fallbackAttempts,
|
|
643
|
+
terminalEligible,
|
|
644
|
+
});
|
|
645
|
+
continueFallbackResolution(fallbackAttempts + 1 >= MAX_FALLBACK_ATTEMPTS);
|
|
646
|
+
return { message: { ...message, role: "assistant" as const, content: [] } };
|
|
647
|
+
}
|
|
648
|
+
resolutionPending = false;
|
|
649
|
+
recordDiagnostic(`Fallback resolution exhausted without final:true (${fallbackAttempts}/${MAX_FALLBACK_ATTEMPTS}); the preserved response and current state remain`, "finalization", ctx, {
|
|
650
|
+
content: message.content,
|
|
651
|
+
resolutionAttempt: fallbackAttempts,
|
|
652
|
+
terminalEligible,
|
|
653
|
+
});
|
|
654
|
+
if (!fallbackFailureReported) {
|
|
655
|
+
fallbackFailureReported = true;
|
|
656
|
+
ctx.ui.notify(`State Flow kept the preserved answer; no final:true patch arrived after ${MAX_FALLBACK_ATTEMPTS} fallback turns, so the iteration closed with its current state.`, "warning");
|
|
657
|
+
}
|
|
658
|
+
return { message: { ...message, role: "assistant" as const, content: [] } };
|
|
659
|
+
}
|
|
660
|
+
|
|
591
661
|
pi.registerTool({
|
|
592
662
|
name: READ_STATE_TOOL_NAME,
|
|
593
663
|
label: "Read State",
|
|
@@ -654,7 +724,6 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
654
724
|
if (params.final !== true) throw new Error('patch_state requires at least one scope patch or {"final":true}');
|
|
655
725
|
validateFinalEligibility(scopeStates, skillReads.successful.values(), runtime!.causalBasis(), artifactReads.successful.values());
|
|
656
726
|
terminalEligible = true;
|
|
657
|
-
terminalDraftIntercepted = false;
|
|
658
727
|
return { content: [{ type: "text", text: "\nState iteration is terminal-eligible." }], details: { final: true } };
|
|
659
728
|
}
|
|
660
729
|
const stage = stageAtomicScopePatches(scopeStates, patches, skillReads.successful.values(), runtime!.causalBasis(), artifactReads.successful.values());
|
|
@@ -664,7 +733,6 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
664
733
|
commitStage(stage, ctx, false);
|
|
665
734
|
if (params.final === true) {
|
|
666
735
|
terminalEligible = true;
|
|
667
|
-
terminalDraftIntercepted = false;
|
|
668
736
|
}
|
|
669
737
|
updateUi(ctx);
|
|
670
738
|
const publication = pendingPublication === undefined ? "" : "; durable publication pending";
|
|
@@ -676,7 +744,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
676
744
|
input: attempted,
|
|
677
745
|
tool: PATCH_STATE_TOOL_NAME,
|
|
678
746
|
toolCallId,
|
|
679
|
-
resolutionAttempt:
|
|
747
|
+
resolutionAttempt: fallbackAttempts,
|
|
680
748
|
terminalEligible,
|
|
681
749
|
});
|
|
682
750
|
throw separatedFailure(error);
|
|
@@ -684,74 +752,79 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
684
752
|
},
|
|
685
753
|
});
|
|
686
754
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
setPendingPublication(snapshot.meta.pendingPublication);
|
|
709
|
-
branchHasSnapshot = true;
|
|
710
|
-
}
|
|
711
|
-
const existingBranch = branchHasSnapshot;
|
|
712
|
-
snapshot = existingBranch
|
|
713
|
-
? resumeEpisode(snapshot, bootstrap)
|
|
714
|
-
: startEpisode(bootstrap);
|
|
715
|
-
if (snapshot.meta.remotePublication === undefined) {
|
|
716
|
-
snapshot.meta.remotePublication = serializeRemotePublicationPolicyDocument(
|
|
717
|
-
resolveRemotePublicationPolicy(existingBranch ? undefined : config.remotePublication, { legacyRuntime: existingBranch }),
|
|
718
|
-
);
|
|
719
|
-
}
|
|
755
|
+
function startStateFlow(ctx: ExtensionContext): StateFlowTelegramControlResult {
|
|
756
|
+
if (!activeContext) restoreActiveBranch(ctx);
|
|
757
|
+
telegramStartPending = false;
|
|
758
|
+
const previousSnapshot = structuredClone(snapshot);
|
|
759
|
+
const previousPassiveContinuation = passiveContinuation;
|
|
760
|
+
const previousBootstrapContinuation = bootstrapContinuation;
|
|
761
|
+
const previousArtifactRefreshPending = artifactRefreshPending;
|
|
762
|
+
const previousArtifactInvalidations = structuredClone(artifactInvalidations);
|
|
763
|
+
try {
|
|
764
|
+
if (!runtime?.view && !snapshot.meta.durableBase && !branchStartsWithoutRuntime) {
|
|
765
|
+
throw new Error("Selected branch revision is unavailable; restore its original Git history before starting State Flow");
|
|
766
|
+
}
|
|
767
|
+
const branch = ctx.sessionManager.getBranch();
|
|
768
|
+
activeContext = ctx;
|
|
769
|
+
runtime ??= createRuntime(ctx);
|
|
770
|
+
if (branchStartsWithoutRuntime) runtime.prepare();
|
|
771
|
+
const bootstrap = (!branchHasSnapshot || !snapshot.config.enabled)
|
|
772
|
+
&& (hasPriorConversation(branch) || previousPassiveContinuation !== undefined);
|
|
773
|
+
if (!runtime.view && snapshot.meta.durableBase) {
|
|
774
|
+
snapshot = runtime.restore(snapshot.meta.durableBase, snapshot);
|
|
775
|
+
setPendingPublication(snapshot.meta.pendingPublication);
|
|
720
776
|
branchHasSnapshot = true;
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
bootstrapContinuation = snapshot.meta.bootstrap
|
|
730
|
-
? previousPassiveContinuation ?? previousBootstrapContinuation
|
|
731
|
-
: undefined;
|
|
732
|
-
deferArtifactRefresh();
|
|
733
|
-
syncStateFlowTools();
|
|
734
|
-
persist();
|
|
735
|
-
updateUi(ctx);
|
|
736
|
-
ctx.ui.notify(
|
|
737
|
-
snapshot.meta.bootstrap
|
|
738
|
-
? "State Flow enabled. The next complete agent run will migrate active context into state."
|
|
739
|
-
: "State Flow enabled. The next prompt starts a stateful agent run.",
|
|
740
|
-
"info",
|
|
741
|
-
);
|
|
742
|
-
} catch (error) {
|
|
743
|
-
snapshot = previousSnapshot;
|
|
744
|
-
passiveContinuation = previousPassiveContinuation;
|
|
745
|
-
bootstrapContinuation = previousBootstrapContinuation;
|
|
746
|
-
artifactRefreshPending = previousArtifactRefreshPending;
|
|
747
|
-
artifactInvalidations = previousArtifactInvalidations;
|
|
748
|
-
artifactReads.setCandidates(artifactInvalidations);
|
|
749
|
-
syncStateFlowTools();
|
|
750
|
-
ctx.ui.notify(
|
|
751
|
-
`State Flow could not initialize CWD state: ${error instanceof Error ? error.message : String(error)}`,
|
|
752
|
-
"error",
|
|
777
|
+
}
|
|
778
|
+
const existingBranch = branchHasSnapshot;
|
|
779
|
+
snapshot = existingBranch
|
|
780
|
+
? resumeEpisode(snapshot, bootstrap)
|
|
781
|
+
: startEpisode(bootstrap);
|
|
782
|
+
if (snapshot.meta.remotePublication === undefined) {
|
|
783
|
+
snapshot.meta.remotePublication = serializeRemotePublicationPolicyDocument(
|
|
784
|
+
resolveRemotePublicationPolicy(existingBranch ? undefined : config.remotePublication, { legacyRuntime: existingBranch }),
|
|
753
785
|
);
|
|
754
786
|
}
|
|
787
|
+
branchHasSnapshot = true;
|
|
788
|
+
const publication = runtime.view
|
|
789
|
+
? runtime.promote(snapshot) ?? runtime.publish(snapshot)
|
|
790
|
+
: runtime.initialize(snapshot, true, undefined, branchStartsWithoutRuntime);
|
|
791
|
+
recordPolicyPublication(publication, ctx);
|
|
792
|
+
installScopeStates();
|
|
793
|
+
delete snapshot.legacySession;
|
|
794
|
+
clearRunTransient();
|
|
795
|
+
passiveContinuation = undefined;
|
|
796
|
+
bootstrapContinuation = snapshot.meta.bootstrap
|
|
797
|
+
? previousPassiveContinuation ?? previousBootstrapContinuation
|
|
798
|
+
: undefined;
|
|
799
|
+
deferArtifactRefresh();
|
|
800
|
+
syncStateFlowTools();
|
|
801
|
+
persist();
|
|
802
|
+
updateUi(ctx);
|
|
803
|
+
ctx.ui.notify(
|
|
804
|
+
snapshot.meta.bootstrap
|
|
805
|
+
? "State Flow enabled. The next complete agent run will migrate active context into state."
|
|
806
|
+
: "State Flow enabled. The next prompt starts a stateful agent run.",
|
|
807
|
+
"info",
|
|
808
|
+
);
|
|
809
|
+
return { ok: true, message: "State Flow enabled" };
|
|
810
|
+
} catch (error) {
|
|
811
|
+
snapshot = previousSnapshot;
|
|
812
|
+
passiveContinuation = previousPassiveContinuation;
|
|
813
|
+
bootstrapContinuation = previousBootstrapContinuation;
|
|
814
|
+
artifactRefreshPending = previousArtifactRefreshPending;
|
|
815
|
+
artifactInvalidations = previousArtifactInvalidations;
|
|
816
|
+
artifactReads.setCandidates(artifactInvalidations);
|
|
817
|
+
syncStateFlowTools();
|
|
818
|
+
const message = `State Flow could not initialize CWD state: ${error instanceof Error ? error.message : String(error)}`;
|
|
819
|
+
ctx.ui.notify(message, "error");
|
|
820
|
+
return { ok: false, message };
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
pi.registerCommand("state-flow-start", {
|
|
825
|
+
description: "Start State Flow mode",
|
|
826
|
+
handler: async (_args, ctx) => {
|
|
827
|
+
startStateFlow(ctx);
|
|
755
828
|
},
|
|
756
829
|
});
|
|
757
830
|
|
|
@@ -762,56 +835,94 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
762
835
|
},
|
|
763
836
|
});
|
|
764
837
|
|
|
838
|
+
function stopStateFlow(ctx: ExtensionContext): StateFlowTelegramControlResult {
|
|
839
|
+
if (!activeContext) restoreActiveBranch(ctx);
|
|
840
|
+
telegramStartPending = false;
|
|
841
|
+
let selected = runtime;
|
|
842
|
+
let current = snapshot;
|
|
843
|
+
if (!selected?.view && snapshot.meta.durableBase) {
|
|
844
|
+
selected = createRuntime(ctx);
|
|
845
|
+
current = selected.restore(snapshot.meta.durableBase, snapshot);
|
|
846
|
+
}
|
|
847
|
+
const stoppedAt = Date.now();
|
|
848
|
+
const exitStates = selected?.view ? selected.states() : undefined;
|
|
849
|
+
const exitHandoff = current.config.enabled && exitStates
|
|
850
|
+
? createPassiveContinuation(
|
|
851
|
+
projectStateForModel(overlayStates(exitStates.global, exitStates.cwd, exitStates.session)),
|
|
852
|
+
stoppedAt,
|
|
853
|
+
)
|
|
854
|
+
: undefined;
|
|
855
|
+
const retainedHandoff = exitHandoff ?? (!current.config.enabled ? passiveContinuation : undefined);
|
|
856
|
+
const stopped = stopEpisode(current);
|
|
857
|
+
const publication = selected?.view ? selected.publish(stopped) : undefined;
|
|
858
|
+
if (selected !== runtime) {
|
|
859
|
+
runtime = selected;
|
|
860
|
+
installScopeStates();
|
|
861
|
+
}
|
|
862
|
+
snapshot = stopped;
|
|
863
|
+
recordPolicyPublication(publication, ctx);
|
|
864
|
+
branchHasSnapshot = true;
|
|
865
|
+
clearRunTransient();
|
|
866
|
+
passiveContinuation = retainedHandoff;
|
|
867
|
+
bootstrapContinuation = undefined;
|
|
868
|
+
artifactInvalidations = [];
|
|
869
|
+
artifactReads.setCandidates([]);
|
|
870
|
+
artifactRefreshPending = false;
|
|
871
|
+
if (exitHandoff) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { at: stoppedAt });
|
|
872
|
+
syncStateFlowTools();
|
|
873
|
+
persist();
|
|
874
|
+
updateUi(ctx);
|
|
875
|
+
return { ok: true, message: "State Flow disabled" };
|
|
876
|
+
}
|
|
877
|
+
|
|
765
878
|
pi.registerCommand("state-flow-stop", {
|
|
766
879
|
description: "Stop State Flow on the current session branch",
|
|
767
880
|
handler: async (_args, ctx) => {
|
|
768
|
-
|
|
769
|
-
let selected = runtime;
|
|
770
|
-
let current = snapshot;
|
|
771
|
-
if (!selected?.view && snapshot.meta.durableBase) {
|
|
772
|
-
selected = createRuntime(ctx);
|
|
773
|
-
current = selected.restore(snapshot.meta.durableBase, snapshot);
|
|
774
|
-
}
|
|
775
|
-
const stoppedAt = Date.now();
|
|
776
|
-
const exitStates = selected?.view ? selected.states() : undefined;
|
|
777
|
-
const exitHandoff = current.config.enabled && exitStates
|
|
778
|
-
? createPassiveContinuation(
|
|
779
|
-
projectStateForModel(overlayStates(exitStates.global, exitStates.cwd, exitStates.session)),
|
|
780
|
-
stoppedAt,
|
|
781
|
-
)
|
|
782
|
-
: undefined;
|
|
783
|
-
const retainedHandoff = exitHandoff ?? (!current.config.enabled ? passiveContinuation : undefined);
|
|
784
|
-
const stopped = stopEpisode(current);
|
|
785
|
-
const publication = selected?.view ? selected.publish(stopped) : undefined;
|
|
786
|
-
if (selected !== runtime) {
|
|
787
|
-
runtime = selected;
|
|
788
|
-
installScopeStates();
|
|
789
|
-
}
|
|
790
|
-
snapshot = stopped;
|
|
791
|
-
recordPolicyPublication(publication, ctx);
|
|
792
|
-
branchHasSnapshot = true;
|
|
793
|
-
clearRunTransient();
|
|
794
|
-
passiveContinuation = retainedHandoff;
|
|
795
|
-
bootstrapContinuation = undefined;
|
|
796
|
-
artifactInvalidations = [];
|
|
797
|
-
artifactReads.setCandidates([]);
|
|
798
|
-
artifactRefreshPending = false;
|
|
799
|
-
if (exitHandoff) pi.appendEntry(PASSIVE_STOP_ENTRY_TYPE, { at: stoppedAt });
|
|
800
|
-
syncStateFlowTools();
|
|
801
|
-
persist();
|
|
802
|
-
updateUi(ctx);
|
|
881
|
+
stopStateFlow(ctx);
|
|
803
882
|
},
|
|
804
883
|
});
|
|
805
884
|
|
|
885
|
+
const telegram = createStateFlowTelegramAdapter({
|
|
886
|
+
...(options.telegram?.load === undefined ? {} : { load: options.telegram.load }),
|
|
887
|
+
port: {
|
|
888
|
+
snapshot: () => ({
|
|
889
|
+
enabled: snapshot.config.enabled,
|
|
890
|
+
step: snapshot.meta.step,
|
|
891
|
+
bootstrap: snapshot.meta.bootstrap === true,
|
|
892
|
+
startPending: telegramStartPending,
|
|
893
|
+
}),
|
|
894
|
+
canStartNow: () => activeContext === undefined || activeContext.isIdle(),
|
|
895
|
+
start: () => {
|
|
896
|
+
if (!activeContext) throw new Error("State Flow is not attached to an active session yet");
|
|
897
|
+
return startStateFlow(activeContext);
|
|
898
|
+
},
|
|
899
|
+
stop: () => {
|
|
900
|
+
if (!activeContext) throw new Error("State Flow is not attached to an active session yet");
|
|
901
|
+
try {
|
|
902
|
+
return stopStateFlow(activeContext);
|
|
903
|
+
} catch (error) {
|
|
904
|
+
return { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
905
|
+
}
|
|
906
|
+
},
|
|
907
|
+
deferStart: () => {
|
|
908
|
+
telegramStartPending = true;
|
|
909
|
+
},
|
|
910
|
+
cancelStart: () => {
|
|
911
|
+
telegramStartPending = false;
|
|
912
|
+
},
|
|
913
|
+
},
|
|
914
|
+
});
|
|
915
|
+
void telegram.ensure();
|
|
916
|
+
|
|
806
917
|
pi.on("before_agent_start", (event, ctx) => {
|
|
807
918
|
if (!snapshot.config.enabled) return;
|
|
808
919
|
skillReads.clear();
|
|
809
920
|
artifactReads.clear();
|
|
810
921
|
if (artifactRefreshPending) refreshArtifactInvalidations(ctx);
|
|
811
922
|
terminalEligible = false;
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
923
|
+
resolutionPending = false;
|
|
924
|
+
fallbackAttempts = 0;
|
|
925
|
+
fallbackFailureReported = false;
|
|
815
926
|
responseAwaitingReconciliation = false;
|
|
816
927
|
const rotatesRun = snapshot.meta.specification !== undefined;
|
|
817
928
|
if (rotatesRun && rehydrationPhase !== "new-bootstrap" && rehydrationPhase !== "resume-bootstrap") rehydrationPhase = "step";
|
|
@@ -841,7 +952,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
841
952
|
? passiveContinuationMessages(event.messages as AgentMessage[], bootstrapContinuation)
|
|
842
953
|
: event.messages as AgentMessage[];
|
|
843
954
|
const messages = withoutPrivateValidation(sourceMessages);
|
|
844
|
-
return { messages: [runtimeContextMessage(snapshot, effectiveState, recentTransitions, invalidations, activeRehydrationPhase,
|
|
955
|
+
return { messages: [runtimeContextMessage(snapshot, effectiveState, recentTransitions, invalidations, activeRehydrationPhase, resolutionPending), ...messages] };
|
|
845
956
|
}
|
|
846
957
|
const trajectory = currentRunTrajectory(
|
|
847
958
|
event.messages as AgentMessage[],
|
|
@@ -851,7 +962,7 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
851
962
|
runAnchorTimestamp = trajectory.anchorTimestamp;
|
|
852
963
|
return {
|
|
853
964
|
messages: [
|
|
854
|
-
runtimeContextMessage(snapshot, effectiveState, recentTransitions, invalidations, activeRehydrationPhase,
|
|
965
|
+
runtimeContextMessage(snapshot, effectiveState, recentTransitions, invalidations, activeRehydrationPhase, resolutionPending),
|
|
855
966
|
...trajectory.messages,
|
|
856
967
|
],
|
|
857
968
|
};
|
|
@@ -905,31 +1016,16 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
905
1016
|
recordDiagnostic(`Assistant response ended with ${message.stopReason}`, "finalization", ctx, { content: message.content, terminalEligible });
|
|
906
1017
|
return;
|
|
907
1018
|
}
|
|
1019
|
+
if (resolutionPending) return resolveFallbackTurn(ctx, message);
|
|
908
1020
|
if (!terminalEligible) {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
resolutionAttempts = Math.min(MAX_RESOLUTION_ATTEMPTS, resolutionAttempts + 1);
|
|
912
|
-
recordDiagnostic(`Terminal draft intercepted before State Flow eligibility (attempt ${resolutionAttempts}/${MAX_RESOLUTION_ATTEMPTS})`, "terminal-pending", ctx, {
|
|
913
|
-
content: message.content,
|
|
914
|
-
resolutionAttempt: resolutionAttempts,
|
|
915
|
-
terminalEligible,
|
|
916
|
-
});
|
|
917
|
-
if (resolutionAttempts < MAX_RESOLUTION_ATTEMPTS) {
|
|
918
|
-
continueForResolution();
|
|
919
|
-
} else if (!resolutionFailureReported) {
|
|
920
|
-
resolutionFailureReported = true;
|
|
921
|
-
ctx.ui.notify(`State Flow could not obtain final:true after ${MAX_RESOLUTION_ATTEMPTS} terminal attempts; committed state was preserved and no draft was accepted.`, "error");
|
|
922
|
-
}
|
|
923
|
-
return { message: { ...message, role: "assistant" as const, content: [] } };
|
|
1021
|
+
beginFallbackResolution(ctx, message, "the terminal draft ended without State Flow eligibility");
|
|
1022
|
+
return;
|
|
924
1023
|
}
|
|
925
1024
|
try {
|
|
926
1025
|
validateFinalEligibility(scopeStates, skillReads.successful.values(), runtime!.causalBasis(), artifactReads.successful.values());
|
|
927
1026
|
} catch (error) {
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
recordDiagnostic(error instanceof Error ? error.message : String(error), "terminal-pending", ctx, { content: message.content, terminalEligible });
|
|
931
|
-
continueForResolution();
|
|
932
|
-
return { message: { ...message, role: "assistant" as const, content: [] } };
|
|
1027
|
+
beginFallbackResolution(ctx, message, error instanceof Error ? error.message : String(error));
|
|
1028
|
+
return;
|
|
933
1029
|
}
|
|
934
1030
|
responseAwaitingReconciliation = true;
|
|
935
1031
|
});
|
|
@@ -952,12 +1048,15 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
952
1048
|
ctx.ui.notify(`State Flow could not reconcile the final response: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
953
1049
|
} finally {
|
|
954
1050
|
responseAwaitingReconciliation = false;
|
|
955
|
-
terminalDraftIntercepted = false;
|
|
956
1051
|
}
|
|
957
1052
|
updateUi(ctx);
|
|
958
1053
|
});
|
|
959
1054
|
|
|
960
|
-
pi.on("agent_settled", (_event,
|
|
1055
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
1056
|
+
if (telegramStartPending && !snapshot.config.enabled) {
|
|
1057
|
+
telegramStartPending = false;
|
|
1058
|
+
startStateFlow(ctx);
|
|
1059
|
+
}
|
|
961
1060
|
if (snapshot.meta.remotePublication?.mode === "turn-end") launchPublicationWorker();
|
|
962
1061
|
});
|
|
963
1062
|
|
|
@@ -967,8 +1066,13 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
967
1066
|
retryPendingPush(ctx);
|
|
968
1067
|
if (snapshot.meta.remotePublication?.mode === "turn-end") launchPublicationWorker();
|
|
969
1068
|
updateUi(ctx);
|
|
1069
|
+
void telegram.ensure();
|
|
970
1070
|
});
|
|
971
1071
|
pi.on("session_tree", (_event, ctx) => {
|
|
972
1072
|
restoreActiveBranch(ctx);
|
|
973
1073
|
});
|
|
1074
|
+
pi.on("session_shutdown", () => {
|
|
1075
|
+
telegramStartPending = false;
|
|
1076
|
+
telegram.dispose();
|
|
1077
|
+
});
|
|
974
1078
|
}
|