@oh-my-pi/pi-coding-agent 15.13.3 → 16.0.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 +155 -133
- package/dist/cli.js +621 -530
- package/dist/types/advisor/__tests__/advisor.test.d.ts +1 -0
- package/dist/types/advisor/advise-tool.d.ts +58 -0
- package/dist/types/advisor/index.d.ts +3 -0
- package/dist/types/advisor/runtime.d.ts +52 -0
- package/dist/types/advisor/watchdog.d.ts +5 -0
- package/dist/types/config/model-roles.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +66 -5
- package/dist/types/discovery/helpers.d.ts +7 -0
- package/dist/types/eval/__tests__/prelude-agent.test.d.ts +1 -0
- package/dist/types/extensibility/plugins/runtime-config.d.ts +3 -0
- package/dist/types/modes/components/advisor-message.d.ts +9 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/controllers/command-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +3 -1
- package/dist/types/modes/types.d.ts +8 -1
- package/dist/types/sdk.d.ts +3 -3
- package/dist/types/session/agent-session.d.ts +81 -2
- package/dist/types/session/session-history-format.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +4 -1
- package/dist/types/session/yield-queue.d.ts +2 -0
- package/dist/types/task/index.d.ts +21 -0
- package/dist/types/tools/github-cache.d.ts +5 -4
- package/dist/types/tools/job.d.ts +1 -0
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/report-tool-issue.d.ts +0 -1
- package/dist/types/web/search/index.d.ts +2 -2
- package/dist/types/web/search/provider.d.ts +2 -0
- package/package.json +13 -13
- package/src/advisor/__tests__/advisor.test.ts +586 -0
- package/src/advisor/advise-tool.ts +87 -0
- package/src/advisor/index.ts +3 -0
- package/src/advisor/runtime.ts +248 -0
- package/src/advisor/watchdog.ts +83 -0
- package/src/cli/args.ts +1 -0
- package/src/collab/host.ts +1 -1
- package/src/config/model-roles.ts +13 -1
- package/src/config/settings-schema.ts +65 -6
- package/src/discovery/claude-plugins.ts +3 -42
- package/src/discovery/github.ts +101 -6
- package/src/discovery/helpers.ts +11 -0
- package/src/eval/__tests__/prelude-agent.test.ts +73 -0
- package/src/eval/js/shared/prelude.txt +12 -3
- package/src/eval/py/prelude.py +26 -2
- package/src/extensibility/custom-commands/bundled/review/index.ts +289 -80
- package/src/extensibility/plugins/loader.ts +3 -2
- package/src/extensibility/plugins/manager.ts +4 -3
- package/src/extensibility/plugins/marketplace/fetcher.ts +32 -34
- package/src/extensibility/plugins/runtime-config.ts +9 -0
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/internal-urls/issue-pr-protocol.ts +8 -4
- package/src/main.ts +9 -1
- package/src/modes/acp/acp-agent.ts +3 -3
- package/src/modes/components/advisor-message.ts +99 -0
- package/src/modes/components/agent-hub.ts +7 -0
- package/src/modes/components/assistant-message.ts +86 -0
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/status-line/segments.ts +20 -7
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/controllers/command-controller.ts +69 -2
- package/src/modes/controllers/extension-ui-controller.ts +4 -3
- package/src/modes/controllers/input-controller.ts +1 -0
- package/src/modes/controllers/selector-controller.ts +7 -0
- package/src/modes/interactive-mode.ts +59 -2
- package/src/modes/rpc/rpc-mode.ts +3 -3
- package/src/modes/runtime-init.ts +2 -1
- package/src/modes/types.ts +8 -1
- package/src/modes/utils/ui-helpers.ts +9 -0
- package/src/prompts/advisor/advise-tool.md +1 -0
- package/src/prompts/advisor/system.md +31 -0
- package/src/prompts/agents/designer.md +8 -0
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +4 -1
- package/src/prompts/tools/eval.md +13 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/sdk.ts +61 -14
- package/src/session/agent-session.ts +667 -13
- package/src/session/session-dump-format.ts +15 -131
- package/src/session/session-history-format.ts +30 -11
- package/src/session/session-manager.ts +3 -1
- package/src/session/yield-queue.ts +5 -1
- package/src/slash-commands/builtin-registry.ts +105 -4
- package/src/system-prompt.ts +1 -1
- package/src/task/executor.ts +5 -4
- package/src/task/index.ts +70 -9
- package/src/tools/github-cache.ts +32 -7
- package/src/tools/job.ts +14 -1
- package/src/tools/path-utils.ts +33 -2
- package/src/tools/report-tool-issue.ts +2 -7
- package/src/web/scrapers/docs-rs.ts +2 -3
- package/src/web/search/index.ts +2 -2
- package/src/web/search/provider.ts +14 -2
|
@@ -2,22 +2,10 @@
|
|
|
2
2
|
* Plain-text / markdown session formatting (same shape as /dump clipboard export).
|
|
3
3
|
*/
|
|
4
4
|
import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { canonicalizeMessage } from "../utils/thinking-display";
|
|
10
|
-
import {
|
|
11
|
-
type BashExecutionMessage,
|
|
12
|
-
type BranchSummaryMessage,
|
|
13
|
-
bashExecutionToText,
|
|
14
|
-
type CompactionSummaryMessage,
|
|
15
|
-
type CustomMessage,
|
|
16
|
-
type FileMentionMessage,
|
|
17
|
-
type HookMessage,
|
|
18
|
-
type PythonExecutionMessage,
|
|
19
|
-
pythonExecutionToText,
|
|
20
|
-
} from "./messages";
|
|
5
|
+
import type { Model, ToolExample, TSchema } from "@oh-my-pi/pi-ai";
|
|
6
|
+
import { getDialectDefinition, renderToolInventory } from "@oh-my-pi/pi-ai/dialect";
|
|
7
|
+
import { preferredDialect } from "@oh-my-pi/pi-catalog/identity";
|
|
8
|
+
import { convertToLlm } from "./messages";
|
|
21
9
|
|
|
22
10
|
/** Minimal tool shape for dump output (matches AgentTool fields used by formatSessionDumpText). */
|
|
23
11
|
export interface SessionDumpToolInfo {
|
|
@@ -40,7 +28,7 @@ export interface FormatSessionDumpTextOptions {
|
|
|
40
28
|
*/
|
|
41
29
|
export function formatSessionDumpText(options: FormatSessionDumpTextOptions): string {
|
|
42
30
|
const lines: string[] = [];
|
|
43
|
-
const
|
|
31
|
+
const definition = getDialectDefinition(preferredDialect(options.model?.id ?? ""));
|
|
44
32
|
|
|
45
33
|
const systemPrompt = options.systemPrompt?.filter(prompt => prompt.length > 0) ?? [];
|
|
46
34
|
if (systemPrompt.length > 0) {
|
|
@@ -62,125 +50,21 @@ export function formatSessionDumpText(options: FormatSessionDumpTextOptions): st
|
|
|
62
50
|
lines.push("\n");
|
|
63
51
|
|
|
64
52
|
const tools = options.tools ?? [];
|
|
65
|
-
|
|
53
|
+
const inventoryTools = tools.map(tool => ({
|
|
54
|
+
name: tool.name,
|
|
55
|
+
description: tool.description,
|
|
56
|
+
parameters: tool.parameters as TSchema,
|
|
57
|
+
examples: tool.examples,
|
|
58
|
+
}));
|
|
59
|
+
if (inventoryTools.length > 0) {
|
|
66
60
|
lines.push("## Available Tools\n");
|
|
67
|
-
const inventoryTools = tools.map(tool => ({
|
|
68
|
-
name: tool.name,
|
|
69
|
-
description: tool.description,
|
|
70
|
-
parameters: tool.parameters as TSchema,
|
|
71
|
-
examples: tool.examples,
|
|
72
|
-
}));
|
|
73
61
|
lines.push(renderToolInventory(inventoryTools, options.model?.id ?? ""));
|
|
74
62
|
lines.push("\n");
|
|
75
63
|
}
|
|
76
64
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (typeof msg.content === "string") {
|
|
81
|
-
lines.push(msg.content);
|
|
82
|
-
} else {
|
|
83
|
-
for (const c of msg.content) {
|
|
84
|
-
if (c.type === "text") {
|
|
85
|
-
lines.push(c.text);
|
|
86
|
-
} else if (c.type === "image") {
|
|
87
|
-
lines.push("[Image]");
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
lines.push("\n");
|
|
92
|
-
} else if (msg.role === "assistant") {
|
|
93
|
-
const assistantMsg = msg as AssistantMessage;
|
|
94
|
-
lines.push("## Assistant\n");
|
|
95
|
-
|
|
96
|
-
for (const c of assistantMsg.content) {
|
|
97
|
-
if (c.type === "text") {
|
|
98
|
-
lines.push(c.text);
|
|
99
|
-
} else if (c.type === "thinking") {
|
|
100
|
-
const thinking = canonicalizeMessage(c.thinking);
|
|
101
|
-
if (thinking.length === 0) continue;
|
|
102
|
-
lines.push("<thinking>");
|
|
103
|
-
lines.push(thinking);
|
|
104
|
-
lines.push("</thinking>\n");
|
|
105
|
-
} else if (c.type === "toolCall") {
|
|
106
|
-
const args = { ...(c.arguments as Record<string, unknown>) };
|
|
107
|
-
delete args[INTENT_FIELD];
|
|
108
|
-
lines.push(grammar.renderToolCall({ ...c, arguments: args }));
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
lines.push("");
|
|
112
|
-
} else if (msg.role === "toolResult") {
|
|
113
|
-
lines.push(`### Tool Result: ${msg.toolName}`);
|
|
114
|
-
if (msg.isError) {
|
|
115
|
-
lines.push("(error)");
|
|
116
|
-
}
|
|
117
|
-
for (const c of msg.content) {
|
|
118
|
-
if (c.type === "text") {
|
|
119
|
-
lines.push("```");
|
|
120
|
-
lines.push(c.text);
|
|
121
|
-
lines.push("```");
|
|
122
|
-
} else if (c.type === "image") {
|
|
123
|
-
lines.push("[Image output]");
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
lines.push("");
|
|
127
|
-
} else if (msg.role === "bashExecution") {
|
|
128
|
-
const bashMsg = msg as BashExecutionMessage;
|
|
129
|
-
if (!bashMsg.excludeFromContext) {
|
|
130
|
-
lines.push("## Bash Execution\n");
|
|
131
|
-
lines.push(bashExecutionToText(bashMsg));
|
|
132
|
-
lines.push("\n");
|
|
133
|
-
}
|
|
134
|
-
} else if (msg.role === "pythonExecution") {
|
|
135
|
-
const pythonMsg = msg as PythonExecutionMessage;
|
|
136
|
-
if (!pythonMsg.excludeFromContext) {
|
|
137
|
-
lines.push("## Python Execution\n");
|
|
138
|
-
lines.push(pythonExecutionToText(pythonMsg));
|
|
139
|
-
lines.push("\n");
|
|
140
|
-
}
|
|
141
|
-
} else if (msg.role === "custom" || msg.role === "hookMessage") {
|
|
142
|
-
const customMsg = msg as CustomMessage | HookMessage;
|
|
143
|
-
lines.push(`## ${customMsg.customType}\n`);
|
|
144
|
-
if (typeof customMsg.content === "string") {
|
|
145
|
-
lines.push(customMsg.content);
|
|
146
|
-
} else {
|
|
147
|
-
for (const c of customMsg.content) {
|
|
148
|
-
if (c.type === "text") {
|
|
149
|
-
lines.push(c.text);
|
|
150
|
-
} else if (c.type === "image") {
|
|
151
|
-
lines.push("[Image]");
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
lines.push("\n");
|
|
156
|
-
} else if (msg.role === "branchSummary") {
|
|
157
|
-
const branchMsg = msg as BranchSummaryMessage;
|
|
158
|
-
lines.push("## Branch Summary\n");
|
|
159
|
-
lines.push(`(from branch: ${branchMsg.fromId})\n`);
|
|
160
|
-
lines.push(branchMsg.summary);
|
|
161
|
-
lines.push("\n");
|
|
162
|
-
} else if (msg.role === "compactionSummary") {
|
|
163
|
-
const compactMsg = msg as CompactionSummaryMessage;
|
|
164
|
-
lines.push("## Compaction Summary\n");
|
|
165
|
-
lines.push(`(${compactMsg.tokensBefore} tokens before compaction)\n`);
|
|
166
|
-
lines.push(compactMsg.summary);
|
|
167
|
-
lines.push("\n");
|
|
168
|
-
} else if (msg.role === "fileMention") {
|
|
169
|
-
const fileMsg = msg as FileMentionMessage;
|
|
170
|
-
lines.push("## File Mention\n");
|
|
171
|
-
for (const file of fileMsg.files) {
|
|
172
|
-
lines.push(`<file path="${file.path}">`);
|
|
173
|
-
if (file.content) {
|
|
174
|
-
lines.push(file.content);
|
|
175
|
-
}
|
|
176
|
-
if (file.image) {
|
|
177
|
-
lines.push("[Image attached]");
|
|
178
|
-
}
|
|
179
|
-
lines.push("</file>\n");
|
|
180
|
-
}
|
|
181
|
-
lines.push("\n");
|
|
182
|
-
}
|
|
183
|
-
}
|
|
65
|
+
lines.push("## Transcript\n");
|
|
66
|
+
lines.push(definition.renderTranscript(convertToLlm([...options.messages]), { tools: inventoryTools }));
|
|
67
|
+
lines.push("\n");
|
|
184
68
|
|
|
185
69
|
return lines.join("\n").trim();
|
|
186
70
|
}
|
|
@@ -22,6 +22,10 @@ import type {
|
|
|
22
22
|
export interface HistoryFormatOptions {
|
|
23
23
|
/** Optional H1 prepended to the transcript. */
|
|
24
24
|
title?: string;
|
|
25
|
+
/** Render assistant thinking blocks (default: elided). */
|
|
26
|
+
includeThinking?: boolean;
|
|
27
|
+
/** Render tool intent comment before tool call lines. */
|
|
28
|
+
includeToolIntent?: boolean;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
/** Max length of the primary-arg summary inside `→ tool(...)` lines. */
|
|
@@ -100,17 +104,30 @@ function toolCallLine(
|
|
|
100
104
|
name: string,
|
|
101
105
|
args: Record<string, unknown> | undefined,
|
|
102
106
|
result: ToolResultMessage | undefined,
|
|
107
|
+
includeToolIntent?: boolean,
|
|
103
108
|
): string {
|
|
104
109
|
const head = `→ ${name}(${primaryArg(args)})`;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
110
|
+
let base: string;
|
|
111
|
+
if (!result) {
|
|
112
|
+
base = `${head} ⇒ pending`;
|
|
113
|
+
} else {
|
|
114
|
+
const text = contentToText(result.content);
|
|
115
|
+
const lines = lineCount(text);
|
|
116
|
+
const count = `${lines} ${lines === 1 ? "line" : "lines"}`;
|
|
117
|
+
if (result.isError) {
|
|
118
|
+
const firstLine = oneLine(text.split("\n", 1)[0] ?? "");
|
|
119
|
+
base = firstLine ? `${head} ⇒ error · ${count} — ${firstLine}` : `${head} ⇒ error · ${count}`;
|
|
120
|
+
} else {
|
|
121
|
+
base = `${head} ⇒ ok · ${count}`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const intent = includeToolIntent ? args?.[INTENT_FIELD] : undefined;
|
|
126
|
+
if (typeof intent === "string" && intent.trim()) {
|
|
127
|
+
const formattedIntent = oneLine(intent, 80);
|
|
128
|
+
return `# ${formattedIntent}\n${base}`;
|
|
112
129
|
}
|
|
113
|
-
return
|
|
130
|
+
return base;
|
|
114
131
|
}
|
|
115
132
|
|
|
116
133
|
/** One line for a user-initiated `!`/`$` execution. */
|
|
@@ -193,9 +210,11 @@ export function formatSessionHistoryMarkdown(messages: unknown[], opts?: History
|
|
|
193
210
|
} else if (block.type === "toolCall") {
|
|
194
211
|
const result = resultsByCallId.get(block.id);
|
|
195
212
|
if (result) consumed.add(block.id);
|
|
196
|
-
body.push(toolCallLine(block.name, block.arguments, result));
|
|
213
|
+
body.push(toolCallLine(block.name, block.arguments, result, opts?.includeToolIntent));
|
|
214
|
+
} else if (opts?.includeThinking && block.type === "thinking" && block.thinking.trim()) {
|
|
215
|
+
body.push(`_thinking:_ ${block.thinking}`);
|
|
197
216
|
}
|
|
198
|
-
//
|
|
217
|
+
// redactedThinking elided entirely (no readable text)
|
|
199
218
|
}
|
|
200
219
|
if (body.length === 0) break;
|
|
201
220
|
lines.push("## assistant", "", ...body, "");
|
|
@@ -204,7 +223,7 @@ export function formatSessionHistoryMarkdown(messages: unknown[], opts?: History
|
|
|
204
223
|
case "toolResult": {
|
|
205
224
|
// Normally consumed by its toolCall; orphans (e.g. truncated history) get their own line.
|
|
206
225
|
if (consumed.has(msg.toolCallId)) break;
|
|
207
|
-
lines.push(toolCallLine(msg.toolName, undefined, msg), "");
|
|
226
|
+
lines.push(toolCallLine(msg.toolName, undefined, msg, opts?.includeToolIntent), "");
|
|
208
227
|
break;
|
|
209
228
|
}
|
|
210
229
|
case "bashExecution": {
|
|
@@ -1522,15 +1522,17 @@ export class SessionManager {
|
|
|
1522
1522
|
/**
|
|
1523
1523
|
* Open a specific session file.
|
|
1524
1524
|
* @param sessionDir Optional dir for /new or /branch; defaults to the file's parent.
|
|
1525
|
+
* @param options.initialCwd Cwd to use when the file is empty or missing.
|
|
1525
1526
|
*/
|
|
1526
1527
|
static async open(
|
|
1527
1528
|
filePath: string,
|
|
1528
1529
|
sessionDir?: string,
|
|
1529
1530
|
storage: SessionStorage = new FileSessionStorage(),
|
|
1531
|
+
options?: { initialCwd?: string },
|
|
1530
1532
|
): Promise<SessionManager> {
|
|
1531
1533
|
const loaded = await loadEntriesFromFile(filePath, storage);
|
|
1532
1534
|
const header = loaded.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
1533
|
-
const cwd = header?.cwd ?? getProjectDir();
|
|
1535
|
+
const cwd = header?.cwd ?? options?.initialCwd ?? getProjectDir();
|
|
1534
1536
|
const dir = sessionDir ?? path.dirname(path.resolve(filePath));
|
|
1535
1537
|
const manager = new SessionManager(cwd, dir, true, storage);
|
|
1536
1538
|
await manager.setSessionFile(filePath);
|
|
@@ -6,6 +6,8 @@ export interface YieldDispatcher<P> {
|
|
|
6
6
|
isStale?(entry: P): boolean;
|
|
7
7
|
/** Produce one batched AgentMessage from non-stale entries. Return null to skip. */
|
|
8
8
|
build(survivors: P[]): AgentMessage | null;
|
|
9
|
+
/** If true, entries for this kind are drained only by {@link drainLazy} and never trigger the idle flush. */
|
|
10
|
+
skipIdleFlush?: boolean;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export interface YieldQueueOptions {
|
|
@@ -20,6 +22,7 @@ type YieldFlushMode = "streaming" | "idle";
|
|
|
20
22
|
interface StoredDispatcher {
|
|
21
23
|
isStale?: (entry: unknown) => boolean;
|
|
22
24
|
build: (survivors: unknown[]) => AgentMessage | null;
|
|
25
|
+
skipIdleFlush?: boolean;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
function formatError(error: unknown): string {
|
|
@@ -40,6 +43,7 @@ export class YieldQueue {
|
|
|
40
43
|
const stored: StoredDispatcher = {
|
|
41
44
|
...(dispatcher.isStale ? { isStale: entry => dispatcher.isStale?.(entry as P) ?? false } : {}),
|
|
42
45
|
build: survivors => dispatcher.build(survivors as P[]),
|
|
46
|
+
...(dispatcher.skipIdleFlush ? { skipIdleFlush: true } : {}),
|
|
43
47
|
};
|
|
44
48
|
this.#dispatchers.set(kind, stored);
|
|
45
49
|
return () => {
|
|
@@ -60,7 +64,7 @@ export class YieldQueue {
|
|
|
60
64
|
this.#entries.set(kind, entries);
|
|
61
65
|
}
|
|
62
66
|
entries.push(entry);
|
|
63
|
-
if (!this.#options.isStreaming()) {
|
|
67
|
+
if (!this.#options.isStreaming() && !this.#dispatchers.get(kind)!.skipIdleFlush) {
|
|
64
68
|
this.#scheduleIdleFlush();
|
|
65
69
|
}
|
|
66
70
|
}
|
|
@@ -419,6 +419,103 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
419
419
|
runtime.ctx.editor.setText("");
|
|
420
420
|
},
|
|
421
421
|
},
|
|
422
|
+
{
|
|
423
|
+
name: "advisor",
|
|
424
|
+
description: "Toggle the advisor (a second model that reviews each turn and injects notes)",
|
|
425
|
+
acpDescription: "Toggle advisor",
|
|
426
|
+
acpInputHint: "[on|off|status|dump [raw]]",
|
|
427
|
+
subcommands: [
|
|
428
|
+
{ name: "on", description: "Enable the advisor" },
|
|
429
|
+
{ name: "off", description: "Disable the advisor" },
|
|
430
|
+
{ name: "status", description: "Show advisor status" },
|
|
431
|
+
{ name: "dump", description: "Copy the advisor's transcript to clipboard", usage: "[raw]" },
|
|
432
|
+
],
|
|
433
|
+
allowArgs: true,
|
|
434
|
+
handle: async (command, runtime) => {
|
|
435
|
+
const { verb, rest } = parseSubcommand(command.args);
|
|
436
|
+
if (!verb || verb === "toggle") {
|
|
437
|
+
const active = runtime.session.toggleAdvisorEnabled();
|
|
438
|
+
const configured = runtime.session.isAdvisorEnabled();
|
|
439
|
+
if (active) {
|
|
440
|
+
await runtime.output("Advisor enabled.");
|
|
441
|
+
} else if (configured) {
|
|
442
|
+
await runtime.output("Advisor setting enabled, but no model is assigned to the 'advisor' role.");
|
|
443
|
+
} else {
|
|
444
|
+
await runtime.output("Advisor disabled.");
|
|
445
|
+
}
|
|
446
|
+
return commandConsumed();
|
|
447
|
+
}
|
|
448
|
+
if (verb === "on") {
|
|
449
|
+
const active = runtime.session.setAdvisorEnabled(true);
|
|
450
|
+
await runtime.output(
|
|
451
|
+
active ? "Advisor enabled." : "Advisor setting enabled, but no model is assigned to the 'advisor' role.",
|
|
452
|
+
);
|
|
453
|
+
return commandConsumed();
|
|
454
|
+
}
|
|
455
|
+
if (verb === "off") {
|
|
456
|
+
runtime.session.setAdvisorEnabled(false);
|
|
457
|
+
await runtime.output("Advisor disabled.");
|
|
458
|
+
return commandConsumed();
|
|
459
|
+
}
|
|
460
|
+
if (verb === "status") {
|
|
461
|
+
await runtime.output(runtime.session.formatAdvisorStatus());
|
|
462
|
+
return commandConsumed();
|
|
463
|
+
}
|
|
464
|
+
if (verb === "dump") {
|
|
465
|
+
const isRaw = rest.toLowerCase() === "raw";
|
|
466
|
+
const text = runtime.session.formatAdvisorHistoryAsText({ compact: !isRaw });
|
|
467
|
+
await runtime.output(text ?? "Advisor is not active for this session.");
|
|
468
|
+
return commandConsumed();
|
|
469
|
+
}
|
|
470
|
+
return usage("Usage: /advisor [on|off|status|dump [raw]]", runtime);
|
|
471
|
+
},
|
|
472
|
+
handleTui: async (command, runtime) => {
|
|
473
|
+
const { verb, rest } = parseSubcommand(command.args);
|
|
474
|
+
if (!verb || verb === "toggle") {
|
|
475
|
+
const active = runtime.ctx.session.toggleAdvisorEnabled();
|
|
476
|
+
const configured = runtime.ctx.session.isAdvisorEnabled();
|
|
477
|
+
if (active) {
|
|
478
|
+
runtime.ctx.showStatus("Advisor enabled.");
|
|
479
|
+
} else if (configured) {
|
|
480
|
+
runtime.ctx.showStatus("Advisor setting enabled, but no model is assigned to the 'advisor' role.");
|
|
481
|
+
} else {
|
|
482
|
+
runtime.ctx.showStatus("Advisor disabled.");
|
|
483
|
+
}
|
|
484
|
+
refreshStatusLine(runtime.ctx);
|
|
485
|
+
runtime.ctx.editor.setText("");
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (verb === "on") {
|
|
489
|
+
const active = runtime.ctx.session.setAdvisorEnabled(true);
|
|
490
|
+
runtime.ctx.showStatus(
|
|
491
|
+
active ? "Advisor enabled." : "Advisor setting enabled, but no model is assigned to the 'advisor' role.",
|
|
492
|
+
);
|
|
493
|
+
refreshStatusLine(runtime.ctx);
|
|
494
|
+
runtime.ctx.editor.setText("");
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (verb === "off") {
|
|
498
|
+
runtime.ctx.session.setAdvisorEnabled(false);
|
|
499
|
+
runtime.ctx.showStatus("Advisor disabled.");
|
|
500
|
+
refreshStatusLine(runtime.ctx);
|
|
501
|
+
runtime.ctx.editor.setText("");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (verb === "status") {
|
|
505
|
+
await runtime.ctx.handleAdvisorStatusCommand();
|
|
506
|
+
runtime.ctx.editor.setText("");
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (verb === "dump") {
|
|
510
|
+
const isRaw = rest.toLowerCase() === "raw";
|
|
511
|
+
runtime.ctx.handleAdvisorDumpCommand(isRaw);
|
|
512
|
+
runtime.ctx.editor.setText("");
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
runtime.ctx.showStatus("Usage: /advisor [on|off|status|dump [raw]]");
|
|
516
|
+
runtime.ctx.editor.setText("");
|
|
517
|
+
},
|
|
518
|
+
},
|
|
422
519
|
{
|
|
423
520
|
name: "export",
|
|
424
521
|
description: "Export session to HTML file",
|
|
@@ -450,13 +547,17 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
450
547
|
name: "dump",
|
|
451
548
|
description: "Copy session transcript to clipboard",
|
|
452
549
|
acpDescription: "Return full transcript as plain text",
|
|
453
|
-
|
|
454
|
-
|
|
550
|
+
inlineHint: "[raw]",
|
|
551
|
+
allowArgs: true,
|
|
552
|
+
handle: async (command, runtime) => {
|
|
553
|
+
const isRaw = command.args.trim().toLowerCase() === "raw";
|
|
554
|
+
const text = runtime.session.formatSessionAsText({ compact: !isRaw });
|
|
455
555
|
await runtime.output(text || "No messages to dump yet.");
|
|
456
556
|
return commandConsumed();
|
|
457
557
|
},
|
|
458
|
-
handleTui:
|
|
459
|
-
|
|
558
|
+
handleTui: (command, runtime) => {
|
|
559
|
+
const isRaw = command.args.trim().toLowerCase() === "raw";
|
|
560
|
+
runtime.ctx.handleDumpCommand(isRaw);
|
|
460
561
|
runtime.ctx.editor.setText("");
|
|
461
562
|
},
|
|
462
563
|
},
|
package/src/system-prompt.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
7
7
|
import type { ToolExample, TSchema } from "@oh-my-pi/pi-ai";
|
|
8
|
-
import { renderToolInventory } from "@oh-my-pi/pi-ai/
|
|
8
|
+
import { renderToolInventory } from "@oh-my-pi/pi-ai/dialect";
|
|
9
9
|
import { $env, getGpuCachePath, getProjectDir, hasFsCode, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import { $ } from "bun";
|
|
11
11
|
import { contextFileCapability } from "./capability/context-file";
|
package/src/task/executor.ts
CHANGED
|
@@ -33,7 +33,7 @@ import { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage
|
|
|
33
33
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
34
34
|
import type { ArtifactManager } from "../session/artifacts";
|
|
35
35
|
import type { AuthStorage } from "../session/auth-storage";
|
|
36
|
-
import { SKILL_PROMPT_MESSAGE_TYPE } from "../session/messages";
|
|
36
|
+
import { SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../session/messages";
|
|
37
37
|
import { SessionManager } from "../session/session-manager";
|
|
38
38
|
import { truncateTail } from "../session/streaming-output";
|
|
39
39
|
import type { ContextFileEntry } from "../tools";
|
|
@@ -1829,9 +1829,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1829
1829
|
? resolvedThinkingLevel
|
|
1830
1830
|
: (thinkingLevel ?? resolvedThinkingLevel);
|
|
1831
1831
|
|
|
1832
|
+
const effectiveCwd = worktree ?? cwd;
|
|
1832
1833
|
const sessionManager = sessionFile
|
|
1833
|
-
? await awaitAbortable(SessionManager.open(sessionFile))
|
|
1834
|
-
: SessionManager.inMemory(
|
|
1834
|
+
? await awaitAbortable(SessionManager.open(sessionFile, undefined, undefined, { initialCwd: effectiveCwd }))
|
|
1835
|
+
: SessionManager.inMemory(effectiveCwd);
|
|
1835
1836
|
if (options.parentArtifactManager) {
|
|
1836
1837
|
sessionManager.adoptArtifactManager(options.parentArtifactManager);
|
|
1837
1838
|
}
|
|
@@ -2047,7 +2048,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2047
2048
|
{
|
|
2048
2049
|
getModel: () => session.model,
|
|
2049
2050
|
isIdle: () => !session.isStreaming,
|
|
2050
|
-
abort: () => session.abort(),
|
|
2051
|
+
abort: () => session.abort({ reason: USER_INTERRUPT_LABEL }),
|
|
2051
2052
|
hasPendingMessages: () => session.queuedMessageCount > 0,
|
|
2052
2053
|
shutdown: () => {},
|
|
2053
2054
|
getContextUsage: () => session.getContextUsage(),
|
package/src/task/index.ts
CHANGED
|
@@ -366,6 +366,49 @@ export function buildSpecializationAdvisory(
|
|
|
366
366
|
);
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Suggestion — never a rejection — nudging the spawner to coordinate via `irc`
|
|
371
|
+
* when one call creates ≥2 live siblings and it still holds spawn capacity.
|
|
372
|
+
* Returns undefined when there is nothing to coordinate or IRC is unavailable.
|
|
373
|
+
*/
|
|
374
|
+
export function buildCoordinationAdvisory(
|
|
375
|
+
items: TaskItem[],
|
|
376
|
+
depthCapacity: boolean,
|
|
377
|
+
ircEnabled: boolean,
|
|
378
|
+
): string | undefined {
|
|
379
|
+
if (!depthCapacity || !ircEnabled || items.length < 2) return undefined;
|
|
380
|
+
return (
|
|
381
|
+
`Coordinate: ${items.length} siblings are running together. If their work overlaps, have them ` +
|
|
382
|
+
`message each other via \`irc\` (by id, or "all" to broadcast) before editing shared files — ` +
|
|
383
|
+
`live coordination beats a serial handoff. Check \`irc\` op:"list" to see who is doing what.`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Compose the non-blocking advisory appended to a `task` result: the
|
|
389
|
+
* specialization nudge, plus — only when the siblings keep running after this
|
|
390
|
+
* call (`willRunAsync`) — the coordination suggestion. Coordination is gated on
|
|
391
|
+
* async because a sync fanout's siblings have already finished, so a
|
|
392
|
+
* "coordinate while they run" hint would misfire. Returns undefined when
|
|
393
|
+
* neither applies.
|
|
394
|
+
*/
|
|
395
|
+
export function composeSpawnAdvisory(args: {
|
|
396
|
+
agentName: string | undefined;
|
|
397
|
+
items: TaskItem[];
|
|
398
|
+
depthCapacity: boolean;
|
|
399
|
+
ircEnabled: boolean;
|
|
400
|
+
willRunAsync: boolean;
|
|
401
|
+
}): string | undefined {
|
|
402
|
+
return (
|
|
403
|
+
[
|
|
404
|
+
buildSpecializationAdvisory(args.agentName, args.items, args.depthCapacity),
|
|
405
|
+
args.willRunAsync ? buildCoordinationAdvisory(args.items, args.depthCapacity, args.ircEnabled) : undefined,
|
|
406
|
+
]
|
|
407
|
+
.filter(Boolean)
|
|
408
|
+
.join("\n\n") || undefined
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
369
412
|
/** Sentinel for async jobs whose subagent finished with a failing result; progress is already updated. */
|
|
370
413
|
class TaskJobError extends Error {}
|
|
371
414
|
|
|
@@ -539,16 +582,35 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
539
582
|
this.session.settings.get("task.maxRecursionDepth") ?? 2,
|
|
540
583
|
this.session.taskDepth ?? 0,
|
|
541
584
|
);
|
|
542
|
-
const
|
|
585
|
+
const ircEnabled = isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0);
|
|
586
|
+
// Coordination only makes sense when the siblings keep running after this
|
|
587
|
+
// call returns (async). In the sync fallback they have already completed,
|
|
588
|
+
// so a "coordinate while they run" hint would misfire.
|
|
589
|
+
const willRunAsync = !!manager && selectedAgent?.blocking !== true;
|
|
590
|
+
const advisory = this.session.suppressSpawnAdvisory
|
|
591
|
+
? undefined
|
|
592
|
+
: composeSpawnAdvisory({
|
|
593
|
+
agentName: params.agent,
|
|
594
|
+
items: spawnItems,
|
|
595
|
+
depthCapacity,
|
|
596
|
+
ircEnabled,
|
|
597
|
+
willRunAsync,
|
|
598
|
+
});
|
|
599
|
+
// Returns a fresh result (copied content array, copied text part) rather
|
|
600
|
+
// than mutating the caller's — task results are short-lived here, but an
|
|
601
|
+
// in-place edit on a shared/cached AgentToolResult would be a hidden trap.
|
|
543
602
|
const withAdvisory = (result: AgentToolResult<TaskToolDetails>): AgentToolResult<TaskToolDetails> => {
|
|
544
603
|
if (!advisory) return result;
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
604
|
+
let appended = false;
|
|
605
|
+
const content = result.content.map(part => {
|
|
606
|
+
if (!appended && part.type === "text" && typeof part.text === "string") {
|
|
607
|
+
appended = true;
|
|
608
|
+
return { ...part, text: `${part.text}\n\n${advisory}` };
|
|
609
|
+
}
|
|
610
|
+
return part;
|
|
611
|
+
});
|
|
612
|
+
if (!appended) content.push({ type: "text", text: advisory });
|
|
613
|
+
return { ...result, content };
|
|
552
614
|
};
|
|
553
615
|
if (!asyncEnabled || !manager || selectedAgent?.blocking === true) {
|
|
554
616
|
// Sync fallback: async execution disabled, orphaned host that never
|
|
@@ -614,7 +676,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
614
676
|
},
|
|
615
677
|
});
|
|
616
678
|
|
|
617
|
-
const ircEnabled = isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0);
|
|
618
679
|
const started: Array<{ agentId: string; jobId: string; description?: string }> = [];
|
|
619
680
|
const failedSchedules: string[] = [];
|
|
620
681
|
for (const spawn of spawns) {
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
* helpers swallow open/IO failures and degrade to "no cache" so a corrupt or
|
|
9
9
|
* unreadable DB never blocks a `gh` call.
|
|
10
10
|
*
|
|
11
|
-
* TTL:
|
|
12
11
|
* Soft TTL → return cached row directly.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Stateful issue/PR rows past soft TTL but within hard TTL → refresh
|
|
13
|
+
* synchronously, falling back to the cached row if the live fetch fails.
|
|
14
|
+
* Expensive PR diff rows past soft TTL but within hard TTL → return cached
|
|
15
|
+
* row AND schedule a background refresh (errors logged, never thrown).
|
|
15
16
|
* Past hard TTL → treat as miss and fetch fresh.
|
|
16
17
|
*/
|
|
17
18
|
|
|
@@ -21,6 +22,7 @@ import * as os from "node:os";
|
|
|
21
22
|
import * as path from "node:path";
|
|
22
23
|
import { getGithubCacheDbPath, logger } from "@oh-my-pi/pi-utils";
|
|
23
24
|
import type { Settings } from "../config/settings";
|
|
25
|
+
import { ToolAbortError } from "./tool-errors";
|
|
24
26
|
|
|
25
27
|
// ────────────────────────────────────────────────────────────────────────────
|
|
26
28
|
// Storage layer
|
|
@@ -449,7 +451,7 @@ export interface CacheLookupOptions<T> {
|
|
|
449
451
|
now?: number;
|
|
450
452
|
}
|
|
451
453
|
|
|
452
|
-
export type CacheStatus = "miss" | "fresh" | "stale" | "disabled";
|
|
454
|
+
export type CacheStatus = "miss" | "fresh" | "refreshed" | "stale" | "disabled";
|
|
453
455
|
|
|
454
456
|
export interface CacheLookupResult<T> {
|
|
455
457
|
rendered: string;
|
|
@@ -595,7 +597,7 @@ export async function getOrFetchView<T>(options: CacheLookupOptions<T>): Promise
|
|
|
595
597
|
status: "fresh",
|
|
596
598
|
fetchedAt: cached.fetchedAt,
|
|
597
599
|
};
|
|
598
|
-
} else {
|
|
600
|
+
} else if (options.kind === "pr-diff") {
|
|
599
601
|
scheduleBackgroundRefresh(
|
|
600
602
|
authKey,
|
|
601
603
|
options.repo,
|
|
@@ -611,6 +613,28 @@ export async function getOrFetchView<T>(options: CacheLookupOptions<T>): Promise
|
|
|
611
613
|
status: "stale",
|
|
612
614
|
fetchedAt: cached.fetchedAt,
|
|
613
615
|
};
|
|
616
|
+
} else {
|
|
617
|
+
try {
|
|
618
|
+
const fresh = await options.fetchFresh();
|
|
619
|
+
const fetchedAt = Date.now();
|
|
620
|
+
storeResult(authKey, options.repo, options.kind, options.number, options.includeComments, fresh, fetchedAt);
|
|
621
|
+
return { ...fresh, status: "refreshed", fetchedAt };
|
|
622
|
+
} catch (err) {
|
|
623
|
+
if (err instanceof ToolAbortError) throw err;
|
|
624
|
+
logger.debug("github cache: synchronous refresh failed; returning stale view", {
|
|
625
|
+
err: String(err),
|
|
626
|
+
repo: options.repo,
|
|
627
|
+
kind: options.kind,
|
|
628
|
+
number: options.number,
|
|
629
|
+
});
|
|
630
|
+
return {
|
|
631
|
+
rendered: cached.rendered,
|
|
632
|
+
sourceUrl: cached.sourceUrl,
|
|
633
|
+
payload: cached.payload,
|
|
634
|
+
status: "stale",
|
|
635
|
+
fetchedAt: cached.fetchedAt,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
614
638
|
}
|
|
615
639
|
}
|
|
616
640
|
|
|
@@ -624,7 +648,7 @@ export async function getOrFetchView<T>(options: CacheLookupOptions<T>): Promise
|
|
|
624
648
|
* Human-friendly freshness note for protocol-handler `notes[]` rendering.
|
|
625
649
|
*/
|
|
626
650
|
export function formatFreshnessNote(status: CacheStatus, fetchedAtMs: number, now: number = Date.now()): string {
|
|
627
|
-
if (status === "miss") return "Fetched live";
|
|
651
|
+
if (status === "miss" || status === "refreshed") return "Fetched live";
|
|
628
652
|
if (status === "disabled") return "Cache disabled; fetched live";
|
|
629
653
|
const ageSec = Math.max(0, Math.round((now - fetchedAtMs) / 1000));
|
|
630
654
|
const human =
|
|
@@ -633,6 +657,7 @@ export function formatFreshnessNote(status: CacheStatus, fetchedAtMs: number, no
|
|
|
633
657
|
: ageSec < 3600
|
|
634
658
|
? `${Math.round(ageSec / 60)}m ago`
|
|
635
659
|
: `${Math.round(ageSec / 3600)}h ago`;
|
|
636
|
-
if (status === "stale")
|
|
660
|
+
if (status === "stale")
|
|
661
|
+
return `WARNING: showing cached content from ${human}; live refresh failed or is still running`;
|
|
637
662
|
return `Cached: ${human}`;
|
|
638
663
|
}
|