pum-agent 0.2.36-beta.1 → 0.2.37-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/app.tsx +2 -2
- package/src/bash-output.ts +21 -5
- package/src/file-mutation-guard.ts +3 -3
- package/src/project-validation.ts +3 -2
- package/src/provider-error.ts +66 -0
- package/src/replay.ts +5 -2
- package/src/subagents/manager.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pum-agent",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.37-beta.1",
|
|
4
4
|
"description": "A compact terminal coding agent powered by pi and OpenTUI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"pack:check": "bun run scripts/validate-pack.ts"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@earendil-works/pi-ai": "
|
|
54
|
-
"@earendil-works/pi-coding-agent": "
|
|
53
|
+
"@earendil-works/pi-ai": "1.1.0",
|
|
54
|
+
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
55
55
|
"@opentui/core": "^0.5.1",
|
|
56
56
|
"@opentui/react": "^0.5.1",
|
|
57
57
|
"react": "^19.2.8",
|
package/src/app.tsx
CHANGED
|
@@ -62,7 +62,6 @@ import {
|
|
|
62
62
|
rawToolText,
|
|
63
63
|
resolveGoalReview,
|
|
64
64
|
resolvePendingDelivery,
|
|
65
|
-
settleTranscriptMessage,
|
|
66
65
|
streamedDelta,
|
|
67
66
|
StreamLine,
|
|
68
67
|
TextLine,
|
|
@@ -74,6 +73,7 @@ import {
|
|
|
74
73
|
} from "./transcript";
|
|
75
74
|
import { bashOutput, bashResultDisplay, editCounts, toolArgs, type ToolCall } from "./tool-line";
|
|
76
75
|
import { interruptedToolCall, settledToolCall, startedToolCall } from "./tool-row";
|
|
76
|
+
import { settleAssistantTranscript } from "./provider-error";
|
|
77
77
|
import { toolPreviewFromResult, toolPreviewFromStart } from "./tool-preview";
|
|
78
78
|
import { readBranch, watchBranch } from "./git-branch";
|
|
79
79
|
import { HelpPopup, maxHelpScrollOffset } from "./help-popup";
|
|
@@ -2222,7 +2222,7 @@ export function App({
|
|
|
2222
2222
|
}
|
|
2223
2223
|
case "message_end":
|
|
2224
2224
|
if (event.message.role === "assistant") {
|
|
2225
|
-
setTx((value) =>
|
|
2225
|
+
setTx((value) => settleAssistantTranscript(value, event.message));
|
|
2226
2226
|
} else if (event.message.role === "custom" && event.message.customType === VALIDATION_CUSTOM_TYPE) {
|
|
2227
2227
|
append({
|
|
2228
2228
|
kind: "text",
|
package/src/bash-output.ts
CHANGED
|
@@ -580,7 +580,12 @@ export type BashToolLike = {
|
|
|
580
580
|
signal?: AbortSignal,
|
|
581
581
|
onUpdate?: (update: { content: unknown[]; details?: unknown }) => void,
|
|
582
582
|
ctx?: unknown,
|
|
583
|
-
) => Promise<{
|
|
583
|
+
) => Promise<{
|
|
584
|
+
content: Array<{ type: string; text: string }>;
|
|
585
|
+
details?: { fullOutputPath?: string };
|
|
586
|
+
isError?: boolean;
|
|
587
|
+
structuredContent?: unknown;
|
|
588
|
+
}>;
|
|
584
589
|
};
|
|
585
590
|
|
|
586
591
|
export const bashOutputParameters = Type.Object({
|
|
@@ -666,16 +671,27 @@ export function withBashOutput(
|
|
|
666
671
|
await capture.remove();
|
|
667
672
|
return result; // Fail open to pi's bounded native result; never lose output.
|
|
668
673
|
}
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
674
|
+
const nativeText = textOf(result.content);
|
|
675
|
+
// pi 1.1.0 returns nonzero exits as error results, not thrown Errors.
|
|
676
|
+
// Keep native status/error identity while summarizing our trusted stream.
|
|
677
|
+
const exit = result.isError ? EXIT_RE.exec(nativeText) : null;
|
|
678
|
+
const status = exit ? `Command exited with code ${exit[2]}` : undefined;
|
|
679
|
+
if (result.isError && !exit) {
|
|
680
|
+
await capture?.remove();
|
|
681
|
+
return result; // Unknown error shape: preserve native evidence intact.
|
|
682
|
+
}
|
|
683
|
+
const source = captured ?? (exit ? exit[1] : nativeText);
|
|
684
|
+
const summary = summarizeBashOutput(source, status
|
|
685
|
+
? { ...perCall, maxBytes: Math.max(64, perCall.maxBytes - byteLen(status) - 2) }
|
|
686
|
+
: perCall, {
|
|
687
|
+
exitCode: exit ? Number(exit[2]) : 0,
|
|
672
688
|
path: capture?.path,
|
|
673
689
|
patterns,
|
|
674
690
|
});
|
|
675
691
|
if (capture && !summary.lossy) await capture.remove();
|
|
676
692
|
return {
|
|
677
693
|
...result,
|
|
678
|
-
content: [{ type: "text", text: summary.content }],
|
|
694
|
+
content: [{ type: "text", text: status ? `${summary.content}\n\n${status}` : summary.content }],
|
|
679
695
|
details: capture && summary.lossy ? { fullOutputPath: capture.path } : result.details,
|
|
680
696
|
};
|
|
681
697
|
} catch (error) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createReadToolDefinition, type
|
|
1
|
+
import { createReadToolDefinition, type ExtensionToolContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { constants, type Stats } from "node:fs";
|
|
4
4
|
import { lstat, open } from "node:fs/promises";
|
|
@@ -138,11 +138,11 @@ export class FileMutationGuard {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
/** Preserve native image handling and filename retries; observe only a stable successful read. */
|
|
141
|
-
async read(id: string, input: any, signal?: AbortSignal, onUpdate?: any, ctx?:
|
|
141
|
+
async read(id: string, input: any, signal?: AbortSignal, onUpdate?: any, ctx?: ExtensionToolContext, autoResizeImages?: boolean): Promise<any> {
|
|
142
142
|
const target = await this.target(input.path, "read");
|
|
143
143
|
const before = await this.snapshot(target.path, "read");
|
|
144
144
|
const result = await createReadToolDefinition(this.cwd, { autoResizeImages }).execute(
|
|
145
|
-
id, input, signal, onUpdate, ctx ?? { cwd: this.cwd } as
|
|
145
|
+
id, input, signal, onUpdate, ctx ?? { cwd: this.cwd } as ExtensionToolContext,
|
|
146
146
|
);
|
|
147
147
|
const after = await this.snapshot(target.path, "read");
|
|
148
148
|
if (!signal?.aborted && !this.disposed && before.fingerprint === after.fingerprint) this.remember(target.key, before);
|
|
@@ -290,8 +290,9 @@ export class ProjectValidationController {
|
|
|
290
290
|
outcome = deadline.signal.aborted ? "timeout" : "cancelled";
|
|
291
291
|
} else {
|
|
292
292
|
const result = await tool.execute(toolCall.id, args, signal);
|
|
293
|
-
const
|
|
294
|
-
|
|
293
|
+
const isError = result.isError === true;
|
|
294
|
+
const after = await untilAborted(Promise.resolve(session.agent.afterToolCall?.({ assistantMessage, toolCall, args, result, isError, context }, signal)), signal);
|
|
295
|
+
if (after?.isError ?? isError) outcome = "failed";
|
|
295
296
|
output = (after?.content ?? result.content).filter((item) => item.type === "text").map((item) => item.text).join("\n");
|
|
296
297
|
}
|
|
297
298
|
} catch (error) {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { flushStream, settleTranscriptMessage, type Line, type PendingTranscriptState } from "./transcript";
|
|
2
|
+
|
|
3
|
+
/** Redact entire header values, not just the scheme or first credential token. */
|
|
4
|
+
function redactAuthorization(text: string): string {
|
|
5
|
+
const header = /(["']?\b(?:proxy-)?authorization["']?\s*[:=]\s*)/gi;
|
|
6
|
+
let safe = "";
|
|
7
|
+
let copied = 0;
|
|
8
|
+
for (let match = header.exec(text); match; match = header.exec(text)) {
|
|
9
|
+
const start = header.lastIndex;
|
|
10
|
+
let end = start;
|
|
11
|
+
let quote = "";
|
|
12
|
+
for (; end < text.length; end++) {
|
|
13
|
+
const char = text[end]!;
|
|
14
|
+
if (quote) {
|
|
15
|
+
// JSON-escaped Digest quotes are part of the value, not its boundary.
|
|
16
|
+
if (char === "\\") { end++; continue; }
|
|
17
|
+
if (char === quote) quote = "";
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (char === '"' || char === "'") { quote = char; continue; }
|
|
21
|
+
// Digest commas and arbitrary scheme whitespace/semicolons can contain
|
|
22
|
+
// credentials. Only a structured next field or explicit schema diagnostic
|
|
23
|
+
// is a safe boundary; ambiguous/unclosed values consume the remaining text.
|
|
24
|
+
if (char === "," && /^,\s*["'][^"'\n]+["']\s*:/.test(text.slice(end))) break;
|
|
25
|
+
if (char === ";" && /^;\s*(?:[45]\d\d\s+)?tools\.\d+\.custom\b/.test(text.slice(end))) break;
|
|
26
|
+
}
|
|
27
|
+
safe += text.slice(copied, start) + "[redacted]";
|
|
28
|
+
copied = end;
|
|
29
|
+
header.lastIndex = end;
|
|
30
|
+
}
|
|
31
|
+
return safe + text.slice(copied);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Provider text is untrusted display data, never a diagnostic or durable rewrite. */
|
|
35
|
+
export function providerErrorLine(message: unknown): Line | null {
|
|
36
|
+
const assistant = message as { role?: unknown; stopReason?: unknown; errorMessage?: unknown } | null;
|
|
37
|
+
// An aborted response can also carry errorMessage; cancellation is not failure.
|
|
38
|
+
if (assistant?.role !== "assistant" || assistant.stopReason !== "error") return null;
|
|
39
|
+
const raw = typeof assistant.errorMessage === "string" ? assistant.errorMessage : "";
|
|
40
|
+
// As in login-flow.safeError, redact credential-shaped keys. Provider failures
|
|
41
|
+
// can also echo headers/JSON or an OAuth URL, so remove those before bounding.
|
|
42
|
+
const normalized = raw
|
|
43
|
+
// Normalize control sequences first so they cannot split a credential marker.
|
|
44
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
45
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
46
|
+
.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g, "");
|
|
47
|
+
const safe = redactAuthorization(normalized)
|
|
48
|
+
.replace(/https?:\/\/[^\s<>"']+/gi, "[redacted URL]")
|
|
49
|
+
.replace(/\bBearer\s+[^\s,"'}]+/gi, "Bearer [redacted]")
|
|
50
|
+
// Quoted JSON/header values may contain escaped quotes or span whitespace.
|
|
51
|
+
// An unterminated quoted secret is redacted through the end, never partially.
|
|
52
|
+
.replace(/(["']?\b(?:api[_ -]?key|x-api-key|access[_ -]?token|refresh[_ -]?token|password|secret|client[_ -]?secret)["']?\s*[:=]\s*)(?:"(?:\\[\s\S]|[^"\\])*(?:"|\\?$)|'(?:\\[\s\S]|[^'\\])*(?:'|\\?$)|[^\s,}\n]+)/gi, "$1[redacted]")
|
|
53
|
+
.replace(/\b(?:sk|key|token)-[A-Za-z0-9_.-]{8,}\b/gi, "[redacted]")
|
|
54
|
+
.replace(/\b(?:gh[pousr]_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+|AKIA[A-Z0-9]{16}|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)\b/g, "[redacted]")
|
|
55
|
+
.trim();
|
|
56
|
+
const detail = safe ? safe.slice(0, 4000) + (safe.length > 4000 ? "…" : "") : "The request failed without an error description.";
|
|
57
|
+
return { kind: "text", role: "error", text: `Provider error: ${detail}` };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Finish partial output before its failure, then deliver queued messages. */
|
|
61
|
+
export function settleAssistantTranscript<T extends PendingTranscriptState>(value: T, message: unknown): T {
|
|
62
|
+
const error = providerErrorLine(message);
|
|
63
|
+
if (!error) return settleTranscriptMessage(value);
|
|
64
|
+
const flushed = flushStream(value);
|
|
65
|
+
return settleTranscriptMessage({ ...flushed, lines: [...flushed.lines, error] });
|
|
66
|
+
}
|
package/src/replay.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentMessageKind, Line } from "./transcript";
|
|
2
|
+
import { providerErrorLine } from "./provider-error";
|
|
2
3
|
import { toolArgs, type ToolCall } from "./tool-line";
|
|
3
4
|
import { interruptedToolCall, settledToolCall, startedToolCall } from "./tool-row";
|
|
4
5
|
import {
|
|
@@ -303,8 +304,8 @@ export function replayEntries(
|
|
|
303
304
|
continue;
|
|
304
305
|
}
|
|
305
306
|
|
|
306
|
-
if (message?.role === "assistant"
|
|
307
|
-
for (const block of message.content) {
|
|
307
|
+
if (message?.role === "assistant") {
|
|
308
|
+
for (const block of Array.isArray(message.content) ? message.content : []) {
|
|
308
309
|
if (block?.type === "text" && block.text?.trim()) {
|
|
309
310
|
lines.push({ kind: "text", role: "assistant", text: block.text.trim() });
|
|
310
311
|
} else if (showThinking && block?.type === "thinking" && block.thinking?.trim()) {
|
|
@@ -322,6 +323,8 @@ export function replayEntries(
|
|
|
322
323
|
lines.push({ kind: "tool", call });
|
|
323
324
|
}
|
|
324
325
|
}
|
|
326
|
+
const error = providerErrorLine(message);
|
|
327
|
+
if (error) lines.push(error);
|
|
325
328
|
continue;
|
|
326
329
|
}
|
|
327
330
|
|
package/src/subagents/manager.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
usageFromEntries,
|
|
21
21
|
} from "../agent-usage";
|
|
22
22
|
import { replayEntries } from "../replay";
|
|
23
|
+
import { settleAssistantTranscript } from "../provider-error";
|
|
23
24
|
import { registerOperationalNotice } from "../operational-context";
|
|
24
25
|
import { bindFileCheckpointSession } from "../file-checkpoints";
|
|
25
26
|
import { ProjectValidationController, VALIDATION_CUSTOM_TYPE } from "../project-validation";
|
|
@@ -76,7 +77,6 @@ import { GOAL_VERDICT_TOOL_NAME, goalVerdictParameters } from "../goal-judge";
|
|
|
76
77
|
import {
|
|
77
78
|
flushStream,
|
|
78
79
|
resolvePendingDelivery,
|
|
79
|
-
settleTranscriptMessage,
|
|
80
80
|
streamedDelta,
|
|
81
81
|
type Line,
|
|
82
82
|
type PendingLine,
|
|
@@ -1041,7 +1041,7 @@ export class SubagentManager {
|
|
|
1041
1041
|
this.appendLine(record, { kind: "text", role: event.message.details?.outcome === "passed" ? "system" : "error", text: String(event.message.content) });
|
|
1042
1042
|
}
|
|
1043
1043
|
if (event.message?.role === "assistant") {
|
|
1044
|
-
this.updateTranscript(record, (value) =>
|
|
1044
|
+
this.updateTranscript(record, (value) => settleAssistantTranscript(value, event.message));
|
|
1045
1045
|
}
|
|
1046
1046
|
break;
|
|
1047
1047
|
case "message_update": {
|