@oh-my-pi/pi-coding-agent 15.13.2 → 16.0.0
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 +62 -0
- package/dist/cli.js +587 -499
- 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 +75 -5
- package/dist/types/eval/js/context-manager.d.ts +15 -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 +4 -1
- package/dist/types/modes/types.d.ts +9 -1
- package/dist/types/sdk.d.ts +3 -3
- package/dist/types/session/agent-session.d.ts +71 -2
- package/dist/types/session/session-history-format.d.ts +4 -0
- package/dist/types/session/unexpected-stop-classifier.d.ts +13 -0
- package/dist/types/session/yield-queue.d.ts +2 -0
- package/dist/types/stt/asr-client.d.ts +1 -1
- package/dist/types/tiny/title-client.d.ts +1 -1
- 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/tts/tts-client.d.ts +1 -1
- package/dist/types/utils/thinking-display.d.ts +1 -17
- 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.ts +25 -12
- package/src/config/model-registry.ts +6 -2
- package/src/config/model-roles.ts +13 -1
- package/src/config/settings-schema.ts +67 -5
- package/src/eval/__tests__/agent-bridge.test.ts +106 -46
- package/src/eval/__tests__/js-context-manager.test.ts +12 -2
- package/src/eval/js/context-manager.ts +40 -3
- package/src/eval/js/worker-entry.ts +7 -0
- package/src/export/html/template.js +18 -22
- package/src/internal-urls/docs-index.generated.ts +8 -5
- package/src/main.ts +19 -5
- package/src/modes/acp/acp-agent.ts +2 -2
- package/src/modes/acp/acp-event-mapper.ts +2 -2
- package/src/modes/components/advisor-message.ts +99 -0
- package/src/modes/components/agent-hub.ts +38 -7
- package/src/modes/components/assistant-message.ts +110 -15
- package/src/modes/components/snapcompact-shape-preview-doc.md +2 -2
- package/src/modes/components/snapcompact-shape-preview.ts +2 -2
- package/src/modes/components/status-line/segments.ts +20 -7
- package/src/modes/components/tree-selector.ts +3 -2
- package/src/modes/controllers/command-controller.ts +69 -2
- package/src/modes/controllers/event-controller.ts +3 -3
- package/src/modes/controllers/input-controller.ts +7 -1
- package/src/modes/controllers/streaming-reveal.ts +4 -4
- package/src/modes/interactive-mode.ts +14 -2
- package/src/modes/types.ts +9 -1
- package/src/modes/utils/ui-helpers.ts +12 -3
- package/src/prompts/advisor/advise-tool.md +1 -0
- package/src/prompts/advisor/system.md +31 -0
- package/src/prompts/agents/oracle.md +0 -1
- package/src/prompts/agents/reviewer.md +0 -1
- package/src/prompts/system/unexpected-stop-classifier.md +17 -0
- package/src/prompts/system/unexpected-stop-retry.md +4 -0
- package/src/sdk.ts +52 -13
- package/src/session/agent-session.ts +722 -21
- package/src/session/session-dump-format.ts +15 -142
- package/src/session/session-history-format.ts +30 -11
- package/src/session/unexpected-stop-classifier.ts +129 -0
- package/src/session/yield-queue.ts +5 -1
- package/src/slash-commands/builtin-registry.ts +102 -4
- package/src/stt/asr-client.ts +1 -1
- package/src/system-prompt.ts +1 -1
- package/src/tiny/title-client.ts +1 -1
- package/src/tools/browser/tab-supervisor.ts +1 -1
- package/src/tools/browser/tab-worker-entry.ts +12 -4
- package/src/tools/job.ts +1 -0
- package/src/tools/path-utils.ts +33 -2
- package/src/tools/report-tool-issue.ts +2 -7
- package/src/tts/tts-client.ts +1 -1
- package/src/utils/thinking-display.ts +8 -34
- package/src/web/scrapers/docs-rs.ts +2 -3
|
@@ -2,21 +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 {
|
|
10
|
-
type BashExecutionMessage,
|
|
11
|
-
type BranchSummaryMessage,
|
|
12
|
-
bashExecutionToText,
|
|
13
|
-
type CompactionSummaryMessage,
|
|
14
|
-
type CustomMessage,
|
|
15
|
-
type FileMentionMessage,
|
|
16
|
-
type HookMessage,
|
|
17
|
-
type PythonExecutionMessage,
|
|
18
|
-
pythonExecutionToText,
|
|
19
|
-
} 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";
|
|
20
9
|
|
|
21
10
|
/** Minimal tool shape for dump output (matches AgentTool fields used by formatSessionDumpText). */
|
|
22
11
|
export interface SessionDumpToolInfo {
|
|
@@ -34,22 +23,12 @@ export interface FormatSessionDumpTextOptions {
|
|
|
34
23
|
tools?: readonly SessionDumpToolInfo[];
|
|
35
24
|
}
|
|
36
25
|
|
|
37
|
-
/** Serialize an object as XML parameter elements, one per key. */
|
|
38
|
-
function formatArgsAsXml(args: Record<string, unknown>, indent = "\t"): string {
|
|
39
|
-
const parts: string[] = [];
|
|
40
|
-
for (const [key, value] of Object.entries(args)) {
|
|
41
|
-
if (key === INTENT_FIELD) continue;
|
|
42
|
-
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
43
|
-
parts.push(`${indent}<parameter name="${key}">${text}</parameter>`);
|
|
44
|
-
}
|
|
45
|
-
return parts.join("\n");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
26
|
/**
|
|
49
27
|
* Format messages and session metadata as markdown/plain text (same as AgentSession.formatSessionAsText / /dump).
|
|
50
28
|
*/
|
|
51
29
|
export function formatSessionDumpText(options: FormatSessionDumpTextOptions): string {
|
|
52
30
|
const lines: string[] = [];
|
|
31
|
+
const definition = getDialectDefinition(preferredDialect(options.model?.id ?? ""));
|
|
53
32
|
|
|
54
33
|
const systemPrompt = options.systemPrompt?.filter(prompt => prompt.length > 0) ?? [];
|
|
55
34
|
if (systemPrompt.length > 0) {
|
|
@@ -71,127 +50,21 @@ export function formatSessionDumpText(options: FormatSessionDumpTextOptions): st
|
|
|
71
50
|
lines.push("\n");
|
|
72
51
|
|
|
73
52
|
const tools = options.tools ?? [];
|
|
74
|
-
|
|
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) {
|
|
75
60
|
lines.push("## Available Tools\n");
|
|
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
61
|
lines.push(renderToolInventory(inventoryTools, options.model?.id ?? ""));
|
|
83
62
|
lines.push("\n");
|
|
84
63
|
}
|
|
85
64
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (typeof msg.content === "string") {
|
|
90
|
-
lines.push(msg.content);
|
|
91
|
-
} else {
|
|
92
|
-
for (const c of msg.content) {
|
|
93
|
-
if (c.type === "text") {
|
|
94
|
-
lines.push(c.text);
|
|
95
|
-
} else if (c.type === "image") {
|
|
96
|
-
lines.push("[Image]");
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
lines.push("\n");
|
|
101
|
-
} else if (msg.role === "assistant") {
|
|
102
|
-
const assistantMsg = msg as AssistantMessage;
|
|
103
|
-
lines.push("## Assistant\n");
|
|
104
|
-
|
|
105
|
-
for (const c of assistantMsg.content) {
|
|
106
|
-
if (c.type === "text") {
|
|
107
|
-
lines.push(c.text);
|
|
108
|
-
} else if (c.type === "thinking") {
|
|
109
|
-
const thinking = getVisibleThinkingText(c);
|
|
110
|
-
if (thinking.length === 0) continue;
|
|
111
|
-
lines.push("<thinking>");
|
|
112
|
-
lines.push(thinking);
|
|
113
|
-
lines.push("</thinking>\n");
|
|
114
|
-
} else if (c.type === "toolCall") {
|
|
115
|
-
lines.push(`<invoke name="${c.name}">`);
|
|
116
|
-
if (c.arguments && typeof c.arguments === "object") {
|
|
117
|
-
lines.push(formatArgsAsXml(c.arguments as Record<string, unknown>));
|
|
118
|
-
}
|
|
119
|
-
lines.push("<" + "/invoke>\n");
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
lines.push("");
|
|
123
|
-
} else if (msg.role === "toolResult") {
|
|
124
|
-
lines.push(`### Tool Result: ${msg.toolName}`);
|
|
125
|
-
if (msg.isError) {
|
|
126
|
-
lines.push("(error)");
|
|
127
|
-
}
|
|
128
|
-
for (const c of msg.content) {
|
|
129
|
-
if (c.type === "text") {
|
|
130
|
-
lines.push("```");
|
|
131
|
-
lines.push(c.text);
|
|
132
|
-
lines.push("```");
|
|
133
|
-
} else if (c.type === "image") {
|
|
134
|
-
lines.push("[Image output]");
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
lines.push("");
|
|
138
|
-
} else if (msg.role === "bashExecution") {
|
|
139
|
-
const bashMsg = msg as BashExecutionMessage;
|
|
140
|
-
if (!bashMsg.excludeFromContext) {
|
|
141
|
-
lines.push("## Bash Execution\n");
|
|
142
|
-
lines.push(bashExecutionToText(bashMsg));
|
|
143
|
-
lines.push("\n");
|
|
144
|
-
}
|
|
145
|
-
} else if (msg.role === "pythonExecution") {
|
|
146
|
-
const pythonMsg = msg as PythonExecutionMessage;
|
|
147
|
-
if (!pythonMsg.excludeFromContext) {
|
|
148
|
-
lines.push("## Python Execution\n");
|
|
149
|
-
lines.push(pythonExecutionToText(pythonMsg));
|
|
150
|
-
lines.push("\n");
|
|
151
|
-
}
|
|
152
|
-
} else if (msg.role === "custom" || msg.role === "hookMessage") {
|
|
153
|
-
const customMsg = msg as CustomMessage | HookMessage;
|
|
154
|
-
lines.push(`## ${customMsg.customType}\n`);
|
|
155
|
-
if (typeof customMsg.content === "string") {
|
|
156
|
-
lines.push(customMsg.content);
|
|
157
|
-
} else {
|
|
158
|
-
for (const c of customMsg.content) {
|
|
159
|
-
if (c.type === "text") {
|
|
160
|
-
lines.push(c.text);
|
|
161
|
-
} else if (c.type === "image") {
|
|
162
|
-
lines.push("[Image]");
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
lines.push("\n");
|
|
167
|
-
} else if (msg.role === "branchSummary") {
|
|
168
|
-
const branchMsg = msg as BranchSummaryMessage;
|
|
169
|
-
lines.push("## Branch Summary\n");
|
|
170
|
-
lines.push(`(from branch: ${branchMsg.fromId})\n`);
|
|
171
|
-
lines.push(branchMsg.summary);
|
|
172
|
-
lines.push("\n");
|
|
173
|
-
} else if (msg.role === "compactionSummary") {
|
|
174
|
-
const compactMsg = msg as CompactionSummaryMessage;
|
|
175
|
-
lines.push("## Compaction Summary\n");
|
|
176
|
-
lines.push(`(${compactMsg.tokensBefore} tokens before compaction)\n`);
|
|
177
|
-
lines.push(compactMsg.summary);
|
|
178
|
-
lines.push("\n");
|
|
179
|
-
} else if (msg.role === "fileMention") {
|
|
180
|
-
const fileMsg = msg as FileMentionMessage;
|
|
181
|
-
lines.push("## File Mention\n");
|
|
182
|
-
for (const file of fileMsg.files) {
|
|
183
|
-
lines.push(`<file path="${file.path}">`);
|
|
184
|
-
if (file.content) {
|
|
185
|
-
lines.push(file.content);
|
|
186
|
-
}
|
|
187
|
-
if (file.image) {
|
|
188
|
-
lines.push("[Image attached]");
|
|
189
|
-
}
|
|
190
|
-
lines.push("</file>\n");
|
|
191
|
-
}
|
|
192
|
-
lines.push("\n");
|
|
193
|
-
}
|
|
194
|
-
}
|
|
65
|
+
lines.push("## Transcript\n");
|
|
66
|
+
lines.push(definition.renderTranscript(convertToLlm([...options.messages]), { tools: inventoryTools }));
|
|
67
|
+
lines.push("\n");
|
|
195
68
|
|
|
196
69
|
return lines.join("\n").trim();
|
|
197
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": {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { type AssistantMessage, completeSimple } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
|
3
|
+
|
|
4
|
+
import type { ModelRegistry } from "../config/model-registry";
|
|
5
|
+
import { resolveRoleSelection } from "../config/model-resolver";
|
|
6
|
+
import type { Settings } from "../config/settings";
|
|
7
|
+
import unexpectedStopClassifierPrompt from "../prompts/system/unexpected-stop-classifier.md" with { type: "text" };
|
|
8
|
+
import { isTinyMemoryLocalModelKey, ONLINE_MEMORY_MODEL_KEY } from "../tiny/models";
|
|
9
|
+
import { tinyModelClient } from "../tiny/title-client";
|
|
10
|
+
|
|
11
|
+
const CLASSIFIER_SYSTEM_PROMPT = prompt.render(unexpectedStopClassifierPrompt);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The answer is a single word. OpenAI-compatible endpoints reject values below
|
|
15
|
+
* 16, so 16 is the smallest portable budget for this classifier.
|
|
16
|
+
*/
|
|
17
|
+
const ANSWER_MAX_TOKENS = 16;
|
|
18
|
+
/**
|
|
19
|
+
* Reasoning backends ignore `disableReasoning` on some providers, so reserve
|
|
20
|
+
* enough output room for the keyword to still land after unavoidable thinking.
|
|
21
|
+
*/
|
|
22
|
+
const REASONING_SAFE_MAX_TOKENS = 1024;
|
|
23
|
+
|
|
24
|
+
export interface ClassifyUnexpectedStopDeps {
|
|
25
|
+
settings: Settings;
|
|
26
|
+
registry: ModelRegistry;
|
|
27
|
+
sessionId: string;
|
|
28
|
+
metadataResolver?: (provider: string) => Record<string, unknown> | undefined;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isUnexpectedStopCandidate(message: AssistantMessage): boolean {
|
|
33
|
+
if (message.stopReason !== "stop") return false;
|
|
34
|
+
let hasText = false;
|
|
35
|
+
for (const content of message.content) {
|
|
36
|
+
if (content.type === "toolCall") return false;
|
|
37
|
+
if (content.type === "text" && /\S/.test(content.text)) {
|
|
38
|
+
hasText = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return hasText;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function classifyUnexpectedStop(
|
|
45
|
+
text: string,
|
|
46
|
+
deps: ClassifyUnexpectedStopDeps,
|
|
47
|
+
): Promise<boolean | undefined> {
|
|
48
|
+
const backend = deps.settings.get("providers.unexpectedStopModel");
|
|
49
|
+
try {
|
|
50
|
+
if (backend === ONLINE_MEMORY_MODEL_KEY) {
|
|
51
|
+
return await classifyOnline(text, deps);
|
|
52
|
+
}
|
|
53
|
+
if (isTinyMemoryLocalModelKey(backend)) {
|
|
54
|
+
return await classifyLocal(text, backend, deps);
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
logger.debug("unexpected-stop: classification failed", {
|
|
59
|
+
error: error instanceof Error ? error.message : String(error),
|
|
60
|
+
backend,
|
|
61
|
+
});
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function classifyOnline(text: string, deps: ClassifyUnexpectedStopDeps): Promise<boolean | undefined> {
|
|
67
|
+
const resolved = resolveRoleSelection(["smol"], deps.settings, deps.registry.getAvailable(), deps.registry);
|
|
68
|
+
const model = resolved?.model;
|
|
69
|
+
if (!model) {
|
|
70
|
+
throw new Error("unexpected-stop: no smol model available for classification");
|
|
71
|
+
}
|
|
72
|
+
const apiKey = await deps.registry.getApiKey(model, deps.sessionId);
|
|
73
|
+
if (!apiKey) {
|
|
74
|
+
throw new Error(`unexpected-stop: no API key for ${model.provider}/${model.id}`);
|
|
75
|
+
}
|
|
76
|
+
const metadata = deps.metadataResolver?.(model.provider);
|
|
77
|
+
const maxTokens = model.reasoning ? Math.max(ANSWER_MAX_TOKENS, REASONING_SAFE_MAX_TOKENS) : ANSWER_MAX_TOKENS;
|
|
78
|
+
|
|
79
|
+
const response = await completeSimple(
|
|
80
|
+
model,
|
|
81
|
+
{
|
|
82
|
+
systemPrompt: [CLASSIFIER_SYSTEM_PROMPT],
|
|
83
|
+
messages: [{ role: "user", content: text, timestamp: Date.now() }],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
apiKey: deps.registry.resolver(model, deps.sessionId),
|
|
87
|
+
maxTokens,
|
|
88
|
+
disableReasoning: true,
|
|
89
|
+
metadata,
|
|
90
|
+
signal: deps.signal,
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (response.stopReason === "error") {
|
|
95
|
+
throw new Error(`unexpected-stop: online classification failed: ${response.errorMessage ?? "unknown error"}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const outputText = response.content
|
|
99
|
+
.filter((part): part is { type: "text"; text: string } => part.type === "text")
|
|
100
|
+
.map(part => part.text)
|
|
101
|
+
.join("\n");
|
|
102
|
+
return parseUnexpectedStopClassification(outputText);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function classifyLocal(
|
|
106
|
+
text: string,
|
|
107
|
+
modelKey: string,
|
|
108
|
+
deps: ClassifyUnexpectedStopDeps,
|
|
109
|
+
): Promise<boolean | undefined> {
|
|
110
|
+
if (!isTinyMemoryLocalModelKey(modelKey)) {
|
|
111
|
+
throw new Error(`unexpected-stop: unsupported local classifier model: ${modelKey}`);
|
|
112
|
+
}
|
|
113
|
+
const builtPrompt = prompt.render(unexpectedStopClassifierPrompt, { message: text });
|
|
114
|
+
const output = await tinyModelClient.complete(modelKey, builtPrompt, {
|
|
115
|
+
maxTokens: ANSWER_MAX_TOKENS,
|
|
116
|
+
signal: deps.signal,
|
|
117
|
+
});
|
|
118
|
+
if (!output) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
return parseUnexpectedStopClassification(output);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function parseUnexpectedStopClassification(text: string): boolean | undefined {
|
|
125
|
+
const trimmed = text.trim().toLowerCase();
|
|
126
|
+
if (trimmed.startsWith("yes")) return true;
|
|
127
|
+
if (trimmed.startsWith("no")) return false;
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
@@ -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,100 @@ 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.settings.get("advisor.enabled") as boolean;
|
|
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.settings.get("advisor.enabled") as boolean;
|
|
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
|
+
runtime.ctx.editor.setText("");
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (verb === "on") {
|
|
488
|
+
const active = runtime.ctx.session.setAdvisorEnabled(true);
|
|
489
|
+
runtime.ctx.showStatus(
|
|
490
|
+
active ? "Advisor enabled." : "Advisor setting enabled, but no model is assigned to the 'advisor' role.",
|
|
491
|
+
);
|
|
492
|
+
runtime.ctx.editor.setText("");
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (verb === "off") {
|
|
496
|
+
runtime.ctx.session.setAdvisorEnabled(false);
|
|
497
|
+
runtime.ctx.showStatus("Advisor disabled.");
|
|
498
|
+
runtime.ctx.editor.setText("");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (verb === "status") {
|
|
502
|
+
await runtime.ctx.handleAdvisorStatusCommand();
|
|
503
|
+
runtime.ctx.editor.setText("");
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (verb === "dump") {
|
|
507
|
+
const isRaw = rest.toLowerCase() === "raw";
|
|
508
|
+
runtime.ctx.handleAdvisorDumpCommand(isRaw);
|
|
509
|
+
runtime.ctx.editor.setText("");
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
runtime.ctx.showStatus("Usage: /advisor [on|off|status|dump [raw]]");
|
|
513
|
+
runtime.ctx.editor.setText("");
|
|
514
|
+
},
|
|
515
|
+
},
|
|
422
516
|
{
|
|
423
517
|
name: "export",
|
|
424
518
|
description: "Export session to HTML file",
|
|
@@ -450,13 +544,17 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
450
544
|
name: "dump",
|
|
451
545
|
description: "Copy session transcript to clipboard",
|
|
452
546
|
acpDescription: "Return full transcript as plain text",
|
|
453
|
-
|
|
454
|
-
|
|
547
|
+
inlineHint: "[raw]",
|
|
548
|
+
allowArgs: true,
|
|
549
|
+
handle: async (command, runtime) => {
|
|
550
|
+
const isRaw = command.args.trim().toLowerCase() === "raw";
|
|
551
|
+
const text = runtime.session.formatSessionAsText({ compact: !isRaw });
|
|
455
552
|
await runtime.output(text || "No messages to dump yet.");
|
|
456
553
|
return commandConsumed();
|
|
457
554
|
},
|
|
458
|
-
handleTui:
|
|
459
|
-
|
|
555
|
+
handleTui: (command, runtime) => {
|
|
556
|
+
const isRaw = command.args.trim().toLowerCase() === "raw";
|
|
557
|
+
runtime.ctx.handleDumpCommand(isRaw);
|
|
460
558
|
runtime.ctx.editor.setText("");
|
|
461
559
|
},
|
|
462
560
|
},
|
package/src/stt/asr-client.ts
CHANGED
|
@@ -72,7 +72,7 @@ const SMOKE_TEST_TIMEOUT_MS = 30_000;
|
|
|
72
72
|
* Hidden subcommand on the main CLI that boots the speech-recognition worker in
|
|
73
73
|
* the spawned subprocess. Kept in sync with the dispatch in `cli.ts`.
|
|
74
74
|
*/
|
|
75
|
-
export const STT_WORKER_ARG = "
|
|
75
|
+
export const STT_WORKER_ARG = "__omp_worker_stt";
|
|
76
76
|
|
|
77
77
|
function readTinyModelSetting(key: "providers.tinyModelDevice" | "providers.tinyModelDtype"): string | undefined {
|
|
78
78
|
try {
|
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/tiny/title-client.ts
CHANGED
|
@@ -69,7 +69,7 @@ function normalizeTinyTitleGenerateOptions(
|
|
|
69
69
|
* Hidden subcommand on the main CLI that boots the tiny-model worker in the
|
|
70
70
|
* spawned subprocess. Kept in sync with the dispatch in `cli.ts`.
|
|
71
71
|
*/
|
|
72
|
-
export const TINY_WORKER_ARG = "
|
|
72
|
+
export const TINY_WORKER_ARG = "__omp_worker_tiny_inference";
|
|
73
73
|
|
|
74
74
|
function readTinyModelSetting(path: "providers.tinyModelDevice" | "providers.tinyModelDtype"): string | undefined {
|
|
75
75
|
try {
|
|
@@ -685,7 +685,7 @@ async function spawnTabWorker(): Promise<WorkerHandle> {
|
|
|
685
685
|
try {
|
|
686
686
|
const hostEntry = workerHostEntry();
|
|
687
687
|
const worker = hostEntry
|
|
688
|
-
? new Worker(hostEntry, { type: "module", argv: ["
|
|
688
|
+
? new Worker(hostEntry, { type: "module", argv: ["__omp_worker_tab"] })
|
|
689
689
|
: new Worker(new URL("./tab-worker-entry.ts", import.meta.url).href, { type: "module" });
|
|
690
690
|
return wrapBunWorker(worker);
|
|
691
691
|
} catch (err) {
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import { parentPort } from "node:worker_threads";
|
|
2
|
+
import { consumeWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
|
|
2
3
|
import type { Transport, WorkerInbound, WorkerOutbound } from "./tab-protocol";
|
|
3
4
|
import { WorkerCore } from "./tab-worker";
|
|
4
5
|
|
|
5
6
|
if (!parentPort) throw new Error("tab-worker-entry: missing parentPort");
|
|
6
7
|
|
|
8
|
+
const port = parentPort;
|
|
9
|
+
// When the CLI host pre-buffered messages (it imports this module dynamically),
|
|
10
|
+
// bind that inbox so the parent's already-delivered `init` is replayed. Loaded
|
|
11
|
+
// directly (test/SDK fallback), this module's top-level runs synchronously at
|
|
12
|
+
// worker start, so the direct `parentPort.on` below wins the flush on its own.
|
|
13
|
+
const inbox = consumeWorkerInbox();
|
|
7
14
|
const transport: Transport = {
|
|
8
15
|
send(msg, transferList) {
|
|
9
|
-
|
|
16
|
+
port.postMessage(msg, transferList ?? []);
|
|
10
17
|
},
|
|
11
18
|
onMessage(handler) {
|
|
19
|
+
if (inbox) return inbox.bind(data => handler(data as WorkerOutbound | WorkerInbound));
|
|
12
20
|
const wrap = (message: unknown): void => handler(message as WorkerOutbound | WorkerInbound);
|
|
13
|
-
|
|
14
|
-
return () =>
|
|
21
|
+
port.on("message", wrap);
|
|
22
|
+
return () => port.off("message", wrap);
|
|
15
23
|
},
|
|
16
24
|
close() {
|
|
17
|
-
|
|
25
|
+
port.close();
|
|
18
26
|
},
|
|
19
27
|
};
|
|
20
28
|
|
package/src/tools/job.ts
CHANGED
|
@@ -87,6 +87,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
87
87
|
readonly description: string;
|
|
88
88
|
readonly parameters = jobSchema;
|
|
89
89
|
readonly strict = true;
|
|
90
|
+
readonly interruptible = true;
|
|
90
91
|
readonly loadMode = "discoverable";
|
|
91
92
|
constructor(private readonly session: ToolSession) {
|
|
92
93
|
this.description = prompt.render(jobDescription);
|