@oh-my-pi/pi-coding-agent 14.6.2 → 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 +95 -2
- package/README.md +21 -0
- package/package.json +23 -7
- package/src/cli/grievances-cli.ts +89 -4
- package/src/commands/grievances.ts +33 -7
- package/src/config/prompt-templates.ts +14 -7
- package/src/config/settings-schema.ts +610 -100
- package/src/config/settings.ts +42 -0
- package/src/discovery/helpers.ts +13 -6
- package/src/edit/index.ts +3 -3
- package/src/edit/line-hash.ts +73 -25
- package/src/edit/modes/hashline.lark +10 -3
- package/src/edit/modes/hashline.ts +295 -40
- package/src/edit/renderer.ts +3 -3
- package/src/hindsight/backend.ts +205 -0
- package/src/hindsight/bank.ts +131 -0
- package/src/hindsight/client.ts +598 -0
- package/src/hindsight/config.ts +175 -0
- package/src/hindsight/content.ts +210 -0
- package/src/hindsight/index.ts +8 -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/hindsight/transcript.ts +71 -0
- package/src/main.ts +7 -10
- package/src/memories/index.ts +1 -1
- package/src/memory-backend/index.ts +4 -0
- package/src/memory-backend/local-backend.ts +30 -0
- package/src/memory-backend/off-backend.ts +16 -0
- package/src/memory-backend/resolve.ts +24 -0
- package/src/memory-backend/types.ts +79 -0
- package/src/modes/components/settings-defs.ts +50 -451
- package/src/modes/components/settings-selector.ts +2 -2
- package/src/modes/components/status-line/presets.ts +1 -1
- package/src/modes/controllers/command-controller.ts +266 -6
- package/src/modes/controllers/event-controller.ts +12 -0
- package/src/modes/controllers/selector-controller.ts +3 -12
- package/src/modes/theme/theme.ts +4 -0
- package/src/prompts/tools/github.md +3 -0
- package/src/prompts/tools/hashline.md +21 -16
- package/src/prompts/tools/read.md +10 -6
- package/src/prompts/tools/recall.md +5 -0
- package/src/prompts/tools/reflect.md +5 -0
- package/src/prompts/tools/retain.md +5 -0
- package/src/prompts/tools/search.md +1 -1
- package/src/sdk.ts +21 -9
- package/src/session/agent-session.ts +118 -3
- package/src/slash-commands/builtin-registry.ts +12 -12
- package/src/task/executor.ts +3 -0
- package/src/task/index.ts +2 -0
- package/src/tools/ast-edit.ts +14 -5
- package/src/tools/ast-grep.ts +12 -3
- package/src/tools/find.ts +47 -7
- package/src/tools/gh-renderer.ts +10 -1
- package/src/tools/gh.ts +233 -5
- package/src/tools/hindsight-recall.ts +68 -0
- package/src/tools/hindsight-reflect.ts +55 -0
- package/src/tools/hindsight-retain.ts +60 -0
- package/src/tools/index.ts +20 -0
- package/src/tools/path-utils.ts +55 -0
- package/src/tools/read.ts +1 -1
- package/src/tools/search.ts +45 -8
|
@@ -17,7 +17,16 @@ 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 {
|
|
20
|
+
import {
|
|
21
|
+
diffMentalModelContent,
|
|
22
|
+
type HindsightApi,
|
|
23
|
+
type HindsightSessionState,
|
|
24
|
+
loadHindsightConfig,
|
|
25
|
+
reloadMentalModelsForSession,
|
|
26
|
+
resolveSeedsForScope,
|
|
27
|
+
summarizeMentalModel,
|
|
28
|
+
} from "../../hindsight";
|
|
29
|
+
import { resolveMemoryBackend } from "../../memory-backend";
|
|
21
30
|
import { BashExecutionComponent } from "../../modes/components/bash-execution";
|
|
22
31
|
import { BorderedLoader } from "../../modes/components/bordered-loader";
|
|
23
32
|
import { DynamicBorder } from "../../modes/components/dynamic-border";
|
|
@@ -570,11 +579,12 @@ export class CommandController {
|
|
|
570
579
|
const argumentText = text.slice(7).trim();
|
|
571
580
|
const action = argumentText.split(/\s+/, 1)[0]?.toLowerCase() || "view";
|
|
572
581
|
const agentDir = this.ctx.settings.getAgentDir();
|
|
582
|
+
const backend = resolveMemoryBackend(this.ctx.settings);
|
|
573
583
|
|
|
574
584
|
if (action === "view") {
|
|
575
|
-
const payload = await
|
|
585
|
+
const payload = await backend.buildDeveloperInstructions(agentDir, this.ctx.settings, this.ctx.session);
|
|
576
586
|
if (!payload) {
|
|
577
|
-
this.ctx.showWarning("Memory payload is empty (
|
|
587
|
+
this.ctx.showWarning("Memory payload is empty (memory backend off, disabled, or no memory available).");
|
|
578
588
|
return;
|
|
579
589
|
}
|
|
580
590
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
@@ -589,7 +599,7 @@ export class CommandController {
|
|
|
589
599
|
|
|
590
600
|
if (action === "reset" || action === "clear") {
|
|
591
601
|
try {
|
|
592
|
-
await
|
|
602
|
+
await backend.clear(agentDir, this.ctx.sessionManager.getCwd(), this.ctx.session);
|
|
593
603
|
await this.ctx.session.refreshBaseSystemPrompt();
|
|
594
604
|
this.ctx.showStatus("Memory data cleared and system prompt refreshed.");
|
|
595
605
|
} catch (error) {
|
|
@@ -600,7 +610,7 @@ export class CommandController {
|
|
|
600
610
|
|
|
601
611
|
if (action === "enqueue" || action === "rebuild") {
|
|
602
612
|
try {
|
|
603
|
-
|
|
613
|
+
await backend.enqueue(agentDir, this.ctx.sessionManager.getCwd(), this.ctx.session);
|
|
604
614
|
this.ctx.showStatus("Memory consolidation enqueued.");
|
|
605
615
|
} catch (error) {
|
|
606
616
|
this.ctx.showError(`Memory enqueue failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -608,7 +618,257 @@ export class CommandController {
|
|
|
608
618
|
return;
|
|
609
619
|
}
|
|
610
620
|
|
|
611
|
-
|
|
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
|
+
}
|
|
612
872
|
}
|
|
613
873
|
|
|
614
874
|
async #runNewSessionFlow(options?: NewSessionOptions, label: string = "New session started"): Promise<void> {
|
|
@@ -53,6 +53,7 @@ export class EventController {
|
|
|
53
53
|
todo_reminder: e => this.#handleTodoReminder(e),
|
|
54
54
|
todo_auto_clear: e => this.#handleTodoAutoClear(e),
|
|
55
55
|
irc_message: e => this.#handleIrcMessage(e),
|
|
56
|
+
notice: e => this.#handleNotice(e),
|
|
56
57
|
} satisfies AgentSessionEventHandlers;
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -223,6 +224,17 @@ export class EventController {
|
|
|
223
224
|
this.ctx.ui.requestRender();
|
|
224
225
|
}
|
|
225
226
|
|
|
227
|
+
async #handleNotice(event: Extract<AgentSessionEvent, { type: "notice" }>): Promise<void> {
|
|
228
|
+
const message = event.source ? `${event.source}: ${event.message}` : event.message;
|
|
229
|
+
if (event.level === "error") {
|
|
230
|
+
this.ctx.showError(message);
|
|
231
|
+
} else if (event.level === "warning") {
|
|
232
|
+
this.ctx.showWarning(message);
|
|
233
|
+
} else {
|
|
234
|
+
this.ctx.showStatus(message);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
226
238
|
async #handleMessageUpdate(event: Extract<AgentSessionEvent, { type: "message_update" }>): Promise<void> {
|
|
227
239
|
if (this.ctx.streamingComponent && event.message.role === "assistant") {
|
|
228
240
|
this.ctx.streamingMessage = event.message;
|
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import * as os from "node:os";
|
|
2
|
-
import * as path from "node:path";
|
|
3
1
|
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
4
2
|
import { getOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
|
|
5
3
|
import type { OAuthProvider } from "@oh-my-pi/pi-ai/utils/oauth/types";
|
|
6
4
|
import type { Component, OverlayHandle } from "@oh-my-pi/pi-tui";
|
|
7
5
|
import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
8
|
-
import { getAgentDbPath,
|
|
9
|
-
import { invalidate as invalidateFsCache } from "../../capability/fs";
|
|
6
|
+
import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
10
7
|
import { getRoleInfo } from "../../config/model-registry";
|
|
11
8
|
import { formatModelSelectorValue } from "../../config/model-resolver";
|
|
12
9
|
import { settings } from "../../config/settings";
|
|
13
10
|
import { DebugSelectorComponent } from "../../debug";
|
|
14
11
|
import { disableProvider, enableProvider } from "../../discovery";
|
|
15
|
-
import {
|
|
12
|
+
import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
|
|
16
13
|
import {
|
|
17
14
|
getInstalledPluginsRegistryPath,
|
|
18
15
|
getMarketplacesCacheDir,
|
|
@@ -451,13 +448,7 @@ export class SelectorController {
|
|
|
451
448
|
projectInstalledRegistryPath: (await resolveActiveProjectRegistryPath(getProjectDir())) ?? undefined,
|
|
452
449
|
marketplacesCacheDir: getMarketplacesCacheDir(),
|
|
453
450
|
pluginsCacheDir: getPluginsCacheDir(),
|
|
454
|
-
clearPluginRootsCache:
|
|
455
|
-
const home = os.homedir();
|
|
456
|
-
invalidateFsCache(path.join(home, ".claude", "plugins", "installed_plugins.json"));
|
|
457
|
-
invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
|
|
458
|
-
for (const p of extraPaths ?? []) invalidateFsCache(p);
|
|
459
|
-
clearClaudePluginRootsCache();
|
|
460
|
-
},
|
|
451
|
+
clearPluginRootsCache: clearPluginRootsAndCaches,
|
|
461
452
|
});
|
|
462
453
|
|
|
463
454
|
const [marketplaces, installed] = await Promise.all([mgr.listMarketplaces(), mgr.listInstalledPlugins()]);
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -186,6 +186,7 @@ export type SymbolKey =
|
|
|
186
186
|
| "tab.context"
|
|
187
187
|
| "tab.editing"
|
|
188
188
|
| "tab.tools"
|
|
189
|
+
| "tab.memory"
|
|
189
190
|
| "tab.tasks"
|
|
190
191
|
| "tab.providers";
|
|
191
192
|
|
|
@@ -346,6 +347,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
346
347
|
"tab.context": "📋",
|
|
347
348
|
"tab.editing": "💻",
|
|
348
349
|
"tab.tools": "🔧",
|
|
350
|
+
"tab.memory": "🧠",
|
|
349
351
|
"tab.tasks": "📦",
|
|
350
352
|
"tab.providers": "🌐",
|
|
351
353
|
};
|
|
@@ -599,6 +601,7 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
599
601
|
"tab.context": "",
|
|
600
602
|
"tab.editing": "",
|
|
601
603
|
"tab.tools": "",
|
|
604
|
+
"tab.memory": "",
|
|
602
605
|
"tab.tasks": "",
|
|
603
606
|
"tab.providers": "",
|
|
604
607
|
};
|
|
@@ -757,6 +760,7 @@ const ASCII_SYMBOLS: SymbolMap = {
|
|
|
757
760
|
"tab.context": "[X]",
|
|
758
761
|
"tab.editing": "[E]",
|
|
759
762
|
"tab.tools": "[T]",
|
|
763
|
+
"tab.memory": "[Y]",
|
|
760
764
|
"tab.tasks": "[K]",
|
|
761
765
|
"tab.providers": "[P]",
|
|
762
766
|
};
|
|
@@ -10,6 +10,9 @@ Pick the operation via `op`. Each op uses a subset of the parameters:
|
|
|
10
10
|
- `pr_push` — Push a checked-out PR branch back to its source branch. Requires the branch to have been checked out via `op: pr_checkout` (carries push metadata). Optional `branch`; defaults to the current checked-out git branch. Optional `forceWithLease`.
|
|
11
11
|
- `search_issues` — Search issues using normal GitHub issue search syntax. Required `query`. Optional `repo`, `limit`.
|
|
12
12
|
- `search_prs` — Search pull requests using normal GitHub PR search syntax. Required `query`. Optional `repo`, `limit`.
|
|
13
|
+
- `search_code` — Search code with GitHub code search syntax. Required `query`. Optional `repo`, `limit`. Returns matching paths with surrounding fragments.
|
|
14
|
+
- `search_commits` — Search commits across GitHub. Required `query`. Optional `repo`, `limit`. Returns short SHA, author, and the first line of each commit message.
|
|
15
|
+
- `search_repos` — Search repositories across GitHub. Required `query`. Optional `limit` (use query qualifiers like `org:`, `language:` instead of `repo`).
|
|
13
16
|
- `run_watch` — Watch a GitHub Actions workflow run. Optional `run` (id or URL). Omitting `run` watches all workflow runs for the current HEAD commit; `branch` falls back to the current branch. Optional `tail` (log lines per failed job). Streams snapshots, fast-fails on the first detected job failure (with a brief grace period to capture concurrent failures), then fetches tailed logs for the failed jobs. The full failed-job logs are saved as a session artifact for on-demand reads.
|
|
14
17
|
</instruction>
|
|
15
18
|
|
|
@@ -8,15 +8,15 @@ This format is purely textual. The tool has NO awareness of language, indentatio
|
|
|
8
8
|
|
|
9
9
|
<ops>
|
|
10
10
|
@PATH header: subsequent ops apply to PATH
|
|
11
|
-
< ANCHOR insert lines BEFORE the anchored line (or BOF); payload follows as
|
|
12
|
-
+ ANCHOR insert lines AFTER the anchored line (or EOF); payload follows as
|
|
11
|
+
< ANCHOR insert lines BEFORE the anchored line (or BOF); payload follows as `{{hsep}}TEXT` lines
|
|
12
|
+
+ ANCHOR insert lines AFTER the anchored line (or EOF); payload follows as `{{hsep}}TEXT` lines
|
|
13
13
|
- A..B delete the line range (inclusive); `- A` for one line
|
|
14
|
-
= A..B replace the range with payload
|
|
14
|
+
= A..B replace the range with payload `{{hsep}}TEXT` lines, or with one blank line if no payload follows
|
|
15
15
|
</ops>
|
|
16
16
|
|
|
17
17
|
<rules>
|
|
18
|
-
- Every line of inserted/replacement content **MUST** be emitted as a payload line starting with
|
|
19
|
-
-
|
|
18
|
+
- Every line of inserted/replacement content **MUST** be emitted as a payload line starting with `{{hsep}}`.
|
|
19
|
+
- `{{hsep}}` is syntax, not content. The inserted text begins after the first `{{hsep}}`; use a bare `{{hsep}}` to insert a blank line.
|
|
20
20
|
- `< A` inserts before line A; `+ A` inserts after line A. `< BOF` / `+ BOF` both prepend; `< EOF` / `+ EOF` both append.
|
|
21
21
|
- `= A..B` replaces the inclusive range with the following payload lines. `= A` (or `= A..B`) with no payload blanks the range to a single empty line.
|
|
22
22
|
- `- A..B` deletes the inclusive range; omit `..B` for one line.
|
|
@@ -35,30 +35,34 @@ This format is purely textual. The tool has NO awareness of language, indentatio
|
|
|
35
35
|
# Replace one line (preserve the leading tab from the original)
|
|
36
36
|
@a.ts
|
|
37
37
|
= {{hrefr 5}}
|
|
38
|
-
|
|
38
|
+
{{hsep}} return clean.trim().toUpperCase();
|
|
39
39
|
|
|
40
40
|
# Replace a contiguous range with multiple lines
|
|
41
41
|
@a.ts
|
|
42
42
|
= {{hrefr 3}}..{{hrefr 6}}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
{{hsep}}export function label(name: string): string {
|
|
44
|
+
{{hsep}} const clean = (name || DEF).trim();
|
|
45
|
+
{{hsep}} return clean.length === 0 ? DEF : clean.toUpperCase();
|
|
46
|
+
{{hsep}}}
|
|
47
47
|
|
|
48
48
|
# Insert BEFORE a line
|
|
49
49
|
@a.ts
|
|
50
50
|
< {{hrefr 5}}
|
|
51
|
-
|
|
51
|
+
{{hsep}} const debug = false;
|
|
52
52
|
|
|
53
53
|
# Insert AFTER a line
|
|
54
54
|
@a.ts
|
|
55
55
|
+ {{hrefr 4}}
|
|
56
|
-
|
|
56
|
+
{{hsep}} if (clean.length === 0) return DEF;
|
|
57
|
+
|
|
58
|
+
# Append WITHIN a line
|
|
59
|
+
@a.ts
|
|
60
|
+
+ {{hrefr 4}}{{hsep}} // first run
|
|
57
61
|
|
|
58
62
|
# Append to end of file
|
|
59
63
|
@a.ts
|
|
60
64
|
+ EOF
|
|
61
|
-
|
|
65
|
+
{{hsep}}export const done = true;
|
|
62
66
|
|
|
63
67
|
# Delete a single line
|
|
64
68
|
@a.ts
|
|
@@ -70,9 +74,10 @@ This format is purely textual. The tool has NO awareness of language, indentatio
|
|
|
70
74
|
</examples>
|
|
71
75
|
|
|
72
76
|
<critical>
|
|
73
|
-
- Always copy anchors exactly from tool output, but **NEVER** include line content after the
|
|
77
|
+
- Always copy anchors exactly from tool output, but **NEVER** include line content after the `{{hsep}}` separator in the op line.
|
|
74
78
|
- Only emit changed lines. Do not restate unchanged context as payload.
|
|
75
|
-
- Every inserted/replacement content line **MUST** start with
|
|
79
|
+
- Every inserted/replacement content line **MUST** start with `{{hsep}}`; raw content lines are invalid.
|
|
76
80
|
- Do not write unified diff syntax (`@@`, `-OLD`, `+NEW`).
|
|
77
|
-
- To replace a block, use one `= A..B` op followed by all replacement
|
|
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.
|
|
78
83
|
</critical>
|
|
@@ -14,7 +14,7 @@ The `read` tool is multi-purpose and more capable than it looks — inspects fil
|
|
|
14
14
|
|
|
15
15
|
|`sel` value|Behavior|
|
|
16
16
|
|---|---|
|
|
17
|
-
|
|
17
|
+
|_(omitted)_|Read full file (up to {{DEFAULT_LIMIT}} lines)|
|
|
18
18
|
|`50`|Read from line 50 onward|
|
|
19
19
|
|`50-200`|Read lines 50-200|
|
|
20
20
|
|`50+150`|Read 150 lines starting at line 50|
|
|
@@ -22,21 +22,24 @@ The `read` tool is multi-purpose and more capable than it looks — inspects fil
|
|
|
22
22
|
|
|
23
23
|
# Filesystem
|
|
24
24
|
- Reading a directory path returns a list of dirents.
|
|
25
|
-
{{#if
|
|
25
|
+
{{#if IS_HL_MODE}}
|
|
26
26
|
- Reading a file returns lines prefixed with anchors (line+hash): `41th|def alpha():`
|
|
27
|
-
{{else}}
|
|
28
|
-
{{#if IS_LINE_NUMBER_MODE}}
|
|
27
|
+
{{else}}
|
|
28
|
+
{{#if IS_LINE_NUMBER_MODE}}
|
|
29
29
|
- Reading a file returns lines prefixed with line numbers: `41|def alpha():`
|
|
30
|
-
{{/if}}
|
|
31
|
-
{{/if}}
|
|
30
|
+
{{/if}}
|
|
31
|
+
{{/if}}
|
|
32
32
|
|
|
33
33
|
# Inspection
|
|
34
|
+
|
|
34
35
|
Extracts text from PDF, Word, PowerPoint, Excel, RTF, EPUB, and Jupyter notebook files. Can inspect images.
|
|
35
36
|
|
|
36
37
|
# Directories & Archives
|
|
38
|
+
|
|
37
39
|
Directories and archive roots return a list of entries. Supports `.tar`, `.tar.gz`, `.tgz`, `.zip`. Use `archive.ext:path/inside/archive` to read contents.
|
|
38
40
|
|
|
39
41
|
# SQLite Databases
|
|
42
|
+
|
|
40
43
|
For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
|
|
41
44
|
- `file.db` — list tables with row counts
|
|
42
45
|
- `file.db:table` — schema + sample rows
|
|
@@ -46,6 +49,7 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
|
|
|
46
49
|
- `file.db?q=SELECT …` — read-only SELECT query
|
|
47
50
|
|
|
48
51
|
# URLs
|
|
52
|
+
|
|
49
53
|
Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, PDFs at URLs, and similar text-based resources. Returns clean reader-mode text/markdown — no browser required. Use `sel="raw"` for untouched HTML; `timeout` to override the default request timeout.
|
|
50
54
|
</instruction>
|
|
51
55
|
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Search long-term memory for relevant information. Returns raw matching entries ranked by relevance.
|
|
2
|
+
|
|
3
|
+
Use proactively — before answering questions about past conversations, user preferences, project decisions, or any topic where prior context would help accuracy. When in doubt, recall first.
|
|
4
|
+
|
|
5
|
+
Prefer `recall` when you need specific facts or entries. Use `reflect` instead when you need a synthesised answer across many memories.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Generate a synthesised answer by reasoning over long-term memory. Unlike `recall` (which returns raw entries), `reflect` blends relevant memories into a single coherent response.
|
|
2
|
+
|
|
3
|
+
Use for open-ended questions that span many stored facts: "What do you know about this user?", "Summarize project decisions.", "What are my preferences for X?"
|
|
4
|
+
|
|
5
|
+
Provide an optional `context` to focus the synthesis on a specific angle or sub-topic.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Store one or more facts in long-term memory for future sessions.
|
|
2
|
+
|
|
3
|
+
Use for durable, reusable knowledge: user preferences, project decisions, architectural choices, and anything that would improve future responses if recalled. Ephemeral task state does not belong here.
|
|
4
|
+
|
|
5
|
+
Each item must be specific and self-contained — include who, what, when, and why. Batch related facts in a single call; they are deduplicated and consolidated together.
|
|
@@ -7,7 +7,7 @@ Searches files using powerful regex matching.
|
|
|
7
7
|
</instruction>
|
|
8
8
|
|
|
9
9
|
<output>
|
|
10
|
-
{{#if
|
|
10
|
+
{{#if IS_HL_MODE}}
|
|
11
11
|
- Text output is anchor-prefixed: `*5th|content` (match) or ` 9x}|content` (context, leading space). The 2-char suffix is a content fingerprint.
|
|
12
12
|
{{else}}
|
|
13
13
|
{{#if IS_LINE_NUMBER_MODE}}
|
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,
|
|
@@ -82,7 +83,8 @@ import {
|
|
|
82
83
|
selectDiscoverableMCPToolNamesByServer,
|
|
83
84
|
summarizeDiscoverableMCPTools,
|
|
84
85
|
} from "./mcp/discoverable-tool-metadata";
|
|
85
|
-
import {
|
|
86
|
+
import { getMemoryRoot } from "./memories";
|
|
87
|
+
import { resolveMemoryBackend } from "./memory-backend";
|
|
86
88
|
import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
|
|
87
89
|
import { AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
|
|
88
90
|
import {
|
|
@@ -215,6 +217,8 @@ export interface CreateAgentSessionOptions {
|
|
|
215
217
|
requireYieldTool?: boolean;
|
|
216
218
|
/** Task recursion depth (for subagent sessions). Default: 0 */
|
|
217
219
|
taskDepth?: number;
|
|
220
|
+
/** Parent Hindsight state to alias for subagent memory tools. */
|
|
221
|
+
parentHindsightSessionState?: HindsightSessionState;
|
|
218
222
|
/** Pre-allocated agent identity for IRC routing. Default: "0-Main" for top-level, parentTaskPrefix-derived for sub. */
|
|
219
223
|
agentId?: string;
|
|
220
224
|
/** Display name for the agent in IRC. Default: "main" or "sub". */
|
|
@@ -967,6 +971,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
967
971
|
trackEvalExecution: (execution, abortController) =>
|
|
968
972
|
session ? session.trackEvalExecution(execution, abortController) : execution,
|
|
969
973
|
getSessionId: () => sessionManager.getSessionId?.() ?? null,
|
|
974
|
+
getHindsightSessionState: () => session?.getHindsightSessionState(),
|
|
970
975
|
getAgentId: () => resolvedAgentId,
|
|
971
976
|
getToolByName: name => session?.getToolByName(name),
|
|
972
977
|
agentRegistry,
|
|
@@ -1334,7 +1339,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1334
1339
|
const promptTools = buildSystemPromptToolMetadata(tools, {
|
|
1335
1340
|
search_tool_bm25: { description: renderSearchToolBm25Description(discoverableMCPTools) },
|
|
1336
1341
|
});
|
|
1337
|
-
const memoryInstructions = await
|
|
1342
|
+
const memoryInstructions = await resolveMemoryBackend(settings).buildDeveloperInstructions(
|
|
1343
|
+
agentDir,
|
|
1344
|
+
settings,
|
|
1345
|
+
session,
|
|
1346
|
+
);
|
|
1338
1347
|
|
|
1339
1348
|
// Build combined append prompt: memory instructions + MCP server instructions
|
|
1340
1349
|
const serverInstructions = mcpManager?.getServerInstructions();
|
|
@@ -1747,13 +1756,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1747
1756
|
}
|
|
1748
1757
|
|
|
1749
1758
|
logger.time("startMemoryStartupTask", () =>
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1759
|
+
Promise.resolve(
|
|
1760
|
+
resolveMemoryBackend(settings).start({
|
|
1761
|
+
session,
|
|
1762
|
+
settings,
|
|
1763
|
+
modelRegistry,
|
|
1764
|
+
agentDir,
|
|
1765
|
+
taskDepth,
|
|
1766
|
+
parentHindsightSessionState: options.parentHindsightSessionState,
|
|
1767
|
+
}),
|
|
1768
|
+
),
|
|
1757
1769
|
);
|
|
1758
1770
|
|
|
1759
1771
|
// Wire MCP manager callbacks to session for reactive tool updates.
|