@oh-my-pi/pi-coding-agent 15.13.1 → 15.13.2
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 +25 -0
- package/dist/cli.js +957 -214
- package/dist/types/config/model-registry.d.ts +1 -0
- package/dist/types/config/models-config-schema.d.ts +3 -0
- package/dist/types/config/models-config.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +66 -0
- package/dist/types/edit/hashline/block-resolver.d.ts +1 -1
- package/dist/types/edit/index.d.ts +2 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/input-controller.d.ts +4 -4
- package/dist/types/modes/rpc/rpc-types.d.ts +2 -1
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/session-dump-format.d.ts +2 -1
- package/dist/types/system-prompt.d.ts +11 -0
- package/dist/types/tools/ask.d.ts +2 -0
- package/dist/types/tools/ast-edit.d.ts +2 -0
- package/dist/types/tools/ast-grep.d.ts +2 -0
- package/dist/types/tools/browser.d.ts +2 -0
- package/dist/types/tools/debug.d.ts +2 -0
- package/dist/types/tools/eval.d.ts +2 -0
- package/dist/types/tools/find.d.ts +2 -0
- package/dist/types/tools/inspect-image.d.ts +2 -1
- package/dist/types/tools/irc.d.ts +2 -0
- package/dist/types/tools/ssh.d.ts +2 -0
- package/dist/types/tools/todo.d.ts +2 -0
- package/dist/types/tui/tree-list.d.ts +1 -0
- package/package.json +12 -12
- package/src/config/model-registry.ts +10 -0
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/models-config.ts +1 -0
- package/src/config/settings-schema.ts +53 -0
- package/src/edit/hashline/block-resolver.ts +1 -1
- package/src/edit/hashline/execute.ts +1 -6
- package/src/edit/index.ts +48 -0
- package/src/eval/__tests__/js-context-manager.test.ts +41 -1
- package/src/eval/js/context-manager.ts +92 -26
- package/src/eval/js/worker-core.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +9 -2
- package/src/modes/components/welcome.ts +14 -4
- package/src/modes/controllers/input-controller.ts +21 -38
- package/src/modes/rpc/rpc-mode.ts +1 -0
- package/src/modes/rpc/rpc-types.ts +2 -2
- package/src/prompts/system/system-prompt.md +17 -21
- package/src/prompts/tools/ask.md +0 -8
- package/src/prompts/tools/ast-edit.md +0 -15
- package/src/prompts/tools/ast-grep.md +0 -13
- package/src/prompts/tools/browser.md +0 -21
- package/src/prompts/tools/debug.md +0 -13
- package/src/prompts/tools/eval.md +0 -9
- package/src/prompts/tools/find.md +0 -13
- package/src/prompts/tools/inspect-image.md +0 -9
- package/src/prompts/tools/irc.md +0 -15
- package/src/prompts/tools/patch.md +0 -13
- package/src/prompts/tools/ssh.md +0 -9
- package/src/prompts/tools/todo.md +1 -19
- package/src/sdk.ts +19 -0
- package/src/session/agent-session.ts +125 -19
- package/src/session/session-dump-format.ts +10 -31
- package/src/system-prompt.ts +31 -0
- package/src/tools/ask.ts +41 -0
- package/src/tools/ast-edit.ts +46 -0
- package/src/tools/ast-grep.ts +24 -0
- package/src/tools/browser.ts +52 -0
- package/src/tools/debug.ts +17 -0
- package/src/tools/eval.ts +20 -1
- package/src/tools/find.ts +24 -0
- package/src/tools/inspect-image.ts +27 -1
- package/src/tools/irc.ts +41 -0
- package/src/tools/ssh.ts +16 -0
- package/src/tools/todo.ts +82 -3
- package/src/tui/tree-list.ts +68 -19
|
@@ -46,7 +46,9 @@ import {
|
|
|
46
46
|
collectEntriesForBranchSummary,
|
|
47
47
|
collectShakeRegions,
|
|
48
48
|
compact,
|
|
49
|
+
createCompactionSummaryMessage,
|
|
49
50
|
DEFAULT_SHAKE_CONFIG,
|
|
51
|
+
effectiveReserveTokens,
|
|
50
52
|
estimateTokens,
|
|
51
53
|
generateBranchSummary,
|
|
52
54
|
generateHandoff,
|
|
@@ -6423,6 +6425,37 @@ export class AgentSession {
|
|
|
6423
6425
|
let tokensBefore: number;
|
|
6424
6426
|
let details: unknown;
|
|
6425
6427
|
|
|
6428
|
+
// Snapcompact runs locally first; if its frame archive plus the kept
|
|
6429
|
+
// history still overflows the model window, fall back to an LLM summary
|
|
6430
|
+
// (far cheaper than ~FRAME_TOKEN_ESTIMATE per frame).
|
|
6431
|
+
let snapcompactResult: snapcompact.CompactionResult | undefined;
|
|
6432
|
+
if (snapcompactReady) {
|
|
6433
|
+
snapcompactResult = await snapcompact.compact(preparation, {
|
|
6434
|
+
convertToLlm,
|
|
6435
|
+
model: this.model,
|
|
6436
|
+
shape: snapcompact.resolveShape(this.model, this.settings.get("snapcompact.shape")),
|
|
6437
|
+
// Providers with hard image caps (OpenRouter: 8) silently drop
|
|
6438
|
+
// frames past the cap — keep the archive within budget.
|
|
6439
|
+
maxFrames: snapcompact.providerFrameBudget(this.model?.provider),
|
|
6440
|
+
});
|
|
6441
|
+
const ctxWindow = this.model?.contextWindow ?? 0;
|
|
6442
|
+
const budget =
|
|
6443
|
+
ctxWindow > 0
|
|
6444
|
+
? ctxWindow - effectiveReserveTokens(ctxWindow, compactionSettings)
|
|
6445
|
+
: Number.POSITIVE_INFINITY;
|
|
6446
|
+
if (this.#projectSnapcompactContextTokens(preparation, snapcompactResult) > budget) {
|
|
6447
|
+
logger.warn("Snapcompact still overflows the window; falling back to an LLM summary", {
|
|
6448
|
+
model: this.model?.id,
|
|
6449
|
+
});
|
|
6450
|
+
this.emitNotice(
|
|
6451
|
+
"warning",
|
|
6452
|
+
"snapcompact could not bring the context under the limit — using an LLM summary instead",
|
|
6453
|
+
"compaction",
|
|
6454
|
+
);
|
|
6455
|
+
snapcompactResult = undefined;
|
|
6456
|
+
}
|
|
6457
|
+
}
|
|
6458
|
+
|
|
6426
6459
|
if (compactionPrep.kind === "fromHook") {
|
|
6427
6460
|
summary = compactionPrep.summary;
|
|
6428
6461
|
shortSummary = compactionPrep.shortSummary;
|
|
@@ -6430,15 +6463,7 @@ export class AgentSession {
|
|
|
6430
6463
|
tokensBefore = compactionPrep.tokensBefore;
|
|
6431
6464
|
details = compactionPrep.details;
|
|
6432
6465
|
preserveData = compactionPrep.preserveData;
|
|
6433
|
-
} else if (
|
|
6434
|
-
const snapcompactResult = await snapcompact.compact(preparation, {
|
|
6435
|
-
convertToLlm,
|
|
6436
|
-
model: this.model,
|
|
6437
|
-
shape: snapcompact.resolveShape(this.model, this.settings.get("snapcompact.shape")),
|
|
6438
|
-
// Providers with hard image caps (OpenRouter: 8) silently drop
|
|
6439
|
-
// frames past the cap — keep the archive within budget.
|
|
6440
|
-
maxFrames: snapcompact.providerFrameBudget(this.model.provider),
|
|
6441
|
-
});
|
|
6466
|
+
} else if (snapcompactResult) {
|
|
6442
6467
|
summary = snapcompactResult.summary;
|
|
6443
6468
|
shortSummary = snapcompactResult.shortSummary;
|
|
6444
6469
|
firstKeptEntryId = snapcompactResult.firstKeptEntryId;
|
|
@@ -6755,6 +6780,19 @@ export class AgentSession {
|
|
|
6755
6780
|
const contextTokens = this.#estimatePendingPromptTokens(messages);
|
|
6756
6781
|
if (!shouldCompact(contextTokens, contextWindow, compactionSettings)) return;
|
|
6757
6782
|
|
|
6783
|
+
// Auto-promote first: switching to a larger-context model avoids compacting
|
|
6784
|
+
// the history at all. The post-turn threshold path already promotes before
|
|
6785
|
+
// compacting; without this, the pre-prompt path would pre-empt promotion and
|
|
6786
|
+
// compact (snapcompact/summary) a session that should have just been promoted.
|
|
6787
|
+
if (await this.#promoteContextModel()) {
|
|
6788
|
+
logger.debug("Pre-prompt context promotion avoided compaction", {
|
|
6789
|
+
contextTokens,
|
|
6790
|
+
contextWindow,
|
|
6791
|
+
model: `${model.provider}/${model.id}`,
|
|
6792
|
+
});
|
|
6793
|
+
return;
|
|
6794
|
+
}
|
|
6795
|
+
|
|
6758
6796
|
logger.debug("Pre-prompt context maintenance triggered by pending prompt size", {
|
|
6759
6797
|
contextTokens,
|
|
6760
6798
|
contextWindow,
|
|
@@ -7324,12 +7362,28 @@ export class AgentSession {
|
|
|
7324
7362
|
* Returns true if promotion succeeded (caller should retry without compacting).
|
|
7325
7363
|
*/
|
|
7326
7364
|
async #tryContextPromotion(assistantMessage: AssistantMessage): Promise<boolean> {
|
|
7327
|
-
const promotionSettings = this.settings.getGroup("contextPromotion");
|
|
7328
|
-
if (!promotionSettings.enabled) return false;
|
|
7329
7365
|
const currentModel = this.model;
|
|
7330
7366
|
if (!currentModel) return false;
|
|
7367
|
+
// The overflow/length error may have come from a model the user already
|
|
7368
|
+
// switched away from; only promote when the failing turn was this model.
|
|
7331
7369
|
if (assistantMessage.provider !== currentModel.provider || assistantMessage.model !== currentModel.id)
|
|
7332
7370
|
return false;
|
|
7371
|
+
return this.#promoteContextModel();
|
|
7372
|
+
}
|
|
7373
|
+
|
|
7374
|
+
/**
|
|
7375
|
+
* Switch to a larger-context sibling when context promotion is enabled and a
|
|
7376
|
+
* target with a strictly larger window (and a usable key) exists. Returns true
|
|
7377
|
+
* when the model was switched, so the caller can retry without compacting.
|
|
7378
|
+
* Message-independent core shared by the post-turn overflow path
|
|
7379
|
+
* ({@link #tryContextPromotion}) and the pre-prompt threshold path
|
|
7380
|
+
* ({@link #runPrePromptCompactionIfNeeded}).
|
|
7381
|
+
*/
|
|
7382
|
+
async #promoteContextModel(): Promise<boolean> {
|
|
7383
|
+
const promotionSettings = this.settings.getGroup("contextPromotion");
|
|
7384
|
+
if (!promotionSettings.enabled) return false;
|
|
7385
|
+
const currentModel = this.model;
|
|
7386
|
+
if (!currentModel) return false;
|
|
7333
7387
|
const contextWindow = currentModel.contextWindow ?? 0;
|
|
7334
7388
|
if (contextWindow <= 0) return false;
|
|
7335
7389
|
const targetModel = await this.#resolveContextPromotionTarget(currentModel, contextWindow);
|
|
@@ -7806,6 +7860,32 @@ export class AgentSession {
|
|
|
7806
7860
|
return { kind: "needsLlm", hookContext, hookPrompt, preserveData };
|
|
7807
7861
|
}
|
|
7808
7862
|
|
|
7863
|
+
/**
|
|
7864
|
+
* Project the post-compaction context size of a snapcompact result: kept
|
|
7865
|
+
* recent messages + the summary message with its re-attached frames + the
|
|
7866
|
+
* fixed non-message overhead (system prompt + tools). Mirrors how the
|
|
7867
|
+
* compacted context is rebuilt, so the estimate matches the wire shape, and
|
|
7868
|
+
* lets the caller decide whether snapcompact brought the context under the
|
|
7869
|
+
* window or should fall back to an LLM summary.
|
|
7870
|
+
*/
|
|
7871
|
+
#projectSnapcompactContextTokens(preparation: CompactionPreparation, result: snapcompact.CompactionResult): number {
|
|
7872
|
+
const archive = snapcompact.getPreservedArchive(result.preserveData);
|
|
7873
|
+
const frames = archive ? snapcompact.images(archive) : undefined;
|
|
7874
|
+
const summaryMessage = createCompactionSummaryMessage(
|
|
7875
|
+
result.summary,
|
|
7876
|
+
result.tokensBefore,
|
|
7877
|
+
new Date().toISOString(),
|
|
7878
|
+
result.shortSummary,
|
|
7879
|
+
undefined,
|
|
7880
|
+
frames,
|
|
7881
|
+
);
|
|
7882
|
+
let tokens = computeNonMessageTokens(this) + estimateTokens(summaryMessage);
|
|
7883
|
+
for (const message of preparation.recentMessages) {
|
|
7884
|
+
tokens += estimateTokens(message);
|
|
7885
|
+
}
|
|
7886
|
+
return tokens;
|
|
7887
|
+
}
|
|
7888
|
+
|
|
7809
7889
|
/**
|
|
7810
7890
|
* Internal: Run auto-compaction with events.
|
|
7811
7891
|
*
|
|
@@ -8018,6 +8098,39 @@ export class AgentSession {
|
|
|
8018
8098
|
let tokensBefore: number;
|
|
8019
8099
|
let details: unknown;
|
|
8020
8100
|
|
|
8101
|
+
// Snapcompact runs locally first; if its frame archive plus the kept
|
|
8102
|
+
// history still overflows the model window (frames are capped by the
|
|
8103
|
+
// image budget and cost ~FRAME_TOKEN_ESTIMATE each), an LLM summary is
|
|
8104
|
+
// far cheaper — downgrade to context-full and take the summarizer path.
|
|
8105
|
+
let snapcompactResult: snapcompact.CompactionResult | undefined;
|
|
8106
|
+
if (action === "snapcompact" && compactionPrep.kind !== "fromHook") {
|
|
8107
|
+
snapcompactResult = await snapcompact.compact(preparation, {
|
|
8108
|
+
convertToLlm,
|
|
8109
|
+
model: this.model,
|
|
8110
|
+
maxFrames: snapcompact.providerFrameBudget(this.model?.provider),
|
|
8111
|
+
});
|
|
8112
|
+
const ctxWindow = this.model?.contextWindow ?? 0;
|
|
8113
|
+
const budget =
|
|
8114
|
+
ctxWindow > 0
|
|
8115
|
+
? ctxWindow - effectiveReserveTokens(ctxWindow, compactionSettings)
|
|
8116
|
+
: Number.POSITIVE_INFINITY;
|
|
8117
|
+
const projected = this.#projectSnapcompactContextTokens(preparation, snapcompactResult);
|
|
8118
|
+
if (projected > budget) {
|
|
8119
|
+
logger.warn("Snapcompact still overflows the window; falling back to an LLM summary", {
|
|
8120
|
+
model: this.model?.id,
|
|
8121
|
+
projected,
|
|
8122
|
+
budget,
|
|
8123
|
+
});
|
|
8124
|
+
this.emitNotice(
|
|
8125
|
+
"warning",
|
|
8126
|
+
"snapcompact could not bring the context under the limit — using an LLM summary instead",
|
|
8127
|
+
"compaction",
|
|
8128
|
+
);
|
|
8129
|
+
action = "context-full";
|
|
8130
|
+
snapcompactResult = undefined;
|
|
8131
|
+
}
|
|
8132
|
+
}
|
|
8133
|
+
|
|
8021
8134
|
if (compactionPrep.kind === "fromHook") {
|
|
8022
8135
|
summary = compactionPrep.summary;
|
|
8023
8136
|
shortSummary = compactionPrep.shortSummary;
|
|
@@ -8025,14 +8138,7 @@ export class AgentSession {
|
|
|
8025
8138
|
tokensBefore = compactionPrep.tokensBefore;
|
|
8026
8139
|
details = compactionPrep.details;
|
|
8027
8140
|
preserveData = compactionPrep.preserveData;
|
|
8028
|
-
} else if (
|
|
8029
|
-
// Local, deterministic: render discarded history onto PNG frames.
|
|
8030
|
-
// No model candidates, no API key, no retry loop.
|
|
8031
|
-
const snapcompactResult = await snapcompact.compact(preparation, {
|
|
8032
|
-
convertToLlm,
|
|
8033
|
-
model: this.model,
|
|
8034
|
-
maxFrames: snapcompact.providerFrameBudget(this.model?.provider),
|
|
8035
|
-
});
|
|
8141
|
+
} else if (snapcompactResult) {
|
|
8036
8142
|
summary = snapcompactResult.summary;
|
|
8037
8143
|
shortSummary = snapcompactResult.shortSummary;
|
|
8038
8144
|
firstKeptEntryId = snapcompactResult.firstKeptEntryId;
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
|
|
6
|
-
import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai";
|
|
7
|
-
import {
|
|
6
|
+
import type { AssistantMessage, Model, ToolExample, TSchema } from "@oh-my-pi/pi-ai";
|
|
7
|
+
import { renderToolInventory } from "@oh-my-pi/pi-ai/grammar";
|
|
8
8
|
import { getVisibleThinkingText } from "../utils/thinking-display";
|
|
9
9
|
import {
|
|
10
10
|
type BashExecutionMessage,
|
|
@@ -23,6 +23,7 @@ export interface SessionDumpToolInfo {
|
|
|
23
23
|
name: string;
|
|
24
24
|
description: string;
|
|
25
25
|
parameters: unknown;
|
|
26
|
+
examples?: readonly ToolExample[];
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export interface FormatSessionDumpTextOptions {
|
|
@@ -33,28 +34,6 @@ export interface FormatSessionDumpTextOptions {
|
|
|
33
34
|
tools?: readonly SessionDumpToolInfo[];
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
function stripTypeBoxFields(obj: unknown): unknown {
|
|
37
|
-
if (Array.isArray(obj)) {
|
|
38
|
-
return obj.map(stripTypeBoxFields);
|
|
39
|
-
}
|
|
40
|
-
if (obj && typeof obj === "object") {
|
|
41
|
-
const result: Record<string, unknown> = {};
|
|
42
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
43
|
-
if (!k.startsWith("TypeBox.")) {
|
|
44
|
-
result[k] = stripTypeBoxFields(v);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return result;
|
|
48
|
-
}
|
|
49
|
-
return obj;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Resolve tool parameters to a plain JSON Schema object for dump output. */
|
|
53
|
-
function toolParametersToJsonSchema(parameters: unknown): unknown {
|
|
54
|
-
if (isZodSchema(parameters)) return zodToWireSchema(parameters);
|
|
55
|
-
return stripTypeBoxFields(parameters);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
37
|
/** Serialize an object as XML parameter elements, one per key. */
|
|
59
38
|
function formatArgsAsXml(args: Record<string, unknown>, indent = "\t"): string {
|
|
60
39
|
const parts: string[] = [];
|
|
@@ -94,13 +73,13 @@ export function formatSessionDumpText(options: FormatSessionDumpTextOptions): st
|
|
|
94
73
|
const tools = options.tools ?? [];
|
|
95
74
|
if (tools.length > 0) {
|
|
96
75
|
lines.push("## Available Tools\n");
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
76
|
+
const inventoryTools = tools.map(tool => ({
|
|
77
|
+
name: tool.name,
|
|
78
|
+
description: tool.description,
|
|
79
|
+
parameters: tool.parameters as TSchema,
|
|
80
|
+
examples: tool.examples,
|
|
81
|
+
}));
|
|
82
|
+
lines.push(renderToolInventory(inventoryTools, options.model?.id ?? ""));
|
|
104
83
|
lines.push("\n");
|
|
105
84
|
}
|
|
106
85
|
|
package/src/system-prompt.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
7
|
+
import type { ToolExample, TSchema } from "@oh-my-pi/pi-ai";
|
|
8
|
+
import { renderToolInventory } from "@oh-my-pi/pi-ai/grammar";
|
|
7
9
|
import { $env, getGpuCachePath, getProjectDir, hasFsCode, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
|
|
8
10
|
import { $ } from "bun";
|
|
9
11
|
import { contextFileCapability } from "./capability/context-file";
|
|
@@ -330,6 +332,10 @@ export interface SystemPromptToolMetadata {
|
|
|
330
332
|
description: string;
|
|
331
333
|
/** Tool name the model sees on the provider wire. Defaults to the internal tool name. */
|
|
332
334
|
wireName?: string;
|
|
335
|
+
/** Tool parameters schema (Zod or JSON Schema), fed to the verbose inventory renderer. */
|
|
336
|
+
parameters?: TSchema;
|
|
337
|
+
/** Illustrative examples rendered into the verbose inventory. */
|
|
338
|
+
examples?: readonly ToolExample[];
|
|
333
339
|
}
|
|
334
340
|
|
|
335
341
|
export function buildSystemPromptToolMetadata(
|
|
@@ -349,6 +355,8 @@ export function buildSystemPromptToolMetadata(
|
|
|
349
355
|
label: override?.label ?? (typeof toolRecord.label === "string" ? toolRecord.label : ""),
|
|
350
356
|
description:
|
|
351
357
|
override?.description ?? (typeof toolRecord.description === "string" ? toolRecord.description : ""),
|
|
358
|
+
parameters: toolRecord.parameters,
|
|
359
|
+
examples: toolRecord.examples,
|
|
352
360
|
wireName,
|
|
353
361
|
},
|
|
354
362
|
] as const;
|
|
@@ -367,6 +375,12 @@ export interface BuildSystemPromptOptions {
|
|
|
367
375
|
appendSystemPrompt?: string;
|
|
368
376
|
/** Repeat full tool descriptions in system prompt. Default: false */
|
|
369
377
|
repeatToolDescriptions?: boolean;
|
|
378
|
+
/**
|
|
379
|
+
* Whether provider-native tool calling is active (no owned/in-band syntax).
|
|
380
|
+
* When true and `repeatToolDescriptions` is false, the inventory renders as a
|
|
381
|
+
* compact tool-name list; otherwise it renders full `# Tool:` sections. Default: true
|
|
382
|
+
*/
|
|
383
|
+
nativeTools?: boolean;
|
|
370
384
|
/** Skills settings for discovery. */
|
|
371
385
|
skillsSettings?: SkillsSettings;
|
|
372
386
|
/** Working directory. Default: getProjectDir() */
|
|
@@ -420,6 +434,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
420
434
|
tools,
|
|
421
435
|
appendSystemPrompt,
|
|
422
436
|
repeatToolDescriptions = false,
|
|
437
|
+
nativeTools = true,
|
|
423
438
|
skillsSettings,
|
|
424
439
|
toolNames: providedToolNames,
|
|
425
440
|
cwd,
|
|
@@ -575,6 +590,20 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
575
590
|
label: tools?.get(name)?.label ?? "",
|
|
576
591
|
description: tools?.get(name)?.description ?? "",
|
|
577
592
|
}));
|
|
593
|
+
const inventoryTools = toolNames.map(name => {
|
|
594
|
+
const meta = tools?.get(name);
|
|
595
|
+
return {
|
|
596
|
+
name: toolPromptNames.get(name) ?? name,
|
|
597
|
+
description: meta?.description ?? "",
|
|
598
|
+
parameters: meta?.parameters ?? ({ type: "object" } as TSchema),
|
|
599
|
+
examples: meta?.examples,
|
|
600
|
+
};
|
|
601
|
+
});
|
|
602
|
+
// List mode shows a compact tool-name list; it only applies when descriptions
|
|
603
|
+
// are not repeated AND native tool calling is active (the model already has the
|
|
604
|
+
// schemas). Otherwise render full `# Tool:` sections.
|
|
605
|
+
const toolListMode = !repeatToolDescriptions && nativeTools;
|
|
606
|
+
const toolInventory = toolListMode ? "" : renderToolInventory(inventoryTools, model ?? "");
|
|
578
607
|
|
|
579
608
|
// Filter skills for the rendered system prompt:
|
|
580
609
|
// - require the `read` tool so the model can actually fetch skill content;
|
|
@@ -596,7 +625,9 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
596
625
|
appendPrompt: resolvedAppendPrompt ?? "",
|
|
597
626
|
tools: toolNames,
|
|
598
627
|
toolInfo,
|
|
628
|
+
toolInventory,
|
|
599
629
|
repeatToolDescriptions,
|
|
630
|
+
toolListMode,
|
|
600
631
|
toolRefs,
|
|
601
632
|
environment,
|
|
602
633
|
contextFiles,
|
package/src/tools/ask.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
19
|
+
import type { ToolExample } from "@oh-my-pi/pi-ai";
|
|
19
20
|
import { type Component, Markdown, type MarkdownTheme, renderInlineMarkdown, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
20
21
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
21
22
|
import { z } from "zod/v4";
|
|
@@ -422,6 +423,46 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
422
423
|
readonly description: string;
|
|
423
424
|
readonly parameters = askSchema;
|
|
424
425
|
readonly strict = true;
|
|
426
|
+
|
|
427
|
+
readonly examples: readonly ToolExample<z.input<typeof askSchema>>[] = [
|
|
428
|
+
{
|
|
429
|
+
caption: "Single question",
|
|
430
|
+
call: {
|
|
431
|
+
questions: [
|
|
432
|
+
{
|
|
433
|
+
id: "auth_method",
|
|
434
|
+
question: "Which authentication method should this API use?",
|
|
435
|
+
options: [
|
|
436
|
+
{ label: "JWT", description: "Bearer tokens for stateless API clients." },
|
|
437
|
+
{ label: "OAuth2", description: "Delegated authorization with external identity providers." },
|
|
438
|
+
{
|
|
439
|
+
label: "Session cookies",
|
|
440
|
+
description: "Browser-first authentication backed by server-side sessions.",
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
recommended: 0,
|
|
444
|
+
},
|
|
445
|
+
],
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
caption: "Multiple questions",
|
|
450
|
+
call: {
|
|
451
|
+
questions: [
|
|
452
|
+
{
|
|
453
|
+
id: "storage_type",
|
|
454
|
+
question: "Which storage backend?",
|
|
455
|
+
options: [{ label: "SQLite" }, { label: "PostgreSQL" }],
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
id: "auth_method",
|
|
459
|
+
question: "Which auth method?",
|
|
460
|
+
options: [{ label: "JWT" }, { label: "Session cookies" }],
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
];
|
|
425
466
|
// Run alone in its tool batch. The interactive selector/editor is a single
|
|
426
467
|
// shared UI surface (`ExtensionUiController.showHookSelector` has no queue and
|
|
427
468
|
// overwrites `ctx.hookSelector` on each call), so two concurrent `ask` calls
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { formatHashlineHeader } from "@oh-my-pi/hashline";
|
|
3
3
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
4
|
+
import type { ToolExample } from "@oh-my-pi/pi-ai";
|
|
4
5
|
import { type AstReplaceChange, type AstReplaceFileChange, astEdit } from "@oh-my-pi/pi-natives";
|
|
5
6
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
7
|
import { replaceTabs, Text } from "@oh-my-pi/pi-tui";
|
|
@@ -194,6 +195,51 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
194
195
|
readonly description: string;
|
|
195
196
|
readonly parameters = astEditSchema;
|
|
196
197
|
readonly strict = true;
|
|
198
|
+
|
|
199
|
+
readonly examples: readonly ToolExample<z.input<typeof astEditSchema>>[] = [
|
|
200
|
+
{
|
|
201
|
+
caption: "Rename a call site across TypeScript files",
|
|
202
|
+
call: {
|
|
203
|
+
ops: [{ pat: "oldApi($$$ARGS)", out: "newApi($$$ARGS)" }],
|
|
204
|
+
paths: ["src/**/*.ts"],
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
caption: "Delete matching calls",
|
|
209
|
+
call: {
|
|
210
|
+
ops: [{ pat: "console.log($$$ARGS)", out: "" }],
|
|
211
|
+
paths: ["src/**/*.ts"],
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
caption: "Rewrite import source path",
|
|
216
|
+
call: {
|
|
217
|
+
ops: [{ pat: 'import { $$$IMPORTS } from "old-package"', out: 'import { $$$IMPORTS } from "new-package"' }],
|
|
218
|
+
paths: ["src/**/*.ts"],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
caption: "Modernize to optional chaining (same metavariable enforces identity)",
|
|
223
|
+
call: {
|
|
224
|
+
ops: [{ pat: "$A && $A()", out: "$A?.()" }],
|
|
225
|
+
paths: ["src/**/*.ts"],
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
caption: "Swap two arguments using captures",
|
|
230
|
+
call: {
|
|
231
|
+
ops: [{ pat: "assertEqual($A, $B)", out: "assertEqual($B, $A)" }],
|
|
232
|
+
paths: ["tests/**/*.ts"],
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
caption: "Python — convert print calls to logging",
|
|
237
|
+
call: {
|
|
238
|
+
ops: [{ pat: "print($$$ARGS)", out: "logger.info($$$ARGS)" }],
|
|
239
|
+
paths: ["src/**/*.py"],
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
];
|
|
197
243
|
readonly deferrable = true;
|
|
198
244
|
readonly loadMode = "discoverable";
|
|
199
245
|
constructor(private readonly session: ToolSession) {
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { formatHashlineHeader } from "@oh-my-pi/hashline";
|
|
3
3
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
4
|
+
import type { ToolExample } from "@oh-my-pi/pi-ai";
|
|
4
5
|
import { type AstFindMatch, astGrep } from "@oh-my-pi/pi-natives";
|
|
5
6
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
7
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
@@ -130,6 +131,29 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
130
131
|
readonly description: string;
|
|
131
132
|
readonly parameters = astGrepSchema;
|
|
132
133
|
readonly strict = true;
|
|
134
|
+
|
|
135
|
+
readonly examples: readonly ToolExample<z.input<typeof astGrepSchema>>[] = [
|
|
136
|
+
{
|
|
137
|
+
caption: "Search TypeScript files under src",
|
|
138
|
+
call: { pat: "console.log($$$)", paths: ["src/**/*.ts"] },
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
caption: "Named imports from a specific package",
|
|
142
|
+
call: { pat: 'import { $$$IMPORTS } from "react"', paths: ["src/**/*.ts"] },
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
caption: "Arrow functions assigned to a const",
|
|
146
|
+
call: { pat: "const $NAME = ($$$ARGS) => $BODY", paths: ["src/utils/**/*.ts"] },
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
caption: "Method call on any object, ignoring method name with `$_`",
|
|
150
|
+
call: { pat: "logger.$_($$$ARGS)", paths: ["src/**/*.ts"] },
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
caption: "Loosest existence check for a symbol in one file",
|
|
154
|
+
call: { pat: "processItems", paths: ["src/worker.ts"] },
|
|
155
|
+
},
|
|
156
|
+
];
|
|
133
157
|
readonly loadMode = "discoverable";
|
|
134
158
|
|
|
135
159
|
constructor(private readonly session: ToolSession) {
|
package/src/tools/browser.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { ToolExample } from "@oh-my-pi/pi-ai";
|
|
2
3
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
3
4
|
import { z } from "zod/v4";
|
|
4
5
|
import browserDescription from "../prompts/tools/browser.md" with { type: "text" };
|
|
@@ -118,6 +119,57 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
118
119
|
readonly parameters = browserSchema;
|
|
119
120
|
readonly strict = true;
|
|
120
121
|
|
|
122
|
+
readonly examples: readonly ToolExample<z.input<typeof browserSchema>>[] = [
|
|
123
|
+
{
|
|
124
|
+
caption: "Open a tab",
|
|
125
|
+
call: { action: "open", name: "docs", url: "https://example.com" },
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
caption: "Read structured page data in the opened tab",
|
|
129
|
+
call: {
|
|
130
|
+
action: "run",
|
|
131
|
+
name: "docs",
|
|
132
|
+
code: "const obs = await tab.observe(); display(obs); return obs.elements.length;",
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
caption: "Click an observed element by id",
|
|
137
|
+
call: {
|
|
138
|
+
action: "run",
|
|
139
|
+
name: "docs",
|
|
140
|
+
code: "const obs = await tab.observe(); const link = obs.elements.find(e => e.role === 'link' && e.name === 'Sign in'); assert(link, 'Sign in link missing'); await (await tab.id(link.id)).click();",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
caption: "Fill and submit a form via selectors",
|
|
145
|
+
call: {
|
|
146
|
+
action: "run",
|
|
147
|
+
name: "docs",
|
|
148
|
+
code: "await tab.fill('input[name=email]', 'me@example.com'); await tab.click('text/Continue');",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
caption: "Screenshot to look at the page — no save path",
|
|
153
|
+
call: {
|
|
154
|
+
action: "run",
|
|
155
|
+
name: "docs",
|
|
156
|
+
code: "await tab.screenshot();",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
caption: "Attach to an existing Electron app",
|
|
161
|
+
call: {
|
|
162
|
+
action: "open",
|
|
163
|
+
name: "cursor",
|
|
164
|
+
app: { path: "/Applications/Cursor.app/Contents/MacOS/Cursor" },
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
caption: "Close every tab and kill spawned-app processes",
|
|
169
|
+
call: { action: "close", all: true, kill: true },
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
|
|
121
173
|
constructor(private readonly session: ToolSession) {}
|
|
122
174
|
#description?: string;
|
|
123
175
|
get description(): string {
|
package/src/tools/debug.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
RenderResultOptions,
|
|
8
8
|
ToolApprovalDecision,
|
|
9
9
|
} from "@oh-my-pi/pi-agent-core";
|
|
10
|
+
import type { ToolExample } from "@oh-my-pi/pi-ai";
|
|
10
11
|
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
11
12
|
import { isEnoent, prompt } from "@oh-my-pi/pi-utils";
|
|
12
13
|
import { z } from "zod/v4";
|
|
@@ -659,6 +660,22 @@ export class DebugTool implements AgentTool<typeof debugSchema, DebugToolDetails
|
|
|
659
660
|
readonly description: string;
|
|
660
661
|
readonly parameters = debugSchema;
|
|
661
662
|
readonly strict = true;
|
|
663
|
+
|
|
664
|
+
readonly examples: readonly ToolExample<z.input<typeof debugSchema>>[] = [
|
|
665
|
+
{
|
|
666
|
+
caption: "Launch and inspect hang",
|
|
667
|
+
note: '1. debug(action: "launch", program: "./my_app")\n2. debug(action: "set_breakpoint", file: "src/main.c", line: 42)\n3. debug(action: "continue")\n4. If the program appears hung: debug(action: "pause")\n5. Inspect state with `threads`, `stack_trace`, `scopes`, and `variables`',
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
caption: "Launch a Python script with debugpy",
|
|
671
|
+
call: { action: "launch", adapter: "debugpy", program: "scripts/job.py", args: ["--flag"] },
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
caption: "Raw debugger command through repl",
|
|
675
|
+
call: { action: "evaluate", expression: "info registers", context: "repl" },
|
|
676
|
+
},
|
|
677
|
+
];
|
|
678
|
+
|
|
662
679
|
readonly concurrency = "exclusive";
|
|
663
680
|
readonly loadMode = "discoverable";
|
|
664
681
|
|
package/src/tools/eval.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import type { ImageContent, ToolExample } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { z } from "zod/v4";
|
|
5
5
|
import { jsBackend, pythonBackend } from "../eval";
|
|
@@ -183,6 +183,25 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
|
|
183
183
|
const spawnsAllowed = sessionSpawns !== "" && sessionSpawns !== null;
|
|
184
184
|
return getEvalToolDescription({ py: backends.python, js: backends.js, spawns: spawnsAllowed });
|
|
185
185
|
}
|
|
186
|
+
readonly examples: readonly ToolExample<z.input<typeof evalSchema>>[] = [
|
|
187
|
+
{
|
|
188
|
+
call: {
|
|
189
|
+
cells: [
|
|
190
|
+
{
|
|
191
|
+
language: "py",
|
|
192
|
+
title: "imports",
|
|
193
|
+
timeout: 10,
|
|
194
|
+
code: "import json\nfrom pathlib import Path",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
language: "py",
|
|
198
|
+
title: "load config",
|
|
199
|
+
code: "data = json.loads(read('package.json'))\ndisplay(data)",
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
];
|
|
186
205
|
readonly parameters = evalSchema;
|
|
187
206
|
readonly concurrency = "exclusive";
|
|
188
207
|
readonly strict = true;
|
package/src/tools/find.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
4
|
+
import type { ToolExample } from "@oh-my-pi/pi-ai";
|
|
4
5
|
import * as natives from "@oh-my-pi/pi-natives";
|
|
5
6
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
7
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
@@ -106,6 +107,29 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
106
107
|
readonly label = "Find";
|
|
107
108
|
readonly description: string;
|
|
108
109
|
readonly parameters = findSchema;
|
|
110
|
+
|
|
111
|
+
readonly examples: readonly ToolExample<z.input<typeof findSchema>>[] = [
|
|
112
|
+
{
|
|
113
|
+
caption: "Find files",
|
|
114
|
+
call: { paths: ["src/**/*.ts"] },
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
caption: "Multiple targets — separate array elements",
|
|
118
|
+
call: { paths: ["src/**/*.ts", "test/**/*.ts"] },
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
caption: "Find gitignored files like .env",
|
|
122
|
+
call: { paths: [".env*"], gitignore: false },
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
caption: "Find directories matching a name (returns both files and dirs; directories are suffixed with `/`)",
|
|
126
|
+
call: { paths: ["**/tests"] },
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
caption: "Long-running search on a slow volume",
|
|
130
|
+
call: { paths: ["/Volumes/Storage/**/*.py"], timeout: 30 },
|
|
131
|
+
},
|
|
132
|
+
];
|
|
109
133
|
readonly strict = true;
|
|
110
134
|
|
|
111
135
|
readonly #customOps?: FindOperations;
|