@oh-my-pi/pi-coding-agent 13.15.3 → 13.16.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 +30 -16
- package/package.json +7 -7
- package/src/commit/agentic/tools/analyze-file.ts +1 -0
- package/src/config/model-registry.ts +215 -57
- package/src/config/settings-schema.ts +27 -0
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +7 -0
- package/src/extensibility/extensions/types.ts +10 -1
- package/src/extensibility/hooks/types.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/ipy/cancellation.ts +28 -0
- package/src/ipy/executor.ts +252 -77
- package/src/ipy/kernel.ts +181 -35
- package/src/ipy/modules.ts +39 -4
- package/src/modes/acp/acp-agent.ts +1 -0
- package/src/modes/components/hook-editor.ts +57 -8
- package/src/modes/components/model-selector.ts +48 -29
- package/src/modes/components/settings-defs.ts +10 -1
- package/src/modes/components/settings-selector.ts +92 -5
- package/src/modes/controllers/extension-ui-controller.ts +35 -4
- package/src/modes/controllers/input-controller.ts +4 -3
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/interactive-mode.ts +7 -2
- package/src/modes/print-mode.ts +1 -0
- package/src/modes/prompt-action-autocomplete.ts +5 -3
- package/src/modes/rpc/rpc-mode.ts +79 -30
- package/src/modes/rpc/rpc-types.ts +9 -1
- package/src/modes/theme/theme.ts +70 -0
- package/src/modes/types.ts +6 -1
- package/src/prompts/system/custom-system-prompt.md +5 -0
- package/src/prompts/system/system-prompt.md +6 -0
- package/src/prompts/tools/ask.md +1 -0
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/hashline.md +20 -5
- package/src/sdk.ts +26 -2
- package/src/session/agent-session.ts +18 -11
- package/src/system-prompt.ts +63 -2
- package/src/task/executor.ts +4 -0
- package/src/task/index.ts +2 -0
- package/src/tools/ask.ts +109 -61
- package/src/tools/ast-edit.ts +2 -16
- package/src/tools/ast-grep.ts +2 -17
- package/src/tools/browser.ts +35 -17
- package/src/tools/find.ts +1 -0
- package/src/tools/grep.ts +25 -34
- package/src/tools/index.ts +3 -0
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/python.ts +3 -2
- package/src/tools/render-utils.ts +27 -0
- package/src/tui/tree-list.ts +51 -22
|
@@ -29,6 +29,77 @@ import type {
|
|
|
29
29
|
// Re-export types for consumers
|
|
30
30
|
export type * from "./rpc-types";
|
|
31
31
|
|
|
32
|
+
export type PendingExtensionRequest = {
|
|
33
|
+
resolve: (response: RpcExtensionUIResponse) => void;
|
|
34
|
+
reject: (error: Error) => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type RpcOutput = (obj: RpcResponse | RpcExtensionUIRequest | object) => void;
|
|
38
|
+
|
|
39
|
+
export function requestRpcEditor(
|
|
40
|
+
pendingRequests: Map<string, PendingExtensionRequest>,
|
|
41
|
+
output: RpcOutput,
|
|
42
|
+
title: string,
|
|
43
|
+
prefill?: string,
|
|
44
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
45
|
+
editorOptions?: { promptStyle?: boolean },
|
|
46
|
+
): Promise<string | undefined> {
|
|
47
|
+
if (dialogOptions?.signal?.aborted) return Promise.resolve(undefined);
|
|
48
|
+
|
|
49
|
+
const id = Snowflake.next() as string;
|
|
50
|
+
const { promise, resolve, reject } = Promise.withResolvers<string | undefined>();
|
|
51
|
+
let settled = false;
|
|
52
|
+
|
|
53
|
+
const cleanup = () => {
|
|
54
|
+
dialogOptions?.signal?.removeEventListener("abort", onAbort);
|
|
55
|
+
pendingRequests.delete(id);
|
|
56
|
+
};
|
|
57
|
+
const finish = (value: string | undefined) => {
|
|
58
|
+
if (settled) return;
|
|
59
|
+
settled = true;
|
|
60
|
+
cleanup();
|
|
61
|
+
resolve(value);
|
|
62
|
+
};
|
|
63
|
+
const fail = (error: Error) => {
|
|
64
|
+
if (settled) return;
|
|
65
|
+
settled = true;
|
|
66
|
+
cleanup();
|
|
67
|
+
reject(error);
|
|
68
|
+
};
|
|
69
|
+
const onAbort = () => {
|
|
70
|
+
output({
|
|
71
|
+
type: "extension_ui_request",
|
|
72
|
+
id: Snowflake.next() as string,
|
|
73
|
+
method: "cancel",
|
|
74
|
+
targetId: id,
|
|
75
|
+
} as RpcExtensionUIRequest);
|
|
76
|
+
finish(undefined);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
dialogOptions?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
80
|
+
pendingRequests.set(id, {
|
|
81
|
+
resolve: response => {
|
|
82
|
+
if ("cancelled" in response && response.cancelled) {
|
|
83
|
+
finish(undefined);
|
|
84
|
+
} else if ("value" in response) {
|
|
85
|
+
finish(response.value);
|
|
86
|
+
} else {
|
|
87
|
+
finish(undefined);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
reject: fail,
|
|
91
|
+
});
|
|
92
|
+
output({
|
|
93
|
+
type: "extension_ui_request",
|
|
94
|
+
id,
|
|
95
|
+
method: "editor",
|
|
96
|
+
title,
|
|
97
|
+
prefill,
|
|
98
|
+
promptStyle: editorOptions?.promptStyle,
|
|
99
|
+
} as RpcExtensionUIRequest);
|
|
100
|
+
return promise;
|
|
101
|
+
}
|
|
102
|
+
|
|
32
103
|
/**
|
|
33
104
|
* Run in RPC mode.
|
|
34
105
|
* Listens for JSON commands on stdin, outputs events and responses on stdout.
|
|
@@ -55,12 +126,6 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
55
126
|
return { id, type: "response", command, success: false, error: message };
|
|
56
127
|
};
|
|
57
128
|
|
|
58
|
-
// Pending extension UI requests waiting for response
|
|
59
|
-
type PendingExtensionRequest = {
|
|
60
|
-
resolve: (response: RpcExtensionUIResponse) => void;
|
|
61
|
-
reject: (error: Error) => void;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
129
|
const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
|
|
65
130
|
|
|
66
131
|
// Shutdown request flag (wrapped in object to allow mutation with const)
|
|
@@ -261,30 +326,13 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
261
326
|
return "";
|
|
262
327
|
}
|
|
263
328
|
|
|
264
|
-
async editor(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
resolve(undefined);
|
|
272
|
-
} else if ("value" in response) {
|
|
273
|
-
resolve(response.value);
|
|
274
|
-
} else {
|
|
275
|
-
resolve(undefined);
|
|
276
|
-
}
|
|
277
|
-
},
|
|
278
|
-
reject,
|
|
279
|
-
});
|
|
280
|
-
this.output({
|
|
281
|
-
type: "extension_ui_request",
|
|
282
|
-
id,
|
|
283
|
-
method: "editor",
|
|
284
|
-
title,
|
|
285
|
-
prefill,
|
|
286
|
-
} as RpcExtensionUIRequest);
|
|
287
|
-
return promise;
|
|
329
|
+
async editor(
|
|
330
|
+
title: string,
|
|
331
|
+
prefill?: string,
|
|
332
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
333
|
+
editorOptions?: { promptStyle?: boolean },
|
|
334
|
+
): Promise<string | undefined> {
|
|
335
|
+
return requestRpcEditor(this.pendingRequests, this.output, title, prefill, dialogOptions, editorOptions);
|
|
288
336
|
}
|
|
289
337
|
|
|
290
338
|
get theme(): Theme {
|
|
@@ -356,6 +404,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
356
404
|
// ExtensionContextActions
|
|
357
405
|
{
|
|
358
406
|
getModel: () => session.agent.state.model,
|
|
407
|
+
getSearchDb: () => session.searchDb,
|
|
359
408
|
isIdle: () => !session.isStreaming,
|
|
360
409
|
abort: () => session.abort(),
|
|
361
410
|
hasPendingMessages: () => session.queuedMessageCount > 0,
|
|
@@ -194,7 +194,15 @@ export type RpcExtensionUIRequest =
|
|
|
194
194
|
placeholder?: string;
|
|
195
195
|
timeout?: number;
|
|
196
196
|
}
|
|
197
|
-
| {
|
|
197
|
+
| {
|
|
198
|
+
type: "extension_ui_request";
|
|
199
|
+
id: string;
|
|
200
|
+
method: "editor";
|
|
201
|
+
title: string;
|
|
202
|
+
prefill?: string;
|
|
203
|
+
promptStyle?: boolean;
|
|
204
|
+
}
|
|
205
|
+
| { type: "extension_ui_request"; id: string; method: "cancel"; targetId: string }
|
|
198
206
|
| {
|
|
199
207
|
type: "extension_ui_request";
|
|
200
208
|
id: string;
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -955,6 +955,76 @@ export type ThemeColor =
|
|
|
955
955
|
| "statusLineCost"
|
|
956
956
|
| "statusLineSubagents";
|
|
957
957
|
|
|
958
|
+
/** Set of all valid ThemeColor string values for runtime validation */
|
|
959
|
+
const THEME_COLOR_RECORD = {
|
|
960
|
+
accent: true,
|
|
961
|
+
border: true,
|
|
962
|
+
borderAccent: true,
|
|
963
|
+
borderMuted: true,
|
|
964
|
+
success: true,
|
|
965
|
+
error: true,
|
|
966
|
+
warning: true,
|
|
967
|
+
muted: true,
|
|
968
|
+
dim: true,
|
|
969
|
+
text: true,
|
|
970
|
+
thinkingText: true,
|
|
971
|
+
userMessageText: true,
|
|
972
|
+
customMessageText: true,
|
|
973
|
+
customMessageLabel: true,
|
|
974
|
+
toolTitle: true,
|
|
975
|
+
toolOutput: true,
|
|
976
|
+
mdHeading: true,
|
|
977
|
+
mdLink: true,
|
|
978
|
+
mdLinkUrl: true,
|
|
979
|
+
mdCode: true,
|
|
980
|
+
mdCodeBlock: true,
|
|
981
|
+
mdCodeBlockBorder: true,
|
|
982
|
+
mdQuote: true,
|
|
983
|
+
mdQuoteBorder: true,
|
|
984
|
+
mdHr: true,
|
|
985
|
+
mdListBullet: true,
|
|
986
|
+
toolDiffAdded: true,
|
|
987
|
+
toolDiffRemoved: true,
|
|
988
|
+
toolDiffContext: true,
|
|
989
|
+
syntaxComment: true,
|
|
990
|
+
syntaxKeyword: true,
|
|
991
|
+
syntaxFunction: true,
|
|
992
|
+
syntaxVariable: true,
|
|
993
|
+
syntaxString: true,
|
|
994
|
+
syntaxNumber: true,
|
|
995
|
+
syntaxType: true,
|
|
996
|
+
syntaxOperator: true,
|
|
997
|
+
syntaxPunctuation: true,
|
|
998
|
+
thinkingOff: true,
|
|
999
|
+
thinkingMinimal: true,
|
|
1000
|
+
thinkingLow: true,
|
|
1001
|
+
thinkingMedium: true,
|
|
1002
|
+
thinkingHigh: true,
|
|
1003
|
+
thinkingXhigh: true,
|
|
1004
|
+
bashMode: true,
|
|
1005
|
+
pythonMode: true,
|
|
1006
|
+
statusLineSep: true,
|
|
1007
|
+
statusLineModel: true,
|
|
1008
|
+
statusLinePath: true,
|
|
1009
|
+
statusLineGitClean: true,
|
|
1010
|
+
statusLineGitDirty: true,
|
|
1011
|
+
statusLineContext: true,
|
|
1012
|
+
statusLineSpend: true,
|
|
1013
|
+
statusLineStaged: true,
|
|
1014
|
+
statusLineDirty: true,
|
|
1015
|
+
statusLineUntracked: true,
|
|
1016
|
+
statusLineOutput: true,
|
|
1017
|
+
statusLineCost: true,
|
|
1018
|
+
statusLineSubagents: true,
|
|
1019
|
+
} satisfies Record<ThemeColor, true>;
|
|
1020
|
+
|
|
1021
|
+
const VALID_THEME_COLORS: ReadonlySet<string> = new Set(Object.keys(THEME_COLOR_RECORD));
|
|
1022
|
+
|
|
1023
|
+
/** Check if a string is a valid ThemeColor value */
|
|
1024
|
+
export function isValidThemeColor(color: string): color is ThemeColor {
|
|
1025
|
+
return VALID_THEME_COLORS.has(color);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
958
1028
|
export type ThemeBg =
|
|
959
1029
|
| "selectedBg"
|
|
960
1030
|
| "userMessageBg"
|
package/src/modes/types.ts
CHANGED
|
@@ -243,7 +243,12 @@ export interface InteractiveModeContext {
|
|
|
243
243
|
hideHookSelector(): void;
|
|
244
244
|
showHookInput(title: string, placeholder?: string): Promise<string | undefined>;
|
|
245
245
|
hideHookInput(): void;
|
|
246
|
-
showHookEditor(
|
|
246
|
+
showHookEditor(
|
|
247
|
+
title: string,
|
|
248
|
+
prefill?: string,
|
|
249
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
250
|
+
editorOptions?: { promptStyle?: boolean },
|
|
251
|
+
): Promise<string | undefined>;
|
|
247
252
|
hideHookEditor(): void;
|
|
248
253
|
showHookNotify(message: string, type?: "info" | "warning" | "error"): void;
|
|
249
254
|
showHookCustom<T>(
|
|
@@ -40,6 +40,11 @@ If a skill covers your output, you **MUST** read `skill://<name>` before proceed
|
|
|
40
40
|
{{/list}}
|
|
41
41
|
</skills>
|
|
42
42
|
{{/if}}
|
|
43
|
+
{{#if alwaysApplyRules.length}}
|
|
44
|
+
{{#each alwaysApplyRules}}
|
|
45
|
+
{{content}}
|
|
46
|
+
{{/each}}
|
|
47
|
+
{{/if}}
|
|
43
48
|
{{#if rules.length}}
|
|
44
49
|
Rules are local constraints.
|
|
45
50
|
You **MUST** read `rule://<name>` when working in that domain.
|
|
@@ -124,6 +124,12 @@ You **MUST** use the following skills, to save you time, when working in their d
|
|
|
124
124
|
{{/each}}
|
|
125
125
|
{{/if}}
|
|
126
126
|
|
|
127
|
+
{{#if alwaysApplyRules.length}}
|
|
128
|
+
{{#each alwaysApplyRules}}
|
|
129
|
+
{{content}}
|
|
130
|
+
{{/each}}
|
|
131
|
+
{{/if}}
|
|
132
|
+
|
|
127
133
|
{{#if rules.length}}
|
|
128
134
|
# Rules
|
|
129
135
|
Domain-specific rules from past experience. **MUST** read `rule://<name>` when working in their territory.
|
package/src/prompts/tools/ask.md
CHANGED
|
@@ -8,6 +8,7 @@ Asks user when you need clarification or input during task execution.
|
|
|
8
8
|
- Use `recommended: <index>` to mark default (0-indexed); " (Recommended)" added automatically
|
|
9
9
|
- Use `questions` for multiple related questions instead of asking one at a time
|
|
10
10
|
- Set `multi: true` on question to allow multiple selections
|
|
11
|
+
- `ask.timeout` only applies while choosing options; once the user selects "Other (type your own)", there is no timeout
|
|
11
12
|
</instruction>
|
|
12
13
|
|
|
13
14
|
<caution>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Searches files using powerful regex matching
|
|
1
|
+
Searches files using powerful regex matching.
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
4
|
- Supports full regex syntax (e.g., `log.*Error`, `function\\s+\\w+`); literal braces need escaping (`interface\\{\\}` for `interface{}` in Go)
|
|
@@ -2,8 +2,6 @@ Applies precise file edits using `LINE#ID` anchors from `read` output.
|
|
|
2
2
|
|
|
3
3
|
Read the file first. Copy anchors exactly from the latest `read` output. In one `edit` call, batch all edits for one file. After any successful edit, re-read before editing that file again.
|
|
4
4
|
|
|
5
|
-
This matters: your output is checked against the real file state. Invalid anchors, duplicated boundary lines, or semantically equivalent rewrites will fail.
|
|
6
|
-
|
|
7
5
|
<operations>
|
|
8
6
|
**Top level**
|
|
9
7
|
- `path` — file path
|
|
@@ -61,6 +59,24 @@ Replace only the catch body. Do not target the shared boundary line `} catch (er
|
|
|
61
59
|
```
|
|
62
60
|
</example>
|
|
63
61
|
|
|
62
|
+
<example name="replace whole block including closing brace">
|
|
63
|
+
Replace the entire body of `alpha`, including its closing `}`. `end` **MUST** be {{hlineref 7 "}"}} because `content` includes `}`.
|
|
64
|
+
```
|
|
65
|
+
{
|
|
66
|
+
path: "util.ts",
|
|
67
|
+
edits: [{
|
|
68
|
+
loc: { block: { pos: {{hlineref 6 "\tlog();"}}, end: {{hlineref 7 "}"}} } },
|
|
69
|
+
content: [
|
|
70
|
+
"\tvalidate();",
|
|
71
|
+
"\tlog();",
|
|
72
|
+
"}"
|
|
73
|
+
]
|
|
74
|
+
}]
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
**Wrong**: using `end: {{hlineref 6 "\tlog();"}}` with the same content — line 7 (`}`) survives the replacement AND content emits `}`, producing two closing braces.
|
|
78
|
+
</example>
|
|
79
|
+
|
|
64
80
|
<example name="replace one line">
|
|
65
81
|
```
|
|
66
82
|
{
|
|
@@ -108,9 +124,8 @@ When adding a sibling declaration, prefer `prepend` on the next declaration.
|
|
|
108
124
|
- Make the minimum exact edit. Do not rewrite nearby code unless the consumed range requires it.
|
|
109
125
|
- Use anchors exactly as `N#ID` from the latest `read` output.
|
|
110
126
|
- `block` requires both `pos` and `end`. Other anchored ops require one anchor.
|
|
111
|
-
-
|
|
112
|
-
- **
|
|
113
|
-
- Do not target shared boundary lines such as `} else {`, `} catch (…) {`, `}),`, or `},{`.
|
|
127
|
+
- When your replacement `content` ends with a closing delimiter (`}`, `*/`, `)`, `]`), verify `end` includes the original line carrying that delimiter. If `end` stops one line too early, the original delimiter survives and your content adds a second copy.
|
|
128
|
+
- **Self-check**: compare the last line of `content` with the line immediately after `end` in the file. If they match (e.g., both are `}`), extend `end` to include that line.
|
|
114
129
|
- For a block, either replace only the body or replace the whole block. Do not split block boundaries.
|
|
115
130
|
- `content` must be literal file content with matching indentation. If the file uses tabs, use real tabs.
|
|
116
131
|
- Do not use this tool to reformat or clean up unrelated code.
|
package/src/sdk.ts
CHANGED
|
@@ -9,8 +9,17 @@ import {
|
|
|
9
9
|
import type { Message, Model } from "@oh-my-pi/pi-ai";
|
|
10
10
|
|
|
11
11
|
import { prewarmOpenAICodexResponses } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
|
|
12
|
+
import { SearchDb } from "@oh-my-pi/pi-natives";
|
|
12
13
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
$env,
|
|
16
|
+
getAgentDbPath,
|
|
17
|
+
getAgentDir,
|
|
18
|
+
getProjectDir,
|
|
19
|
+
getSearchDbDir,
|
|
20
|
+
logger,
|
|
21
|
+
postmortem,
|
|
22
|
+
} from "@oh-my-pi/pi-utils";
|
|
14
23
|
import chalk from "chalk";
|
|
15
24
|
import { AsyncJobManager } from "./async";
|
|
16
25
|
import { createAutoresearchExtension } from "./autoresearch";
|
|
@@ -131,6 +140,8 @@ export interface CreateAgentSessionOptions {
|
|
|
131
140
|
authStorage?: AuthStorage;
|
|
132
141
|
/** Model registry. Default: discoverModels(authStorage, agentDir) */
|
|
133
142
|
modelRegistry?: ModelRegistry;
|
|
143
|
+
/** Shared native search DB for grep/glob/fuzzyFind-backed workflows. */
|
|
144
|
+
searchDb?: SearchDb;
|
|
134
145
|
|
|
135
146
|
/** Model to use. Default: from settings, else first available */
|
|
136
147
|
model?: Model;
|
|
@@ -381,6 +392,7 @@ function createCustomToolContext(ctx: ExtensionContext): CustomToolContext {
|
|
|
381
392
|
sessionManager: ctx.sessionManager,
|
|
382
393
|
modelRegistry: ctx.modelRegistry,
|
|
383
394
|
model: ctx.model,
|
|
395
|
+
searchDb: ctx.searchDb,
|
|
384
396
|
isIdle: ctx.isIdle,
|
|
385
397
|
hasQueuedMessages: ctx.hasPendingMessages,
|
|
386
398
|
abort: ctx.abort,
|
|
@@ -796,6 +808,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
796
808
|
}),
|
|
797
809
|
);
|
|
798
810
|
|
|
811
|
+
// collect alwaysApply rules — full content injected into system prompt
|
|
812
|
+
const alwaysApplyRules = rulesResult.items.filter((rule: Rule) => {
|
|
813
|
+
if (registeredTtsrRuleNames.has(rule.name)) return false;
|
|
814
|
+
return rule.alwaysApply === true;
|
|
815
|
+
});
|
|
816
|
+
|
|
799
817
|
const contextFiles = await logger.timeAsync(
|
|
800
818
|
"discoverContextFiles",
|
|
801
819
|
async () => options.contextFiles ?? (await discoverContextFiles(cwd, agentDir)),
|
|
@@ -856,6 +874,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
856
874
|
})
|
|
857
875
|
: undefined;
|
|
858
876
|
|
|
877
|
+
const searchDb = options.searchDb ?? new SearchDb(getSearchDbDir(agentDir));
|
|
859
878
|
const pendingActionStore = new PendingActionStore();
|
|
860
879
|
const toolSession: ToolSession = {
|
|
861
880
|
cwd,
|
|
@@ -905,6 +924,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
905
924
|
modelRegistry,
|
|
906
925
|
asyncJobManager,
|
|
907
926
|
pendingActionStore,
|
|
927
|
+
searchDb,
|
|
908
928
|
};
|
|
909
929
|
|
|
910
930
|
// Initialize internal URL router for internal protocols (agent://, artifact://, memory://, skill://, rule://, mcp://, local://)
|
|
@@ -930,7 +950,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
930
950
|
);
|
|
931
951
|
internalRouter.register(
|
|
932
952
|
new RuleProtocolHandler({
|
|
933
|
-
getRules: () => rulebookRules,
|
|
953
|
+
getRules: () => [...rulebookRules, ...alwaysApplyRules],
|
|
934
954
|
}),
|
|
935
955
|
);
|
|
936
956
|
internalRouter.register(new PiProtocolHandler());
|
|
@@ -1167,6 +1187,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1167
1187
|
sessionManager,
|
|
1168
1188
|
modelRegistry,
|
|
1169
1189
|
model: agent?.state.model,
|
|
1190
|
+
searchDb,
|
|
1170
1191
|
isIdle: () => !session?.isStreaming,
|
|
1171
1192
|
hasQueuedMessages: () => (session?.queuedMessageCount ?? 0) > 0,
|
|
1172
1193
|
abort: () => session?.abort(),
|
|
@@ -1252,6 +1273,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1252
1273
|
tools: promptTools,
|
|
1253
1274
|
toolNames,
|
|
1254
1275
|
rules: rulebookRules,
|
|
1276
|
+
alwaysApplyRules,
|
|
1255
1277
|
skillsSettings: settings.getGroup("skills"),
|
|
1256
1278
|
appendSystemPrompt: appendPrompt,
|
|
1257
1279
|
repeatToolDescriptions,
|
|
@@ -1272,6 +1294,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1272
1294
|
tools: promptTools,
|
|
1273
1295
|
toolNames,
|
|
1274
1296
|
rules: rulebookRules,
|
|
1297
|
+
alwaysApplyRules,
|
|
1275
1298
|
skillsSettings: settings.getGroup("skills"),
|
|
1276
1299
|
customPrompt: options.systemPrompt,
|
|
1277
1300
|
appendSystemPrompt: appendPrompt,
|
|
@@ -1531,6 +1554,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1531
1554
|
obfuscator,
|
|
1532
1555
|
asyncJobManager,
|
|
1533
1556
|
pendingActionStore,
|
|
1557
|
+
searchDb,
|
|
1534
1558
|
});
|
|
1535
1559
|
|
|
1536
1560
|
if (model?.api === "openai-codex-responses") {
|
|
@@ -50,10 +50,11 @@ import {
|
|
|
50
50
|
modelsAreEqual,
|
|
51
51
|
parseRateLimitReason,
|
|
52
52
|
} from "@oh-my-pi/pi-ai";
|
|
53
|
+
import type { SearchDb } from "@oh-my-pi/pi-natives";
|
|
53
54
|
import { abortableSleep, getAgentDbPath, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
54
55
|
import type { AsyncJob, AsyncJobManager } from "../async";
|
|
55
56
|
import type { Rule } from "../capability/rule";
|
|
56
|
-
import { MODEL_ROLE_IDS, type ModelRegistry
|
|
57
|
+
import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
|
|
57
58
|
import { extractExplicitThinkingSelector, parseModelString, resolveModelRoleValue } from "../config/model-resolver";
|
|
58
59
|
import { expandPromptTemplate, type PromptTemplate, renderPromptTemplate } from "../config/prompt-templates";
|
|
59
60
|
import type { Settings, SkillsSettings } from "../config/settings";
|
|
@@ -237,6 +238,8 @@ export interface AgentSessionConfig {
|
|
|
237
238
|
obfuscator?: SecretObfuscator;
|
|
238
239
|
/** Pending action store for preview/apply workflows */
|
|
239
240
|
pendingActionStore?: PendingActionStore;
|
|
241
|
+
/** Shared native search DB for grep/glob/fuzzyFind-backed workflows. */
|
|
242
|
+
searchDb?: SearchDb;
|
|
240
243
|
}
|
|
241
244
|
|
|
242
245
|
/** Options for AgentSession.prompt() */
|
|
@@ -269,7 +272,7 @@ export interface ModelCycleResult {
|
|
|
269
272
|
export interface RoleModelCycleResult {
|
|
270
273
|
model: Model;
|
|
271
274
|
thinkingLevel: ThinkingLevel | undefined;
|
|
272
|
-
role:
|
|
275
|
+
role: string;
|
|
273
276
|
}
|
|
274
277
|
|
|
275
278
|
/** Session statistics for /session command */
|
|
@@ -348,6 +351,7 @@ export class AgentSession {
|
|
|
348
351
|
readonly agent: Agent;
|
|
349
352
|
readonly sessionManager: SessionManager;
|
|
350
353
|
readonly settings: Settings;
|
|
354
|
+
readonly searchDb: SearchDb | undefined;
|
|
351
355
|
|
|
352
356
|
#asyncJobManager: AsyncJobManager | undefined = undefined;
|
|
353
357
|
#scopedModels: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
|
@@ -462,6 +466,7 @@ export class AgentSession {
|
|
|
462
466
|
this.agent = config.agent;
|
|
463
467
|
this.sessionManager = config.sessionManager;
|
|
464
468
|
this.settings = config.settings;
|
|
469
|
+
this.searchDb = config.searchDb;
|
|
465
470
|
this.#asyncJobManager = config.asyncJobManager;
|
|
466
471
|
this.#scopedModels = config.scopedModels ?? [];
|
|
467
472
|
this.#thinkingLevel = config.thinkingLevel;
|
|
@@ -1888,6 +1893,7 @@ export class AgentSession {
|
|
|
1888
1893
|
sessionManager: this.sessionManager,
|
|
1889
1894
|
modelRegistry: this.#modelRegistry,
|
|
1890
1895
|
model: this.model,
|
|
1896
|
+
searchDb: this.searchDb,
|
|
1891
1897
|
isIdle: () => !this.isStreaming,
|
|
1892
1898
|
hasQueuedMessages: () => this.queuedMessageCount > 0,
|
|
1893
1899
|
abort: () => {
|
|
@@ -2039,7 +2045,7 @@ export class AgentSession {
|
|
|
2039
2045
|
);
|
|
2040
2046
|
}
|
|
2041
2047
|
|
|
2042
|
-
resolveRoleModel(role:
|
|
2048
|
+
resolveRoleModel(role: string): Model | undefined {
|
|
2043
2049
|
return this.#resolveRoleModel(role, this.#modelRegistry.getAvailable(), this.model);
|
|
2044
2050
|
}
|
|
2045
2051
|
|
|
@@ -3096,7 +3102,7 @@ export class AgentSession {
|
|
|
3096
3102
|
* Validates API key, saves to session and settings.
|
|
3097
3103
|
* @throws Error if no API key available for the model
|
|
3098
3104
|
*/
|
|
3099
|
-
async setModel(model: Model, role:
|
|
3105
|
+
async setModel(model: Model, role: string = "default"): Promise<void> {
|
|
3100
3106
|
const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
|
|
3101
3107
|
if (!apiKey) {
|
|
3102
3108
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
@@ -3150,7 +3156,7 @@ export class AgentSession {
|
|
|
3150
3156
|
* @param options - Optional settings: `temporary` to not persist to settings
|
|
3151
3157
|
*/
|
|
3152
3158
|
async cycleRoleModels(
|
|
3153
|
-
roleOrder: readonly
|
|
3159
|
+
roleOrder: readonly string[],
|
|
3154
3160
|
options?: { temporary?: boolean },
|
|
3155
3161
|
): Promise<RoleModelCycleResult | undefined> {
|
|
3156
3162
|
const availableModels = this.#modelRegistry.getAvailable();
|
|
@@ -3160,7 +3166,7 @@ export class AgentSession {
|
|
|
3160
3166
|
if (!currentModel) return undefined;
|
|
3161
3167
|
const matchPreferences = { usageOrder: this.settings.getStorage()?.getModelUsageOrder() };
|
|
3162
3168
|
const roleModels: Array<{
|
|
3163
|
-
role:
|
|
3169
|
+
role: string;
|
|
3164
3170
|
model: Model;
|
|
3165
3171
|
thinkingLevel?: ThinkingLevel;
|
|
3166
3172
|
explicitThinkingLevel: boolean;
|
|
@@ -3190,9 +3196,10 @@ export class AgentSession {
|
|
|
3190
3196
|
if (roleModels.length <= 1) return undefined;
|
|
3191
3197
|
|
|
3192
3198
|
const lastRole = this.sessionManager.getLastModelChangeRole();
|
|
3193
|
-
let currentIndex = lastRole
|
|
3194
|
-
|
|
3195
|
-
|
|
3199
|
+
let currentIndex = lastRole ? roleModels.findIndex(entry => entry.role === lastRole) : -1;
|
|
3200
|
+
if (currentIndex === -1) {
|
|
3201
|
+
currentIndex = roleModels.findIndex(entry => modelsAreEqual(entry.model, currentModel));
|
|
3202
|
+
}
|
|
3196
3203
|
if (currentIndex === -1) currentIndex = 0;
|
|
3197
3204
|
|
|
3198
3205
|
const nextIndex = (currentIndex + 1) % roleModels.length;
|
|
@@ -4273,7 +4280,7 @@ export class AgentSession {
|
|
|
4273
4280
|
return `${model.provider}/${model.id}`;
|
|
4274
4281
|
}
|
|
4275
4282
|
|
|
4276
|
-
#formatRoleModelValue(role:
|
|
4283
|
+
#formatRoleModelValue(role: string, model: Model): string {
|
|
4277
4284
|
const modelKey = `${model.provider}/${model.id}`;
|
|
4278
4285
|
const existingRoleValue = this.settings.getModelRole(role);
|
|
4279
4286
|
if (!existingRoleValue) return modelKey;
|
|
@@ -4295,7 +4302,7 @@ export class AgentSession {
|
|
|
4295
4302
|
return availableModels.find(m => m.provider === currentModel.provider && m.id === configuredTarget);
|
|
4296
4303
|
}
|
|
4297
4304
|
|
|
4298
|
-
#resolveRoleModel(role:
|
|
4305
|
+
#resolveRoleModel(role: string, availableModels: Model[], currentModel: Model | undefined): Model | undefined {
|
|
4299
4306
|
const roleModelStr =
|
|
4300
4307
|
role === "default"
|
|
4301
4308
|
? (this.settings.getModelRole("default") ??
|
package/src/system-prompt.ts
CHANGED
|
@@ -16,6 +16,57 @@ import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile
|
|
|
16
16
|
import { loadSkills, type Skill } from "./extensibility/skills";
|
|
17
17
|
import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
|
|
18
18
|
import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
|
|
19
|
+
import { formatPromptContent } from "./utils/prompt-format";
|
|
20
|
+
|
|
21
|
+
interface AlwaysApplyRule {
|
|
22
|
+
name: string;
|
|
23
|
+
content: string;
|
|
24
|
+
path: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizePromptBlock(content: string): string {
|
|
28
|
+
return formatPromptContent(content, { renderPhase: "post-render" }).trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function splitComparablePromptBlocks(content: string | null | undefined): string[] {
|
|
32
|
+
const normalized = firstNonEmpty(content);
|
|
33
|
+
if (!normalized) return [];
|
|
34
|
+
|
|
35
|
+
return normalizePromptBlock(normalized)
|
|
36
|
+
.split(/\n{2,}/)
|
|
37
|
+
.map(block => block.trim())
|
|
38
|
+
.filter(block => block.length > 0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function promptSourceContainsRule(source: string | null | undefined, ruleContent: string): boolean {
|
|
42
|
+
const sourceBlocks = splitComparablePromptBlocks(source);
|
|
43
|
+
const ruleBlocks = splitComparablePromptBlocks(ruleContent);
|
|
44
|
+
if (sourceBlocks.length === 0 || ruleBlocks.length === 0 || ruleBlocks.length > sourceBlocks.length) return false;
|
|
45
|
+
|
|
46
|
+
for (let start = 0; start <= sourceBlocks.length - ruleBlocks.length; start += 1) {
|
|
47
|
+
if (ruleBlocks.every((block, offset) => sourceBlocks[start + offset] === block)) return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function dedupeAlwaysApplyRules(
|
|
54
|
+
alwaysApplyRules: AlwaysApplyRule[] | undefined,
|
|
55
|
+
promptSources: Array<string | null | undefined>,
|
|
56
|
+
): AlwaysApplyRule[] {
|
|
57
|
+
if (!alwaysApplyRules || alwaysApplyRules.length === 0) return [];
|
|
58
|
+
|
|
59
|
+
return alwaysApplyRules.filter(
|
|
60
|
+
rule => !promptSources.some(source => promptSourceContainsRule(source, rule.content)),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function dedupePromptSource(source: string | null | undefined, otherSources: Array<string | null | undefined>): string {
|
|
65
|
+
const resolvedSource = firstNonEmpty(source);
|
|
66
|
+
if (!resolvedSource) return "";
|
|
67
|
+
|
|
68
|
+
return otherSources.some(otherSource => promptSourceContainsRule(otherSource, resolvedSource)) ? "" : resolvedSource;
|
|
69
|
+
}
|
|
19
70
|
|
|
20
71
|
function firstNonEmpty(...values: (string | undefined | null)[]): string | null {
|
|
21
72
|
for (const value of values) {
|
|
@@ -379,6 +430,8 @@ export interface BuildSystemPromptOptions {
|
|
|
379
430
|
mcpDiscoveryServerSummaries?: string[];
|
|
380
431
|
/** Encourage the agent to delegate via tasks unless changes are trivial. */
|
|
381
432
|
eagerTasks?: boolean;
|
|
433
|
+
/** Rules with alwaysApply=true — their full content is injected into the prompt. */
|
|
434
|
+
alwaysApplyRules?: AlwaysApplyRule[];
|
|
382
435
|
}
|
|
383
436
|
|
|
384
437
|
/** Build the system prompt with tools, guidelines, and context */
|
|
@@ -398,6 +451,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
398
451
|
contextFiles: providedContextFiles,
|
|
399
452
|
skills: providedSkills,
|
|
400
453
|
rules,
|
|
454
|
+
alwaysApplyRules,
|
|
401
455
|
intentField,
|
|
402
456
|
mcpDiscoveryMode = false,
|
|
403
457
|
mcpDiscoveryServerSummaries = [],
|
|
@@ -519,10 +573,16 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
519
573
|
const hasRead = tools?.has("read");
|
|
520
574
|
const filteredSkills = hasRead ? skills : [];
|
|
521
575
|
|
|
576
|
+
const effectiveSystemPromptCustomization = dedupePromptSource(systemPromptCustomization, [
|
|
577
|
+
resolvedCustomPrompt,
|
|
578
|
+
resolvedAppendPrompt,
|
|
579
|
+
]);
|
|
580
|
+
const promptSources = [effectiveSystemPromptCustomization, resolvedCustomPrompt, resolvedAppendPrompt];
|
|
581
|
+
const injectedAlwaysApplyRules = dedupeAlwaysApplyRules(alwaysApplyRules, promptSources);
|
|
582
|
+
|
|
522
583
|
const environment = await logger.timeAsync("getEnvironmentInfo", getEnvironmentInfo);
|
|
523
584
|
const data = {
|
|
524
|
-
|
|
525
|
-
systemPromptCustomization: resolvedCustomPrompt ? "" : (systemPromptCustomization ?? ""),
|
|
585
|
+
systemPromptCustomization: effectiveSystemPromptCustomization,
|
|
526
586
|
customPrompt: resolvedCustomPrompt,
|
|
527
587
|
appendPrompt: resolvedAppendPrompt ?? "",
|
|
528
588
|
tools: toolNames,
|
|
@@ -533,6 +593,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
533
593
|
agentsMdSearch,
|
|
534
594
|
skills: filteredSkills,
|
|
535
595
|
rules: rules ?? [],
|
|
596
|
+
alwaysApplyRules: injectedAlwaysApplyRules,
|
|
536
597
|
date,
|
|
537
598
|
dateTime,
|
|
538
599
|
cwd: promptCwd,
|