@oh-my-pi/pi-coding-agent 14.6.3 → 14.6.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 +24 -0
- package/package.json +7 -7
- package/src/config/settings-schema.ts +25 -0
- package/src/edit/modes/hashline.ts +191 -2
- package/src/hindsight/backend.ts +85 -324
- package/src/hindsight/client.ts +153 -0
- package/src/hindsight/config.ts +10 -0
- package/src/hindsight/content.ts +9 -4
- package/src/hindsight/index.ts +2 -0
- package/src/hindsight/mental-models.ts +382 -0
- package/src/hindsight/seeds.json +32 -0
- package/src/hindsight/state.ts +469 -0
- package/src/memory-backend/types.ts +14 -4
- package/src/modes/controllers/command-controller.ts +263 -4
- package/src/prompts/tools/hashline.md +1 -0
- package/src/sdk.ts +10 -1
- package/src/session/agent-session.ts +44 -1
- package/src/slash-commands/builtin-registry.ts +10 -0
- package/src/task/executor.ts +3 -0
- package/src/task/index.ts +2 -0
- package/src/tools/hindsight-recall.ts +1 -3
- package/src/tools/hindsight-reflect.ts +1 -3
- package/src/tools/hindsight-retain.ts +6 -9
- package/src/tools/index.ts +3 -0
- package/src/hindsight/retain-queue.ts +0 -166
|
@@ -17,6 +17,15 @@ import { clearClaudePluginRootsCache } from "../../discovery/helpers";
|
|
|
17
17
|
import { getGatewayStatus } from "../../eval/py/gateway-coordinator";
|
|
18
18
|
import { loadCustomShare } from "../../export/custom-share";
|
|
19
19
|
import type { CompactOptions } from "../../extensibility/extensions/types";
|
|
20
|
+
import {
|
|
21
|
+
diffMentalModelContent,
|
|
22
|
+
type HindsightApi,
|
|
23
|
+
type HindsightSessionState,
|
|
24
|
+
loadHindsightConfig,
|
|
25
|
+
reloadMentalModelsForSession,
|
|
26
|
+
resolveSeedsForScope,
|
|
27
|
+
summarizeMentalModel,
|
|
28
|
+
} from "../../hindsight";
|
|
20
29
|
import { resolveMemoryBackend } from "../../memory-backend";
|
|
21
30
|
import { BashExecutionComponent } from "../../modes/components/bash-execution";
|
|
22
31
|
import { BorderedLoader } from "../../modes/components/bordered-loader";
|
|
@@ -573,7 +582,7 @@ export class CommandController {
|
|
|
573
582
|
const backend = resolveMemoryBackend(this.ctx.settings);
|
|
574
583
|
|
|
575
584
|
if (action === "view") {
|
|
576
|
-
const payload = await backend.buildDeveloperInstructions(agentDir, this.ctx.settings);
|
|
585
|
+
const payload = await backend.buildDeveloperInstructions(agentDir, this.ctx.settings, this.ctx.session);
|
|
577
586
|
if (!payload) {
|
|
578
587
|
this.ctx.showWarning("Memory payload is empty (memory backend off, disabled, or no memory available).");
|
|
579
588
|
return;
|
|
@@ -590,7 +599,7 @@ export class CommandController {
|
|
|
590
599
|
|
|
591
600
|
if (action === "reset" || action === "clear") {
|
|
592
601
|
try {
|
|
593
|
-
await backend.clear(agentDir, this.ctx.sessionManager.getCwd());
|
|
602
|
+
await backend.clear(agentDir, this.ctx.sessionManager.getCwd(), this.ctx.session);
|
|
594
603
|
await this.ctx.session.refreshBaseSystemPrompt();
|
|
595
604
|
this.ctx.showStatus("Memory data cleared and system prompt refreshed.");
|
|
596
605
|
} catch (error) {
|
|
@@ -601,7 +610,7 @@ export class CommandController {
|
|
|
601
610
|
|
|
602
611
|
if (action === "enqueue" || action === "rebuild") {
|
|
603
612
|
try {
|
|
604
|
-
await backend.enqueue(agentDir, this.ctx.sessionManager.getCwd());
|
|
613
|
+
await backend.enqueue(agentDir, this.ctx.sessionManager.getCwd(), this.ctx.session);
|
|
605
614
|
this.ctx.showStatus("Memory consolidation enqueued.");
|
|
606
615
|
} catch (error) {
|
|
607
616
|
this.ctx.showError(`Memory enqueue failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -609,7 +618,257 @@ export class CommandController {
|
|
|
609
618
|
return;
|
|
610
619
|
}
|
|
611
620
|
|
|
612
|
-
|
|
621
|
+
if (action === "mm") {
|
|
622
|
+
await this.#handleMentalModelsSubcommand(argumentText);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
this.ctx.showError("Usage: /memory <view|clear|reset|enqueue|rebuild|mm ...>");
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async #handleMentalModelsSubcommand(argumentText: string): Promise<void> {
|
|
630
|
+
// Parse: "mm <verb> [arg]"
|
|
631
|
+
const parts = argumentText.split(/\s+/).slice(1);
|
|
632
|
+
const verb = parts[0]?.toLowerCase() ?? "list";
|
|
633
|
+
const arg = parts[1];
|
|
634
|
+
|
|
635
|
+
const state = this.ctx.session.getHindsightSessionState();
|
|
636
|
+
const primary = state && !state.aliasOf ? state : undefined;
|
|
637
|
+
if (!primary) {
|
|
638
|
+
this.ctx.showError("Hindsight backend is not active for this session.");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (!primary.config.mentalModelsEnabled) {
|
|
642
|
+
this.ctx.showError("Mental models are disabled (hindsight.mentalModelsEnabled = false).");
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
switch (verb) {
|
|
647
|
+
case "list":
|
|
648
|
+
await this.#mmList(primary);
|
|
649
|
+
return;
|
|
650
|
+
case "show":
|
|
651
|
+
if (!arg) return this.ctx.showError("Usage: /memory mm show <id>");
|
|
652
|
+
await this.#mmShow(primary, arg);
|
|
653
|
+
return;
|
|
654
|
+
case "refresh":
|
|
655
|
+
await this.#mmRefresh(primary, arg);
|
|
656
|
+
return;
|
|
657
|
+
case "history":
|
|
658
|
+
if (!arg) return this.ctx.showError("Usage: /memory mm history <id>");
|
|
659
|
+
await this.#mmHistory(primary, arg);
|
|
660
|
+
return;
|
|
661
|
+
case "seed":
|
|
662
|
+
await this.#mmSeed(primary);
|
|
663
|
+
return;
|
|
664
|
+
case "reload":
|
|
665
|
+
await this.#mmReload(primary);
|
|
666
|
+
return;
|
|
667
|
+
case "delete":
|
|
668
|
+
case "remove":
|
|
669
|
+
if (!arg) return this.ctx.showError("Usage: /memory mm delete <id>");
|
|
670
|
+
await this.#mmDelete(primary, arg);
|
|
671
|
+
return;
|
|
672
|
+
default:
|
|
673
|
+
this.ctx.showError("Usage: /memory mm <list|show|refresh|history|seed|reload|delete>");
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async #mmList(state: HindsightSessionState): Promise<void> {
|
|
678
|
+
const client: HindsightApi = state.client;
|
|
679
|
+
try {
|
|
680
|
+
const response = await client.listMentalModels(state.bankId, { detail: "metadata" });
|
|
681
|
+
const items = response.items ?? [];
|
|
682
|
+
if (items.length === 0) {
|
|
683
|
+
this.ctx.showStatus(`No mental models on bank ${state.bankId}.`);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const lines = items
|
|
687
|
+
.slice()
|
|
688
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
689
|
+
.map(summarizeMentalModel);
|
|
690
|
+
showMarkdownPanel(this.ctx, `Mental Models — ${state.bankId}`, lines.join("\n"));
|
|
691
|
+
} catch (error) {
|
|
692
|
+
this.ctx.showError(`mm list failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async #mmShow(state: HindsightSessionState, id: string): Promise<void> {
|
|
697
|
+
try {
|
|
698
|
+
const model = await state.client.getMentalModel(state.bankId, id, { detail: "content" });
|
|
699
|
+
if (!model) {
|
|
700
|
+
this.ctx.showError(`Mental model not found: ${id}`);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const tags = model.tags && model.tags.length > 0 ? `\n_tags: ${model.tags.join(", ")}_` : "";
|
|
704
|
+
const refreshed = model.last_refreshed_at ? `\n_last refreshed: ${model.last_refreshed_at}_` : "";
|
|
705
|
+
const sourceQuery = model.source_query ? `\n\n**Source query:** ${model.source_query}` : "";
|
|
706
|
+
const content = (model.content ?? "_(empty — background reflect may still be running)_").trim();
|
|
707
|
+
showMarkdownPanel(
|
|
708
|
+
this.ctx,
|
|
709
|
+
model.name,
|
|
710
|
+
`**id:** \`${model.id}\`${tags}${refreshed}${sourceQuery}\n\n${content}`,
|
|
711
|
+
);
|
|
712
|
+
} catch (error) {
|
|
713
|
+
this.ctx.showError(`mm show failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async #mmRefresh(state: HindsightSessionState, id: string | undefined): Promise<void> {
|
|
718
|
+
try {
|
|
719
|
+
if (id) {
|
|
720
|
+
// Single-model refresh is explicit operator intent: bypass the
|
|
721
|
+
// auto-refresh filter so curated/manual models can still be
|
|
722
|
+
// refreshed on demand.
|
|
723
|
+
await state.client.refreshMentalModel(state.bankId, id);
|
|
724
|
+
this.ctx.showStatus(`Refresh queued for mental model ${id}.`);
|
|
725
|
+
} else {
|
|
726
|
+
// Bulk refresh: only touch models that opted into automatic
|
|
727
|
+
// refresh via `trigger.refresh_after_consolidation`. Curated
|
|
728
|
+
// models are reviewed before publishing and must not be
|
|
729
|
+
// silently regenerated by a bank-wide refresh sweep. Reading
|
|
730
|
+
// `detail: "content"` here is required because the trigger
|
|
731
|
+
// field is excluded from `detail: "metadata"`.
|
|
732
|
+
const list = await state.client.listMentalModels(state.bankId, { detail: "content" });
|
|
733
|
+
const items = list.items ?? [];
|
|
734
|
+
if (items.length === 0) {
|
|
735
|
+
this.ctx.showStatus(`No mental models on bank ${state.bankId}.`);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const targets = items.filter(m => m.trigger?.refresh_after_consolidation === true);
|
|
739
|
+
const skipped = items.length - targets.length;
|
|
740
|
+
if (targets.length === 0) {
|
|
741
|
+
this.ctx.showStatus(
|
|
742
|
+
`No mental models opted into auto-refresh; ${skipped} curated model(s) left untouched. Pass an explicit id to refresh one of them.`,
|
|
743
|
+
);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
let queued = 0;
|
|
747
|
+
for (const item of targets) {
|
|
748
|
+
try {
|
|
749
|
+
await state.client.refreshMentalModel(state.bankId, item.id);
|
|
750
|
+
queued++;
|
|
751
|
+
} catch (error) {
|
|
752
|
+
this.ctx.showWarning(
|
|
753
|
+
`Refresh failed for ${item.id}: ${error instanceof Error ? error.message : String(error)}`,
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const skippedSuffix = skipped > 0 ? `; skipped ${skipped} curated model(s)` : "";
|
|
758
|
+
this.ctx.showStatus(
|
|
759
|
+
`Refresh queued for ${queued}/${targets.length} auto-refresh model(s)${skippedSuffix}.`,
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
// Reload the cache after a brief grace so the new content (if the refresh
|
|
763
|
+
// completes synchronously on the server) flows into the system prompt.
|
|
764
|
+
await Bun.sleep(500);
|
|
765
|
+
await reloadMentalModelsForSession(state.session);
|
|
766
|
+
} catch (error) {
|
|
767
|
+
this.ctx.showError(`mm refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
async #mmHistory(state: HindsightSessionState, id: string): Promise<void> {
|
|
772
|
+
try {
|
|
773
|
+
const [model, history] = await Promise.all([
|
|
774
|
+
state.client.getMentalModel(state.bankId, id, { detail: "content" }),
|
|
775
|
+
state.client.getMentalModelHistory(state.bankId, id),
|
|
776
|
+
]);
|
|
777
|
+
if (!model) {
|
|
778
|
+
this.ctx.showError(`Mental model not found: ${id}`);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (history.length === 0) {
|
|
782
|
+
this.ctx.showStatus(`No history recorded for ${id}.`);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
// History is most-recent first. Each entry stores the content BEFORE that
|
|
786
|
+
// change. To diff "what changed at entry N", compare entry N's
|
|
787
|
+
// previous_content (= state before that change) with entry N-1's
|
|
788
|
+
// previous_content (= state after that change, which was state before
|
|
789
|
+
// the next change). For the most recent change, compare against the
|
|
790
|
+
// model's CURRENT content.
|
|
791
|
+
const sections: string[] = [];
|
|
792
|
+
for (let i = 0; i < history.length; i++) {
|
|
793
|
+
const before = history[i].previous_content ?? "";
|
|
794
|
+
const after = i === 0 ? (model.content ?? "") : (history[i - 1].previous_content ?? "");
|
|
795
|
+
const diff = diffMentalModelContent(before, after);
|
|
796
|
+
sections.push(`### ${history[i].changed_at}\n\n\`\`\`diff\n${diff}\n\`\`\``);
|
|
797
|
+
}
|
|
798
|
+
showMarkdownPanel(this.ctx, `History — ${model.name}`, sections.join("\n\n"));
|
|
799
|
+
} catch (error) {
|
|
800
|
+
this.ctx.showError(`mm history failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async #mmSeed(state: HindsightSessionState): Promise<void> {
|
|
805
|
+
try {
|
|
806
|
+
const config = loadHindsightConfig(this.ctx.settings);
|
|
807
|
+
const seeds = resolveSeedsForScope(
|
|
808
|
+
{
|
|
809
|
+
bankId: state.bankId,
|
|
810
|
+
retainTags: state.retainTags,
|
|
811
|
+
recallTags: state.recallTags,
|
|
812
|
+
recallTagsMatch: state.recallTagsMatch,
|
|
813
|
+
},
|
|
814
|
+
config.scoping,
|
|
815
|
+
);
|
|
816
|
+
if (seeds.length === 0) {
|
|
817
|
+
this.ctx.showStatus(`No built-in seeds apply to scoping=${config.scoping}.`);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const list = await state.client.listMentalModels(state.bankId, { detail: "metadata" });
|
|
821
|
+
const existing = new Set((list.items ?? []).map(m => m.id));
|
|
822
|
+
let created = 0;
|
|
823
|
+
let skipped = 0;
|
|
824
|
+
for (const seed of seeds) {
|
|
825
|
+
if (existing.has(seed.id)) {
|
|
826
|
+
skipped++;
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
await state.client.createMentalModel(state.bankId, seed.name, seed.sourceQuery, {
|
|
831
|
+
id: seed.id,
|
|
832
|
+
tags: seed.tags.length > 0 ? seed.tags : undefined,
|
|
833
|
+
maxTokens: seed.maxTokens,
|
|
834
|
+
trigger: seed.trigger,
|
|
835
|
+
});
|
|
836
|
+
created++;
|
|
837
|
+
} catch (error) {
|
|
838
|
+
this.ctx.showWarning(
|
|
839
|
+
`Seed failed for ${seed.id}: ${error instanceof Error ? error.message : String(error)}`,
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
this.ctx.showStatus(`Seeded ${created} new mental model(s); ${skipped} already present.`);
|
|
844
|
+
} catch (error) {
|
|
845
|
+
this.ctx.showError(`mm seed failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
async #mmReload(state: HindsightSessionState): Promise<void> {
|
|
850
|
+
const ok = await reloadMentalModelsForSession(state.session);
|
|
851
|
+
if (ok) {
|
|
852
|
+
this.ctx.showStatus("Mental-model cache reloaded.");
|
|
853
|
+
} else {
|
|
854
|
+
this.ctx.showError("Reload failed (Hindsight backend not active or mental models disabled).");
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async #mmDelete(state: HindsightSessionState, id: string): Promise<void> {
|
|
859
|
+
try {
|
|
860
|
+
const removed = await state.client.deleteMentalModel(state.bankId, id);
|
|
861
|
+
if (!removed) {
|
|
862
|
+
this.ctx.showError(`Mental model not found: ${id}`);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
// Drop the cached snippet so the closing tag does not silently keep
|
|
866
|
+
// stale content in the system prompt until the next agent_end TTL.
|
|
867
|
+
await reloadMentalModelsForSession(state.session);
|
|
868
|
+
this.ctx.showStatus(`Deleted mental model ${id} from bank ${state.bankId}.`);
|
|
869
|
+
} catch (error) {
|
|
870
|
+
this.ctx.showError(`mm delete failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
871
|
+
}
|
|
613
872
|
}
|
|
614
873
|
|
|
615
874
|
async #runNewSessionFlow(options?: NewSessionOptions, label: string = "New session started"): Promise<void> {
|
|
@@ -79,4 +79,5 @@ This format is purely textual. The tool has NO awareness of language, indentatio
|
|
|
79
79
|
- Every inserted/replacement content line **MUST** start with `{{hsep}}`; raw content lines are invalid.
|
|
80
80
|
- Do not write unified diff syntax (`@@`, `-OLD`, `+NEW`).
|
|
81
81
|
- To replace a block, use one `= A..B` op followed by all replacement `{{hsep}}TEXT` payload lines.
|
|
82
|
+
- `= A..B` deletes the range; payload is what's written. If a payload edge line already exists immediately outside `A..B`, widen the range to cover it — otherwise it duplicates.
|
|
82
83
|
</critical>
|
package/src/sdk.ts
CHANGED
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
} from "./extensibility/extensions";
|
|
62
62
|
import { loadSkills as loadSkillsInternal, type Skill, type SkillWarning } from "./extensibility/skills";
|
|
63
63
|
import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from "./extensibility/slash-commands";
|
|
64
|
+
import type { HindsightSessionState } from "./hindsight/state";
|
|
64
65
|
import {
|
|
65
66
|
AgentProtocolHandler,
|
|
66
67
|
ArtifactProtocolHandler,
|
|
@@ -216,6 +217,8 @@ export interface CreateAgentSessionOptions {
|
|
|
216
217
|
requireYieldTool?: boolean;
|
|
217
218
|
/** Task recursion depth (for subagent sessions). Default: 0 */
|
|
218
219
|
taskDepth?: number;
|
|
220
|
+
/** Parent Hindsight state to alias for subagent memory tools. */
|
|
221
|
+
parentHindsightSessionState?: HindsightSessionState;
|
|
219
222
|
/** Pre-allocated agent identity for IRC routing. Default: "0-Main" for top-level, parentTaskPrefix-derived for sub. */
|
|
220
223
|
agentId?: string;
|
|
221
224
|
/** Display name for the agent in IRC. Default: "main" or "sub". */
|
|
@@ -968,6 +971,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
968
971
|
trackEvalExecution: (execution, abortController) =>
|
|
969
972
|
session ? session.trackEvalExecution(execution, abortController) : execution,
|
|
970
973
|
getSessionId: () => sessionManager.getSessionId?.() ?? null,
|
|
974
|
+
getHindsightSessionState: () => session?.getHindsightSessionState(),
|
|
971
975
|
getAgentId: () => resolvedAgentId,
|
|
972
976
|
getToolByName: name => session?.getToolByName(name),
|
|
973
977
|
agentRegistry,
|
|
@@ -1335,7 +1339,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1335
1339
|
const promptTools = buildSystemPromptToolMetadata(tools, {
|
|
1336
1340
|
search_tool_bm25: { description: renderSearchToolBm25Description(discoverableMCPTools) },
|
|
1337
1341
|
});
|
|
1338
|
-
const memoryInstructions = await resolveMemoryBackend(settings).buildDeveloperInstructions(
|
|
1342
|
+
const memoryInstructions = await resolveMemoryBackend(settings).buildDeveloperInstructions(
|
|
1343
|
+
agentDir,
|
|
1344
|
+
settings,
|
|
1345
|
+
session,
|
|
1346
|
+
);
|
|
1339
1347
|
|
|
1340
1348
|
// Build combined append prompt: memory instructions + MCP server instructions
|
|
1341
1349
|
const serverInstructions = mcpManager?.getServerInstructions();
|
|
@@ -1755,6 +1763,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1755
1763
|
modelRegistry,
|
|
1756
1764
|
agentDir,
|
|
1757
1765
|
taskDepth,
|
|
1766
|
+
parentHindsightSessionState: options.parentHindsightSessionState,
|
|
1758
1767
|
}),
|
|
1759
1768
|
),
|
|
1760
1769
|
);
|
|
@@ -102,6 +102,7 @@ import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
|
|
|
102
102
|
import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
103
103
|
import type { Skill, SkillWarning } from "../extensibility/skills";
|
|
104
104
|
import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
|
|
105
|
+
import type { HindsightSessionState } from "../hindsight/state";
|
|
105
106
|
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
106
107
|
import {
|
|
107
108
|
buildDiscoverableMCPSearchIndex,
|
|
@@ -563,6 +564,7 @@ export class AgentSession {
|
|
|
563
564
|
#lastSuccessfulYieldToolCallId: string | undefined = undefined;
|
|
564
565
|
#promptGeneration = 0;
|
|
565
566
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
567
|
+
#hindsightSessionState: HindsightSessionState | undefined = undefined;
|
|
566
568
|
|
|
567
569
|
#startPowerAssertion(): void {
|
|
568
570
|
if (process.platform !== "darwin") {
|
|
@@ -702,6 +704,16 @@ export class AgentSession {
|
|
|
702
704
|
return this.#providerSessionState;
|
|
703
705
|
}
|
|
704
706
|
|
|
707
|
+
getHindsightSessionState(): HindsightSessionState | undefined {
|
|
708
|
+
return this.#hindsightSessionState;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
setHindsightSessionState(state: HindsightSessionState | undefined): HindsightSessionState | undefined {
|
|
712
|
+
const previous = this.#hindsightSessionState;
|
|
713
|
+
this.#hindsightSessionState = state;
|
|
714
|
+
return previous;
|
|
715
|
+
}
|
|
716
|
+
|
|
705
717
|
/** TTSR manager for time-traveling stream rules */
|
|
706
718
|
get ttsrManager(): TtsrManager | undefined {
|
|
707
719
|
return this.#ttsrManager;
|
|
@@ -1964,6 +1976,22 @@ export class AgentSession {
|
|
|
1964
1976
|
this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
|
|
1965
1977
|
}
|
|
1966
1978
|
|
|
1979
|
+
/** Keep Hindsight metadata aligned when the underlying agent session id changes. */
|
|
1980
|
+
#rekeyHindsightMemoryForCurrentSessionId(): void {
|
|
1981
|
+
if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
|
|
1982
|
+
const sid = this.agent.sessionId;
|
|
1983
|
+
if (!sid) return;
|
|
1984
|
+
this.getHindsightSessionState()?.setSessionId(sid);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
/** New session file: reset auto-recall / retain-threshold counters for the new transcript. */
|
|
1988
|
+
#resetHindsightConversationTrackingIfHindsight(): void {
|
|
1989
|
+
if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
|
|
1990
|
+
const state = this.getHindsightSessionState();
|
|
1991
|
+
if (!state || state.aliasOf) return;
|
|
1992
|
+
state.resetConversationTracking();
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1967
1995
|
/**
|
|
1968
1996
|
* Remove all listeners, flush pending writes, and disconnect from agent.
|
|
1969
1997
|
* Call this when completely done with the session.
|
|
@@ -1994,6 +2022,9 @@ export class AgentSession {
|
|
|
1994
2022
|
this.#stopPowerAssertion();
|
|
1995
2023
|
await this.sessionManager.close();
|
|
1996
2024
|
this.#closeAllProviderSessions("dispose");
|
|
2025
|
+
const hindsightState = this.setHindsightSessionState(undefined);
|
|
2026
|
+
await hindsightState?.flushRetainQueue();
|
|
2027
|
+
hindsightState?.dispose();
|
|
1997
2028
|
this.#disconnectFromAgent();
|
|
1998
2029
|
this.#eventListeners = [];
|
|
1999
2030
|
}
|
|
@@ -3626,6 +3657,8 @@ export class AgentSession {
|
|
|
3626
3657
|
await this.sessionManager.newSession(options);
|
|
3627
3658
|
this.setTodoPhases([]);
|
|
3628
3659
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
3660
|
+
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
3661
|
+
this.#resetHindsightConversationTrackingIfHindsight();
|
|
3629
3662
|
this.#steeringMessages = [];
|
|
3630
3663
|
this.#followUpMessages = [];
|
|
3631
3664
|
this.#pendingNextTurnMessages = [];
|
|
@@ -3719,6 +3752,7 @@ export class AgentSession {
|
|
|
3719
3752
|
|
|
3720
3753
|
// Update agent session ID
|
|
3721
3754
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
3755
|
+
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
3722
3756
|
|
|
3723
3757
|
// Emit session_switch event with reason "fork" to hooks
|
|
3724
3758
|
if (this.#extensionRunner) {
|
|
@@ -4261,7 +4295,7 @@ export class AgentSession {
|
|
|
4261
4295
|
if (!backend.preCompactionContext) return undefined;
|
|
4262
4296
|
const messages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
|
|
4263
4297
|
try {
|
|
4264
|
-
return await backend.preCompactionContext(messages, this.settings);
|
|
4298
|
+
return await backend.preCompactionContext(messages, this.settings, this);
|
|
4265
4299
|
} catch (err) {
|
|
4266
4300
|
logger.debug("Memory backend preCompactionContext failed", {
|
|
4267
4301
|
backend: backend.id,
|
|
@@ -4425,6 +4459,8 @@ export class AgentSession {
|
|
|
4425
4459
|
await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
|
|
4426
4460
|
this.agent.reset();
|
|
4427
4461
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
4462
|
+
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
4463
|
+
this.#resetHindsightConversationTrackingIfHindsight();
|
|
4428
4464
|
this.#steeringMessages = [];
|
|
4429
4465
|
this.#followUpMessages = [];
|
|
4430
4466
|
this.#pendingNextTurnMessages = [];
|
|
@@ -6571,6 +6607,7 @@ export class AgentSession {
|
|
|
6571
6607
|
try {
|
|
6572
6608
|
await this.sessionManager.setSessionFile(sessionPath);
|
|
6573
6609
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
6610
|
+
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
6574
6611
|
|
|
6575
6612
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6576
6613
|
const didReloadConversationChange =
|
|
@@ -6640,11 +6677,15 @@ export class AgentSession {
|
|
|
6640
6677
|
? undefined
|
|
6641
6678
|
: configuredServiceTier;
|
|
6642
6679
|
|
|
6680
|
+
if (switchingToDifferentSession) {
|
|
6681
|
+
this.#resetHindsightConversationTrackingIfHindsight();
|
|
6682
|
+
}
|
|
6643
6683
|
this.#reconnectToAgent();
|
|
6644
6684
|
return true;
|
|
6645
6685
|
} catch (error) {
|
|
6646
6686
|
this.sessionManager.restoreState(previousSessionState);
|
|
6647
6687
|
this.agent.sessionId = previousSessionState.sessionId;
|
|
6688
|
+
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
6648
6689
|
let restoreMcpError: unknown;
|
|
6649
6690
|
try {
|
|
6650
6691
|
await this.#restoreMCPSelectionsForSessionContext(previousSessionContext, {
|
|
@@ -6736,6 +6777,8 @@ export class AgentSession {
|
|
|
6736
6777
|
}
|
|
6737
6778
|
this.#syncTodoPhasesFromBranch();
|
|
6738
6779
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
6780
|
+
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
6781
|
+
this.#resetHindsightConversationTrackingIfHindsight();
|
|
6739
6782
|
|
|
6740
6783
|
// Reload messages from entries (works for both file and in-memory mode)
|
|
6741
6784
|
const sessionContext = this.buildDisplaySessionContext();
|
|
@@ -598,6 +598,16 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
598
598
|
{ name: "reset", description: "Alias for clear" },
|
|
599
599
|
{ name: "enqueue", description: "Enqueue memory consolidation maintenance" },
|
|
600
600
|
{ name: "rebuild", description: "Alias for enqueue" },
|
|
601
|
+
{ name: "mm list", description: "List mental models on the active bank" },
|
|
602
|
+
{ name: "mm show", description: "Show one mental model (id required)" },
|
|
603
|
+
{
|
|
604
|
+
name: "mm refresh",
|
|
605
|
+
description: "Refresh auto-refresh models bank-wide, or one model by id",
|
|
606
|
+
},
|
|
607
|
+
{ name: "mm history", description: "Diff the change history of a mental model" },
|
|
608
|
+
{ name: "mm seed", description: "Create any built-in mental models that are missing" },
|
|
609
|
+
{ name: "mm delete", description: "Delete a mental model from the bank (id required)" },
|
|
610
|
+
{ name: "mm reload", description: "Re-pull the cached <mental_models> block" },
|
|
601
611
|
],
|
|
602
612
|
allowArgs: true,
|
|
603
613
|
handle: async (command, runtime) => {
|
package/src/task/executor.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
|
|
|
17
17
|
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
18
18
|
import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
|
|
19
19
|
import type { Skill } from "../extensibility/skills";
|
|
20
|
+
import type { HindsightSessionState } from "../hindsight/state";
|
|
20
21
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
21
22
|
import { callTool } from "../mcp/client";
|
|
22
23
|
import type { MCPManager } from "../mcp/manager";
|
|
@@ -163,6 +164,7 @@ export interface ExecutorOptions {
|
|
|
163
164
|
settings?: Settings;
|
|
164
165
|
/** Override local:// protocol options so subagent shares parent's local:// root */
|
|
165
166
|
localProtocolOptions?: LocalProtocolOptions;
|
|
167
|
+
parentHindsightSessionState?: HindsightSessionState;
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
function parseStringifiedJson(value: unknown): unknown {
|
|
@@ -979,6 +981,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
979
981
|
hasUI: false,
|
|
980
982
|
spawns: spawnsEnv,
|
|
981
983
|
taskDepth: childDepth,
|
|
984
|
+
parentHindsightSessionState: options.parentHindsightSessionState,
|
|
982
985
|
parentTaskPrefix: id,
|
|
983
986
|
agentId: id,
|
|
984
987
|
agentDisplayName: agent.name,
|
package/src/task/index.ts
CHANGED
|
@@ -864,6 +864,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
864
864
|
skills: availableSkills,
|
|
865
865
|
promptTemplates,
|
|
866
866
|
localProtocolOptions,
|
|
867
|
+
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
867
868
|
});
|
|
868
869
|
}
|
|
869
870
|
|
|
@@ -918,6 +919,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
918
919
|
skills: availableSkills,
|
|
919
920
|
promptTemplates,
|
|
920
921
|
localProtocolOptions,
|
|
922
|
+
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
921
923
|
});
|
|
922
924
|
if (mergeMode === "branch" && result.exitCode === 0) {
|
|
923
925
|
try {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { type Static, Type } from "@sinclair/typebox";
|
|
4
|
-
import { getHindsightSessionState } from "../hindsight/backend";
|
|
5
4
|
import { formatCurrentTime, formatMemories } from "../hindsight/content";
|
|
6
5
|
import recallDescription from "../prompts/tools/recall.md" with { type: "text" };
|
|
7
6
|
import type { ToolSession } from ".";
|
|
@@ -30,8 +29,7 @@ export class HindsightRecallTool implements AgentTool<typeof hindsightRecallSche
|
|
|
30
29
|
|
|
31
30
|
async execute(_id: string, params: HindsightRecallParams, signal?: AbortSignal): Promise<AgentToolResult> {
|
|
32
31
|
return untilAborted(signal, async () => {
|
|
33
|
-
const
|
|
34
|
-
const state = sessionId ? getHindsightSessionState(sessionId) : undefined;
|
|
32
|
+
const state = this.session.getHindsightSessionState?.();
|
|
35
33
|
if (!state) {
|
|
36
34
|
throw new Error("Hindsight backend is not initialised for this session.");
|
|
37
35
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { type Static, Type } from "@sinclair/typebox";
|
|
4
|
-
import { getHindsightSessionState } from "../hindsight/backend";
|
|
5
4
|
import { ensureBankMission } from "../hindsight/bank";
|
|
6
5
|
import reflectDescription from "../prompts/tools/reflect.md" with { type: "text" };
|
|
7
6
|
import type { ToolSession } from ".";
|
|
@@ -29,8 +28,7 @@ export class HindsightReflectTool implements AgentTool<typeof hindsightReflectSc
|
|
|
29
28
|
|
|
30
29
|
async execute(_id: string, params: HindsightReflectParams, signal?: AbortSignal): Promise<AgentToolResult> {
|
|
31
30
|
return untilAborted(signal, async () => {
|
|
32
|
-
const
|
|
33
|
-
const state = sessionId ? getHindsightSessionState(sessionId) : undefined;
|
|
31
|
+
const state = this.session.getHindsightSessionState?.();
|
|
34
32
|
if (!state) {
|
|
35
33
|
throw new Error("Hindsight backend is not initialised for this session.");
|
|
36
34
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { type Static, Type } from "@sinclair/typebox";
|
|
3
|
-
import { getHindsightSessionState } from "../hindsight/backend";
|
|
4
|
-
import { enqueueRetain } from "../hindsight/retain-queue";
|
|
5
3
|
import retainDescription from "../prompts/tools/retain.md" with { type: "text" };
|
|
6
4
|
import type { ToolSession } from ".";
|
|
7
5
|
|
|
@@ -39,18 +37,17 @@ export class HindsightRetainTool implements AgentTool<typeof hindsightRetainSche
|
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
async execute(_id: string, params: HindsightRetainParams): Promise<AgentToolResult> {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
if (!state || !sessionId) {
|
|
40
|
+
const state = this.session.getHindsightSessionState?.();
|
|
41
|
+
if (!state) {
|
|
45
42
|
throw new Error("Hindsight backend is not initialised for this session.");
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
// Push every item onto the
|
|
49
|
-
// queue flushes either when it reaches its batch threshold or when
|
|
50
|
-
// debounce timer fires. If the eventual batch fails, the queue
|
|
45
|
+
// Push every item onto the session-owned queue and return immediately.
|
|
46
|
+
// The queue flushes either when it reaches its batch threshold or when
|
|
47
|
+
// its debounce timer fires. If the eventual batch fails, the queue
|
|
51
48
|
// surfaces a UI-only warning notice — the LLM is not informed.
|
|
52
49
|
for (const item of params.items) {
|
|
53
|
-
enqueueRetain(
|
|
50
|
+
state.enqueueRetain(item.content, item.context);
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
const count = params.items.length;
|
package/src/tools/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { Settings } from "../config/settings";
|
|
|
7
7
|
import { EditTool } from "../edit";
|
|
8
8
|
import { checkPythonKernelAvailability } from "../eval/py/kernel";
|
|
9
9
|
import type { Skill } from "../extensibility/skills";
|
|
10
|
+
import type { HindsightSessionState } from "../hindsight/state";
|
|
10
11
|
import type { InternalUrlRouter } from "../internal-urls";
|
|
11
12
|
import { LspTool } from "../lsp";
|
|
12
13
|
import type { DiscoverableMCPSearchIndex, DiscoverableMCPTool } from "../mcp/discoverable-tool-metadata";
|
|
@@ -141,6 +142,8 @@ export interface ToolSession {
|
|
|
141
142
|
trackEvalExecution?<T>(execution: Promise<T>, abortController: AbortController): Promise<T>;
|
|
142
143
|
/** Get session ID */
|
|
143
144
|
getSessionId?: () => string | null;
|
|
145
|
+
/** Get Hindsight runtime state for this agent session. */
|
|
146
|
+
getHindsightSessionState?: () => HindsightSessionState | undefined;
|
|
144
147
|
/** Agent identity used for IRC routing. Returns the registry id (e.g. "0-Main", "0-AuthLoader"). */
|
|
145
148
|
getAgentId?: () => string | null;
|
|
146
149
|
/** Look up a registered tool by name (used by the eval js backend's tool bridge). */
|