@oh-my-pi/pi-coding-agent 15.13.0 → 15.13.1
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 +1656 -613
- package/dist/cli.js +12765 -12731
- package/dist/types/autolearn/managed-skills.d.ts +1 -1
- package/dist/types/capability/mcp.d.ts +2 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/flag-tables.d.ts +126 -0
- package/dist/types/cli/profile-alias.d.ts +29 -0
- package/dist/types/cli/profile-bootstrap.d.ts +55 -0
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/model-roles.d.ts +3 -2
- package/dist/types/config/settings-schema.d.ts +2 -0
- package/dist/types/edit/file-snapshot-store.d.ts +14 -0
- package/dist/types/extensibility/extensions/runner.d.ts +11 -0
- package/dist/types/mcp/manager.d.ts +5 -1
- package/dist/types/mcp/oauth-credentials.d.ts +17 -0
- package/dist/types/mcp/oauth-flow.d.ts +41 -0
- package/dist/types/mcp/types.d.ts +2 -0
- package/dist/types/modes/components/background-tan-message.d.ts +9 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +9 -5
- package/dist/types/modes/interactive-mode.d.ts +4 -0
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/messages.d.ts +8 -0
- package/dist/types/session/session-manager.d.ts +6 -0
- package/dist/types/tools/builtin-names.d.ts +2 -0
- package/dist/types/tools/index.d.ts +3 -2
- package/dist/types/utils/external-editor.d.ts +11 -1
- package/package.json +12 -12
- package/src/autolearn/managed-skills.ts +3 -5
- package/src/capability/mcp.ts +2 -1
- package/src/cli/args.ts +61 -103
- package/src/cli/completion-gen.ts +2 -2
- package/src/cli/flag-tables.ts +270 -0
- package/src/cli/profile-alias.ts +338 -0
- package/src/cli/profile-bootstrap.ts +243 -0
- package/src/cli.ts +83 -16
- package/src/commands/launch.ts +7 -0
- package/src/config/mcp-schema.json +4 -0
- package/src/config/model-roles.ts +17 -4
- package/src/config/settings-schema.ts +2 -0
- package/src/discovery/builtin.ts +15 -9
- package/src/discovery/helpers.ts +25 -0
- package/src/discovery/mcp-json.ts +1 -0
- package/src/discovery/omp-extension-roots.ts +2 -2
- package/src/edit/file-snapshot-store.ts +43 -0
- package/src/eval/__tests__/agent-bridge.test.ts +3 -2
- package/src/eval/__tests__/helpers-local-roots.test.ts +1 -1
- package/src/eval/js/shared/runtime.ts +54 -0
- package/src/extensibility/extensions/runner.ts +25 -2
- package/src/goals/runtime.ts +4 -1
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/mcp/manager.ts +108 -71
- package/src/mcp/oauth-credentials.ts +104 -0
- package/src/mcp/oauth-flow.ts +67 -0
- package/src/mcp/types.ts +2 -0
- package/src/modes/components/agent-hub.ts +6 -0
- package/src/modes/components/background-tan-message.ts +36 -0
- package/src/modes/components/mcp-add-wizard.ts +17 -10
- package/src/modes/components/model-selector.ts +50 -6
- package/src/modes/components/tool-execution.ts +12 -0
- package/src/modes/controllers/input-controller.ts +21 -10
- package/src/modes/controllers/mcp-command-controller.ts +184 -112
- package/src/modes/controllers/tan-command-controller.ts +27 -11
- package/src/modes/interactive-mode.ts +6 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/ui-helpers.ts +6 -0
- package/src/prompts/bench.md +9 -4
- package/src/sdk.ts +6 -5
- package/src/session/agent-session.ts +30 -1
- package/src/session/messages.ts +9 -0
- package/src/session/session-manager.ts +7 -2
- package/src/tiny/text.ts +5 -1
- package/src/tools/ast-grep.ts +5 -1
- package/src/tools/builtin-names.ts +35 -0
- package/src/tools/index.ts +3 -2
- package/src/tools/read.ts +9 -0
- package/src/tools/search.ts +5 -1
- package/src/tts/tts-worker.ts +13 -5
- package/src/utils/external-editor.ts +15 -2
- package/src/utils/title-generator.ts +1 -1
- package/src/workspace-tree.ts +46 -6
- package/dist/types/utils/tools-manager.test.d.ts +0 -1
- package/src/utils/tools-manager.test.ts +0 -25
package/src/prompts/bench.md
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
You are given a relational schema and a multi-way analytical query, and you must work out from first principles the execution plan a cost-based optimizer should choose. This is a hard estimation problem with a large search space, so think it all the way through before you settle on anything and reason your way to each number instead of answering from intuition. Do not recite how query optimization works in general — actually do the analysis for this query, deriving every estimate.
|
|
2
|
+
|
|
3
|
+
Schema and statistics: orders(id, customer_id, status, total) holds 50,000,000 rows with 5 distinct status values; customers(id, country, segment) holds 4,000,000 rows across 200 countries; line_items(order_id, product_id, qty) holds 300,000,000 rows; products(id, category, price) holds 80,000 rows across 600 categories. The query reports total revenue per product category for shipped orders placed by customers in one given country.
|
|
4
|
+
|
|
5
|
+
Reason step by step and keep going: estimate the selectivity and output cardinality of each predicate and each join, then enumerate every join order over the four tables and derive the cost of each under both nested-loop and hash-join operators, weigh index access against full scans for each table, decide where the aggregation belongs and whether a partial pre-aggregation or a semi-join reduction earns its keep, and account for a memory limit that forces a hash build side to spill to disk. Compute the number behind every decision before you commit to it; when you finish one candidate plan, move on to the next and derive its cost too, and choose a winner only after you have costed the whole field. Never assert a choice you have not justified with an estimate.
|
|
2
6
|
|
|
3
7
|
Form:
|
|
4
|
-
- Plain paragraphs only: no headings, no lists, no code fences, no preamble.
|
|
5
|
-
-
|
|
8
|
+
- Plain paragraphs only: no headings, no lists, no code fences, no tables, no preamble.
|
|
9
|
+
- Derive each estimate explicitly; state no conclusion you have not computed.
|
|
10
|
+
- Do not wrap up early or summarize; keep reasoning until you are cut off.
|
|
6
11
|
|
|
7
|
-
Output only the
|
|
12
|
+
Output only the analysis.
|
package/src/sdk.ts
CHANGED
|
@@ -404,7 +404,7 @@ export interface CreateAgentSessionOptions {
|
|
|
404
404
|
scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
|
405
405
|
|
|
406
406
|
/** System prompt blocks. Array replaces default, function receives default blocks and returns final blocks. */
|
|
407
|
-
systemPrompt?: string[] | ((defaultPrompt: string[]) => string[]);
|
|
407
|
+
systemPrompt?: string | string[] | ((defaultPrompt: string[]) => string | string[]);
|
|
408
408
|
/** Optional provider-facing session identifier for prompt caches and sticky auth selection.
|
|
409
409
|
* Keeps persisted session files isolated while reusing provider-side caches. */
|
|
410
410
|
providerSessionId?: string;
|
|
@@ -2176,11 +2176,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2176
2176
|
if (options.systemPrompt === undefined) {
|
|
2177
2177
|
return defaultPrompt;
|
|
2178
2178
|
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2179
|
+
const customPrompt =
|
|
2180
|
+
typeof options.systemPrompt === "function"
|
|
2181
|
+
? options.systemPrompt(defaultPrompt.systemPrompt)
|
|
2182
|
+
: options.systemPrompt;
|
|
2182
2183
|
return {
|
|
2183
|
-
systemPrompt:
|
|
2184
|
+
systemPrompt: typeof customPrompt === "string" ? [customPrompt] : customPrompt,
|
|
2184
2185
|
};
|
|
2185
2186
|
};
|
|
2186
2187
|
|
|
@@ -965,6 +965,13 @@ export class AgentSession {
|
|
|
965
965
|
#activeRetryFallback: ActiveRetryFallbackState | undefined = undefined;
|
|
966
966
|
// Todo completion reminder state
|
|
967
967
|
#todoReminderCount = 0;
|
|
968
|
+
/**
|
|
969
|
+
* Set true after a todo reminder is appended; cleared when the agent makes any tool-level
|
|
970
|
+
* progress (toolResult) or a new user prompt arrives. Suppresses follow-up reminders within
|
|
971
|
+
* the same agent self-continuation chain so a text-only acknowledgement ("paused at your
|
|
972
|
+
* instruction") does not drive 1/3 → 2/3 → 3/3 without user input.
|
|
973
|
+
*/
|
|
974
|
+
#todoReminderAwaitingProgress = false;
|
|
968
975
|
#todoPhases: TodoPhase[] = [];
|
|
969
976
|
#toolChoiceQueue = new ToolChoiceQueue();
|
|
970
977
|
|
|
@@ -1828,6 +1835,10 @@ export class AgentSession {
|
|
|
1828
1835
|
isError?: boolean;
|
|
1829
1836
|
content?: Array<TextContent | ImageContent>;
|
|
1830
1837
|
};
|
|
1838
|
+
// A tool actually ran. Clear the post-reminder suppression: the agent did
|
|
1839
|
+
// productive work in response to the prior nudge, so the next text-only stop
|
|
1840
|
+
// is allowed to escalate to the next reminder if todos remain incomplete.
|
|
1841
|
+
this.#todoReminderAwaitingProgress = false;
|
|
1831
1842
|
// Invalidate streaming edit cache when edit tool completes to prevent stale data
|
|
1832
1843
|
if (toolName === "edit" && details?.path) {
|
|
1833
1844
|
this.#invalidateFileCacheForPath(details.path);
|
|
@@ -4750,6 +4761,7 @@ export class AgentSession {
|
|
|
4750
4761
|
|
|
4751
4762
|
// Reset todo reminder count on new user prompt
|
|
4752
4763
|
this.#todoReminderCount = 0;
|
|
4764
|
+
this.#todoReminderAwaitingProgress = false;
|
|
4753
4765
|
this.#emptyStopRetryCount = 0;
|
|
4754
4766
|
|
|
4755
4767
|
await this.#maybeRestoreRetryFallbackPrimary();
|
|
@@ -5545,6 +5557,7 @@ export class AgentSession {
|
|
|
5545
5557
|
);
|
|
5546
5558
|
|
|
5547
5559
|
this.#todoReminderCount = 0;
|
|
5560
|
+
this.#todoReminderAwaitingProgress = false;
|
|
5548
5561
|
this.#planReferenceSent = false;
|
|
5549
5562
|
this.#planReferencePath = "local://PLAN.md";
|
|
5550
5563
|
this.#reconnectToAgent();
|
|
@@ -6680,6 +6693,7 @@ export class AgentSession {
|
|
|
6680
6693
|
this.#pendingNextTurnMessages = [];
|
|
6681
6694
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
6682
6695
|
this.#todoReminderCount = 0;
|
|
6696
|
+
this.#todoReminderAwaitingProgress = false;
|
|
6683
6697
|
|
|
6684
6698
|
// Inject the handoff document as a custom message
|
|
6685
6699
|
const handoffContent = createHandoffContext(handoffText);
|
|
@@ -7219,10 +7233,22 @@ export class AgentSession {
|
|
|
7219
7233
|
return;
|
|
7220
7234
|
}
|
|
7221
7235
|
|
|
7236
|
+
// Suppress within a self-continuation chain: if the agent's last turn was driven by a
|
|
7237
|
+
// prior reminder (and the agent took no tool-level action since), do not re-ping.
|
|
7238
|
+
// The agent has already acknowledged; further escalation just wastes context and
|
|
7239
|
+
// pressures the agent into busy-work or destructive ops (issue #2590).
|
|
7240
|
+
if (this.#todoReminderAwaitingProgress) {
|
|
7241
|
+
logger.debug("Todo completion: prior reminder still awaiting agent action; staying silent", {
|
|
7242
|
+
attempt: this.#todoReminderCount,
|
|
7243
|
+
});
|
|
7244
|
+
return;
|
|
7245
|
+
}
|
|
7246
|
+
|
|
7222
7247
|
const remindersEnabled = this.settings.get("todo.reminders");
|
|
7223
7248
|
const todosEnabled = this.settings.get("todo.enabled");
|
|
7224
7249
|
if (!remindersEnabled || !todosEnabled) {
|
|
7225
7250
|
this.#todoReminderCount = 0;
|
|
7251
|
+
this.#todoReminderAwaitingProgress = false;
|
|
7226
7252
|
return;
|
|
7227
7253
|
}
|
|
7228
7254
|
|
|
@@ -7235,6 +7261,7 @@ export class AgentSession {
|
|
|
7235
7261
|
const phases = this.getTodoPhases();
|
|
7236
7262
|
if (phases.length === 0) {
|
|
7237
7263
|
this.#todoReminderCount = 0;
|
|
7264
|
+
this.#todoReminderAwaitingProgress = false;
|
|
7238
7265
|
return;
|
|
7239
7266
|
}
|
|
7240
7267
|
|
|
@@ -7252,6 +7279,7 @@ export class AgentSession {
|
|
|
7252
7279
|
const incomplete = incompleteByPhase.flatMap(phase => phase.tasks);
|
|
7253
7280
|
if (incomplete.length === 0) {
|
|
7254
7281
|
this.#todoReminderCount = 0;
|
|
7282
|
+
this.#todoReminderAwaitingProgress = false;
|
|
7255
7283
|
return;
|
|
7256
7284
|
}
|
|
7257
7285
|
|
|
@@ -7280,6 +7308,7 @@ export class AgentSession {
|
|
|
7280
7308
|
maxAttempts: remindersMax,
|
|
7281
7309
|
});
|
|
7282
7310
|
|
|
7311
|
+
this.#todoReminderAwaitingProgress = true;
|
|
7283
7312
|
// Inject reminder and continue the conversation
|
|
7284
7313
|
this.agent.appendMessage({
|
|
7285
7314
|
role: "developer",
|
|
@@ -8466,7 +8495,7 @@ export class AgentSession {
|
|
|
8466
8495
|
// src/http/h2_client/dispatch.zig)
|
|
8467
8496
|
return (
|
|
8468
8497
|
isUnexpectedSocketCloseMessage(errorMessage) ||
|
|
8469
|
-
/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|upstream.?request.?failed|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)/i.test(
|
|
8498
|
+
/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|upstream.?request.?failed|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)|malformed.?function.?call/i.test(
|
|
8470
8499
|
errorMessage,
|
|
8471
8500
|
)
|
|
8472
8501
|
);
|
package/src/session/messages.ts
CHANGED
|
@@ -34,6 +34,15 @@ import { formatOutputNotice } from "../tools/output-meta";
|
|
|
34
34
|
|
|
35
35
|
export const SKILL_PROMPT_MESSAGE_TYPE = "skill-prompt";
|
|
36
36
|
export const LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE = "lsp-late-diagnostic";
|
|
37
|
+
export const BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE = "background-tan-dispatch";
|
|
38
|
+
|
|
39
|
+
/** Details persisted on a `/tan` background-dispatch breadcrumb. */
|
|
40
|
+
export interface BackgroundTanDispatchDetails {
|
|
41
|
+
jobId: string;
|
|
42
|
+
work: string;
|
|
43
|
+
/** Forked clone session file, named `<agentId>.jsonl`; the Agent Hub reads its transcript. */
|
|
44
|
+
sessionFile: string;
|
|
45
|
+
}
|
|
37
46
|
|
|
38
47
|
export interface SkillPromptDetails {
|
|
39
48
|
name: string;
|
|
@@ -1483,13 +1483,18 @@ export class SessionManager {
|
|
|
1483
1483
|
/**
|
|
1484
1484
|
* Fork a session into the current project directory: copy history from another
|
|
1485
1485
|
* session file while creating a fresh session file in this sessionDir.
|
|
1486
|
+
*
|
|
1487
|
+
* `options.sessionFile` pins the new session's file path (default: an
|
|
1488
|
+
* auto-named `<timestamp>_<id>.jsonl` in `sessionDir`). Callers that register
|
|
1489
|
+
* the fork as a named agent (e.g. `/tan`) pass `<agentId>.jsonl` so the
|
|
1490
|
+
* persisted-subagent scan keys the agent by the same id the live ref uses.
|
|
1486
1491
|
*/
|
|
1487
1492
|
static async forkFrom(
|
|
1488
1493
|
sourcePath: string,
|
|
1489
1494
|
cwd: string,
|
|
1490
1495
|
sessionDir?: string,
|
|
1491
1496
|
storage: SessionStorage = new FileSessionStorage(),
|
|
1492
|
-
options?: { suppressBreadcrumb?: boolean },
|
|
1497
|
+
options?: { suppressBreadcrumb?: boolean; sessionFile?: string },
|
|
1493
1498
|
): Promise<SessionManager> {
|
|
1494
1499
|
const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
|
|
1495
1500
|
const manager = new SessionManager(cwd, dir, true, storage);
|
|
@@ -1501,7 +1506,7 @@ export class SessionManager {
|
|
|
1501
1506
|
|
|
1502
1507
|
const sourceHeader = sourceEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
1503
1508
|
const history = sourceEntries.filter(entry => entry.type !== "session") as SessionEntry[];
|
|
1504
|
-
manager.#resetToNewSession({ parentSession: sourceHeader?.id });
|
|
1509
|
+
manager.#resetToNewSession({ parentSession: sourceHeader?.id }, options?.sessionFile);
|
|
1505
1510
|
manager.#header.title = sourceHeader?.title;
|
|
1506
1511
|
manager.#header.titleSource = sourceHeader?.titleSource;
|
|
1507
1512
|
manager.#sessionName = manager.#header.title;
|
package/src/tiny/text.ts
CHANGED
|
@@ -161,5 +161,9 @@ export function normalizeGeneratedTitle(value: string | null | undefined): strin
|
|
|
161
161
|
.replace(/[.!?]$/, "")
|
|
162
162
|
.trim();
|
|
163
163
|
if (!title || title.toLowerCase() === NO_TITLE_SENTINEL) return null;
|
|
164
|
-
return title;
|
|
164
|
+
return titleCase(title);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function titleCase(value: string): string {
|
|
168
|
+
return value.replace(/\b\p{Ll}/gu, c => c.toUpperCase());
|
|
165
169
|
}
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
6
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import { z } from "zod/v4";
|
|
9
|
-
import { recordFileSnapshot } from "../edit/file-snapshot-store";
|
|
9
|
+
import { recordFileSnapshot, recordSeenLinesFromBody } from "../edit/file-snapshot-store";
|
|
10
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
11
|
import type { Theme } from "../modes/theme/theme";
|
|
12
12
|
import astGrepDescription from "../prompts/tools/ast-grep.md" with { type: "text" };
|
|
@@ -270,6 +270,10 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
270
270
|
}
|
|
271
271
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
272
272
|
}
|
|
273
|
+
if (hashContext?.tag) {
|
|
274
|
+
const absoluteFilePath = path.resolve(this.session.cwd, relativePath);
|
|
275
|
+
recordSeenLinesFromBody(this.session, absoluteFilePath, hashContext.tag, modelOut.join("\n"));
|
|
276
|
+
}
|
|
273
277
|
return { model: modelOut, display: displayOut };
|
|
274
278
|
};
|
|
275
279
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const BUILTIN_TOOL_NAMES = [
|
|
2
|
+
"read",
|
|
3
|
+
"bash",
|
|
4
|
+
"edit",
|
|
5
|
+
"ast_grep",
|
|
6
|
+
"ast_edit",
|
|
7
|
+
"render_mermaid",
|
|
8
|
+
"ask",
|
|
9
|
+
"debug",
|
|
10
|
+
"eval",
|
|
11
|
+
"ssh",
|
|
12
|
+
"github",
|
|
13
|
+
"find",
|
|
14
|
+
"search",
|
|
15
|
+
"lsp",
|
|
16
|
+
"inspect_image",
|
|
17
|
+
"browser",
|
|
18
|
+
"checkpoint",
|
|
19
|
+
"rewind",
|
|
20
|
+
"task",
|
|
21
|
+
"job",
|
|
22
|
+
"irc",
|
|
23
|
+
"todo",
|
|
24
|
+
"web_search",
|
|
25
|
+
"search_tool_bm25",
|
|
26
|
+
"write",
|
|
27
|
+
"memory_edit",
|
|
28
|
+
"retain",
|
|
29
|
+
"recall",
|
|
30
|
+
"reflect",
|
|
31
|
+
"learn",
|
|
32
|
+
"manage_skill",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
export type BuiltinToolName = (typeof BUILTIN_TOOL_NAMES)[number];
|
package/src/tools/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { AstEditTool } from "./ast-edit";
|
|
|
37
37
|
import { AstGrepTool } from "./ast-grep";
|
|
38
38
|
import { BashTool } from "./bash";
|
|
39
39
|
import { BrowserTool } from "./browser";
|
|
40
|
+
import type { BuiltinToolName } from "./builtin-names";
|
|
40
41
|
import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
|
|
41
42
|
import { DebugTool } from "./debug";
|
|
42
43
|
import { EvalTool } from "./eval";
|
|
@@ -413,7 +414,7 @@ export function filterInitialToolsForDiscoveryAll(
|
|
|
413
414
|
* Public callable factory map. External callers may invoke `BUILTIN_TOOLS.read(session)` or
|
|
414
415
|
* `BUILTIN_TOOLS[name](session)` to construct a tool directly.
|
|
415
416
|
*/
|
|
416
|
-
export const BUILTIN_TOOLS: Record<
|
|
417
|
+
export const BUILTIN_TOOLS: Record<BuiltinToolName, ToolFactory> = {
|
|
417
418
|
read: s => new ReadTool(s),
|
|
418
419
|
bash: s => new BashTool(s),
|
|
419
420
|
edit: s => new EditTool(s),
|
|
@@ -455,7 +456,7 @@ export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
|
|
|
455
456
|
goal: s => new GoalTool(s),
|
|
456
457
|
};
|
|
457
458
|
|
|
458
|
-
export type ToolName =
|
|
459
|
+
export type ToolName = BuiltinToolName;
|
|
459
460
|
|
|
460
461
|
/**
|
|
461
462
|
* Create tools from BUILTIN_TOOLS registry.
|
package/src/tools/read.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
canonicalSnapshotKey,
|
|
15
15
|
getFileSnapshotStore,
|
|
16
16
|
recordFileSnapshot,
|
|
17
|
+
recordSeenLinesFromBody,
|
|
17
18
|
SNAPSHOT_MAX_BYTES,
|
|
18
19
|
} from "../edit/file-snapshot-store";
|
|
19
20
|
import { normalizeToLF } from "../edit/normalize";
|
|
@@ -1356,6 +1357,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1356
1357
|
if (shouldAddHashLines && outputText) {
|
|
1357
1358
|
const tag = await recordFileSnapshot(this.session, absolutePath);
|
|
1358
1359
|
if (tag) {
|
|
1360
|
+
recordSeenLinesFromBody(this.session, absolutePath, tag, outputText);
|
|
1359
1361
|
outputText = `${formatHashlineHeader(formatPathRelativeToCwd(absolutePath, this.session.cwd), tag)}\n${outputText}`;
|
|
1360
1362
|
}
|
|
1361
1363
|
}
|
|
@@ -2059,6 +2061,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2059
2061
|
: undefined;
|
|
2060
2062
|
const bodyText = footer ? `${renderedSummary.text}\n\n${footer}` : renderedSummary.text;
|
|
2061
2063
|
const modelText = prependHashlineHeader(bodyText, summaryHashContext);
|
|
2064
|
+
if (summaryHashContext?.tag) {
|
|
2065
|
+
recordSeenLinesFromBody(this.session, absolutePath, summaryHashContext.tag, renderedSummary.text);
|
|
2066
|
+
}
|
|
2062
2067
|
details = {
|
|
2063
2068
|
displayContent: { text: renderedSummary.displayText, startLine: 1 },
|
|
2064
2069
|
summary: {
|
|
@@ -2354,6 +2359,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2354
2359
|
sourcePath = absolutePath;
|
|
2355
2360
|
}
|
|
2356
2361
|
|
|
2362
|
+
if (hashContext?.tag) {
|
|
2363
|
+
recordSeenLinesFromBody(this.session, absolutePath, hashContext.tag, outputText);
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2357
2366
|
if (capturedDisplayContent) {
|
|
2358
2367
|
details.displayContent = capturedDisplayContent;
|
|
2359
2368
|
}
|
package/src/tools/search.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
8
8
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
9
9
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import { z } from "zod/v4";
|
|
11
|
-
import { recordFileSnapshot } from "../edit/file-snapshot-store";
|
|
11
|
+
import { recordFileSnapshot, recordSeenLinesFromBody } from "../edit/file-snapshot-store";
|
|
12
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
13
13
|
import type { LocalProtocolOptions } from "../internal-urls/local-protocol";
|
|
14
14
|
import { InternalUrlRouter } from "../internal-urls/router";
|
|
@@ -1197,6 +1197,10 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
1197
1197
|
}
|
|
1198
1198
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
1199
1199
|
}
|
|
1200
|
+
if (hashContext?.tag) {
|
|
1201
|
+
const absoluteFilePath = path.resolve(this.session.cwd, relativePath);
|
|
1202
|
+
recordSeenLinesFromBody(this.session, absoluteFilePath, hashContext.tag, modelOut.join("\n"));
|
|
1203
|
+
}
|
|
1200
1204
|
return { model: modelOut, display: displayOut };
|
|
1201
1205
|
};
|
|
1202
1206
|
const useGroupedOutput = isDirectory || isMultiScope;
|
package/src/tts/tts-worker.ts
CHANGED
|
@@ -76,8 +76,13 @@ interface TransformersEnv {
|
|
|
76
76
|
cacheDir?: string;
|
|
77
77
|
allowLocalModels?: boolean;
|
|
78
78
|
logLevel?: unknown;
|
|
79
|
+
backends?: {
|
|
80
|
+
onnx?: {
|
|
81
|
+
logLevel?: unknown;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
79
84
|
};
|
|
80
|
-
LogLevel
|
|
85
|
+
LogLevel?: {
|
|
81
86
|
ERROR: unknown;
|
|
82
87
|
};
|
|
83
88
|
}
|
|
@@ -146,7 +151,8 @@ function toKokoroDevice(device: TinyModelDevice): KokoroDevice {
|
|
|
146
151
|
function configureTransformers(transformers: TransformersEnv): void {
|
|
147
152
|
transformers.env.cacheDir = getTinyModelsCacheDir();
|
|
148
153
|
transformers.env.allowLocalModels = false;
|
|
149
|
-
transformers.env.logLevel = transformers.LogLevel
|
|
154
|
+
transformers.env.logLevel = transformers.LogLevel?.ERROR ?? "error";
|
|
155
|
+
if (transformers.env.backends?.onnx) transformers.env.backends.onnx.logLevel = "error";
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
/**
|
|
@@ -182,9 +188,11 @@ async function loadKokoroRuntime(
|
|
|
182
188
|
installRuntimeModuleResolver({ runtimeNodeModules: nodeModules, stubs: { sharp: sharpStub } });
|
|
183
189
|
const kokoroEntry = resolveRuntimeModule(nodeModules, KOKORO_PACKAGE);
|
|
184
190
|
if (!kokoroEntry) throw new Error(`Unable to resolve ${KOKORO_PACKAGE} in runtime at ${nodeModules}`);
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
191
|
+
const transformersEntry = resolveRuntimeModule(nodeModules, TRANSFORMERS_PACKAGE);
|
|
192
|
+
if (!transformersEntry) throw new Error(`Unable to resolve ${TRANSFORMERS_PACKAGE} in runtime at ${nodeModules}`);
|
|
193
|
+
const runtimeRequire = createRequire(kokoroEntry);
|
|
194
|
+
configureTransformers(runtimeRequire(transformersEntry) as TransformersEnv);
|
|
195
|
+
return runtimeRequire(kokoroEntry) as KokoroRuntime;
|
|
188
196
|
})().catch(error => {
|
|
189
197
|
kokoroRuntime = null;
|
|
190
198
|
throw error;
|
|
@@ -7,9 +7,22 @@ import * as os from "node:os";
|
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { $env, Snowflake } from "@oh-my-pi/pi-utils";
|
|
9
9
|
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Returns the user's preferred editor command, or a platform default.
|
|
12
|
+
*
|
|
13
|
+
* Resolution order:
|
|
14
|
+
* 1. `$VISUAL`
|
|
15
|
+
* 2. `$EDITOR`
|
|
16
|
+
* 3. `notepad` on Windows (always present in `%SystemRoot%\System32`)
|
|
17
|
+
*
|
|
18
|
+
* POSIX returns `undefined` when neither variable is set so the caller can
|
|
19
|
+
* surface a warning that nudges the user to configure one.
|
|
20
|
+
*/
|
|
11
21
|
export function getEditorCommand(): string | undefined {
|
|
12
|
-
|
|
22
|
+
const configured = $env.VISUAL?.trim() || $env.EDITOR?.trim();
|
|
23
|
+
if (configured) return configured;
|
|
24
|
+
if (process.platform === "win32") return "notepad";
|
|
25
|
+
return undefined;
|
|
13
26
|
}
|
|
14
27
|
|
|
15
28
|
export interface OpenInEditorOptions {
|
|
@@ -73,7 +73,7 @@ function getTitleModel(registry: ModelRegistry, settings: Settings, currentModel
|
|
|
73
73
|
const availableModels = registry.getAvailable();
|
|
74
74
|
if (availableModels.length === 0) return undefined;
|
|
75
75
|
|
|
76
|
-
const titleModel = resolveRoleSelection(["commit", "smol"], settings, availableModels, registry)?.model;
|
|
76
|
+
const titleModel = resolveRoleSelection(["title", "commit", "smol"], settings, availableModels, registry)?.model;
|
|
77
77
|
if (titleModel) return titleModel;
|
|
78
78
|
|
|
79
79
|
if (currentModel) return currentModel;
|
package/src/workspace-tree.ts
CHANGED
|
@@ -75,6 +75,9 @@ export async function buildDirectoryTree(cwd: string, options: BuildDirectoryTre
|
|
|
75
75
|
rootLimit,
|
|
76
76
|
lineCap: options.lineCap === undefined ? null : options.lineCap,
|
|
77
77
|
nativeTruncated,
|
|
78
|
+
// Tool output (read tool directory listing), not a cached prefix —
|
|
79
|
+
// the human-friendly relative "ago" is appropriate here.
|
|
80
|
+
ageMode: "relative",
|
|
78
81
|
});
|
|
79
82
|
}
|
|
80
83
|
|
|
@@ -99,6 +102,10 @@ export async function buildWorkspaceTree(cwd: string, options: BuildWorkspaceTre
|
|
|
99
102
|
rootLimit: WORKSPACE_DEFAULTS.perDirLimit,
|
|
100
103
|
lineCap: WORKSPACE_DEFAULTS.lineCap,
|
|
101
104
|
nativeTruncated: result.truncated,
|
|
105
|
+
// This tree is embedded in the cached system prompt. Render absolute
|
|
106
|
+
// mtimes so the block is byte-identical across sessions and does not
|
|
107
|
+
// bust the prompt cache (a relative "Nm ago" drifts every build).
|
|
108
|
+
ageMode: "absolute",
|
|
102
109
|
});
|
|
103
110
|
return { ...tree, agentsMdFiles: result.agentsMdFiles };
|
|
104
111
|
} catch {
|
|
@@ -132,6 +139,13 @@ interface AssembleOptions {
|
|
|
132
139
|
rootLimit: number | null;
|
|
133
140
|
lineCap: number | null;
|
|
134
141
|
nativeTruncated: boolean;
|
|
142
|
+
/**
|
|
143
|
+
* How per-entry modification times are rendered.
|
|
144
|
+
* - "relative": render-time "Nm ago" (fine for tool output).
|
|
145
|
+
* - "absolute": deterministic UTC timestamp (prompt-cache-stable; used for
|
|
146
|
+
* the system-prompt workspace tree). See {@link makeAgeFormatter}.
|
|
147
|
+
*/
|
|
148
|
+
ageMode: "relative" | "absolute";
|
|
135
149
|
}
|
|
136
150
|
|
|
137
151
|
function assembleTree(rootPath: string, entries: readonly GlobMatch[], opts: AssembleOptions): DirectoryTree {
|
|
@@ -187,7 +201,7 @@ function assembleTree(rootPath: string, entries: readonly GlobMatch[], opts: Ass
|
|
|
187
201
|
}
|
|
188
202
|
|
|
189
203
|
const rawLines: RenderedLine[] = [];
|
|
190
|
-
renderNode(root,
|
|
204
|
+
renderNode(root, makeAgeFormatter(opts.ageMode), rawLines);
|
|
191
205
|
const { lines, elidedCount } = applyLineCap(rawLines, opts.lineCap);
|
|
192
206
|
|
|
193
207
|
return {
|
|
@@ -202,7 +216,33 @@ function byRecency(a: Node, b: Node): number {
|
|
|
202
216
|
return b.mtimeMs - a.mtimeMs || a.name.localeCompare(b.name);
|
|
203
217
|
}
|
|
204
218
|
|
|
205
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Build the per-node age formatter for a single render pass.
|
|
221
|
+
*
|
|
222
|
+
* - "relative": a render-time "Nm ago" string (computed once from `Date.now()`).
|
|
223
|
+
* Used for tool output that is not part of any cached prefix.
|
|
224
|
+
* - "absolute": a deterministic UTC `YYYY-MM-DD HH:MM` derived purely from the
|
|
225
|
+
* file's mtime. Used for the system-prompt workspace tree so the rendered
|
|
226
|
+
* block stays byte-identical across sessions. A relative age is recomputed on
|
|
227
|
+
* every build, so two sessions seconds apart differ ("9m ago" → "10m ago");
|
|
228
|
+
* because KV cache is contextual, that early change invalidates the cache for
|
|
229
|
+
* everything after the tree — including the multi-thousand-token tool block —
|
|
230
|
+
* forcing a full prompt re-prefill on every new session. An absolute mtime
|
|
231
|
+
* only changes when the file itself changes, which is the correct trigger.
|
|
232
|
+
*/
|
|
233
|
+
function makeAgeFormatter(mode: "relative" | "absolute"): (mtimeMs: number) => string {
|
|
234
|
+
if (mode === "absolute") return formatMtimeStable;
|
|
235
|
+
const nowMs = Date.now();
|
|
236
|
+
return (mtimeMs: number) => formatAge(Math.max(0, Math.floor((nowMs - mtimeMs) / 1000)));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Deterministic, render-time-independent timestamp: UTC `YYYY-MM-DD HH:MM`. */
|
|
240
|
+
function formatMtimeStable(mtimeMs: number): string {
|
|
241
|
+
if (!mtimeMs) return "";
|
|
242
|
+
return new Date(mtimeMs).toISOString().slice(0, 16).replace("T", " ");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function renderNode(node: Node, formatNodeAge: (mtimeMs: number) => string, out: RenderedLine[]): void {
|
|
206
246
|
if (node.depth === 0) {
|
|
207
247
|
out.push({ label: node.name, depth: 0, isRoot: true });
|
|
208
248
|
} else {
|
|
@@ -213,26 +253,26 @@ function renderNode(node: Node, nowMs: number, out: RenderedLine[]): void {
|
|
|
213
253
|
depth: node.depth,
|
|
214
254
|
isRoot: false,
|
|
215
255
|
size: node.isDir ? undefined : formatBytes(node.size),
|
|
216
|
-
age:
|
|
256
|
+
age: formatNodeAge(node.mtimeMs),
|
|
217
257
|
});
|
|
218
258
|
}
|
|
219
259
|
|
|
220
260
|
if (node.droppedCount === 0) {
|
|
221
|
-
for (const child of node.children) renderNode(child,
|
|
261
|
+
for (const child of node.children) renderNode(child, formatNodeAge, out);
|
|
222
262
|
return;
|
|
223
263
|
}
|
|
224
264
|
|
|
225
265
|
// Layout: recent children, then "… N more" marker, then the oldest child.
|
|
226
266
|
const recent = node.children.slice(0, -1);
|
|
227
267
|
const oldest = node.children.at(-1);
|
|
228
|
-
for (const child of recent) renderNode(child,
|
|
268
|
+
for (const child of recent) renderNode(child, formatNodeAge, out);
|
|
229
269
|
const childDepth = node.depth + 1;
|
|
230
270
|
out.push({
|
|
231
271
|
label: `${" ".repeat(childDepth)}- … ${node.droppedCount} more`,
|
|
232
272
|
depth: childDepth,
|
|
233
273
|
isRoot: false,
|
|
234
274
|
});
|
|
235
|
-
if (oldest) renderNode(oldest,
|
|
275
|
+
if (oldest) renderNode(oldest, formatNodeAge, out);
|
|
236
276
|
}
|
|
237
277
|
|
|
238
278
|
/**
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { ffmpegAssetName } from "./tools-manager";
|
|
3
|
-
|
|
4
|
-
describe("ffmpegAssetName", () => {
|
|
5
|
-
it("maps supported platform/arch pairs to direct-binary asset names", () => {
|
|
6
|
-
expect(ffmpegAssetName("b6.1.1", "darwin", "arm64")).toBe("ffmpeg-darwin-arm64");
|
|
7
|
-
expect(ffmpegAssetName("b6.1.1", "darwin", "x64")).toBe("ffmpeg-darwin-x64");
|
|
8
|
-
expect(ffmpegAssetName("b6.1.1", "linux", "arm64")).toBe("ffmpeg-linux-arm64");
|
|
9
|
-
expect(ffmpegAssetName("b6.1.1", "linux", "x64")).toBe("ffmpeg-linux-x64");
|
|
10
|
-
expect(ffmpegAssetName("b6.1.1", "win32", "x64")).toBe("ffmpeg-win32-x64");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("returns null for win32 on arm64 (no static asset published)", () => {
|
|
14
|
-
expect(ffmpegAssetName("b6.1.1", "win32", "arm64")).toBeNull();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("returns null for unsupported arch", () => {
|
|
18
|
-
expect(ffmpegAssetName("b6.1.1", "darwin", "ia32")).toBeNull();
|
|
19
|
-
expect(ffmpegAssetName("b6.1.1", "linux", "ppc64")).toBeNull();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("returns null for unsupported platform", () => {
|
|
23
|
-
expect(ffmpegAssetName("b6.1.1", "freebsd", "x64")).toBeNull();
|
|
24
|
-
});
|
|
25
|
-
});
|