@oh-my-pi/pi-coding-agent 14.6.1 → 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 +82 -1
- 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 +595 -100
- package/src/config/settings.ts +46 -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 +4 -2
- package/src/modes/components/status-line/presets.ts +1 -1
- package/src/modes/components/status-line.ts +4 -1
- package/src/modes/controllers/command-controller.ts +6 -5
- package/src/modes/controllers/event-controller.ts +12 -0
- package/src/modes/controllers/mcp-command-controller.ts +23 -0
- package/src/modes/controllers/selector-controller.ts +10 -12
- package/src/modes/interactive-mode.ts +3 -2
- 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/ssh/connection-manager.ts +1 -1
- 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/output-meta.ts +1 -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/ast-edit.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
5
5
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
|
-
import { computeLineHash,
|
|
8
|
+
import { computeLineHash, HL_BODY_SEP } from "../edit/line-hash";
|
|
9
9
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
10
10
|
import type { Theme } from "../modes/theme/theme";
|
|
11
11
|
import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
hasGlobPathChars,
|
|
21
21
|
normalizePathLikeInput,
|
|
22
22
|
parseSearchPath,
|
|
23
|
+
partitionExistingPaths,
|
|
23
24
|
resolveExplicitSearchPaths,
|
|
24
25
|
resolveToCwd,
|
|
25
26
|
} from "./path-utils";
|
|
@@ -226,13 +227,21 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
226
227
|
}
|
|
227
228
|
resolvedPathInputs.push(resource.sourcePath);
|
|
228
229
|
}
|
|
229
|
-
|
|
230
|
-
|
|
230
|
+
let effectivePathInputs = resolvedPathInputs;
|
|
231
|
+
if (resolvedPathInputs.length > 1) {
|
|
232
|
+
const partition = await partitionExistingPaths(resolvedPathInputs, this.session.cwd, parseSearchPath);
|
|
233
|
+
if (partition.valid.length === 0) {
|
|
234
|
+
throw new ToolError(`Path not found: ${partition.missing.join(", ")}`);
|
|
235
|
+
}
|
|
236
|
+
effectivePathInputs = partition.valid;
|
|
237
|
+
}
|
|
238
|
+
if (effectivePathInputs.length === 1) {
|
|
239
|
+
const parsedPath = parseSearchPath(effectivePathInputs[0] ?? ".");
|
|
231
240
|
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
232
241
|
globFilter = parsedPath.glob;
|
|
233
242
|
scopePath = formatScopePath(searchPath);
|
|
234
243
|
} else {
|
|
235
|
-
const multiSearchPath = await resolveExplicitSearchPaths(
|
|
244
|
+
const multiSearchPath = await resolveExplicitSearchPaths(effectivePathInputs, this.session.cwd, globFilter);
|
|
236
245
|
if (!multiSearchPath) {
|
|
237
246
|
throw new ToolError("`paths` must contain at least one path or glob");
|
|
238
247
|
}
|
|
@@ -321,7 +330,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
321
330
|
const afterRef = useHashLines
|
|
322
331
|
? `${change.startLine}${computeLineHash(change.startLine, afterFirstLine)}`
|
|
323
332
|
: `${change.startLine}:${change.startColumn}`;
|
|
324
|
-
const lineSeparator = useHashLines ?
|
|
333
|
+
const lineSeparator = useHashLines ? HL_BODY_SEP : " ";
|
|
325
334
|
modelOut.push(`-${beforeRef}${lineSeparator}${beforeLine}`);
|
|
326
335
|
modelOut.push(`+${afterRef}${lineSeparator}${afterLine}`);
|
|
327
336
|
displayOut.push(formatCodeFrameLine("-", change.startLine, beforeLine, lineNumberWidth));
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
hasGlobPathChars,
|
|
21
21
|
normalizePathLikeInput,
|
|
22
22
|
parseSearchPath,
|
|
23
|
+
partitionExistingPaths,
|
|
23
24
|
resolveExplicitSearchPaths,
|
|
24
25
|
resolveToCwd,
|
|
25
26
|
} from "./path-utils";
|
|
@@ -171,13 +172,21 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
171
172
|
}
|
|
172
173
|
resolvedPathInputs.push(resource.sourcePath);
|
|
173
174
|
}
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
let effectivePathInputs = resolvedPathInputs;
|
|
176
|
+
if (resolvedPathInputs.length > 1) {
|
|
177
|
+
const partition = await partitionExistingPaths(resolvedPathInputs, this.session.cwd, parseSearchPath);
|
|
178
|
+
if (partition.valid.length === 0) {
|
|
179
|
+
throw new ToolError(`Path not found: ${partition.missing.join(", ")}`);
|
|
180
|
+
}
|
|
181
|
+
effectivePathInputs = partition.valid;
|
|
182
|
+
}
|
|
183
|
+
if (effectivePathInputs.length === 1) {
|
|
184
|
+
const parsedPath = parseSearchPath(effectivePathInputs[0] ?? ".");
|
|
176
185
|
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
177
186
|
globFilter = parsedPath.glob;
|
|
178
187
|
scopePath = formatScopePath(searchPath);
|
|
179
188
|
} else {
|
|
180
|
-
const multiSearchPath = await resolveExplicitSearchPaths(
|
|
189
|
+
const multiSearchPath = await resolveExplicitSearchPaths(effectivePathInputs, this.session.cwd, globFilter);
|
|
181
190
|
if (!multiSearchPath) {
|
|
182
191
|
throw new ToolError("`paths` must contain at least one path or glob");
|
|
183
192
|
}
|
package/src/tools/find.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
formatPathRelativeToCwd,
|
|
28
28
|
normalizePathLikeInput,
|
|
29
29
|
parseFindPattern,
|
|
30
|
+
partitionExistingPaths,
|
|
30
31
|
resolveExplicitFindPatterns,
|
|
31
32
|
resolveToCwd,
|
|
32
33
|
} from "./path-utils";
|
|
@@ -59,6 +60,10 @@ export interface FindToolDetails {
|
|
|
59
60
|
files?: string[];
|
|
60
61
|
truncated?: boolean;
|
|
61
62
|
error?: string;
|
|
63
|
+
/** User-supplied paths whose base directory was missing on disk. The tool
|
|
64
|
+
* skipped these and continued with the surviving entries; surfaced as a
|
|
65
|
+
* non-fatal warning in the renderer and in the model-facing text. */
|
|
66
|
+
missingPaths?: string[];
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
/**
|
|
@@ -114,8 +119,23 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
114
119
|
throw new ToolError("`paths` must contain non-empty globs or paths");
|
|
115
120
|
}
|
|
116
121
|
|
|
117
|
-
|
|
118
|
-
|
|
122
|
+
// Tolerate missing entries in a multi-path call: skip ones whose base
|
|
123
|
+
// directory is gone, and only error if every entry is missing. Single
|
|
124
|
+
// missing path keeps the original ENOENT semantics — the user explicitly
|
|
125
|
+
// asked about that one path, so silent empty results would be misleading.
|
|
126
|
+
let missingPaths: string[] = [];
|
|
127
|
+
let effectivePatterns = normalizedPatterns;
|
|
128
|
+
if (normalizedPatterns.length > 1 && !this.#customOps) {
|
|
129
|
+
const partition = await partitionExistingPaths(normalizedPatterns, this.session.cwd, parseFindPattern);
|
|
130
|
+
if (partition.valid.length === 0) {
|
|
131
|
+
throw new ToolError(`Path not found: ${partition.missing.join(", ")}`);
|
|
132
|
+
}
|
|
133
|
+
effectivePatterns = partition.valid;
|
|
134
|
+
missingPaths = partition.missing;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const multiPattern = await resolveExplicitFindPatterns(effectivePatterns, this.session.cwd);
|
|
138
|
+
const parsedPattern = multiPattern ? null : parseFindPattern(effectivePatterns[0] ?? ".");
|
|
119
139
|
const hasGlob = multiPattern ? true : (parsedPattern?.hasGlob ?? false);
|
|
120
140
|
const globPattern = multiPattern?.globPattern ?? parsedPattern?.globPattern ?? "**/*";
|
|
121
141
|
const searchPath = resolveToCwd(multiPattern?.basePath ?? parsedPattern?.basePath ?? ".", this.session.cwd);
|
|
@@ -124,7 +144,6 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
124
144
|
if (searchPath === "/") {
|
|
125
145
|
throw new ToolError("Searching from root directory '/' is not allowed");
|
|
126
146
|
}
|
|
127
|
-
|
|
128
147
|
const rawLimit = limit ?? DEFAULT_LIMIT;
|
|
129
148
|
const effectiveLimit = Number.isFinite(rawLimit) ? Math.floor(rawLimit) : Number.NaN;
|
|
130
149
|
if (!Number.isFinite(effectiveLimit) || effectiveLimit <= 0) {
|
|
@@ -141,16 +160,29 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
141
160
|
});
|
|
142
161
|
};
|
|
143
162
|
|
|
163
|
+
const missingPathsNote =
|
|
164
|
+
missingPaths.length > 0 ? `Skipped missing paths: ${missingPaths.join(", ")}` : undefined;
|
|
165
|
+
|
|
144
166
|
const buildResult = (files: string[]): AgentToolResult<FindToolDetails> => {
|
|
145
167
|
if (files.length === 0) {
|
|
146
|
-
const details: FindToolDetails = {
|
|
147
|
-
|
|
168
|
+
const details: FindToolDetails = {
|
|
169
|
+
scopePath,
|
|
170
|
+
fileCount: 0,
|
|
171
|
+
files: [],
|
|
172
|
+
truncated: false,
|
|
173
|
+
missingPaths: missingPaths.length > 0 ? missingPaths : undefined,
|
|
174
|
+
};
|
|
175
|
+
const text = missingPathsNote
|
|
176
|
+
? `No files found matching pattern\n${missingPathsNote}`
|
|
177
|
+
: "No files found matching pattern";
|
|
178
|
+
return toolResult(details).text(text).done();
|
|
148
179
|
}
|
|
149
180
|
|
|
150
181
|
const listLimit = applyListLimit(files, { limit: effectiveLimit });
|
|
151
182
|
const limited = listLimit.items;
|
|
152
183
|
const limitMeta = listLimit.meta;
|
|
153
|
-
const
|
|
184
|
+
const baseOutput = limited.join("\n");
|
|
185
|
+
const rawOutput = missingPathsNote ? `${baseOutput}\n\n${missingPathsNote}` : baseOutput;
|
|
154
186
|
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
155
187
|
|
|
156
188
|
const details: FindToolDetails = {
|
|
@@ -160,6 +192,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
160
192
|
truncated: Boolean(limitMeta.resultLimit || truncation.truncated),
|
|
161
193
|
resultLimitReached: limitMeta.resultLimit?.reached,
|
|
162
194
|
truncation: truncation.truncated ? truncation : undefined,
|
|
195
|
+
missingPaths: missingPaths.length > 0 ? missingPaths : undefined,
|
|
163
196
|
};
|
|
164
197
|
|
|
165
198
|
const resultBuilder = toolResult(details)
|
|
@@ -380,12 +413,18 @@ export const findToolRenderer = {
|
|
|
380
413
|
const truncated = Boolean(details?.truncated || truncation || details?.resultLimitReached || limits?.resultLimit);
|
|
381
414
|
const files = details?.files ?? [];
|
|
382
415
|
|
|
416
|
+
const missingPaths = details?.missingPaths ?? [];
|
|
417
|
+
const missingNote =
|
|
418
|
+
missingPaths.length > 0 ? uiTheme.fg("warning", `skipped missing: ${missingPaths.join(", ")}`) : undefined;
|
|
419
|
+
|
|
383
420
|
if (fileCount === 0) {
|
|
384
421
|
const header = renderStatusLine(
|
|
385
422
|
{ icon: "warning", title: "Find", description: args?.paths?.join(", "), meta: ["0 files"] },
|
|
386
423
|
uiTheme,
|
|
387
424
|
);
|
|
388
|
-
|
|
425
|
+
const lines = [header, formatEmptyMessage("No files found", uiTheme)];
|
|
426
|
+
if (missingNote) lines.push(missingNote);
|
|
427
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
389
428
|
}
|
|
390
429
|
const meta: string[] = [formatCount("file", fileCount)];
|
|
391
430
|
if (details?.scopePath) meta.push(`in ${details.scopePath}`);
|
|
@@ -406,6 +445,7 @@ export const findToolRenderer = {
|
|
|
406
445
|
if (truncationReasons.length > 0) {
|
|
407
446
|
extraLines.push(uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`));
|
|
408
447
|
}
|
|
448
|
+
if (missingNote) extraLines.push(missingNote);
|
|
409
449
|
|
|
410
450
|
let cached: RenderCache | undefined;
|
|
411
451
|
return {
|
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
|
+
}
|