@nettee/beacon 0.4.9 → 0.4.10
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.
|
@@ -8,6 +8,8 @@ export type OutcomeSubmission = {
|
|
|
8
8
|
take(): FinalOutcomeContent;
|
|
9
9
|
takeFeedback(): FeedbackRecord | undefined;
|
|
10
10
|
takeObservabilityFeedback(): ObservabilityFeedbackRecord | undefined;
|
|
11
|
+
/** True when Agent already submitted reply_text / reply_card / no_reply. */
|
|
12
|
+
hasReply(): boolean;
|
|
11
13
|
cancel(): void;
|
|
12
14
|
};
|
|
13
15
|
export type OutcomeSink = {
|
|
@@ -127,6 +127,9 @@ export async function startOutcomeServer(options = {}) {
|
|
|
127
127
|
submittedAt: record.observabilityFeedbackSubmittedAt,
|
|
128
128
|
};
|
|
129
129
|
},
|
|
130
|
+
hasReply() {
|
|
131
|
+
return record.outcome?.reply !== undefined;
|
|
132
|
+
},
|
|
130
133
|
cancel() {
|
|
131
134
|
submissions.delete(runToken);
|
|
132
135
|
},
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
3
|
import { isMissingReplyOutcomeError, MISSING_REPLY_OUTCOME_SUMMARY, } from "../outcome/content.js";
|
|
4
4
|
import { inboundOutOfRoleReply } from "../outcome/instructions.js";
|
|
5
|
-
import { PiRuntimeError } from "../runtime/pi-rpc.js";
|
|
5
|
+
import { PiRuntimeError, RUNTIME_TIMEOUT_OUTCOME_MISSING_NOTE, RUNTIME_TIMEOUT_OUTCOME_SUBMITTED_NOTE, } from "../runtime/pi-rpc.js";
|
|
6
6
|
import { agentSystemPromptTrigger, buildAgentSystemPrompt, profilePromptDigest, } from "../runtime/system-prompt.js";
|
|
7
7
|
function promptFor(input) {
|
|
8
8
|
if (input.kind === "event") {
|
|
@@ -79,6 +79,34 @@ function detailBeyondMissingSummary(summaryText) {
|
|
|
79
79
|
}
|
|
80
80
|
return clipForFeishu(summaryText, FAILURE_DETAIL_LIMIT);
|
|
81
81
|
}
|
|
82
|
+
function formatRuntimeTimeoutReplyText(runId, summaryText) {
|
|
83
|
+
const duration = summaryText.match(/after\s+([^:]+?):\s*Pi process/i)?.[1]?.trim() ??
|
|
84
|
+
summaryText
|
|
85
|
+
.match(/after\s+(\d+min \(\d+ms\)|\d+s \(\d+ms\)|\d+ms)/i)?.[1]
|
|
86
|
+
?.trim() ??
|
|
87
|
+
"configured limit";
|
|
88
|
+
const parts = [
|
|
89
|
+
`处理失败(run_id=${runId},code=runtime_timeout):`,
|
|
90
|
+
`Beacon Run 硬超时(${duration}):Pi 进程未在限时内退出。`,
|
|
91
|
+
];
|
|
92
|
+
if (/Final Outcome was already submitted before timeout/i.test(summaryText)) {
|
|
93
|
+
parts.push("超时前 Agent 已提交 Final Outcome,但仍因 Pi 未退出被超时失败路径覆盖。");
|
|
94
|
+
}
|
|
95
|
+
else if (/No Final Outcome was submitted before timeout/i.test(summaryText)) {
|
|
96
|
+
parts.push("超时前未提交 Final Outcome。");
|
|
97
|
+
}
|
|
98
|
+
const remainingRaw = summaryText.match(/Remaining stderr:\s*([\s\S]+)/i)?.[1];
|
|
99
|
+
if (remainingRaw) {
|
|
100
|
+
const remaining = remainingRaw
|
|
101
|
+
.replace(/\.\s*Final Outcome was already submitted[\s\S]*$/i, "")
|
|
102
|
+
.replace(/\.\s*No Final Outcome was submitted[\s\S]*$/i, "")
|
|
103
|
+
.trim();
|
|
104
|
+
if (remaining) {
|
|
105
|
+
parts.push(`附加诊断:${clipForFeishu(remaining, FAILURE_DETAIL_LIMIT)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return parts.join("");
|
|
109
|
+
}
|
|
82
110
|
/** User-visible (Feishu/admin) failure text. Keep codes stable; prefer concrete reasons. */
|
|
83
111
|
export function formatFailureReplyText(runId, failure) {
|
|
84
112
|
if (failure?.code === "outcome_missing") {
|
|
@@ -93,6 +121,9 @@ export function formatFailureReplyText(runId, failure) {
|
|
|
93
121
|
parts.push(`详情:${detail}`);
|
|
94
122
|
return parts.join("");
|
|
95
123
|
}
|
|
124
|
+
if (failure?.code === "runtime_timeout" && failure.summary?.trim()) {
|
|
125
|
+
return formatRuntimeTimeoutReplyText(runId, failure.summary);
|
|
126
|
+
}
|
|
96
127
|
if (failure?.summary?.trim()) {
|
|
97
128
|
return `处理失败(run_id=${runId},code=${failure.code}):${clipForFeishu(failure.summary, FAILURE_DETAIL_LIMIT)}`;
|
|
98
129
|
}
|
|
@@ -365,13 +396,21 @@ export class RunOrchestrator {
|
|
|
365
396
|
catch (error) {
|
|
366
397
|
const submitted = submission.takeFeedback();
|
|
367
398
|
const submittedObservability = submission.takeObservabilityFeedback();
|
|
399
|
+
const replySubmitted = submission.hasReply();
|
|
368
400
|
submission.cancel();
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
401
|
+
let failError = error;
|
|
402
|
+
if (error instanceof PiRuntimeError && error.code === "runtime_timeout") {
|
|
403
|
+
const note = replySubmitted
|
|
404
|
+
? RUNTIME_TIMEOUT_OUTCOME_SUBMITTED_NOTE
|
|
405
|
+
: RUNTIME_TIMEOUT_OUTCOME_MISSING_NOTE;
|
|
406
|
+
failError = new PiRuntimeError("runtime_timeout", `${error.message}. ${note}`);
|
|
407
|
+
}
|
|
408
|
+
const code = failError instanceof PiRuntimeError
|
|
409
|
+
? failError.code
|
|
410
|
+
: isMissingReplyOutcomeError(failError)
|
|
372
411
|
? "outcome_missing"
|
|
373
412
|
: "runtime_exit_failed";
|
|
374
|
-
await this.fail(triggerKey, runId, code,
|
|
413
|
+
await this.fail(triggerKey, runId, code, failError, submitted, submittedObservability, settle);
|
|
375
414
|
}
|
|
376
415
|
}
|
|
377
416
|
async process(triggerKey, normalize) {
|
|
@@ -43,6 +43,23 @@ export declare class PiRuntimeError extends Error {
|
|
|
43
43
|
export declare function collectThinking(content: unknown[]): string;
|
|
44
44
|
/** Exported for unit tests that assert argv without spawning a real Pi. */
|
|
45
45
|
export declare function buildArguments(request: PiRunRequest): string[];
|
|
46
|
+
/**
|
|
47
|
+
* Drop known-benign Pi stderr lines (e.g. "No project session found… creating
|
|
48
|
+
* a new session") so they are not pasted into failure summaries as if they were
|
|
49
|
+
* the root cause.
|
|
50
|
+
*/
|
|
51
|
+
export declare function sanitizePiStderr(stderr: string): string;
|
|
52
|
+
/** Human-readable timeout budget for operator-facing messages. */
|
|
53
|
+
export declare function formatTimeoutDuration(timeoutMs: number): string;
|
|
54
|
+
/** Stable English note when Outcome was submitted before hard timeout. */
|
|
55
|
+
export declare const RUNTIME_TIMEOUT_OUTCOME_SUBMITTED_NOTE = "Final Outcome was already submitted before timeout; Beacon still marked the Run failed because the Pi process did not exit in time";
|
|
56
|
+
/** Stable English note when Outcome was missing at hard timeout. */
|
|
57
|
+
export declare const RUNTIME_TIMEOUT_OUTCOME_MISSING_NOTE = "No Final Outcome was submitted before timeout";
|
|
58
|
+
/**
|
|
59
|
+
* Beacon-owned hard timeout summary. Makes clear this is Beacon's Run limit,
|
|
60
|
+
* not a Pi "session not found" failure.
|
|
61
|
+
*/
|
|
62
|
+
export declare function formatRuntimeTimeoutMessage(timeoutMs: number, stderr: string): string;
|
|
46
63
|
/**
|
|
47
64
|
* Read NDJSON frames from Pi stdout with a hard per-frame byte cap.
|
|
48
65
|
*
|
|
@@ -142,8 +142,57 @@ function buildPiEnvironment(outcome, configured = {}, runtimeEnvironment = {}) {
|
|
|
142
142
|
}
|
|
143
143
|
return environment;
|
|
144
144
|
}
|
|
145
|
+
/** Pi stderr that is normal session bootstrap, not a failure cause. */
|
|
146
|
+
const BENIGN_PI_STDERR_PATTERNS = [
|
|
147
|
+
/^Warning:\s*No project session found with id '[^']+'; creating a new session with that id\.?\s*$/i,
|
|
148
|
+
];
|
|
149
|
+
/**
|
|
150
|
+
* Drop known-benign Pi stderr lines (e.g. "No project session found… creating
|
|
151
|
+
* a new session") so they are not pasted into failure summaries as if they were
|
|
152
|
+
* the root cause.
|
|
153
|
+
*/
|
|
154
|
+
export function sanitizePiStderr(stderr) {
|
|
155
|
+
return stderr
|
|
156
|
+
.split(/\r?\n/)
|
|
157
|
+
.map((line) => line.trimEnd())
|
|
158
|
+
.filter((line) => {
|
|
159
|
+
const trimmed = line.trim();
|
|
160
|
+
if (!trimmed)
|
|
161
|
+
return false;
|
|
162
|
+
return !BENIGN_PI_STDERR_PATTERNS.some((pattern) => pattern.test(trimmed));
|
|
163
|
+
})
|
|
164
|
+
.join("\n")
|
|
165
|
+
.trim();
|
|
166
|
+
}
|
|
167
|
+
/** Human-readable timeout budget for operator-facing messages. */
|
|
168
|
+
export function formatTimeoutDuration(timeoutMs) {
|
|
169
|
+
if (Number.isSafeInteger(timeoutMs) &&
|
|
170
|
+
timeoutMs > 0 &&
|
|
171
|
+
timeoutMs % 60_000 === 0) {
|
|
172
|
+
return `${timeoutMs / 60_000}min (${timeoutMs}ms)`;
|
|
173
|
+
}
|
|
174
|
+
if (Number.isSafeInteger(timeoutMs) &&
|
|
175
|
+
timeoutMs > 0 &&
|
|
176
|
+
timeoutMs % 1_000 === 0) {
|
|
177
|
+
return `${timeoutMs / 1_000}s (${timeoutMs}ms)`;
|
|
178
|
+
}
|
|
179
|
+
return `${timeoutMs}ms`;
|
|
180
|
+
}
|
|
181
|
+
/** Stable English note when Outcome was submitted before hard timeout. */
|
|
182
|
+
export const RUNTIME_TIMEOUT_OUTCOME_SUBMITTED_NOTE = "Final Outcome was already submitted before timeout; Beacon still marked the Run failed because the Pi process did not exit in time";
|
|
183
|
+
/** Stable English note when Outcome was missing at hard timeout. */
|
|
184
|
+
export const RUNTIME_TIMEOUT_OUTCOME_MISSING_NOTE = "No Final Outcome was submitted before timeout";
|
|
185
|
+
/**
|
|
186
|
+
* Beacon-owned hard timeout summary. Makes clear this is Beacon's Run limit,
|
|
187
|
+
* not a Pi "session not found" failure.
|
|
188
|
+
*/
|
|
189
|
+
export function formatRuntimeTimeoutMessage(timeoutMs, stderr) {
|
|
190
|
+
const duration = formatTimeoutDuration(timeoutMs);
|
|
191
|
+
const detail = sanitizePiStderr(stderr);
|
|
192
|
+
return `Beacon Run hard timeout after ${duration}: Pi process did not exit in time${detail ? `. Remaining stderr: ${detail}` : ""}`;
|
|
193
|
+
}
|
|
145
194
|
function describeExit(code, signal, stderr) {
|
|
146
|
-
const detail = stderr
|
|
195
|
+
const detail = sanitizePiStderr(stderr);
|
|
147
196
|
return new PiRuntimeError("runtime_exit_failed", `Pi process exited before the Run settled (code=${String(code)} signal=${String(signal)})${detail ? `: ${detail}` : ""}`);
|
|
148
197
|
}
|
|
149
198
|
async function stopProcess(child, terminateGraceMs) {
|
|
@@ -298,7 +347,7 @@ export async function runPiAgent(request, options = {}) {
|
|
|
298
347
|
clearTimeout(timer);
|
|
299
348
|
reject(error);
|
|
300
349
|
};
|
|
301
|
-
const timer = setTimeout(() => fail(new PiRuntimeError("runtime_timeout",
|
|
350
|
+
const timer = setTimeout(() => fail(new PiRuntimeError("runtime_timeout", formatRuntimeTimeoutMessage(timeoutMs, stderr))), timeoutMs);
|
|
302
351
|
const handleFrame = (line) => {
|
|
303
352
|
if (finished)
|
|
304
353
|
return;
|