pi-ui-extend 0.1.56 → 0.1.57
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.
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
* rejects HTTP 400 `Unknown parameter: 'input[N].content'` when non-message
|
|
4
4
|
* items (reasoning, function_call_output) carry a spurious `content` field.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* (incl. 0.79.10) is affected.
|
|
6
|
+
* Stray fields can come from replayed pi-ai items or from another extension
|
|
7
|
+
* that modifies the provider payload. The sanitizer therefore must be the
|
|
8
|
+
* LAST `before_provider_request` handler registered by pi-tools-suite.
|
|
10
9
|
*
|
|
11
10
|
* The fix must run at the transport layer because the provider has TWO paths:
|
|
12
11
|
*
|
|
@@ -16,16 +15,13 @@
|
|
|
16
15
|
* strip `content` from every non-message input item of each
|
|
17
16
|
* `response.create` frame, on the exact bytes leaving the socket.
|
|
18
17
|
*
|
|
19
|
-
* 2. SSE/fetch fallback path: once a websocket attempt fails
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* `
|
|
23
|
-
*
|
|
24
|
-
* offending item, so we also wrap `globalThis.fetch` once and strip the same
|
|
25
|
-
* spurious `content` from the request body on the exact bytes leaving the
|
|
26
|
-
* HTTP client.
|
|
18
|
+
* 2. SSE/fetch fallback path: once a websocket attempt fails, subsequent
|
|
19
|
+
* requests use the FULL body. pi-ai >= 0.80.6 zstd-compresses that body
|
|
20
|
+
* before fetch, so a fetch wrapper cannot inspect it. The final
|
|
21
|
+
* `before_provider_request` sanitizer is the primary guard for this path;
|
|
22
|
+
* the fetch wrapper remains a best-effort fallback for uncompressed bodies.
|
|
27
23
|
*
|
|
28
|
-
*
|
|
24
|
+
* Module registration order is part of this workaround's correctness.
|
|
29
25
|
*
|
|
30
26
|
* Remove this whole module once an upstream pi-ai release carries the fix.
|
|
31
27
|
*/
|
|
@@ -182,6 +178,9 @@ export function stripContentFromWireFrame(frame: unknown): { frame: Record<strin
|
|
|
182
178
|
}
|
|
183
179
|
|
|
184
180
|
export default function codexReasoningFix(pi: ExtensionAPI): void {
|
|
181
|
+
// src/index.ts deliberately registers this module last. A later payload
|
|
182
|
+
// modifier could otherwise reintroduce invalid content after sanitization,
|
|
183
|
+
// and compressed SSE bodies are too late to repair in the fetch wrapper.
|
|
185
184
|
pi.on("before_provider_request", async (event: ProviderRequestEvent, _ctx: ProviderRequestContext) => {
|
|
186
185
|
const result = stripReasoningContentFromPayload(event.payload);
|
|
187
186
|
return result === event.payload ? undefined : result;
|
|
@@ -132,6 +132,17 @@ function appendDcpControlToMessages(messages: unknown, text: string): unknown {
|
|
|
132
132
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
133
133
|
const message = messages[index] as any
|
|
134
134
|
if (!message || typeof message !== "object") continue
|
|
135
|
+
// Responses items such as reasoning/function_call_output do not accept a
|
|
136
|
+
// `content` field. Keep a function output at the tail by appending to its
|
|
137
|
+
// valid `output` string; otherwise scan back to an actual message item.
|
|
138
|
+
if (typeof message.type === "string" && message.type !== "message" && message.role === undefined) {
|
|
139
|
+
if (message.type === "function_call_output" && typeof message.output === "string") {
|
|
140
|
+
return messages.map((candidate: any, candidateIndex) => candidateIndex === index
|
|
141
|
+
? { ...candidate, output: `${candidate.output}\n\n${block}` }
|
|
142
|
+
: candidate)
|
|
143
|
+
}
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
135
146
|
if (message.role === "system" || message.role === "developer") continue
|
|
136
147
|
targetIndex = index
|
|
137
148
|
break
|
|
@@ -9,8 +9,7 @@ type ExtensionModule = {
|
|
|
9
9
|
default: ExtensionFactory;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
const MODULES: Array<{ name: string; load: () => Promise<ExtensionModule> }> = [
|
|
13
|
-
{ name: "codex-reasoning-fix", load: () => import("./codex-reasoning-fix/index") },
|
|
12
|
+
export const MODULES: Array<{ name: string; load: () => Promise<ExtensionModule> }> = [
|
|
14
13
|
{ name: "coding-discipline", load: () => import("./coding-discipline/index") },
|
|
15
14
|
{ name: "ast-grep", load: () => import("./ast-grep/index") },
|
|
16
15
|
{ name: "async-subagents", load: () => import("./async-subagents/index") },
|
|
@@ -28,6 +27,9 @@ const MODULES: Array<{ name: string; load: () => Promise<ExtensionModule> }> = [
|
|
|
28
27
|
{ name: "prompt-commands", load: () => import("./prompt-commands/index") },
|
|
29
28
|
{ name: "skill-installer", load: () => import("./skill-installer/index") },
|
|
30
29
|
{ name: "telegram-mirror", load: () => import("./telegram-mirror/index") },
|
|
30
|
+
// Keep this last: its before_provider_request handler is the final payload
|
|
31
|
+
// sanitizer after DCP and any other provider-payload modifiers.
|
|
32
|
+
{ name: "codex-reasoning-fix", load: () => import("./codex-reasoning-fix/index") },
|
|
31
33
|
];
|
|
32
34
|
|
|
33
35
|
export default async function piToolsSuite(pi: ExtensionAPI) {
|
|
@@ -10,16 +10,6 @@ import { getState, replaceState } from "./state/store.js";
|
|
|
10
10
|
import { activateTodoStateScope, DEFAULT_PROMPT_GUIDELINES, DEFAULT_PROMPT_SNIPPET, publishTodoState, registerTodosCommand, registerTodoTool } from "./todo.js";
|
|
11
11
|
import type { Task, TaskMutationParams } from "./tool/types.js";
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* Renderer-relayed signal that the session is in an auto-retry cycle.
|
|
15
|
-
* Payload: `{ active: boolean }`. The SDK does not forward retry state to
|
|
16
|
-
* extensions, so the renderer emits this on the extension event bus. We use it
|
|
17
|
-
* to suppress the auto-nudge on intermediate retry agent_end events: nudging
|
|
18
|
-
* then races the pending retry continuation and surfaces a benign
|
|
19
|
-
* "Agent is already processing" extension error in other tabs.
|
|
20
|
-
*/
|
|
21
|
-
const RETRY_ACTIVE_EVENT = "pix:retry-active";
|
|
22
|
-
|
|
23
13
|
type AgentMessageLike = { role?: unknown; stopReason?: unknown; content?: unknown };
|
|
24
14
|
|
|
25
15
|
const TODO_NUDGE_LIMIT = 8;
|
|
@@ -144,10 +134,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
144
134
|
const pendingAskUserToolCallIds = new Set<string>();
|
|
145
135
|
let suppressNextNudgeForThinkingSwitch = false;
|
|
146
136
|
let inProgressAtAgentStart = new Set<number>();
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
let
|
|
137
|
+
// agent_end may be followed by an automatic retry, compaction, or queued
|
|
138
|
+
// continuation. Remember only whether its terminal assistant reply was a
|
|
139
|
+
// successful visible response; schedule a nudge later on agent_settled.
|
|
140
|
+
let settledNudgeEligible = false;
|
|
151
141
|
|
|
152
142
|
function registerTodoToolWithCurrentPrompt(): void {
|
|
153
143
|
const thinkingPrompt = todoThinkingEnabled ? buildThinkingPromptParts(currentModel) : {};
|
|
@@ -336,9 +326,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
336
326
|
const delayMs = attempt === 0 ? TODO_NUDGE_INITIAL_DELAY_MS : TODO_NUDGE_IDLE_RETRY_DELAY_MS;
|
|
337
327
|
nudgeTimer = setTimeout(() => {
|
|
338
328
|
nudgeTimer = undefined;
|
|
339
|
-
// A retry-start signal may have arrived after the agent_end that
|
|
340
|
-
// scheduled this nudge. Bail out so we don't nudge mid-retry.
|
|
341
|
-
if (retryActive) return;
|
|
342
329
|
try {
|
|
343
330
|
activateTodoStateScope(ctx);
|
|
344
331
|
if (!ctx.isIdle()) {
|
|
@@ -358,9 +345,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
358
345
|
if (nudge.signature === lastNudgedSignature) return;
|
|
359
346
|
lastNudgedSignature = nudge.signature;
|
|
360
347
|
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
// queueing followUp from inside agent_end can be too late to be drained.
|
|
348
|
+
// agent_settled means retries, compaction, and queued continuations are
|
|
349
|
+
// finished. Send on the next idle tick as a fresh turn.
|
|
364
350
|
pi.sendUserMessage(nudge.message);
|
|
365
351
|
} catch (err) {
|
|
366
352
|
if (isAgentBusyRaceError(err)) {
|
|
@@ -413,12 +399,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
413
399
|
clearNudgeTimer();
|
|
414
400
|
});
|
|
415
401
|
|
|
416
|
-
pi.events.on(RETRY_ACTIVE_EVENT, (data: unknown) => {
|
|
417
|
-
const active = data != null && typeof data === "object" && (data as { active?: unknown }).active === true;
|
|
418
|
-
retryActive = active;
|
|
419
|
-
if (active) clearNudgeTimer();
|
|
420
|
-
});
|
|
421
|
-
|
|
422
402
|
pi.on("model_select", async (event) => {
|
|
423
403
|
currentModel = event.model;
|
|
424
404
|
if (todoThinkingEnabled) registerTodoToolWithCurrentPrompt();
|
|
@@ -439,7 +419,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
439
419
|
pi.on("agent_start", async (_event, ctx) => {
|
|
440
420
|
activateTodoStateScope(ctx);
|
|
441
421
|
pendingAskUserToolCallIds.clear();
|
|
442
|
-
|
|
422
|
+
settledNudgeEligible = false;
|
|
443
423
|
inProgressAtAgentStart = new Set(selectVisibleTasks(getState()).filter((task) => task.status === "in_progress").map((task) => task.id));
|
|
444
424
|
});
|
|
445
425
|
|
|
@@ -455,26 +435,42 @@ export default function (pi: ExtensionAPI) {
|
|
|
455
435
|
pi.on("agent_end", async (event, ctx) => {
|
|
456
436
|
activateTodoStateScope(ctx);
|
|
457
437
|
const completedAssistantReply = hasCompletedAssistantReply((event as { messages?: readonly unknown[] } | undefined)?.messages);
|
|
438
|
+
settledNudgeEligible = completedAssistantReply;
|
|
458
439
|
|
|
459
440
|
if (suppressNextNudgeForThinkingSwitch) {
|
|
460
441
|
suppressNextNudgeForThinkingSwitch = false;
|
|
461
442
|
if (!completedAssistantReply) {
|
|
443
|
+
settledNudgeEligible = false;
|
|
462
444
|
clearNudgeTimer();
|
|
463
445
|
return;
|
|
464
446
|
}
|
|
465
447
|
}
|
|
466
448
|
|
|
467
449
|
if (pendingAskUserToolCallIds.size > 0) {
|
|
450
|
+
settledNudgeEligible = false;
|
|
468
451
|
clearNudgeTimer();
|
|
469
452
|
return;
|
|
470
453
|
}
|
|
471
454
|
|
|
472
455
|
if (completedAssistantReply && maybeRecoverCompletedCurrentTask((event as { messages?: readonly unknown[] } | undefined)?.messages, ctx)) {
|
|
456
|
+
settledNudgeEligible = false;
|
|
473
457
|
lastNudgedSignature = undefined;
|
|
474
458
|
clearNudgeTimer();
|
|
475
459
|
return;
|
|
476
460
|
}
|
|
461
|
+
});
|
|
477
462
|
|
|
463
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
464
|
+
activateTodoStateScope(ctx);
|
|
465
|
+
if (!settledNudgeEligible) {
|
|
466
|
+
clearNudgeTimer();
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
settledNudgeEligible = false;
|
|
470
|
+
if (pendingAskUserToolCallIds.size > 0) {
|
|
471
|
+
clearNudgeTimer();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
478
474
|
const nudge = getUnfinishedTodoNudge();
|
|
479
475
|
if (!nudge) {
|
|
480
476
|
lastNudgedSignature = undefined;
|