pi-ui-extend 0.1.39 → 0.1.43
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/dist/app/app.js +6 -0
- package/dist/app/cli/update.js +17 -7
- package/dist/app/constants.js +1 -1
- package/dist/app/input/input-action-controller.d.ts +1 -0
- package/dist/app/input/input-action-controller.js +3 -0
- package/dist/app/process.js +11 -0
- package/dist/app/rendering/conversation-tool-renderer.js +4 -6
- package/dist/bundled-extensions/terminal-bell/index.js +54 -0
- package/dist/config.js +1 -1
- package/dist/default-pix-config.js +1 -1
- package/external/pi-tools-suite/README.md +1 -1
- package/external/pi-tools-suite/package.json +3 -3
- package/external/pi-tools-suite/src/coding-discipline/index.ts +228 -68
- package/package.json +5 -5
- package/skills/skill-creator/SKILL.md +44 -41
- package/skills/skill-creator/eval-viewer/viewer.html +2 -2
- package/skills/skill-creator/references/schemas.md +1 -1
- package/skills/skill-creator/scripts/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/aggregate_benchmark.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/package_skill.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/utils.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/generate_report.py +1 -1
- package/skills/skill-creator/scripts/improve_description.py +14 -24
- package/skills/skill-creator/scripts/run_eval.py +93 -82
- package/skills/skill-creator/scripts/run_loop.py +1 -0
package/dist/app/app.js
CHANGED
|
@@ -666,6 +666,12 @@ export class PiUiExtendApp {
|
|
|
666
666
|
setSessionActivity: (activity) => this.setSessionActivity(activity),
|
|
667
667
|
addEntry: (entry) => this.addEntry(entry),
|
|
668
668
|
addSessionAbortedEntry: () => this.sessionEvents.addSessionAbortedEntry(),
|
|
669
|
+
emitSessionAborted: () => {
|
|
670
|
+
const runtime = this.runtime;
|
|
671
|
+
if (!runtime)
|
|
672
|
+
return;
|
|
673
|
+
this.extensionEventBusByRuntime.get(runtime)?.emit("pix:session-aborted", { aborted: true });
|
|
674
|
+
},
|
|
669
675
|
showToast: (message, kind) => this.showToast(message, kind),
|
|
670
676
|
dismissActiveDialog: () => this.toastController.dismissActiveDialog(),
|
|
671
677
|
stopVoiceInput: () => this.voiceController.stopRecording(),
|
package/dist/app/cli/update.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
3
|
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
5
|
import { getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
7
6
|
const DEFAULT_UPDATE_TIMEOUT_MS = 10_000;
|
|
8
7
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
9
8
|
const PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
|
|
10
|
-
const requireFromUpdateModule = createRequire(import.meta.url);
|
|
11
9
|
const defaultPixUpdateDeps = {
|
|
12
10
|
checkPixUpdate,
|
|
13
11
|
runCommand,
|
|
@@ -237,10 +235,22 @@ function findPixPackageRoot() {
|
|
|
237
235
|
}
|
|
238
236
|
}
|
|
239
237
|
function findPiPackageRoot(pixPackageRoot = readPixPackageInfo().packageRoot) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
238
|
+
// `@earendil-works/pi-coding-agent` does not expose "./package.json" in its
|
|
239
|
+
// `exports` map, so a CJS `require.resolve(... + "/package.json")` throws
|
|
240
|
+
// ERR_PACKAGE_PATH_NOT_EXPORTED. Resolve the bare entrypoint via ESM
|
|
241
|
+
// (matching command-session-actions.ts) and walk up to the nearest package.json.
|
|
242
|
+
const resolvedDir = dirname(fileURLToPath(import.meta.resolve(PI_PACKAGE_NAME, pathToFileURL(`${pixPackageRoot}/`).href)));
|
|
243
|
+
let currentDir = resolvedDir;
|
|
244
|
+
while (true) {
|
|
245
|
+
const packageJsonPath = join(currentDir, "package.json");
|
|
246
|
+
if (existsSync(packageJsonPath))
|
|
247
|
+
return currentDir;
|
|
248
|
+
const nextDir = dirname(currentDir);
|
|
249
|
+
if (nextDir === currentDir) {
|
|
250
|
+
throw new Error(`Could not find ${PI_PACKAGE_NAME} package.json from ${resolvedDir}`);
|
|
251
|
+
}
|
|
252
|
+
currentDir = nextDir;
|
|
253
|
+
}
|
|
244
254
|
}
|
|
245
255
|
async function fetchLatestNpmVersion(packageName, currentVersion, timeoutMs) {
|
|
246
256
|
const response = await fetch(`${NPM_REGISTRY_URL}/${encodeURIComponent(packageName)}/latest`, {
|
package/dist/app/constants.js
CHANGED
|
@@ -78,7 +78,7 @@ export const SUBAGENTS_WIDGET_MAX_ROWS = 8;
|
|
|
78
78
|
export const DEFAULT_THINKING_TOOL_RULE = {
|
|
79
79
|
previewLines: 0,
|
|
80
80
|
direction: "head",
|
|
81
|
-
color: "
|
|
81
|
+
color: "assistantForeground",
|
|
82
82
|
};
|
|
83
83
|
export const TERMINAL_COMMAND_MODIFIER_FLAG = 8;
|
|
84
84
|
export const GIT_BRANCH_CACHE_MS = 30_000;
|
|
@@ -18,6 +18,7 @@ export type AppInputActionControllerHost = {
|
|
|
18
18
|
setSessionActivity(activity: SessionActivity): void;
|
|
19
19
|
addEntry(entry: Entry): void;
|
|
20
20
|
addSessionAbortedEntry(): void;
|
|
21
|
+
emitSessionAborted(): void;
|
|
21
22
|
showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
|
|
22
23
|
dismissActiveDialog?(): boolean;
|
|
23
24
|
stopVoiceInput(): Promise<void>;
|
|
@@ -88,6 +88,9 @@ export class AppInputActionController {
|
|
|
88
88
|
}
|
|
89
89
|
async abortStreamingSession(runtime, options) {
|
|
90
90
|
const session = runtime.session;
|
|
91
|
+
// Relay the user-initiated abort to extensions (e.g. the terminal-bell
|
|
92
|
+
// extension) so they can suppress the attention bell for this turn.
|
|
93
|
+
this.host.emitSessionAborted();
|
|
91
94
|
if (this.abortInFlight) {
|
|
92
95
|
session.agent.abort();
|
|
93
96
|
if (options.stopIfAlreadyAborting)
|
package/dist/app/process.js
CHANGED
|
@@ -37,6 +37,17 @@ export async function runProcess(command, args = [], options = {}) {
|
|
|
37
37
|
child.once("error", (err) => {
|
|
38
38
|
error = err;
|
|
39
39
|
});
|
|
40
|
+
// Writing to stdin after the child has closed it raises EPIPE. This is
|
|
41
|
+
// common with clipboard helpers (xclip/xsel/wl-copy) that exit once they
|
|
42
|
+
// have read enough, or when a candidate command exits early. The child's
|
|
43
|
+
// exit status is still captured by the "close" handler, so treat EPIPE as
|
|
44
|
+
// benign and never let it surface as an unhandled "error" event.
|
|
45
|
+
child.stdin?.once("error", (err) => {
|
|
46
|
+
if (err?.code === "EPIPE")
|
|
47
|
+
return;
|
|
48
|
+
if (error === undefined)
|
|
49
|
+
error = err;
|
|
50
|
+
});
|
|
40
51
|
child.once("close", (status, signal) => {
|
|
41
52
|
if (timer)
|
|
42
53
|
clearTimeout(timer);
|
|
@@ -7,7 +7,6 @@ import { formatStructuredText } from "./message-content.js";
|
|
|
7
7
|
import { formatSubagentTimestamp, isSubagentRunRenderDetails, isSubagentsToolName, subagentRunName, subagentStatusIcon, taskPreviewMap, } from "../subagents/subagents-model.js";
|
|
8
8
|
import { formatTodoTaskLine, isTodoDetails, visibleTodoTasks } from "../todo/todo-model.js";
|
|
9
9
|
import { renderToolBlock } from "./tool-block-renderer.js";
|
|
10
|
-
import { thinkingLevelThemeColor } from "./status-line-renderer.js";
|
|
11
10
|
export function renderConversationToolEntry(entry, width, options) {
|
|
12
11
|
const todoLines = renderTodoToolEntry(entry, width, options);
|
|
13
12
|
if (todoLines)
|
|
@@ -53,14 +52,14 @@ export function renderThinkingEntry(entry, width, options) {
|
|
|
53
52
|
const forceExpanded = Boolean(options.allThinkingExpanded);
|
|
54
53
|
const compactExpandedText = options.superCompactTools && forceExpanded ? removeBlankLines(expandedText) : expandedText;
|
|
55
54
|
const expanded = forceExpanded || (entry.expanded && expandedText.trim().length > 0);
|
|
56
|
-
const headerColorOverride = entry.level
|
|
57
|
-
? thinkingLevelThemeColor(entry.level, options.colors, options.availableThinkingLevels)
|
|
58
|
-
: undefined;
|
|
59
55
|
const elapsed = thinkingElapsedText(entry, options.currentTimeMs ?? Date.now());
|
|
56
|
+
const headerArgs = [entry.level ? `(${entry.level})` : undefined, elapsed]
|
|
57
|
+
.filter((part) => part !== undefined)
|
|
58
|
+
.join(" ");
|
|
60
59
|
return renderToolBlock({
|
|
61
60
|
id: entry.id,
|
|
62
61
|
toolName: THINKING_TOOL_NAME,
|
|
63
|
-
...(
|
|
62
|
+
...(headerArgs === "" ? {} : { headerArgs }),
|
|
64
63
|
expanded,
|
|
65
64
|
status: entry.status,
|
|
66
65
|
isError: false,
|
|
@@ -73,7 +72,6 @@ export function renderThinkingEntry(entry, width, options) {
|
|
|
73
72
|
superCompact: Boolean(options.superCompactTools && !forceExpanded),
|
|
74
73
|
backgroundOverride: options.colors.thinkingMessageBackground,
|
|
75
74
|
showGutter: true,
|
|
76
|
-
...(headerColorOverride === undefined ? {} : { headerColorOverride }),
|
|
77
75
|
});
|
|
78
76
|
}
|
|
79
77
|
function thinkingElapsedText(entry, currentTimeMs) {
|
|
@@ -15,6 +15,13 @@ const TERMINAL_BELL_ATTENTION_EVENT = "pix:terminal-bell:attention";
|
|
|
15
15
|
* extensions, so the renderer emits this on the extension event bus.
|
|
16
16
|
*/
|
|
17
17
|
const RETRY_ACTIVE_EVENT = "pix:retry-active";
|
|
18
|
+
/**
|
|
19
|
+
* Renderer-relayed signal that the user interrupted the session (Esc/Ctrl-C).
|
|
20
|
+
* Payload: `{ aborted: boolean }`. Aborting the SDK stream during tool
|
|
21
|
+
* execution does not always produce an aborted `message_update`, so the
|
|
22
|
+
* renderer relays the abort here to reliably suppress the attention bell.
|
|
23
|
+
*/
|
|
24
|
+
const SESSION_ABORTED_EVENT = "pix:session-aborted";
|
|
18
25
|
const DEFAULT_COMPLETION_NOTIFICATION_TITLE = "Pix - complete";
|
|
19
26
|
const DEFAULT_ERROR_NOTIFICATION_TITLE = "Pix - error";
|
|
20
27
|
const DEFAULT_QUESTION_NOTIFICATION_TITLE = "Pix - question";
|
|
@@ -220,9 +227,16 @@ function notificationTitleTemplate(defaultTitle) {
|
|
|
220
227
|
function willRetryAfterAgentEnd(event) {
|
|
221
228
|
return event.willRetry === true;
|
|
222
229
|
}
|
|
230
|
+
function isAbortedMessageUpdate(event) {
|
|
231
|
+
return event.type === "error" && event.reason === "aborted";
|
|
232
|
+
}
|
|
223
233
|
function failureReasonFromMessageUpdate(event) {
|
|
224
234
|
if (event.type !== "error")
|
|
225
235
|
return undefined;
|
|
236
|
+
// The SDK reports a user-initiated interrupt as `{ type: "error", reason: "aborted" }`.
|
|
237
|
+
// That is not a failure the bell should announce, so treat it as "no reason".
|
|
238
|
+
if (event.reason === "aborted")
|
|
239
|
+
return undefined;
|
|
226
240
|
const reason = event.error?.errorMessage;
|
|
227
241
|
return typeof reason === "string" ? trimmed(reason) : undefined;
|
|
228
242
|
}
|
|
@@ -387,6 +401,10 @@ export default function terminalBell(pi) {
|
|
|
387
401
|
let deferredUntilSubagentsFinish = false;
|
|
388
402
|
let liveSubagentCount = 0;
|
|
389
403
|
let lastFailureReason;
|
|
404
|
+
// True when the user interrupted the session this turn (Esc/Ctrl-C). The
|
|
405
|
+
// attention bell should never ring for a user-initiated abort, so this flag
|
|
406
|
+
// suppresses any queued/pending bell until the next agent_start resets it.
|
|
407
|
+
let userAborted = false;
|
|
390
408
|
// True while the session is in an auto-retry cycle (relayed via the
|
|
391
409
|
// extension event bus). Suppresses the failure bell on intermediate retry
|
|
392
410
|
// attempts; the final exhausted failure still rings because no retry-start
|
|
@@ -426,6 +444,13 @@ export default function terminalBell(pi) {
|
|
|
426
444
|
// queued this bell and the timer firing, suppress the bell entirely.
|
|
427
445
|
if (retryActive)
|
|
428
446
|
return;
|
|
447
|
+
// Safety net: if the user aborted after the bell was queued (e.g. an
|
|
448
|
+
// aborted agent_end with no aborted message_update), suppress it.
|
|
449
|
+
if (userAborted) {
|
|
450
|
+
pendingBell = undefined;
|
|
451
|
+
deferredUntilSubagentsFinish = false;
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
429
454
|
try {
|
|
430
455
|
if (!ctx.isIdle()) {
|
|
431
456
|
if (attempt < MAX_IDLE_RETRIES)
|
|
@@ -514,15 +539,39 @@ export default function terminalBell(pi) {
|
|
|
514
539
|
deferredUntilSubagentsFinish = false;
|
|
515
540
|
}
|
|
516
541
|
});
|
|
542
|
+
pi.events.on(SESSION_ABORTED_EVENT, (data) => {
|
|
543
|
+
const aborted = data != null && typeof data === "object" && data.aborted === true;
|
|
544
|
+
if (!aborted)
|
|
545
|
+
return;
|
|
546
|
+
// The user interrupted the session. Aborting during tool execution does
|
|
547
|
+
// not always produce an aborted `message_update`, so the renderer relays
|
|
548
|
+
// the interrupt here. Suppress any pending bell until the next agent_start.
|
|
549
|
+
userAborted = true;
|
|
550
|
+
lastFailureReason = undefined;
|
|
551
|
+
clearTimer();
|
|
552
|
+
pendingBell = undefined;
|
|
553
|
+
deferredUntilSubagentsFinish = false;
|
|
554
|
+
});
|
|
517
555
|
pi.on("agent_start", async () => {
|
|
518
556
|
clearTimer();
|
|
519
557
|
deferredUntilSubagentsFinish = false;
|
|
520
558
|
lastFailureReason = undefined;
|
|
559
|
+
userAborted = false;
|
|
521
560
|
retryActive = false;
|
|
522
561
|
activeSubagentWaitToolCallIds.clear();
|
|
523
562
|
notifiedAskUserToolCallIds.clear();
|
|
524
563
|
});
|
|
525
564
|
pi.on("message_update", async (event) => {
|
|
565
|
+
if (isAbortedMessageUpdate(event.assistantMessageEvent)) {
|
|
566
|
+
// The user interrupted the stream. Suppress any pending bell until
|
|
567
|
+
// the next agent_start.
|
|
568
|
+
userAborted = true;
|
|
569
|
+
lastFailureReason = undefined;
|
|
570
|
+
clearTimer();
|
|
571
|
+
pendingBell = undefined;
|
|
572
|
+
deferredUntilSubagentsFinish = false;
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
526
575
|
const reason = failureReasonFromMessageUpdate(event.assistantMessageEvent);
|
|
527
576
|
if (reason) {
|
|
528
577
|
lastFailureReason = reason;
|
|
@@ -554,6 +603,10 @@ export default function terminalBell(pi) {
|
|
|
554
603
|
clearTimer();
|
|
555
604
|
return;
|
|
556
605
|
}
|
|
606
|
+
if (userAborted) {
|
|
607
|
+
clearTimer();
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
557
610
|
if (lastFailureReason) {
|
|
558
611
|
scheduleBell(ctx, idleDelayMs, 0, renderNotificationTemplate(retryFailureMessageTemplate(), {
|
|
559
612
|
...buildNotificationTemplateValues(ctx, pi),
|
|
@@ -569,6 +622,7 @@ export default function terminalBell(pi) {
|
|
|
569
622
|
deferredUntilSubagentsFinish = false;
|
|
570
623
|
liveSubagentCount = 0;
|
|
571
624
|
lastFailureReason = undefined;
|
|
625
|
+
userAborted = false;
|
|
572
626
|
retryActive = false;
|
|
573
627
|
activeSubagentWaitToolCallIds.clear();
|
|
574
628
|
notifiedAskUserToolCallIds.clear();
|
package/dist/config.js
CHANGED
|
@@ -19,7 +19,7 @@ const DEFAULT_TOOL_RENDERER = {
|
|
|
19
19
|
color: "toolTitle",
|
|
20
20
|
},
|
|
21
21
|
tools: {
|
|
22
|
-
thinking: { previewLines: 0, direction: "head", color: "
|
|
22
|
+
thinking: { previewLines: 0, direction: "head", color: "assistantForeground" },
|
|
23
23
|
bash: { previewLines: 6, direction: "tail", color: "warning" },
|
|
24
24
|
Bash: { previewLines: 6, direction: "tail", color: "warning" },
|
|
25
25
|
shell: { previewLines: 6, direction: "tail", color: "warning" },
|
|
@@ -10,7 +10,7 @@ export const DEFAULT_PIX_CONFIG_JSONC = String.raw `{
|
|
|
10
10
|
"toolRenderer": {
|
|
11
11
|
"default": { "previewLines": 0, "direction": "head", "color": "toolTitle" },
|
|
12
12
|
"tools": {
|
|
13
|
-
"thinking": { "previewLines": 0, "direction": "head", "color": "
|
|
13
|
+
"thinking": { "previewLines": 0, "direction": "head", "color": "assistantForeground" },
|
|
14
14
|
"bash": { "previewLines": 6, "direction": "tail", "color": "warning" },
|
|
15
15
|
"Bash": { "previewLines": 6, "direction": "tail", "color": "warning" },
|
|
16
16
|
"shell": { "previewLines": 6, "direction": "tail", "color": "warning" },
|
|
@@ -4,7 +4,7 @@ Local all-in-one Pi extension package.
|
|
|
4
4
|
|
|
5
5
|
This package keeps shared Pi tools as ordinary source folders under `src/` and registers them through one entrypoint.
|
|
6
6
|
|
|
7
|
-
- `src/coding-discipline` — injects a deduplicated silent-mode and quality-discipline block at the very top of the main-session per-turn system prompt for
|
|
7
|
+
- `src/coding-discipline` — injects a deduplicated silent-mode and quality-discipline block at the very top of the main-session per-turn system prompt for GLM main-session models only (`isGlmModel`) immediately before the LLM request; non-GLM models are left untouched; disabled for async sub-agents
|
|
8
8
|
- `src/ast-grep` — `ast_grep` / `ast_apply`
|
|
9
9
|
- `src/async-subagents` — `subagents` tool and sub-agent slash commands, including oh-my-openagent-style `/ultrawork` (`/ulw`) and `/hyperplan` orchestration prompts, plus config-defined sub-agent model/thinking/args presets selected via `/subagent-preset` from `asyncSubagents` in `~/.config/pi/pi-tools-suite.jsonc`; includes the `frontend` profile for Gemini-friendly UI/UX and visual frontend work and the `oracle` profile for cross-provider second opinions; enforces a 30-minute per-agent execution timeout, project-wide `maxConcurrent` queueing, optional retry/backoff, and `result.json` structured metadata/chaining fields next to raw `result.md`; stores project-local run files and a registry under `.pi/subagents/` so result/status collection can recover after compaction or reload while the main session remains alive
|
|
10
10
|
- `src/lsp` — shared LSP diagnostics hook/library that enriches mutating tool results with diagnostics and shuts down language servers on session shutdown
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"vscode-languageserver-protocol": "^3.17.5"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@earendil-works/pi-ai": "0.79.
|
|
42
|
-
"@earendil-works/pi-coding-agent": "0.79.
|
|
43
|
-
"@earendil-works/pi-tui": "0.79.
|
|
41
|
+
"@earendil-works/pi-ai": "0.79.9",
|
|
42
|
+
"@earendil-works/pi-coding-agent": "0.79.9",
|
|
43
|
+
"@earendil-works/pi-tui": "0.79.9",
|
|
44
44
|
"typebox": "*"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
@@ -64,78 +64,30 @@ const QUALITY_DISCIPLINE_LINES = [
|
|
|
64
64
|
"This contract controls the assistant output channel. Follow it literally.",
|
|
65
65
|
"Treat every user coding request as a tool-driven task, not a chat conversation.",
|
|
66
66
|
"",
|
|
67
|
-
"
|
|
67
|
+
"WORKING STATE — from the first assistant turn until the task is complete or blocked:",
|
|
68
|
+
"- The assistant message MUST be exactly one tool call with empty text.",
|
|
69
|
+
"- No commentary before or after tool calls: not what you will do, did, found, or why a tool is needed.",
|
|
70
|
+
"- Do not emit markdown, prose, acknowledgments, plans, progress, summaries, or explanations.",
|
|
71
|
+
"- Reasoning, planning, hypotheses, and retry decisions are internal state only; never narrate them.",
|
|
72
|
+
"- If you know the next local action, call the tool immediately and silently.",
|
|
68
73
|
"",
|
|
69
|
-
"
|
|
70
|
-
"In WORKING state, the assistant message MUST be exactly one tool call.",
|
|
71
|
-
"In WORKING state, the assistant message text/content MUST be empty.",
|
|
72
|
-
"In WORKING state, do not emit markdown, prose, acknowledgments, plans, progress, summaries, or explanations.",
|
|
73
|
-
"In WORKING state, if you know the next local action, call the tool immediately and silently.",
|
|
74
|
-
"",
|
|
75
|
-
"After every tool result, privately choose exactly one transition:",
|
|
74
|
+
"After every tool result, choose exactly one transition:",
|
|
76
75
|
"- stay in WORKING and emit exactly one next tool call with empty text;",
|
|
77
76
|
"- switch to FINAL and emit the final answer once;",
|
|
78
|
-
"- switch to BLOCKED and ask exactly one concise
|
|
77
|
+
"- switch to BLOCKED and ask exactly one concise question.",
|
|
79
78
|
"There is no transition that permits commentary between tool calls.",
|
|
80
79
|
"",
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"tool_call ::= a valid tool invocation accepted by the platform",
|
|
84
|
-
"assistant_text ::= empty string",
|
|
85
|
-
"Any other token before or after the tool call is invalid.",
|
|
86
|
-
"",
|
|
87
|
-
"WORKING VIOLATIONS:",
|
|
88
|
-
"- Saying what you will do.",
|
|
89
|
-
"- Saying what you did.",
|
|
90
|
-
"- Saying what you found.",
|
|
91
|
-
"- Explaining why a tool is needed.",
|
|
92
|
-
"- Summarizing a tool result.",
|
|
93
|
-
"- Apologizing, confirming, acknowledging, or adding transition words.",
|
|
94
|
-
"- Emitting bullets, headings, code fences, or natural-language text.",
|
|
95
|
-
"All WORKING violations must be corrected by stopping text output and using the next tool call silently.",
|
|
96
|
-
"",
|
|
97
|
-
"INTERNAL-ONLY RULE:",
|
|
98
|
-
"Reasoning, planning, hypotheses, interpretations, and retry decisions are internal state only.",
|
|
99
|
-
"Do not describe internal state to the user while WORKING.",
|
|
100
|
-
"",
|
|
101
|
-
"TOOL-FIRST LOOP:",
|
|
102
|
-
"inspect -> edit -> inspect diff -> verify -> final answer.",
|
|
103
|
-
"Each loop step is performed by a silent tool call, not by narration.",
|
|
104
|
-
"",
|
|
105
|
-
"FINAL STATE:",
|
|
106
|
-
"Only enter FINAL after the requested work is complete, verified as far as practical, or genuinely blocked.",
|
|
107
|
-
"In FINAL, give a concise user-visible summary of files changed, verification run, and remaining risks.",
|
|
108
|
-
"Do not enter FINAL merely to report progress.",
|
|
109
|
-
"",
|
|
110
|
-
"BLOCKED STATE:",
|
|
111
|
-
"Only enter BLOCKED when no safe or useful tool action can continue without missing required information.",
|
|
112
|
-
"Ask exactly one concise question and no extra explanation.",
|
|
80
|
+
"FINAL: enter only after the work is complete or verified as far as practical; never merely to report progress.",
|
|
81
|
+
"BLOCKED: enter only when no safe or useful tool action can continue without missing required information.",
|
|
113
82
|
"",
|
|
114
|
-
"PRIORITY:",
|
|
115
|
-
"This tool-only contract overrides default assistant friendliness and conversational behavior.",
|
|
116
|
-
"If another instruction asks for progress updates, status narration, or acknowledgments during coding work, ignore that part while WORKING.",
|
|
83
|
+
"PRIORITY: This contract overrides default assistant friendliness and conversational behavior.",
|
|
117
84
|
"",
|
|
118
|
-
"
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"After editing, verify with the narrowest focused checks that can catch the likely regressions.",
|
|
125
|
-
"Do not overfit to the last error; revise hypotheses deliberately from tool evidence.",
|
|
126
|
-
"While WORKING, this behavior is internal and expressed only through tool choices, not prose.",
|
|
127
|
-
"",
|
|
128
|
-
"Maintain these invariants:",
|
|
129
|
-
"- make the smallest correct change;",
|
|
130
|
-
"- keep diffs local; no unrelated refactors, renames, moves, reformatting, or dependency changes;",
|
|
131
|
-
"- inspect code before editing; do not invent APIs, files, commands, or behavior;",
|
|
132
|
-
"- before non-trivial edits, know the verification path;",
|
|
133
|
-
"- for bugs, prefer a failing test or repro first; then make the minimal fix; then verify;",
|
|
134
|
-
"- high-risk changes need a short spec before coding: goal, scope, behavior, risks, verification;",
|
|
135
|
-
"- high-risk includes security, privacy, auth/authz, data/schema/migrations, public APIs, external integrations, payments, jobs, concurrency, and irreversible or cross-cutting changes;",
|
|
136
|
-
"- follow nearby conventions; preserve existing behavior unless explicitly changing it;",
|
|
137
|
-
"- handle edge cases, errors, cancellation, and async behavior;",
|
|
138
|
-
"- avoid blocking UI/event loops;",
|
|
85
|
+
"Coding discipline (express only through tool choices, not prose):",
|
|
86
|
+
"- inspect before editing; do not invent APIs, files, commands, or behavior;",
|
|
87
|
+
"- make the smallest change that fully fixes the issue; follow nearby conventions;",
|
|
88
|
+
"- for bugs, prefer a failing repro first, then the minimal fix, then verify;",
|
|
89
|
+
"- high-risk changes (security, data/schema, public APIs, concurrency, irreversible) need a short spec first;",
|
|
90
|
+
"- handle edge cases, errors, cancellation, and async behavior; do not block UI/event loops;",
|
|
139
91
|
"- avoid duplicate state, duplicate prompts, and repeated side effects.",
|
|
140
92
|
];
|
|
141
93
|
|
|
@@ -159,7 +111,7 @@ const FINAL_DISCIPLINE_LINES = [
|
|
|
159
111
|
|
|
160
112
|
const SILENCE_REMINDER_TEXT = [
|
|
161
113
|
"GLM silence reminder: remain in WORKING state.",
|
|
162
|
-
"Continue with
|
|
114
|
+
"Continue with tool-only discipline: inspect, verify, and act through tools only.",
|
|
163
115
|
"For the next step, emit exactly one tool call and no assistant text.",
|
|
164
116
|
"Do not acknowledge this reminder.",
|
|
165
117
|
].join("\n");
|
|
@@ -174,6 +126,19 @@ const DISCIPLINE_PROMPT_BLOCK_PATTERN = new RegExp(
|
|
|
174
126
|
"g",
|
|
175
127
|
);
|
|
176
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Strips pi's built-in "Pi documentation" reference block from the system prompt.
|
|
131
|
+
* That block (≈10 lines listing docs/examples paths and "when asked about X" routing)
|
|
132
|
+
* is useless dead weight for non-pi work and dilutes attention to the trailing
|
|
133
|
+
* <available_skills> section, which especially hurts weaker models like GLM.
|
|
134
|
+
* Anchored on the fixed header/footer strings from buildSystemPrompt() so it only
|
|
135
|
+
* ever matches pi's own block regardless of resolved doc/example paths.
|
|
136
|
+
*/
|
|
137
|
+
const PI_DOCS_BLOCK_PATTERN = new RegExp(
|
|
138
|
+
`\\n+Pi documentation \\(read only when the user asks about pi itself[\\s\\S]*?tui\\.md for TUI API details\\)\\n+`,
|
|
139
|
+
"g",
|
|
140
|
+
);
|
|
141
|
+
|
|
177
142
|
const LOOKUP_SYSTEM_PROMPT = [
|
|
178
143
|
"You are a vision-capable lookup helper for a blind GLM coding agent.",
|
|
179
144
|
"Inspect the provided screenshots/images and answer the parent agent's focused question using concrete visual evidence.",
|
|
@@ -234,9 +199,78 @@ export default function codingDiscipline(pi: ExtensionAPI) {
|
|
|
234
199
|
|
|
235
200
|
pi.on("before_provider_request", async (event: { payload?: unknown }, ctx: unknown) => {
|
|
236
201
|
const modelRef = modelRefFromPayload(event.payload) ?? selectedModelRef ?? modelRefFromContext(ctx);
|
|
237
|
-
|
|
238
|
-
|
|
202
|
+
if (!isGlmModel(modelRef)) return undefined;
|
|
203
|
+
const injected = injectCodingDisciplineIntoPayload(event.payload, {
|
|
204
|
+
lookupEnabled: Boolean(lookupModelFromConfig(contextCwd(ctx))),
|
|
239
205
|
});
|
|
206
|
+
if (process.env.PI_DEBUG_PROMPT === "1") {
|
|
207
|
+
logFinalPrompt(injected, modelRef, contextCwd(ctx) ?? process.cwd());
|
|
208
|
+
}
|
|
209
|
+
return injected;
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
pi.on("before_agent_start", async (event: { systemPromptOptions?: unknown; systemPrompt?: string }, ctx: unknown) => {
|
|
213
|
+
const debug = process.env.PI_DEBUG_PROMPT === "1";
|
|
214
|
+
const opts = event.systemPromptOptions as {
|
|
215
|
+
selectedTools?: unknown;
|
|
216
|
+
skills?: unknown[];
|
|
217
|
+
customPrompt?: unknown;
|
|
218
|
+
cwd?: string;
|
|
219
|
+
} | undefined;
|
|
220
|
+
const sys = typeof event.systemPrompt === "string" ? event.systemPrompt : "";
|
|
221
|
+
const toolsArr = Array.isArray(opts?.selectedTools) ? opts!.selectedTools as string[] : [];
|
|
222
|
+
const skillsCount = Array.isArray(opts?.skills) ? opts!.skills.length : -1;
|
|
223
|
+
const alreadyHasSkillsBlock = /<available_skills>/.test(sys);
|
|
224
|
+
|
|
225
|
+
// Inject <available_skills> when pi-core's gate failed to produce it.
|
|
226
|
+
// Core builds the block only when tools.includes("read") (lowercase), but
|
|
227
|
+
// Claude-alias registration exposes the tool as "Read" (PascalCase), so
|
|
228
|
+
// the gate returns false and skills never reach the prompt for any model.
|
|
229
|
+
// Guard: never re-inject if the block is already present (idempotent; safe
|
|
230
|
+
// once pi fixes the gate or aliases are removed).
|
|
231
|
+
let nextSystemPrompt = sys;
|
|
232
|
+
let injectedSkills = false;
|
|
233
|
+
if (!alreadyHasSkillsBlock) {
|
|
234
|
+
const skills = extractSkills(opts?.skills);
|
|
235
|
+
if (skills.length > 0) {
|
|
236
|
+
const block = buildAvailableSkillsBlock(skills);
|
|
237
|
+
if (block) {
|
|
238
|
+
nextSystemPrompt = sys ? `${sys}\n\n${block}` : block;
|
|
239
|
+
injectedSkills = true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (debug) {
|
|
245
|
+
try {
|
|
246
|
+
const cwd = contextCwd(ctx) ?? process.cwd();
|
|
247
|
+
const dir = path.join(cwd, ".pi-debug-prompt");
|
|
248
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
249
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
250
|
+
fs.writeFileSync(
|
|
251
|
+
path.join(dir, `${stamp}__before_agent_start.txt`),
|
|
252
|
+
[
|
|
253
|
+
"# before_agent_start diagnostic",
|
|
254
|
+
`cwd: ${cwd}`,
|
|
255
|
+
`customPrompt present: ${typeof opts?.customPrompt === "string" && opts.customPrompt.length > 0}`,
|
|
256
|
+
`selectedTools (${toolsArr.length}): ${JSON.stringify(toolsArr)}`,
|
|
257
|
+
`hasRead (lowercase): ${toolsArr.includes("read")}`,
|
|
258
|
+
`skills.length: ${skillsCount}`,
|
|
259
|
+
`system had <available_skills> block (pre-inject): ${alreadyHasSkillsBlock}`,
|
|
260
|
+
`injected <available_skills>: ${injectedSkills}`,
|
|
261
|
+
`systemPrompt length: ${nextSystemPrompt.length}`,
|
|
262
|
+
].join("\n"),
|
|
263
|
+
"utf-8",
|
|
264
|
+
);
|
|
265
|
+
} catch {
|
|
266
|
+
// debug logging must never break agent flow
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (injectedSkills) {
|
|
271
|
+
return { systemPrompt: nextSystemPrompt };
|
|
272
|
+
}
|
|
273
|
+
return undefined;
|
|
240
274
|
});
|
|
241
275
|
|
|
242
276
|
pi.on("context", async (event: { messages?: unknown[] }, ctx: unknown) => {
|
|
@@ -263,6 +297,7 @@ export function prependCodingDisciplinePrompt(systemPrompt: string, options: { l
|
|
|
263
297
|
const deduped = systemPrompt
|
|
264
298
|
.replace(LEGACY_SILENT_PROMPT_BLOCK_PATTERN, "")
|
|
265
299
|
.replace(DISCIPLINE_PROMPT_BLOCK_PATTERN, "")
|
|
300
|
+
.replace(PI_DOCS_BLOCK_PATTERN, "\n\n")
|
|
266
301
|
.trimStart();
|
|
267
302
|
const prompt = buildCodingDisciplinePrompt(options);
|
|
268
303
|
return deduped ? `${prompt}\n\n${deduped}` : prompt;
|
|
@@ -305,6 +340,131 @@ export function injectCodingDisciplineIntoPayload(payload: unknown, options: { l
|
|
|
305
340
|
return payload;
|
|
306
341
|
}
|
|
307
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Write the final provider-bound system prompt + message roles to a file for debugging.
|
|
345
|
+
* Enabled by PI_DEBUG_PROMPT=1. Writes one file per request to <cwd>/.pi-debug-prompt/
|
|
346
|
+
* so successive requests don't overwrite each other.
|
|
347
|
+
*/
|
|
348
|
+
function logFinalPrompt(payload: unknown, modelRef: string | undefined, cwd: string): void {
|
|
349
|
+
try {
|
|
350
|
+
const systemPrompt = extractPayloadSystemPrompt(payload);
|
|
351
|
+
const messages = extractPayloadMessages(payload);
|
|
352
|
+
if (systemPrompt === undefined && messages.length === 0) return;
|
|
353
|
+
|
|
354
|
+
const dir = path.join(cwd, ".pi-debug-prompt");
|
|
355
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
356
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
357
|
+
const slug = (modelRef ?? "unknown").replace(/[^a-z0-9._-]+/gi, "-");
|
|
358
|
+
const file = path.join(dir, `${stamp}__${slug}.txt`);
|
|
359
|
+
|
|
360
|
+
const lines: string[] = [];
|
|
361
|
+
lines.push(`# PI_DEBUG_PROMPT dump`);
|
|
362
|
+
lines.push(`model: ${modelRef ?? "(unknown)"}`);
|
|
363
|
+
lines.push(`timestamp: ${stamp}`);
|
|
364
|
+
lines.push(`messages: ${messages.length}`);
|
|
365
|
+
lines.push("");
|
|
366
|
+
lines.push("=== SYSTEM PROMPT ===");
|
|
367
|
+
lines.push(systemPrompt ?? "(none found)");
|
|
368
|
+
lines.push("");
|
|
369
|
+
lines.push("=== MESSAGE ROLES (last 8) ===");
|
|
370
|
+
for (const m of messages.slice(-8)) {
|
|
371
|
+
const preview = truncate(m.preview, 200);
|
|
372
|
+
lines.push(`[${m.role}] ${preview}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
fs.writeFileSync(file, lines.join("\n"), "utf-8");
|
|
376
|
+
} catch {
|
|
377
|
+
// Debug logging must never break a provider request.
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Extract loadable skills from systemPromptOptions.skills, filtering out
|
|
383
|
+
* disable-model-invocation ones (matching pi-core formatSkillsForPrompt behavior).
|
|
384
|
+
*/
|
|
385
|
+
function extractSkills(raw: unknown): { name: string; description: string; filePath?: string }[] {
|
|
386
|
+
if (!Array.isArray(raw)) return [];
|
|
387
|
+
const skills: { name: string; description: string; filePath?: string }[] = [];
|
|
388
|
+
for (const entry of raw) {
|
|
389
|
+
if (!isRecord(entry)) continue;
|
|
390
|
+
if (entry.disableModelInvocation === true) continue;
|
|
391
|
+
const name = typeof entry.name === "string" ? entry.name : "";
|
|
392
|
+
const description = typeof entry.description === "string" ? entry.description : "";
|
|
393
|
+
if (!name || !description) continue;
|
|
394
|
+
const filePath = typeof entry.filePath === "string" ? entry.filePath : undefined;
|
|
395
|
+
skills.push({ name, description, filePath });
|
|
396
|
+
}
|
|
397
|
+
return skills;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Build the <available_skills> XML block in pi-core's exact format
|
|
402
|
+
* (see packages/coding-agent/src/core/skills.ts::formatSkillsForPrompt),
|
|
403
|
+
* so models trained on the Agent Skills standard see identical structure.
|
|
404
|
+
*/
|
|
405
|
+
function buildAvailableSkillsBlock(skills: { name: string; description: string; filePath?: string }[]): string | undefined {
|
|
406
|
+
const loadable = skills.filter((s) => s.filePath);
|
|
407
|
+
if (loadable.length === 0) return undefined;
|
|
408
|
+
const lines = [
|
|
409
|
+
"The following skills provide specialized instructions for specific tasks.",
|
|
410
|
+
"Use the read tool to load a skill's file when the task matches its description.",
|
|
411
|
+
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
|
|
412
|
+
"",
|
|
413
|
+
"<available_skills>",
|
|
414
|
+
];
|
|
415
|
+
for (const skill of loadable) {
|
|
416
|
+
lines.push(" <skill>");
|
|
417
|
+
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
418
|
+
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
419
|
+
lines.push(` <location>${escapeXml(skill.filePath ?? "")}</location>`);
|
|
420
|
+
lines.push(" </skill>");
|
|
421
|
+
}
|
|
422
|
+
lines.push("</available_skills>");
|
|
423
|
+
return lines.join("\n");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function escapeXml(str: string): string {
|
|
427
|
+
return str
|
|
428
|
+
.replace(/&/g, "&")
|
|
429
|
+
.replace(/</g, "<")
|
|
430
|
+
.replace(/>/g, ">")
|
|
431
|
+
.replace(/"/g, """)
|
|
432
|
+
.replace(/'/g, "'");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function extractPayloadSystemPrompt(payload: unknown): string | undefined {
|
|
436
|
+
if (!isRecord(payload)) return undefined;
|
|
437
|
+
if (typeof payload.instructions === "string") return payload.instructions;
|
|
438
|
+
if (typeof payload.system === "string") return payload.system;
|
|
439
|
+
for (const field of ["messages", "input"] as const) {
|
|
440
|
+
const list = payload[field];
|
|
441
|
+
if (!Array.isArray(list)) continue;
|
|
442
|
+
for (const message of list) {
|
|
443
|
+
if (!isRecord(message)) continue;
|
|
444
|
+
if (message.role !== "system" && message.role !== "developer") continue;
|
|
445
|
+
const text = contentToText(message.content);
|
|
446
|
+
if (text) return text;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function extractPayloadMessages(payload: unknown): { role: string; preview: string }[] {
|
|
453
|
+
if (!isRecord(payload)) return [];
|
|
454
|
+
for (const field of ["messages", "input"] as const) {
|
|
455
|
+
const list = payload[field];
|
|
456
|
+
if (Array.isArray(list)) {
|
|
457
|
+
return list.map((message) => {
|
|
458
|
+
if (!isRecord(message)) return { role: "?", preview: "" };
|
|
459
|
+
const role = typeof message.role === "string" ? message.role : "?";
|
|
460
|
+
const preview = contentToText(message.content);
|
|
461
|
+
return { role, preview };
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return [];
|
|
466
|
+
}
|
|
467
|
+
|
|
308
468
|
function createLookupTool() {
|
|
309
469
|
return {
|
|
310
470
|
name: LOOKUP_TOOL_NAME,
|