@oh-my-pi/pi-coding-agent 17.1.0 → 17.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/dist/cli.js +6325 -4420
- package/dist/types/cli/bench-cli.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +6 -3
- package/dist/types/config/settings-schema.d.ts +62 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
- package/dist/types/launch/broker-list-order.test.d.ts +1 -0
- package/dist/types/live/attestation.d.ts +2 -0
- package/dist/types/live/controller.d.ts +10 -2
- package/dist/types/live/protocol.d.ts +1 -1
- package/dist/types/live/transport.d.ts +6 -19
- package/dist/types/live/visualizer.d.ts +8 -11
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/custom-message.d.ts +1 -1
- package/dist/types/modes/components/message-frame.d.ts +8 -4
- package/dist/types/modes/components/session-account-selector.d.ts +11 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -6
- package/dist/types/modes/types.d.ts +9 -5
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -6
- package/dist/types/session/agent-session-types.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +20 -1
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/eval-runner.d.ts +2 -0
- package/dist/types/session/messages.d.ts +2 -0
- package/dist/types/session/session-tools.d.ts +15 -0
- package/dist/types/session/streaming-output.d.ts +8 -0
- package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
- package/dist/types/stt/index.d.ts +0 -2
- package/dist/types/stt/stt-controller.d.ts +7 -0
- package/dist/types/task/executor.d.ts +3 -1
- package/dist/types/task/structured-subagent.d.ts +3 -0
- package/dist/types/task/types.d.ts +11 -11
- package/dist/types/thinking.d.ts +13 -0
- package/dist/types/tiny/title-client.d.ts +10 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/computer/protocol.d.ts +43 -0
- package/dist/types/tools/computer/supervisor.d.ts +32 -0
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +15 -0
- package/dist/types/tools/computer-renderer.d.ts +22 -0
- package/dist/types/tools/computer.d.ts +71 -0
- package/dist/types/tools/context.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +21 -0
- package/dist/types/tools/eval-format/index.d.ts +7 -0
- package/dist/types/tools/eval-format/javascript.d.ts +2 -0
- package/dist/types/tools/eval-format/julia.d.ts +2 -0
- package/dist/types/tools/eval-format/python.d.ts +2 -0
- package/dist/types/tools/eval-format/ruby.d.ts +2 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +7 -0
- package/dist/types/tools/todo.d.ts +4 -1
- package/dist/types/tts/streaming-player.d.ts +10 -43
- package/dist/types/utils/tools-manager.d.ts +1 -2
- package/dist/types/web/search/providers/base.d.ts +16 -0
- package/dist/types/web/search/providers/brave.d.ts +2 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +2 -0
- package/dist/types/web/search/providers/gemini.d.ts +3 -0
- package/dist/types/web/search/providers/jina.d.ts +2 -0
- package/dist/types/web/search/providers/kagi.d.ts +2 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/parallel.d.ts +2 -0
- package/dist/types/web/search/providers/perplexity.d.ts +3 -0
- package/dist/types/web/search/providers/searxng.d.ts +10 -0
- package/dist/types/web/search/providers/tavily.d.ts +8 -0
- package/dist/types/web/search/query.d.ts +190 -0
- package/package.json +12 -12
- package/src/cli/args.ts +1 -0
- package/src/cli/bench-cli.ts +12 -1
- package/src/cli/setup-cli.ts +2 -14
- package/src/cli/web-search-cli.ts +7 -0
- package/src/cli.ts +8 -0
- package/src/config/model-registry.ts +6 -0
- package/src/config/model-resolver.ts +17 -6
- package/src/config/settings-schema.ts +66 -0
- package/src/debug/raw-sse-buffer.ts +157 -30
- package/src/eval/executor-base.ts +1 -0
- package/src/eval/js/executor.ts +2 -0
- package/src/exec/bash-executor.ts +1 -0
- package/src/export/share.ts +4 -3
- package/src/extensibility/extensions/wrapper.ts +68 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
- package/src/launch/broker-list-order.test.ts +89 -0
- package/src/launch/broker.ts +49 -8
- package/src/live/attestation.ts +91 -0
- package/src/live/controller.ts +76 -23
- package/src/live/protocol.test.ts +3 -3
- package/src/live/protocol.ts +1 -1
- package/src/live/transport.ts +72 -140
- package/src/live/visualizer.ts +114 -134
- package/src/modes/components/assistant-message.ts +7 -2
- package/src/modes/components/custom-message.ts +4 -1
- package/src/modes/components/message-frame.ts +14 -8
- package/src/modes/components/session-account-selector.ts +62 -0
- package/src/modes/components/tool-execution.ts +17 -110
- package/src/modes/controllers/input-controller.ts +55 -47
- package/src/modes/controllers/live-command-controller.ts +82 -5
- package/src/modes/controllers/selector-controller.ts +62 -0
- package/src/modes/interactive-mode.ts +79 -5
- package/src/modes/types.ts +10 -4
- package/src/modes/utils/ui-helpers.ts +7 -8
- package/src/prompts/system/computer-safety.md +14 -0
- package/src/prompts/system/resolve-device-reminder.md +1 -1
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/computer.md +26 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/task.md +5 -2
- package/src/prompts/tools/web-search.md +2 -0
- package/src/sdk.ts +13 -4
- package/src/session/agent-session-types.ts +9 -0
- package/src/session/agent-session.ts +70 -0
- package/src/session/auth-storage.ts +1 -0
- package/src/session/eval-runner.ts +5 -0
- package/src/session/messages.ts +3 -0
- package/src/session/queued-messages.ts +7 -1
- package/src/session/session-tools.ts +37 -0
- package/src/session/streaming-output.ts +52 -5
- package/src/slash-commands/builtin-registry.ts +165 -9
- package/src/slash-commands/helpers/session-pin.ts +44 -0
- package/src/stt/downloader.ts +0 -2
- package/src/stt/index.ts +0 -2
- package/src/stt/stt-controller.ts +57 -146
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +17 -9
- package/src/task/index.ts +14 -34
- package/src/task/structured-subagent.ts +4 -0
- package/src/task/types.ts +15 -13
- package/src/thinking.ts +27 -0
- package/src/tiny/title-client.ts +22 -0
- package/src/tools/ast-edit.ts +4 -1
- package/src/tools/bash-interactive.ts +90 -86
- package/src/tools/bash.ts +13 -0
- package/src/tools/browser/render.ts +2 -1
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/computer/protocol.ts +28 -0
- package/src/tools/computer/supervisor.ts +258 -0
- package/src/tools/computer/worker-entry.ts +25 -0
- package/src/tools/computer/worker.ts +135 -0
- package/src/tools/computer-renderer.ts +108 -0
- package/src/tools/computer.ts +433 -0
- package/src/tools/context.ts +2 -0
- package/src/tools/default-renderer.ts +139 -0
- package/src/tools/essential-tools.ts +1 -0
- package/src/tools/eval-format/index.ts +24 -0
- package/src/tools/eval-format/javascript.ts +952 -0
- package/src/tools/eval-format/julia.ts +446 -0
- package/src/tools/eval-format/python.ts +544 -0
- package/src/tools/eval-format/ruby.ts +380 -0
- package/src/tools/eval-render.ts +12 -6
- package/src/tools/index.ts +5 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +10 -1
- package/src/tools/todo.ts +58 -6
- package/src/tools/xdev.ts +54 -26
- package/src/tts/streaming-player.ts +81 -340
- package/src/utils/clipboard.ts +1 -30
- package/src/utils/mac-file-urls.applescript +37 -0
- package/src/utils/tool-choice.ts +14 -0
- package/src/utils/tools-manager.ts +1 -19
- package/src/web/search/index.ts +28 -5
- package/src/web/search/providers/anthropic.ts +62 -4
- package/src/web/search/providers/base.ts +16 -0
- package/src/web/search/providers/brave.ts +30 -3
- package/src/web/search/providers/codex.ts +14 -1
- package/src/web/search/providers/duckduckgo.ts +23 -1
- package/src/web/search/providers/ecosia.ts +5 -1
- package/src/web/search/providers/exa.ts +28 -1
- package/src/web/search/providers/firecrawl.ts +37 -3
- package/src/web/search/providers/gemini.ts +12 -2
- package/src/web/search/providers/google.ts +2 -1
- package/src/web/search/providers/jina.ts +31 -8
- package/src/web/search/providers/kagi.ts +10 -1
- package/src/web/search/providers/kimi.ts +16 -1
- package/src/web/search/providers/mojeek.ts +14 -1
- package/src/web/search/providers/parallel.ts +48 -2
- package/src/web/search/providers/perplexity.ts +94 -6
- package/src/web/search/providers/searxng.ts +145 -9
- package/src/web/search/providers/startpage.ts +8 -2
- package/src/web/search/providers/synthetic.ts +7 -1
- package/src/web/search/providers/tavily.ts +52 -3
- package/src/web/search/providers/tinyfish.ts +6 -1
- package/src/web/search/providers/xai.ts +49 -2
- package/src/web/search/providers/zai.ts +19 -1
- package/src/web/search/query.ts +850 -0
- package/dist/types/stt/recorder.d.ts +0 -30
- package/dist/types/stt/transcriber.d.ts +0 -14
- package/dist/types/stt/wav.d.ts +0 -29
- package/dist/types/tts/player.d.ts +0 -32
- package/src/live/audio-worklet.txt +0 -59
- package/src/live/browser-runtime.txt +0 -221
- package/src/stt/recorder.ts +0 -551
- package/src/stt/transcriber.ts +0 -60
- package/src/stt/wav.ts +0 -173
- package/src/tts/player.ts +0 -137
|
@@ -3,6 +3,13 @@ import type { Model, ProviderResponseMetadata, RawSseEvent } from "@oh-my-pi/pi-
|
|
|
3
3
|
const MAX_RAW_SSE_EVENTS = 1_000;
|
|
4
4
|
const MAX_RAW_SSE_CHARS = 512_000;
|
|
5
5
|
const MAX_RAW_SSE_EVENT_CHARS = 64_000;
|
|
6
|
+
// Reserve room for the `: omp-debug-truncated` / `: omp-debug-elided` marker
|
|
7
|
+
// lines so a trimmed event stays within MAX_RAW_SSE_EVENT_CHARS overall.
|
|
8
|
+
const TRIM_MARKER_RESERVE = 200;
|
|
9
|
+
// Caps applied to individual tool entries when compacting a `tools` array
|
|
10
|
+
// inside an oversized `data:` payload.
|
|
11
|
+
const MAX_TOOL_SCHEMA_CHARS = 200;
|
|
12
|
+
const MAX_TOOL_DESCRIPTION_CHARS = 200;
|
|
6
13
|
|
|
7
14
|
export type RawSseDebugRecord =
|
|
8
15
|
| {
|
|
@@ -45,42 +52,162 @@ export interface RawSseDebugSnapshot {
|
|
|
45
52
|
// path. The parallel array keeps records as plain monomorphic objects.
|
|
46
53
|
type TrimResult = { raw: string[]; truncated: boolean; originalChars: number; chars: number };
|
|
47
54
|
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
// `chars` uses the historical formula `reduce(line.length + 1, init = 1)` so
|
|
56
|
+
// the accounting matches the previous `countRecordChars` byte-for-byte (the
|
|
57
|
+
// trailing +1 covers the record-level newline that `rawRecordText` appends in
|
|
58
|
+
// `toRawText`).
|
|
59
|
+
function countLines(lines: readonly string[]): number {
|
|
60
|
+
let chars = 0;
|
|
61
|
+
for (let i = 0; i < lines.length; i++) chars += lines[i].length + 1;
|
|
62
|
+
return chars;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function elideText(text: string, max: number): string {
|
|
66
|
+
if (text.length <= max) return text;
|
|
67
|
+
return `${text.slice(0, max)}… (+${text.length - max} chars)`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Shrinks one tool definition in place: schemas (`parameters` for OpenAI
|
|
71
|
+
// shapes, `input_schema` for Anthropic) become elided JSON strings and long
|
|
72
|
+
// descriptions are cut, while `name`/`type` survive untouched. Chat-completions
|
|
73
|
+
// nests the payload under `function`.
|
|
74
|
+
function compactToolEntry(tool: unknown): boolean {
|
|
75
|
+
if (typeof tool !== "object" || tool === null) return false;
|
|
76
|
+
const obj = tool as Record<string, unknown>;
|
|
77
|
+
let changed = false;
|
|
78
|
+
if (typeof obj.function === "object" && obj.function !== null) {
|
|
79
|
+
changed = compactToolEntry(obj.function);
|
|
80
|
+
}
|
|
81
|
+
for (const key of ["parameters", "input_schema"]) {
|
|
82
|
+
const schema = obj[key];
|
|
83
|
+
if (schema === undefined || schema === null) continue;
|
|
84
|
+
const text = typeof schema === "string" ? schema : JSON.stringify(schema);
|
|
85
|
+
if (text.length <= MAX_TOOL_SCHEMA_CHARS) continue;
|
|
86
|
+
obj[key] = elideText(text, MAX_TOOL_SCHEMA_CHARS);
|
|
87
|
+
changed = true;
|
|
88
|
+
}
|
|
89
|
+
if (typeof obj.description === "string" && obj.description.length > MAX_TOOL_DESCRIPTION_CHARS) {
|
|
90
|
+
obj.description = elideText(obj.description, MAX_TOOL_DESCRIPTION_CHARS);
|
|
91
|
+
changed = true;
|
|
92
|
+
}
|
|
93
|
+
return changed;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Walks a parsed SSE payload and compacts every `tools` array it finds
|
|
97
|
+
// (e.g. `response.tools` echoed back by the Responses API). Mutates `node`.
|
|
98
|
+
function compactToolsDeep(node: unknown): boolean {
|
|
99
|
+
if (Array.isArray(node)) {
|
|
100
|
+
let changed = false;
|
|
101
|
+
for (const item of node) changed = compactToolsDeep(item) || changed;
|
|
102
|
+
return changed;
|
|
103
|
+
}
|
|
104
|
+
if (typeof node !== "object" || node === null) return false;
|
|
105
|
+
let changed = false;
|
|
106
|
+
const obj = node as Record<string, unknown>;
|
|
107
|
+
for (const key in obj) {
|
|
108
|
+
const value = obj[key];
|
|
109
|
+
if (key === "tools" && Array.isArray(value)) {
|
|
110
|
+
for (const tool of value) changed = compactToolEntry(tool) || changed;
|
|
111
|
+
} else {
|
|
112
|
+
changed = compactToolsDeep(value) || changed;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return changed;
|
|
116
|
+
}
|
|
59
117
|
|
|
118
|
+
// Rewrites oversized `data:` lines with tool schemas compacted. Returns null
|
|
119
|
+
// when nothing changed (unparseable payloads or no tools to shrink). Only
|
|
120
|
+
// invoked on events that already blew the budget, so the JSON round-trip is
|
|
121
|
+
// off the streaming hot path.
|
|
122
|
+
function compactToolLines(raw: readonly string[]): string[] | null {
|
|
123
|
+
let changed = false;
|
|
124
|
+
const out = raw.map(line => {
|
|
125
|
+
if (!line.startsWith("data:") || line.length <= MAX_TOOL_SCHEMA_CHARS) return line;
|
|
126
|
+
const start = line.charCodeAt(5) === 32 ? 6 : 5;
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(line.slice(start));
|
|
129
|
+
if (!compactToolsDeep(parsed)) return line;
|
|
130
|
+
changed = true;
|
|
131
|
+
return `data: ${JSON.stringify(parsed)}`;
|
|
132
|
+
} catch {
|
|
133
|
+
return line;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return changed ? out : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Keeps the first and last portions of an over-budget event and drops the
|
|
140
|
+
// middle, so leading fields (id/model/status) AND trailing fields
|
|
141
|
+
// (usage/finish_reason) both stay visible. A `: omp-debug-elided` comment
|
|
142
|
+
// marks the cut; split lines carry `…` at the cut edge.
|
|
143
|
+
function headTailTrim(lines: string[], budget: number, elidedTotal: number): string[] {
|
|
144
|
+
const headBudget = budget >> 1;
|
|
145
|
+
const tailBudget = budget - headBudget;
|
|
146
|
+
|
|
147
|
+
let i = 0;
|
|
148
|
+
let headRemaining = headBudget;
|
|
149
|
+
const out: string[] = [];
|
|
150
|
+
while (i < lines.length && lines[i].length + 1 <= headRemaining) {
|
|
151
|
+
headRemaining -= lines[i].length + 1;
|
|
152
|
+
out.push(lines[i]);
|
|
153
|
+
i++;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let j = lines.length - 1;
|
|
157
|
+
let tailRemaining = tailBudget;
|
|
158
|
+
const tail: string[] = [];
|
|
159
|
+
while (j >= i && lines[j].length + 1 <= tailRemaining) {
|
|
160
|
+
tailRemaining -= lines[j].length + 1;
|
|
161
|
+
tail.push(lines[j]);
|
|
162
|
+
j--;
|
|
163
|
+
}
|
|
164
|
+
tail.reverse();
|
|
165
|
+
|
|
166
|
+
let elided = elidedTotal - countLines(out) - countLines(tail);
|
|
167
|
+
if (i <= j) {
|
|
168
|
+
// lines[i..j] straddle the cut: keep a head slice of the first and a
|
|
169
|
+
// tail slice of the last (the same line when i === j).
|
|
170
|
+
const headSlice = lines[i].slice(0, Math.max(0, headRemaining - 2));
|
|
171
|
+
const tailStart =
|
|
172
|
+
i === j
|
|
173
|
+
? Math.max(headSlice.length, lines[j].length - tailRemaining + 2)
|
|
174
|
+
: Math.max(0, lines[j].length - tailRemaining + 2);
|
|
175
|
+
const tailSlice = lines[j].slice(tailStart);
|
|
176
|
+
elided -= headSlice.length + tailSlice.length;
|
|
177
|
+
if (headSlice.length > 0) out.push(`${headSlice}…`);
|
|
178
|
+
out.push(`: omp-debug-elided chars=${Math.max(0, elided)}`);
|
|
179
|
+
if (tailSlice.length > 0) out.push(`…${tailSlice}`);
|
|
180
|
+
} else if (elided > 0) {
|
|
181
|
+
out.push(`: omp-debug-elided chars=${elided}`);
|
|
182
|
+
}
|
|
183
|
+
out.push(...tail);
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Trim pipeline for one SSE event:
|
|
188
|
+
// 1. fits → return `raw` **by reference** (ownership contract at
|
|
189
|
+
// `RawSseDebugBuffer.recordEvent` below).
|
|
190
|
+
// 2. over budget → compact tool schemas inside `data:` JSON payloads;
|
|
191
|
+
// if that alone fits, the payload stays parseable JSON.
|
|
192
|
+
// 3. still over → head+tail trim (middle elided).
|
|
193
|
+
// Any trimmed result ends with the `: omp-debug-truncated` marker carrying
|
|
194
|
+
// the original size.
|
|
195
|
+
function trimRawLines(raw: string[]): TrimResult {
|
|
196
|
+
const originalChars = countLines(raw);
|
|
60
197
|
if (originalChars <= MAX_RAW_SSE_EVENT_CHARS) {
|
|
61
198
|
return { raw, truncated: false, originalChars, chars: originalChars + 1 };
|
|
62
199
|
}
|
|
63
200
|
|
|
64
|
-
const
|
|
65
|
-
let
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
chars += line.length + 1;
|
|
72
|
-
remaining -= line.length + 1;
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
const slice = line.slice(0, Math.max(0, remaining));
|
|
76
|
-
trimmed.push(slice);
|
|
77
|
-
chars += slice.length + 1;
|
|
78
|
-
remaining = 0;
|
|
201
|
+
const budget = MAX_RAW_SSE_EVENT_CHARS - TRIM_MARKER_RESERVE;
|
|
202
|
+
let lines = compactToolLines(raw) ?? raw;
|
|
203
|
+
const compactedChars = lines === raw ? originalChars : countLines(lines);
|
|
204
|
+
if (compactedChars > budget) {
|
|
205
|
+
lines = headTailTrim(lines, budget, compactedChars);
|
|
206
|
+
} else if (lines === raw) {
|
|
207
|
+
lines = raw.slice();
|
|
79
208
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
chars += tail.length + 1;
|
|
83
|
-
return { raw: trimmed, truncated: true, originalChars, chars };
|
|
209
|
+
lines.push(`: omp-debug-truncated originalChars=${originalChars}`);
|
|
210
|
+
return { raw: lines, truncated: true, originalChars, chars: countLines(lines) + 1 };
|
|
84
211
|
}
|
|
85
212
|
|
|
86
213
|
export function formatRawSseIsoTime(timestamp: number): string {
|
package/src/eval/js/executor.ts
CHANGED
package/src/export/share.ts
CHANGED
|
@@ -400,8 +400,9 @@ function redactShareMessage(
|
|
|
400
400
|
};
|
|
401
401
|
case "assistant":
|
|
402
402
|
// Drop opaque provider-replay state (encrypted reasoning / native history) the viewer
|
|
403
|
-
// never reads and we cannot redact field-by-field: `providerPayload
|
|
404
|
-
// `redactedThinking` blocks
|
|
403
|
+
// never reads and we cannot redact field-by-field: `providerPayload`, any
|
|
404
|
+
// `redactedThinking` blocks, and native Anthropic server-tool blocks
|
|
405
|
+
// (`server_tool_use` input / `web_search_tool_result` encrypted_content).
|
|
405
406
|
return {
|
|
406
407
|
...message,
|
|
407
408
|
providerPayload: undefined,
|
|
@@ -410,7 +411,7 @@ function redactShareMessage(
|
|
|
410
411
|
? undefined
|
|
411
412
|
: o.obfuscate(message.errorMessage, sharedRegexSecretValues),
|
|
412
413
|
content: message.content.flatMap((block): AssistantMessage["content"] => {
|
|
413
|
-
if (block.type === "redactedThinking") return [];
|
|
414
|
+
if (block.type === "redactedThinking" || block.type === "anthropicServerTool") return [];
|
|
414
415
|
if (block.type === "text") return [{ ...block, text: o.obfuscate(block.text, sharedRegexSecretValues) }];
|
|
415
416
|
if (block.type === "thinking") {
|
|
416
417
|
return [{ ...block, thinking: o.obfuscate(block.thinking, sharedRegexSecretValues) }];
|
|
@@ -8,10 +8,11 @@ import type {
|
|
|
8
8
|
AgentToolUpdateCallback,
|
|
9
9
|
ToolLoadMode,
|
|
10
10
|
} from "@oh-my-pi/pi-agent-core";
|
|
11
|
-
import type { ImageContent, Static, TextContent, TSchema } from "@oh-my-pi/pi-ai";
|
|
11
|
+
import type { ComputerSafetyCheck, ImageContent, Static, TextContent, TSchema } from "@oh-my-pi/pi-ai";
|
|
12
|
+
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
12
13
|
import type { Settings } from "../../config/settings";
|
|
13
14
|
import type { Theme } from "../../modes/theme/theme";
|
|
14
|
-
import { type ApprovalMode, formatApprovalPrompt, resolveApproval } from "../../tools/approval";
|
|
15
|
+
import { type ApprovalMode, formatApprovalPrompt, resolveApproval, truncateForPrompt } from "../../tools/approval";
|
|
15
16
|
import { defaultLoadModeForToolName } from "../../tools/essential-tools";
|
|
16
17
|
import { normalizeToolEventInput, resolveToolEventInput } from "../tool-event-input";
|
|
17
18
|
import { applyToolProxy } from "../tool-proxy";
|
|
@@ -83,6 +84,42 @@ export function wrapRegisteredTools(registeredTools: RegisteredTool[], runner: E
|
|
|
83
84
|
return registeredTools.map(rt => wrapRegisteredTool(rt, runner));
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
function computerSafetyChecks(context: AgentToolContext | undefined): ComputerSafetyCheck[] {
|
|
88
|
+
const metadata = context?.toolCall?.providerMetadata;
|
|
89
|
+
return metadata?.type === "computer" ? metadata.pendingSafetyChecks : [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function approvalArgs(params: unknown, context: AgentToolContext | undefined): unknown {
|
|
93
|
+
const metadata = context?.toolCall?.providerMetadata;
|
|
94
|
+
return metadata?.type === "computer" ? { actions: metadata.actions } : params;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function toolEventArgs(params: unknown, context: AgentToolContext | undefined): Record<string, unknown> {
|
|
98
|
+
const metadata = context?.toolCall?.providerMetadata;
|
|
99
|
+
if (metadata?.type === "computer") {
|
|
100
|
+
return {
|
|
101
|
+
actions: metadata.actions,
|
|
102
|
+
pendingSafetyChecks: metadata.pendingSafetyChecks,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return params as Record<string, unknown>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function approvalData(value: string): string {
|
|
109
|
+
const sanitized = sanitizeText(value)
|
|
110
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
111
|
+
.trim();
|
|
112
|
+
const truncated = truncateForPrompt(sanitized, 500);
|
|
113
|
+
return truncated.replace(/([\\`*_{}[\]()<>#+\-.!|])/g, "\\$1");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function safetyCheckLines(checks: readonly ComputerSafetyCheck[]): string[] {
|
|
117
|
+
return checks.map((check, index) => {
|
|
118
|
+
const value = check.message || check.code || check.id;
|
|
119
|
+
return `${index + 1}. ${approvalData(value)}`;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
86
123
|
/**
|
|
87
124
|
* Wraps a tool with extension callbacks for interception.
|
|
88
125
|
* - Emits tool_call event before execution (can block)
|
|
@@ -128,19 +165,25 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
128
165
|
const configuredMode = (settings?.get("tools.approvalMode") ?? "yolo") as ApprovalMode;
|
|
129
166
|
const approvalMode: ApprovalMode = cliAutoApprove ? "yolo" : configuredMode;
|
|
130
167
|
const userPolicies = (settings?.get("tools.approval") ?? {}) as Record<string, unknown>;
|
|
131
|
-
const
|
|
168
|
+
const resolvedArgs = approvalArgs(params, context);
|
|
169
|
+
const resolved = resolveApproval(this.tool, resolvedArgs, approvalMode, userPolicies);
|
|
132
170
|
if (resolved.policy === "deny") {
|
|
133
171
|
throw new Error(
|
|
134
172
|
`Tool "${this.tool.name}" is blocked by user policy.\n` +
|
|
135
173
|
`To allow: remove "tools.approval.${this.tool.name}: deny" from config.`,
|
|
136
174
|
);
|
|
137
175
|
}
|
|
176
|
+
const pendingSafetyChecks = computerSafetyChecks(context);
|
|
138
177
|
// An xd:// device dispatch already cleared the write tool's outer gate at
|
|
139
178
|
// this tool's tier — re-prompting would double-ask for one action. Explicit
|
|
140
179
|
// per-tool "prompt" policies and tool-demanded overrides still prompt.
|
|
180
|
+
// Provider safety checks are stronger: yolo, per-tool allow, and xdev approval
|
|
181
|
+
// never acknowledge them on the user's behalf.
|
|
141
182
|
const explicitPrompt = resolved.override || Object.hasOwn(userPolicies, this.tool.name);
|
|
142
183
|
const approvalCheck = {
|
|
143
|
-
required:
|
|
184
|
+
required:
|
|
185
|
+
pendingSafetyChecks.length > 0 ||
|
|
186
|
+
(resolved.policy === "prompt" && (explicitPrompt || context?.xdevApproved !== true)),
|
|
144
187
|
reason: resolved.reason,
|
|
145
188
|
};
|
|
146
189
|
|
|
@@ -171,10 +214,16 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
171
214
|
});
|
|
172
215
|
};
|
|
173
216
|
|
|
174
|
-
//
|
|
217
|
+
// Provider safety checks fail closed without an interactive prompt. Unlike
|
|
218
|
+
// ordinary tier approval, no setting or yolo mode may bypass this gate.
|
|
175
219
|
if (!this.runner.hasUI()) {
|
|
176
220
|
const reason = "no interactive UI available";
|
|
177
221
|
await resolveApproval(false, reason);
|
|
222
|
+
if (pendingSafetyChecks.length > 0) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`Tool "${this.tool.name}" has pending provider safety checks but no interactive UI is available.`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
178
227
|
throw new Error(
|
|
179
228
|
`Tool "${this.tool.name}" requires approval but no interactive UI available.\n` +
|
|
180
229
|
`Options:\n` +
|
|
@@ -185,12 +234,14 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
185
234
|
}
|
|
186
235
|
|
|
187
236
|
const uiContext = this.runner.getUIContext();
|
|
237
|
+
const basePrompt = formatApprovalPrompt(this.tool, resolvedArgs, approvalCheck.reason);
|
|
238
|
+
const safetyPrompt =
|
|
239
|
+
pendingSafetyChecks.length > 0
|
|
240
|
+
? `${basePrompt}\nProvider safety checks:\n${safetyCheckLines(pendingSafetyChecks).join("\n")}`
|
|
241
|
+
: basePrompt;
|
|
188
242
|
let choice: string | undefined;
|
|
189
243
|
try {
|
|
190
|
-
choice = await uiContext.select(
|
|
191
|
-
"Approve",
|
|
192
|
-
"Deny",
|
|
193
|
-
]);
|
|
244
|
+
choice = await uiContext.select(safetyPrompt, ["Approve", "Deny"]);
|
|
194
245
|
} catch (err) {
|
|
195
246
|
await resolveApproval(false, err instanceof Error ? err.message : "approval aborted");
|
|
196
247
|
throw err;
|
|
@@ -200,6 +251,10 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
200
251
|
if (!approved) {
|
|
201
252
|
throw new Error(`Tool call denied by user: ${this.tool.name}`);
|
|
202
253
|
}
|
|
254
|
+
if (pendingSafetyChecks.length > 0) {
|
|
255
|
+
if (!context) throw new Error("Provider safety approval context is unavailable");
|
|
256
|
+
context.providerSafetyApproved = true;
|
|
257
|
+
}
|
|
203
258
|
}
|
|
204
259
|
|
|
205
260
|
// 2. Emit tool_call event - extensions can block execution
|
|
@@ -211,7 +266,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
211
266
|
toolCallId,
|
|
212
267
|
input: normalizeToolEventInput(
|
|
213
268
|
this.tool.name,
|
|
214
|
-
resolveToolEventInput(this.tool, params
|
|
269
|
+
resolveToolEventInput(this.tool, toolEventArgs(params, context)),
|
|
215
270
|
),
|
|
216
271
|
})) as ToolCallEventResult | undefined;
|
|
217
272
|
|
|
@@ -228,7 +283,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
228
283
|
}
|
|
229
284
|
|
|
230
285
|
// Execute the actual tool
|
|
231
|
-
let result:
|
|
286
|
+
let result: AgentToolResult<TDetails, TParameters>;
|
|
232
287
|
let executionError: Error | undefined;
|
|
233
288
|
|
|
234
289
|
try {
|
|
@@ -249,7 +304,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
249
304
|
toolCallId,
|
|
250
305
|
input: normalizeToolEventInput(
|
|
251
306
|
this.tool.name,
|
|
252
|
-
resolveToolEventInput(this.tool, params
|
|
307
|
+
resolveToolEventInput(this.tool, toolEventArgs(params, context)),
|
|
253
308
|
),
|
|
254
309
|
content: result.content,
|
|
255
310
|
details: result.details,
|
|
@@ -275,6 +330,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
275
330
|
return {
|
|
276
331
|
content: modifiedContent,
|
|
277
332
|
details: modifiedDetails,
|
|
333
|
+
providerMetadata: result.providerMetadata,
|
|
278
334
|
...(effectiveError ? { isError: true } : {}),
|
|
279
335
|
};
|
|
280
336
|
}
|
|
@@ -17,7 +17,7 @@ import * as fs from "node:fs";
|
|
|
17
17
|
import * as path from "node:path";
|
|
18
18
|
import type { AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
19
19
|
import { type AuthCredential, SqliteAuthCredentialStore, type TSchema } from "@oh-my-pi/pi-ai";
|
|
20
|
-
import { Text } from "@oh-my-pi/pi-tui";
|
|
20
|
+
import { getKeybindings, type Keybinding, Text } from "@oh-my-pi/pi-tui";
|
|
21
21
|
import {
|
|
22
22
|
getAgentDbPath,
|
|
23
23
|
getAgentDir,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
parseFrontmatter as parseOmpFrontmatter,
|
|
27
27
|
} from "@oh-my-pi/pi-utils";
|
|
28
28
|
import { getPackageDir as getOmpPackageDir } from "../config";
|
|
29
|
+
import { formatKeyHints } from "../config/keybindings";
|
|
29
30
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
30
31
|
import { type SettingPath, Settings } from "../config/settings";
|
|
31
32
|
import { EditTool } from "../edit";
|
|
@@ -373,6 +374,11 @@ async function executeLegacyBashOperations(
|
|
|
373
374
|
}
|
|
374
375
|
}
|
|
375
376
|
|
|
377
|
+
/** Format the active shortcut for legacy extensions that render keybinding hints. */
|
|
378
|
+
export function keyText(action: Keybinding): string {
|
|
379
|
+
return formatKeyHints(getKeybindings().getKeys(action));
|
|
380
|
+
}
|
|
381
|
+
|
|
376
382
|
/** Parse frontmatter using the historical Pi package-root helper. */
|
|
377
383
|
export interface ParsedFrontmatter<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
378
384
|
frontmatter: T;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { TempDir } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { createDaemonBrokerClient, type DaemonBrokerClient } from "./client";
|
|
6
|
+
import type { DaemonSnapshot, DaemonSpec } from "./protocol";
|
|
7
|
+
|
|
8
|
+
const TERMINAL_HISTORY_LIMIT = 10;
|
|
9
|
+
|
|
10
|
+
function spec(name: string, cwd: string): DaemonSpec {
|
|
11
|
+
return {
|
|
12
|
+
name,
|
|
13
|
+
application: process.execPath,
|
|
14
|
+
args: [],
|
|
15
|
+
env: {},
|
|
16
|
+
cwd,
|
|
17
|
+
pty: false,
|
|
18
|
+
restart: "no",
|
|
19
|
+
persist: false,
|
|
20
|
+
detached: false,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function terminalSnapshot(index: number): DaemonSnapshot {
|
|
25
|
+
const name = `exited-${index}`;
|
|
26
|
+
return {
|
|
27
|
+
name,
|
|
28
|
+
id: name,
|
|
29
|
+
state: index % 2 === 0 ? "exited" : "failed",
|
|
30
|
+
createdAt: index * 10,
|
|
31
|
+
startedAt: index * 10,
|
|
32
|
+
exitedAt: index * 10 + 1,
|
|
33
|
+
exitReason: `historical exit ${index}`,
|
|
34
|
+
restartCount: 0,
|
|
35
|
+
outputBytes: 0,
|
|
36
|
+
persist: false,
|
|
37
|
+
detached: false,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function seedTerminalRecord(runtimeDir: string, cwd: string, snapshot: DaemonSnapshot): Promise<void> {
|
|
42
|
+
const metaPath = path.join(runtimeDir, "daemons", snapshot.name, "meta.json");
|
|
43
|
+
await Bun.write(metaPath, JSON.stringify({ daemon: snapshot, spec: spec(snapshot.name, cwd) }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function shutdown(client: DaemonBrokerClient, activeName: string): Promise<void> {
|
|
47
|
+
await client.request({ op: "stop", name: activeName, timeoutMs: 2_000 }).catch(() => undefined);
|
|
48
|
+
await client.request({ op: "shutdown" }).catch(() => undefined);
|
|
49
|
+
client.close();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("broker list", () => {
|
|
53
|
+
it("returns active daemons first and caps recovered terminal history by real exit time", async () => {
|
|
54
|
+
using tempDir = TempDir.createSync("@omp-launch-list-");
|
|
55
|
+
const projectDir = path.join(tempDir.path(), "project");
|
|
56
|
+
const runtimeDir = path.join(tempDir.path(), "runtime");
|
|
57
|
+
await fs.mkdir(projectDir);
|
|
58
|
+
|
|
59
|
+
for (let index = 0; index < TERMINAL_HISTORY_LIMIT + 5; index++) {
|
|
60
|
+
await seedTerminalRecord(runtimeDir, projectDir, terminalSnapshot(index));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const client = await createDaemonBrokerClient(projectDir, { runtimeDir, idleGraceMs: 5_000 });
|
|
64
|
+
const activeName = "active-server";
|
|
65
|
+
try {
|
|
66
|
+
const started = await client.request({
|
|
67
|
+
op: "start",
|
|
68
|
+
spec: {
|
|
69
|
+
...spec(activeName, projectDir),
|
|
70
|
+
args: ["-e", "process.stdin.resume()"],
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
expect(started.op).toBe("start");
|
|
74
|
+
|
|
75
|
+
const listed = await client.request({ op: "list" });
|
|
76
|
+
expect(listed.op).toBe("list");
|
|
77
|
+
if (listed.op !== "list") throw new Error(`Unexpected broker result: ${listed.op}`);
|
|
78
|
+
|
|
79
|
+
expect(listed.daemons.map(daemon => daemon.name)).toEqual([
|
|
80
|
+
activeName,
|
|
81
|
+
...Array.from({ length: TERMINAL_HISTORY_LIMIT }, (_, offset) => `exited-${14 - offset}`),
|
|
82
|
+
]);
|
|
83
|
+
expect(listed.daemons[0]?.state).toBe("running");
|
|
84
|
+
expect(listed.daemons.at(-1)?.exitedAt).toBe(51);
|
|
85
|
+
} finally {
|
|
86
|
+
await shutdown(client, activeName);
|
|
87
|
+
}
|
|
88
|
+
}, 20_000);
|
|
89
|
+
});
|
package/src/launch/broker.ts
CHANGED
|
@@ -33,6 +33,12 @@ const MAX_LOG_BYTES = 25 * 1024 * 1024;
|
|
|
33
33
|
const LOG_READ_BYTES = 2 * 1024 * 1024;
|
|
34
34
|
const READINESS_BUFFER_CHARS = 64 * 1024;
|
|
35
35
|
const RESTART_MAX_DELAY_MS = 30_000;
|
|
36
|
+
/**
|
|
37
|
+
* Cap on terminal (exited/failed) daemons surfaced by `list`. Active daemons
|
|
38
|
+
* are always shown in full; older history is truncated so the response stays
|
|
39
|
+
* bounded over a long-lived project (issue #6517).
|
|
40
|
+
*/
|
|
41
|
+
const MAX_TERMINAL_DAEMONS_LISTED = 10;
|
|
36
42
|
const TOKEN_FILE = "broker.token";
|
|
37
43
|
const PID_FILE = "broker.pid";
|
|
38
44
|
const META_FILE = "meta.json";
|
|
@@ -95,6 +101,41 @@ function terminalState(state: DaemonSnapshot["state"]): boolean {
|
|
|
95
101
|
return state === "exited" || state === "failed";
|
|
96
102
|
}
|
|
97
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Order daemons for the `list` response: non-terminal (active) daemons first,
|
|
106
|
+
* oldest to newest, so the process the user is acting on is immediately visible
|
|
107
|
+
* instead of buried behind exited history; then the most recently exited/failed
|
|
108
|
+
* ones, capped at {@link MAX_TERMINAL_DAEMONS_LISTED} to keep the response from
|
|
109
|
+
* growing without bound. Truncated terminal records stay addressable by name
|
|
110
|
+
* via `describe`/`logs`/`restart`.
|
|
111
|
+
*/
|
|
112
|
+
function orderDaemonsForListing(snapshots: DaemonSnapshot[]): DaemonSnapshot[] {
|
|
113
|
+
const active: DaemonSnapshot[] = [];
|
|
114
|
+
const terminal: DaemonSnapshot[] = [];
|
|
115
|
+
for (const snapshot of snapshots) {
|
|
116
|
+
(terminalState(snapshot.state) ? terminal : active).push(snapshot);
|
|
117
|
+
}
|
|
118
|
+
active.sort((left, right) => left.createdAt - right.createdAt);
|
|
119
|
+
terminal.sort((left, right) => (right.exitedAt ?? right.createdAt) - (left.exitedAt ?? left.createdAt));
|
|
120
|
+
return [...active, ...terminal.slice(0, MAX_TERMINAL_DAEMONS_LISTED)];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Reap a recovered non-detached daemon snapshot in place. Already-terminal
|
|
125
|
+
* records are left untouched so `list` keeps their real {@link DaemonSnapshot.exitedAt}
|
|
126
|
+
* for recency ranking; records that were still alive when the previous broker
|
|
127
|
+
* exited are marked `exited` at `now`, since their process died with that broker
|
|
128
|
+
* (issue #6517). Returns whether the record was reaped.
|
|
129
|
+
*/
|
|
130
|
+
function reapRecoveredSnapshot(snapshot: DaemonSnapshot, now: number): boolean {
|
|
131
|
+
if (terminalState(snapshot.state)) return false;
|
|
132
|
+
snapshot.pid = undefined;
|
|
133
|
+
snapshot.state = "exited";
|
|
134
|
+
snapshot.exitedAt = now;
|
|
135
|
+
snapshot.exitReason = "previous broker exited";
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
98
139
|
/** Mirror per-condition readiness progress into the snapshot so clients can see which condition is unmet. */
|
|
99
140
|
function syncReadyPending(record: ManagedDaemon): void {
|
|
100
141
|
if (record.snapshot.state !== "starting") {
|
|
@@ -402,9 +443,7 @@ class DaemonBroker {
|
|
|
402
443
|
await Promise.all([...this.#records.values()].map(record => this.#refreshDetached(record)));
|
|
403
444
|
return {
|
|
404
445
|
op: "list",
|
|
405
|
-
daemons: [...this.#records.values()]
|
|
406
|
-
.sort((left, right) => left.snapshot.createdAt - right.snapshot.createdAt)
|
|
407
|
-
.map(record => record.snapshot),
|
|
446
|
+
daemons: orderDaemonsForListing([...this.#records.values()].map(record => record.snapshot)),
|
|
408
447
|
};
|
|
409
448
|
}
|
|
410
449
|
case "logs":
|
|
@@ -973,11 +1012,13 @@ class DaemonBroker {
|
|
|
973
1012
|
snapshot.state !== "stopping" &&
|
|
974
1013
|
processRef?.status() === "running";
|
|
975
1014
|
if (!detached) {
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
snapshot.
|
|
980
|
-
|
|
1015
|
+
// Reap only records that were still alive when the previous broker
|
|
1016
|
+
// exited; already-terminal records keep their real exit time so
|
|
1017
|
+
// `list` ranks exited history by true recency (issue #6517).
|
|
1018
|
+
if (!terminalState(snapshot.state) && processRef) {
|
|
1019
|
+
await processRef.terminate({ group: true, gracefulMs: 500, timeoutMs: 2_000 });
|
|
1020
|
+
}
|
|
1021
|
+
reapRecoveredSnapshot(snapshot, Date.now());
|
|
981
1022
|
} else if (snapshot.state === "restarting") {
|
|
982
1023
|
snapshot.state = spec.ready ? "starting" : "running";
|
|
983
1024
|
}
|