@oh-my-pi/pi-coding-agent 14.6.2 → 14.6.3
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 +71 -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 +585 -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 +104 -38
- package/src/edit/renderer.ts +3 -3
- package/src/hindsight/backend.ts +444 -0
- package/src/hindsight/bank.ts +131 -0
- package/src/hindsight/client.ts +445 -0
- package/src/hindsight/config.ts +165 -0
- package/src/hindsight/content.ts +205 -0
- package/src/hindsight/index.ts +6 -0
- package/src/hindsight/retain-queue.ts +166 -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 +69 -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 +6 -5
- 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 +20 -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 +12 -9
- package/src/session/agent-session.ts +75 -3
- package/src/slash-commands/builtin-registry.ts +2 -12
- 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 +70 -0
- package/src/tools/hindsight-reflect.ts +57 -0
- package/src/tools/hindsight-retain.ts +63 -0
- package/src/tools/index.ts +17 -0
- package/src/tools/path-utils.ts +55 -0
- package/src/tools/read.ts +1 -1
- package/src/tools/search.ts +45 -8
package/src/tools/gh-renderer.ts
CHANGED
|
@@ -47,6 +47,9 @@ const OP_TITLES: Record<string, string> = {
|
|
|
47
47
|
pr_push: "GitHub PR Push",
|
|
48
48
|
search_issues: "GitHub Search Issues",
|
|
49
49
|
search_prs: "GitHub Search PRs",
|
|
50
|
+
search_code: "GitHub Search Code",
|
|
51
|
+
search_commits: "GitHub Search Commits",
|
|
52
|
+
search_repos: "GitHub Search Repos",
|
|
50
53
|
run_watch: "GitHub Run Watch",
|
|
51
54
|
};
|
|
52
55
|
|
|
@@ -99,11 +102,17 @@ function buildOpMeta(args: GithubToolRenderArgs): string[] {
|
|
|
99
102
|
break;
|
|
100
103
|
}
|
|
101
104
|
case "search_issues":
|
|
102
|
-
case "search_prs":
|
|
105
|
+
case "search_prs":
|
|
106
|
+
case "search_code":
|
|
107
|
+
case "search_commits": {
|
|
103
108
|
if (args.query) meta.push(truncateVisualWidth(args.query, TRUNCATE_LENGTHS.CONTENT));
|
|
104
109
|
if (args.repo) meta.push(args.repo);
|
|
105
110
|
break;
|
|
106
111
|
}
|
|
112
|
+
case "search_repos": {
|
|
113
|
+
if (args.query) meta.push(truncateVisualWidth(args.query, TRUNCATE_LENGTHS.CONTENT));
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
107
116
|
case "repo_view": {
|
|
108
117
|
if (args.repo) meta.push(args.repo);
|
|
109
118
|
if (args.branch) meta.push(args.branch);
|
package/src/tools/gh.ts
CHANGED
|
@@ -113,6 +113,24 @@ const GH_SEARCH_FIELDS = [
|
|
|
113
113
|
"updatedAt",
|
|
114
114
|
"url",
|
|
115
115
|
];
|
|
116
|
+
const GH_SEARCH_CODE_FIELDS = ["path", "repository", "sha", "textMatches", "url"];
|
|
117
|
+
const GH_SEARCH_COMMITS_FIELDS = ["author", "commit", "committer", "id", "repository", "sha", "url"];
|
|
118
|
+
const GH_SEARCH_REPOS_FIELDS = [
|
|
119
|
+
"createdAt",
|
|
120
|
+
"description",
|
|
121
|
+
"forksCount",
|
|
122
|
+
"fullName",
|
|
123
|
+
"isArchived",
|
|
124
|
+
"isFork",
|
|
125
|
+
"isPrivate",
|
|
126
|
+
"language",
|
|
127
|
+
"openIssuesCount",
|
|
128
|
+
"owner",
|
|
129
|
+
"stargazersCount",
|
|
130
|
+
"updatedAt",
|
|
131
|
+
"url",
|
|
132
|
+
"visibility",
|
|
133
|
+
];
|
|
116
134
|
const SEARCH_LIMIT_DEFAULT = 10;
|
|
117
135
|
const SEARCH_LIMIT_MAX = 50;
|
|
118
136
|
const FILE_PREVIEW_LIMIT = 50;
|
|
@@ -139,6 +157,9 @@ const githubSchema = Type.Object({
|
|
|
139
157
|
"pr_push",
|
|
140
158
|
"search_issues",
|
|
141
159
|
"search_prs",
|
|
160
|
+
"search_code",
|
|
161
|
+
"search_commits",
|
|
162
|
+
"search_repos",
|
|
142
163
|
"run_watch",
|
|
143
164
|
],
|
|
144
165
|
{ description: "github operation" },
|
|
@@ -186,11 +207,16 @@ const githubSchema = Type.Object({
|
|
|
186
207
|
forceWithLease: Type.Optional(Type.Boolean({ description: "force-with-lease push (pr_push)" })),
|
|
187
208
|
query: Type.Optional(
|
|
188
209
|
Type.String({
|
|
189
|
-
description: "search query (search_issues, search_prs)",
|
|
210
|
+
description: "search query (search_issues, search_prs, search_code, search_commits, search_repos)",
|
|
190
211
|
examples: ["is:open label:bug"],
|
|
191
212
|
}),
|
|
192
213
|
),
|
|
193
|
-
limit: Type.Optional(
|
|
214
|
+
limit: Type.Optional(
|
|
215
|
+
Type.Number({
|
|
216
|
+
description: "max results (search_issues, search_prs, search_code, search_commits, search_repos)",
|
|
217
|
+
default: 10,
|
|
218
|
+
}),
|
|
219
|
+
),
|
|
194
220
|
run: Type.Optional(Type.String({ description: "actions run id or url (run_watch)", examples: ["123456"] })),
|
|
195
221
|
tail: Type.Optional(Type.Number({ description: "log lines per failed job (run_watch)", default: 15 })),
|
|
196
222
|
});
|
|
@@ -414,6 +440,58 @@ interface GhSearchResult {
|
|
|
414
440
|
url?: string;
|
|
415
441
|
}
|
|
416
442
|
|
|
443
|
+
interface GhSearchCodeTextMatch {
|
|
444
|
+
fragment?: string;
|
|
445
|
+
property?: string;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
interface GhSearchCodeResult {
|
|
449
|
+
path?: string;
|
|
450
|
+
repository?: GhSearchRepository | null;
|
|
451
|
+
sha?: string;
|
|
452
|
+
textMatches?: GhSearchCodeTextMatch[];
|
|
453
|
+
url?: string;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
interface GhSearchCommitGitActor {
|
|
457
|
+
name?: string;
|
|
458
|
+
email?: string;
|
|
459
|
+
date?: string;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
interface GhSearchCommitDetail {
|
|
463
|
+
author?: GhSearchCommitGitActor | null;
|
|
464
|
+
committer?: GhSearchCommitGitActor | null;
|
|
465
|
+
message?: string;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
interface GhSearchCommitResult {
|
|
469
|
+
author?: GhUser | null;
|
|
470
|
+
commit?: GhSearchCommitDetail | null;
|
|
471
|
+
committer?: GhUser | null;
|
|
472
|
+
id?: string;
|
|
473
|
+
repository?: GhSearchRepository | null;
|
|
474
|
+
sha?: string;
|
|
475
|
+
url?: string;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
interface GhSearchRepoResult {
|
|
479
|
+
createdAt?: string;
|
|
480
|
+
description?: string | null;
|
|
481
|
+
forksCount?: number;
|
|
482
|
+
fullName?: string;
|
|
483
|
+
isArchived?: boolean;
|
|
484
|
+
isFork?: boolean;
|
|
485
|
+
isPrivate?: boolean;
|
|
486
|
+
language?: string | null;
|
|
487
|
+
openIssuesCount?: number;
|
|
488
|
+
owner?: GhUser | null;
|
|
489
|
+
stargazersCount?: number;
|
|
490
|
+
updatedAt?: string;
|
|
491
|
+
url?: string;
|
|
492
|
+
visibility?: string | null;
|
|
493
|
+
}
|
|
494
|
+
|
|
417
495
|
interface GhRunReference {
|
|
418
496
|
repo?: string;
|
|
419
497
|
runId?: number;
|
|
@@ -551,14 +629,25 @@ function appendRepoFlag(args: string[], repo: string | undefined, identifier?: s
|
|
|
551
629
|
args.push("--repo", repo);
|
|
552
630
|
}
|
|
553
631
|
|
|
632
|
+
const SEARCH_FIELDS_BY_COMMAND: Record<"issues" | "prs" | "code" | "commits" | "repos", readonly string[]> = {
|
|
633
|
+
issues: GH_SEARCH_FIELDS,
|
|
634
|
+
prs: GH_SEARCH_FIELDS,
|
|
635
|
+
code: GH_SEARCH_CODE_FIELDS,
|
|
636
|
+
commits: GH_SEARCH_COMMITS_FIELDS,
|
|
637
|
+
repos: GH_SEARCH_REPOS_FIELDS,
|
|
638
|
+
};
|
|
639
|
+
|
|
554
640
|
function buildGhSearchArgs(
|
|
555
|
-
command: "issues" | "prs",
|
|
641
|
+
command: "issues" | "prs" | "code" | "commits" | "repos",
|
|
556
642
|
query: string,
|
|
557
643
|
limit: number,
|
|
558
644
|
repo: string | undefined,
|
|
559
645
|
): string[] {
|
|
560
|
-
const
|
|
561
|
-
|
|
646
|
+
const fields = SEARCH_FIELDS_BY_COMMAND[command];
|
|
647
|
+
const args = ["search", command, "--limit", String(limit), "--json", fields.join(",")];
|
|
648
|
+
if (command !== "repos") {
|
|
649
|
+
appendRepoFlag(args, repo);
|
|
650
|
+
}
|
|
562
651
|
args.push("--", query);
|
|
563
652
|
return args;
|
|
564
653
|
}
|
|
@@ -1826,6 +1915,94 @@ function formatSearchResults(
|
|
|
1826
1915
|
return lines.join("\n").trim();
|
|
1827
1916
|
}
|
|
1828
1917
|
|
|
1918
|
+
function formatSearchCodeResults(query: string, repo: string | undefined, items: GhSearchCodeResult[]): string {
|
|
1919
|
+
const lines: string[] = [`# GitHub code search`, "", `Query: ${query}`];
|
|
1920
|
+
pushLine(lines, "Repository", repo);
|
|
1921
|
+
pushLine(lines, "Results", items.length);
|
|
1922
|
+
|
|
1923
|
+
if (items.length === 0) {
|
|
1924
|
+
lines.push("");
|
|
1925
|
+
lines.push("No code matches found.");
|
|
1926
|
+
return lines.join("\n").trim();
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
for (const item of items) {
|
|
1930
|
+
lines.push("");
|
|
1931
|
+
lines.push(`- ${item.path ?? "(unknown path)"}`);
|
|
1932
|
+
pushLine(lines, " Repo", item.repository?.nameWithOwner);
|
|
1933
|
+
pushLine(lines, " Commit", formatShortSha(item.sha));
|
|
1934
|
+
pushLine(lines, " URL", item.url);
|
|
1935
|
+
const fragment = item.textMatches?.find(match => match.fragment)?.fragment;
|
|
1936
|
+
if (fragment) {
|
|
1937
|
+
pushLine(lines, " Match", normalizeText(fragment).split("\n", 1)[0]);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
return lines.join("\n").trim();
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
function formatSearchCommitMessage(message: string | undefined): string | undefined {
|
|
1945
|
+
if (!message) return undefined;
|
|
1946
|
+
const firstLine = normalizeText(message).split("\n", 1)[0];
|
|
1947
|
+
return firstLine || undefined;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
function formatSearchCommitsResults(query: string, repo: string | undefined, items: GhSearchCommitResult[]): string {
|
|
1951
|
+
const lines: string[] = [`# GitHub commits search`, "", `Query: ${query}`];
|
|
1952
|
+
pushLine(lines, "Repository", repo);
|
|
1953
|
+
pushLine(lines, "Results", items.length);
|
|
1954
|
+
|
|
1955
|
+
if (items.length === 0) {
|
|
1956
|
+
lines.push("");
|
|
1957
|
+
lines.push("No commits found.");
|
|
1958
|
+
return lines.join("\n").trim();
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
for (const item of items) {
|
|
1962
|
+
lines.push("");
|
|
1963
|
+
const sha = formatShortSha(item.sha) ?? "(unknown sha)";
|
|
1964
|
+
const subject = formatSearchCommitMessage(item.commit?.message) ?? "(no commit message)";
|
|
1965
|
+
lines.push(`- ${sha} ${subject}`);
|
|
1966
|
+
pushLine(lines, " Repo", item.repository?.nameWithOwner);
|
|
1967
|
+
pushLine(lines, " Author", formatAuthor(item.author) ?? item.commit?.author?.name);
|
|
1968
|
+
pushLine(lines, " Date", item.commit?.author?.date ?? item.commit?.committer?.date);
|
|
1969
|
+
pushLine(lines, " URL", item.url);
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
return lines.join("\n").trim();
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
function formatSearchReposResults(query: string, items: GhSearchRepoResult[]): string {
|
|
1976
|
+
const lines: string[] = [`# GitHub repositories search`, "", `Query: ${query}`];
|
|
1977
|
+
pushLine(lines, "Results", items.length);
|
|
1978
|
+
|
|
1979
|
+
if (items.length === 0) {
|
|
1980
|
+
lines.push("");
|
|
1981
|
+
lines.push("No repositories found.");
|
|
1982
|
+
return lines.join("\n").trim();
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
for (const item of items) {
|
|
1986
|
+
lines.push("");
|
|
1987
|
+
lines.push(`- ${item.fullName ?? "(unknown repository)"}`);
|
|
1988
|
+
const description = normalizeText(item.description).split("\n", 1)[0];
|
|
1989
|
+
if (description) {
|
|
1990
|
+
pushLine(lines, " Description", description);
|
|
1991
|
+
}
|
|
1992
|
+
pushLine(lines, " Language", item.language ?? undefined);
|
|
1993
|
+
pushLine(lines, " Stars", item.stargazersCount);
|
|
1994
|
+
pushLine(lines, " Forks", item.forksCount);
|
|
1995
|
+
pushLine(lines, " Open issues", item.openIssuesCount);
|
|
1996
|
+
pushLine(lines, " Visibility", item.visibility ?? undefined);
|
|
1997
|
+
pushLine(lines, " Archived", item.isArchived);
|
|
1998
|
+
pushLine(lines, " Fork", item.isFork);
|
|
1999
|
+
pushLine(lines, " Updated", item.updatedAt);
|
|
2000
|
+
pushLine(lines, " URL", item.url);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
return lines.join("\n").trim();
|
|
2004
|
+
}
|
|
2005
|
+
|
|
1829
2006
|
async function saveArtifactText(session: ToolSession, toolType: string, text: string): Promise<string | undefined> {
|
|
1830
2007
|
const { path: artifactPath, id: artifactId } = (await session.allocateOutputArtifact?.(toolType)) ?? {};
|
|
1831
2008
|
if (!artifactPath || !artifactId) {
|
|
@@ -1898,6 +2075,12 @@ export class GithubTool implements AgentTool<typeof githubSchema, GhToolDetails>
|
|
|
1898
2075
|
return executeSearchIssues(this.session, params, signal);
|
|
1899
2076
|
case "search_prs":
|
|
1900
2077
|
return executeSearchPrs(this.session, params, signal);
|
|
2078
|
+
case "search_code":
|
|
2079
|
+
return executeSearchCode(this.session, params, signal);
|
|
2080
|
+
case "search_commits":
|
|
2081
|
+
return executeSearchCommits(this.session, params, signal);
|
|
2082
|
+
case "search_repos":
|
|
2083
|
+
return executeSearchRepos(this.session, params, signal);
|
|
1901
2084
|
case "run_watch":
|
|
1902
2085
|
return executeRunWatch(this.session, this.name, params, signal, onUpdate);
|
|
1903
2086
|
}
|
|
@@ -2291,6 +2474,51 @@ async function executeSearchPrs(
|
|
|
2291
2474
|
return buildTextResult(formatSearchResults("pull requests", query, repo, items));
|
|
2292
2475
|
}
|
|
2293
2476
|
|
|
2477
|
+
async function executeSearchCode(
|
|
2478
|
+
session: ToolSession,
|
|
2479
|
+
params: GithubInput,
|
|
2480
|
+
signal: AbortSignal | undefined,
|
|
2481
|
+
): Promise<AgentToolResult<GhToolDetails>> {
|
|
2482
|
+
const query = requireNonEmpty(params.query, "query");
|
|
2483
|
+
const repo = normalizeOptionalString(params.repo);
|
|
2484
|
+
const limit = resolveSearchLimit(params.limit);
|
|
2485
|
+
const args = buildGhSearchArgs("code", query, limit, repo);
|
|
2486
|
+
|
|
2487
|
+
const items = await git.github.json<GhSearchCodeResult[]>(session.cwd, args, signal, {
|
|
2488
|
+
repoProvided: Boolean(repo),
|
|
2489
|
+
});
|
|
2490
|
+
return buildTextResult(formatSearchCodeResults(query, repo, items));
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
async function executeSearchCommits(
|
|
2494
|
+
session: ToolSession,
|
|
2495
|
+
params: GithubInput,
|
|
2496
|
+
signal: AbortSignal | undefined,
|
|
2497
|
+
): Promise<AgentToolResult<GhToolDetails>> {
|
|
2498
|
+
const query = requireNonEmpty(params.query, "query");
|
|
2499
|
+
const repo = normalizeOptionalString(params.repo);
|
|
2500
|
+
const limit = resolveSearchLimit(params.limit);
|
|
2501
|
+
const args = buildGhSearchArgs("commits", query, limit, repo);
|
|
2502
|
+
|
|
2503
|
+
const items = await git.github.json<GhSearchCommitResult[]>(session.cwd, args, signal, {
|
|
2504
|
+
repoProvided: Boolean(repo),
|
|
2505
|
+
});
|
|
2506
|
+
return buildTextResult(formatSearchCommitsResults(query, repo, items));
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
async function executeSearchRepos(
|
|
2510
|
+
session: ToolSession,
|
|
2511
|
+
params: GithubInput,
|
|
2512
|
+
signal: AbortSignal | undefined,
|
|
2513
|
+
): Promise<AgentToolResult<GhToolDetails>> {
|
|
2514
|
+
const query = requireNonEmpty(params.query, "query");
|
|
2515
|
+
const limit = resolveSearchLimit(params.limit);
|
|
2516
|
+
const args = buildGhSearchArgs("repos", query, limit, undefined);
|
|
2517
|
+
|
|
2518
|
+
const items = await git.github.json<GhSearchRepoResult[]>(session.cwd, args, signal);
|
|
2519
|
+
return buildTextResult(formatSearchReposResults(query, items));
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2294
2522
|
async function executeRunWatch(
|
|
2295
2523
|
session: ToolSession,
|
|
2296
2524
|
toolName: string,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
4
|
+
import { getHindsightSessionState } from "../hindsight/backend";
|
|
5
|
+
import { formatCurrentTime, formatMemories } from "../hindsight/content";
|
|
6
|
+
import recallDescription from "../prompts/tools/recall.md" with { type: "text" };
|
|
7
|
+
import type { ToolSession } from ".";
|
|
8
|
+
|
|
9
|
+
const hindsightRecallSchema = Type.Object({
|
|
10
|
+
query: Type.String({
|
|
11
|
+
description: "Natural language search query. Be specific about what you need to know.",
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type HindsightRecallParams = Static<typeof hindsightRecallSchema>;
|
|
16
|
+
|
|
17
|
+
export class HindsightRecallTool implements AgentTool<typeof hindsightRecallSchema> {
|
|
18
|
+
readonly name = "recall";
|
|
19
|
+
readonly label = "Recall";
|
|
20
|
+
readonly description = recallDescription;
|
|
21
|
+
readonly parameters = hindsightRecallSchema;
|
|
22
|
+
readonly strict = true;
|
|
23
|
+
|
|
24
|
+
constructor(private readonly session: ToolSession) {}
|
|
25
|
+
|
|
26
|
+
static createIf(session: ToolSession): HindsightRecallTool | null {
|
|
27
|
+
if (session.settings.get("memory.backend") !== "hindsight") return null;
|
|
28
|
+
return new HindsightRecallTool(session);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async execute(_id: string, params: HindsightRecallParams, signal?: AbortSignal): Promise<AgentToolResult> {
|
|
32
|
+
return untilAborted(signal, async () => {
|
|
33
|
+
const sessionId = this.session.getSessionId?.();
|
|
34
|
+
const state = sessionId ? getHindsightSessionState(sessionId) : undefined;
|
|
35
|
+
if (!state) {
|
|
36
|
+
throw new Error("Hindsight backend is not initialised for this session.");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const response = await state.client.recall(state.bankId, params.query, {
|
|
41
|
+
budget: state.config.recallBudget,
|
|
42
|
+
maxTokens: state.config.recallMaxTokens,
|
|
43
|
+
types: state.config.recallTypes.length > 0 ? state.config.recallTypes : undefined,
|
|
44
|
+
tags: state.recallTags,
|
|
45
|
+
tagsMatch: state.recallTagsMatch,
|
|
46
|
+
});
|
|
47
|
+
const results = response.results ?? [];
|
|
48
|
+
if (results.length === 0) {
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: "text", text: "No relevant memories found." }],
|
|
51
|
+
details: {},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const formatted = formatMemories(results);
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: "text",
|
|
59
|
+
text: `Found ${results.length} relevant memories (as of ${formatCurrentTime()} UTC):\n\n${formatted}`,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
details: {},
|
|
63
|
+
};
|
|
64
|
+
} catch (err) {
|
|
65
|
+
logger.warn("recall failed", { bankId: state.bankId, error: String(err) });
|
|
66
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
4
|
+
import { getHindsightSessionState } from "../hindsight/backend";
|
|
5
|
+
import { ensureBankMission } from "../hindsight/bank";
|
|
6
|
+
import reflectDescription from "../prompts/tools/reflect.md" with { type: "text" };
|
|
7
|
+
import type { ToolSession } from ".";
|
|
8
|
+
|
|
9
|
+
const hindsightReflectSchema = Type.Object({
|
|
10
|
+
query: Type.String({ description: "The question to answer using long-term memory." }),
|
|
11
|
+
context: Type.Optional(Type.String({ description: "Optional additional context to guide the reflection." })),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export type HindsightReflectParams = Static<typeof hindsightReflectSchema>;
|
|
15
|
+
|
|
16
|
+
export class HindsightReflectTool implements AgentTool<typeof hindsightReflectSchema> {
|
|
17
|
+
readonly name = "reflect";
|
|
18
|
+
readonly label = "Reflect";
|
|
19
|
+
readonly description = reflectDescription;
|
|
20
|
+
readonly parameters = hindsightReflectSchema;
|
|
21
|
+
readonly strict = true;
|
|
22
|
+
|
|
23
|
+
constructor(private readonly session: ToolSession) {}
|
|
24
|
+
|
|
25
|
+
static createIf(session: ToolSession): HindsightReflectTool | null {
|
|
26
|
+
if (session.settings.get("memory.backend") !== "hindsight") return null;
|
|
27
|
+
return new HindsightReflectTool(session);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async execute(_id: string, params: HindsightReflectParams, signal?: AbortSignal): Promise<AgentToolResult> {
|
|
31
|
+
return untilAborted(signal, async () => {
|
|
32
|
+
const sessionId = this.session.getSessionId?.();
|
|
33
|
+
const state = sessionId ? getHindsightSessionState(sessionId) : undefined;
|
|
34
|
+
if (!state) {
|
|
35
|
+
throw new Error("Hindsight backend is not initialised for this session.");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
await ensureBankMission(state.client, state.bankId, state.config, state.missionsSet);
|
|
40
|
+
const response = await state.client.reflect(state.bankId, params.query, {
|
|
41
|
+
context: params.context,
|
|
42
|
+
budget: state.config.recallBudget,
|
|
43
|
+
tags: state.recallTags,
|
|
44
|
+
tagsMatch: state.recallTagsMatch,
|
|
45
|
+
});
|
|
46
|
+
const text = response.text?.trim() || "No relevant information found to reflect on.";
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: "text", text }],
|
|
49
|
+
details: {},
|
|
50
|
+
};
|
|
51
|
+
} catch (err) {
|
|
52
|
+
logger.warn("reflect failed", { bankId: state.bankId, error: String(err) });
|
|
53
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
3
|
+
import { getHindsightSessionState } from "../hindsight/backend";
|
|
4
|
+
import { enqueueRetain } from "../hindsight/retain-queue";
|
|
5
|
+
import retainDescription from "../prompts/tools/retain.md" with { type: "text" };
|
|
6
|
+
import type { ToolSession } from ".";
|
|
7
|
+
|
|
8
|
+
const hindsightRetainSchema = Type.Object({
|
|
9
|
+
items: Type.Array(
|
|
10
|
+
Type.Object({
|
|
11
|
+
content: Type.String({
|
|
12
|
+
description: "The information to remember. Be specific and self-contained — include who, what, when, why.",
|
|
13
|
+
}),
|
|
14
|
+
context: Type.Optional(
|
|
15
|
+
Type.String({ description: "Optional context describing where this information came from." }),
|
|
16
|
+
),
|
|
17
|
+
}),
|
|
18
|
+
{
|
|
19
|
+
minItems: 1,
|
|
20
|
+
description:
|
|
21
|
+
"One or more memories to retain. Batch related facts in a single call rather than calling retain repeatedly — they are deduplicated and consolidated together.",
|
|
22
|
+
},
|
|
23
|
+
),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export type HindsightRetainParams = Static<typeof hindsightRetainSchema>;
|
|
27
|
+
export class HindsightRetainTool implements AgentTool<typeof hindsightRetainSchema> {
|
|
28
|
+
readonly name = "retain";
|
|
29
|
+
readonly label = "Retain";
|
|
30
|
+
readonly description = retainDescription;
|
|
31
|
+
readonly parameters = hindsightRetainSchema;
|
|
32
|
+
readonly strict = true;
|
|
33
|
+
|
|
34
|
+
constructor(private readonly session: ToolSession) {}
|
|
35
|
+
|
|
36
|
+
static createIf(session: ToolSession): HindsightRetainTool | null {
|
|
37
|
+
if (session.settings.get("memory.backend") !== "hindsight") return null;
|
|
38
|
+
return new HindsightRetainTool(session);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async execute(_id: string, params: HindsightRetainParams): Promise<AgentToolResult> {
|
|
42
|
+
const sessionId = this.session.getSessionId?.();
|
|
43
|
+
const state = sessionId ? getHindsightSessionState(sessionId) : undefined;
|
|
44
|
+
if (!state || !sessionId) {
|
|
45
|
+
throw new Error("Hindsight backend is not initialised for this session.");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Push every item onto the global queue and return immediately. The
|
|
49
|
+
// queue flushes either when it reaches its batch threshold or when its
|
|
50
|
+
// debounce timer fires. If the eventual batch fails, the queue
|
|
51
|
+
// surfaces a UI-only warning notice — the LLM is not informed.
|
|
52
|
+
for (const item of params.items) {
|
|
53
|
+
enqueueRetain(sessionId, item.content, item.context);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const count = params.items.length;
|
|
57
|
+
const noun = count === 1 ? "memory" : "memories";
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: "text", text: `${count} ${noun} queued.` }],
|
|
60
|
+
details: { count },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -30,6 +30,9 @@ import { EvalTool } from "./eval";
|
|
|
30
30
|
import { ExitPlanModeTool } from "./exit-plan-mode";
|
|
31
31
|
import { FindTool } from "./find";
|
|
32
32
|
import { GithubTool } from "./gh";
|
|
33
|
+
import { HindsightRecallTool } from "./hindsight-recall";
|
|
34
|
+
import { HindsightReflectTool } from "./hindsight-reflect";
|
|
35
|
+
import { HindsightRetainTool } from "./hindsight-retain";
|
|
33
36
|
import { InspectImageTool } from "./inspect-image";
|
|
34
37
|
import { IrcTool } from "./irc";
|
|
35
38
|
import { JobTool } from "./job";
|
|
@@ -69,6 +72,9 @@ export * from "./eval";
|
|
|
69
72
|
export * from "./exit-plan-mode";
|
|
70
73
|
export * from "./find";
|
|
71
74
|
export * from "./gh";
|
|
75
|
+
export * from "./hindsight-recall";
|
|
76
|
+
export * from "./hindsight-reflect";
|
|
77
|
+
export * from "./hindsight-retain";
|
|
72
78
|
export * from "./image-gen";
|
|
73
79
|
export * from "./inspect-image";
|
|
74
80
|
export * from "./irc";
|
|
@@ -231,6 +237,9 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
231
237
|
web_search: s => new WebSearchTool(s),
|
|
232
238
|
search_tool_bm25: SearchToolBm25Tool.createIf,
|
|
233
239
|
write: s => new WriteTool(s),
|
|
240
|
+
retain: HindsightRetainTool.createIf,
|
|
241
|
+
recall: HindsightRecallTool.createIf,
|
|
242
|
+
reflect: HindsightReflectTool.createIf,
|
|
234
243
|
};
|
|
235
244
|
|
|
236
245
|
export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
|
|
@@ -342,6 +351,11 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
342
351
|
) {
|
|
343
352
|
requestedTools.push("recipe");
|
|
344
353
|
}
|
|
354
|
+
if (session.settings.get("memory.backend") === "hindsight") {
|
|
355
|
+
for (const name of ["recall", "retain", "reflect"]) {
|
|
356
|
+
if (!requestedTools.includes(name)) requestedTools.push(name);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
345
359
|
}
|
|
346
360
|
const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
|
|
347
361
|
const isToolAllowed = (name: string) => {
|
|
@@ -365,6 +379,9 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
365
379
|
if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
|
|
366
380
|
if (name === "irc") return session.settings.get("irc.enabled");
|
|
367
381
|
if (name === "recipe") return session.settings.get("recipe.enabled");
|
|
382
|
+
if (name === "retain" || name === "recall" || name === "reflect") {
|
|
383
|
+
return session.settings.get("memory.backend") === "hindsight";
|
|
384
|
+
}
|
|
368
385
|
if (name === "task") {
|
|
369
386
|
const maxDepth = session.settings.get("task.maxRecursionDepth") ?? 2;
|
|
370
387
|
const currentDepth = session.taskDepth ?? 0;
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as url from "node:url";
|
|
5
|
+
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
5
6
|
|
|
6
7
|
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
7
8
|
const NARROW_NO_BREAK_SPACE = "\u202F";
|
|
@@ -442,6 +443,60 @@ export async function resolveExplicitFindPatterns(
|
|
|
442
443
|
return resolveFindPatternItems([...new Set(patternItems)], cwd);
|
|
443
444
|
}
|
|
444
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Result of partitioning a list of user-supplied paths/globs into entries whose
|
|
448
|
+
* base directory currently exists on disk versus those that do not.
|
|
449
|
+
*
|
|
450
|
+
* Used by multi-path tools (search, find, ast_grep, ast_edit) to tolerate one
|
|
451
|
+
* or more missing entries in a multi-path call: the surviving entries should
|
|
452
|
+
* still be searched, with the missing entries surfaced as a non-fatal warning.
|
|
453
|
+
*/
|
|
454
|
+
export interface PartitionedPaths {
|
|
455
|
+
/** Raw input strings whose resolved base path exists. */
|
|
456
|
+
valid: string[];
|
|
457
|
+
/** Raw input strings whose resolved base path is missing (ENOENT). */
|
|
458
|
+
missing: string[];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Stat each input's base path concurrently; return entries split by existence.
|
|
463
|
+
*
|
|
464
|
+
* `splitter` is expected to be {@link parseFindPattern} or
|
|
465
|
+
* {@link parseSearchPath}: both return a `basePath` field that this helper
|
|
466
|
+
* resolves against `cwd` and stats. ENOENT is the only swallowed error — every
|
|
467
|
+
* other stat failure (permission, IO, etc.) propagates so callers do not silently
|
|
468
|
+
* skip paths that exist but are unreadable.
|
|
469
|
+
*
|
|
470
|
+
* Order of `valid` and `missing` follows the input order, so callers can rely
|
|
471
|
+
* on `valid[0]` matching the first surviving user-supplied entry.
|
|
472
|
+
*/
|
|
473
|
+
export async function partitionExistingPaths(
|
|
474
|
+
items: string[],
|
|
475
|
+
cwd: string,
|
|
476
|
+
splitter: (item: string) => { basePath: string },
|
|
477
|
+
): Promise<PartitionedPaths> {
|
|
478
|
+
const settled = await Promise.all(
|
|
479
|
+
items.map(async item => {
|
|
480
|
+
const { basePath } = splitter(item);
|
|
481
|
+
const absoluteBasePath = resolveToCwd(basePath, cwd);
|
|
482
|
+
try {
|
|
483
|
+
await fs.promises.stat(absoluteBasePath);
|
|
484
|
+
return { item, exists: true } as const;
|
|
485
|
+
} catch (err) {
|
|
486
|
+
if (isEnoent(err)) return { item, exists: false } as const;
|
|
487
|
+
throw err;
|
|
488
|
+
}
|
|
489
|
+
}),
|
|
490
|
+
);
|
|
491
|
+
const valid: string[] = [];
|
|
492
|
+
const missing: string[] = [];
|
|
493
|
+
for (const entry of settled) {
|
|
494
|
+
if (entry.exists) valid.push(entry.item);
|
|
495
|
+
else missing.push(entry.item);
|
|
496
|
+
}
|
|
497
|
+
return { valid, missing };
|
|
498
|
+
}
|
|
499
|
+
|
|
445
500
|
export function resolveReadPath(filePath: string, cwd: string): string {
|
|
446
501
|
const resolved = resolveToCwd(filePath, cwd);
|
|
447
502
|
const shellEscapedVariant = tryShellEscapedPath(resolved);
|
package/src/tools/read.ts
CHANGED
|
@@ -472,7 +472,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
472
472
|
this.description = prompt.render(readDescription, {
|
|
473
473
|
DEFAULT_LIMIT: String(this.#defaultLimit),
|
|
474
474
|
DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
|
|
475
|
-
|
|
475
|
+
IS_HL_MODE: displayMode.hashLines,
|
|
476
476
|
IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
|
|
477
477
|
});
|
|
478
478
|
}
|