@nettee/beacon 0.4.8 → 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.
@@ -3,8 +3,6 @@ import { type ChannelRegistry } from "./channels.js";
3
3
  export declare const profilePersonaFile = "persona.md";
4
4
  export declare const profileTaskFile = "task.md";
5
5
  export declare const playbookProfileDirectoryName = ".beacon-profile";
6
- /** @deprecated Use playbookProfileDirectoryName. */
7
- export declare const workspaceProfileDirectoryName = ".beacon-profile";
8
6
  export type DeliveryDestination = {
9
7
  chatId: string;
10
8
  name?: string | undefined;
@@ -43,8 +41,6 @@ export type LoadProfileOptions = {
43
41
  channelsPath?: string;
44
42
  };
45
43
  export declare function playbookProfileDirectory(playbook: string): string;
46
- /** @deprecated Use playbookProfileDirectory. */
47
- export declare function workspaceProfileDirectory(playbook: string): string;
48
44
  /** Expand `~` / `$HOME` and resolve relative paths against the Profile playbook. */
49
45
  export declare function resolveSkillPath(rawPath: string, playbook: string): string;
50
46
  export declare function loadProfile(profileId: string, profilesDirectory?: string, options?: LoadProfileOptions): Promise<Profile>;
@@ -10,8 +10,6 @@ const profileIdPattern = /^[a-z0-9](?:[a-z0-9_-]{0,62})$/;
10
10
  export const profilePersonaFile = "persona.md";
11
11
  export const profileTaskFile = "task.md";
12
12
  export const playbookProfileDirectoryName = ".beacon-profile";
13
- /** @deprecated Use playbookProfileDirectoryName. */
14
- export const workspaceProfileDirectoryName = playbookProfileDirectoryName;
15
13
  /** Admin DM: raw Feishu chat_id only (bot×person DMs are not channel registry entries). */
16
14
  const adminDestinationSchema = z
17
15
  .object({ chat_id: z.string().trim().min(1) })
@@ -63,12 +61,9 @@ const skillsSchema = z
63
61
  paths: z.array(z.string().trim().min(1)).default([]),
64
62
  })
65
63
  .strict();
66
- const profileDocumentFieldsSchema = z
64
+ const profileDocumentSchema = z
67
65
  .object({
68
- /** Preferred config key for the Profile playbook directory. */
69
- playbook: z.string().min(1).optional(),
70
- /** Legacy alias for `playbook`. Dual-read during rename; prefer `playbook`. */
71
- workspace: z.string().min(1).optional(),
66
+ playbook: z.string().min(1),
72
67
  runtime: z.literal("pi"),
73
68
  model: z
74
69
  .object({
@@ -90,34 +85,6 @@ const profileDocumentFieldsSchema = z
90
85
  skills: skillsSchema.optional(),
91
86
  })
92
87
  .strict();
93
- const profileDocumentSchema = profileDocumentFieldsSchema
94
- .superRefine((document, context) => {
95
- if (!document.playbook && !document.workspace) {
96
- context.addIssue({
97
- code: "custom",
98
- message: "Profile must declare playbook (legacy key: workspace)",
99
- path: ["playbook"],
100
- });
101
- return;
102
- }
103
- if (document.playbook &&
104
- document.workspace &&
105
- document.playbook !== document.workspace) {
106
- context.addIssue({
107
- code: "custom",
108
- message: "Profile playbook and legacy workspace must match when both are set",
109
- path: ["playbook"],
110
- });
111
- }
112
- })
113
- .transform((document) => {
114
- const playbook = document.playbook ?? document.workspace;
115
- if (!playbook) {
116
- throw new Error("Profile must declare playbook (legacy key: workspace)");
117
- }
118
- const { workspace: _legacy, playbook: _preferred, ...rest } = document;
119
- return { ...rest, playbook };
120
- });
121
88
  function assertProfileId(profileId) {
122
89
  if (!profileIdPattern.test(profileId)) {
123
90
  throw new Error(`Invalid Profile ID: ${JSON.stringify(profileId)}`);
@@ -130,10 +97,6 @@ function isWithin(parent, child) {
130
97
  export function playbookProfileDirectory(playbook) {
131
98
  return join(playbook, playbookProfileDirectoryName);
132
99
  }
133
- /** @deprecated Use playbookProfileDirectory. */
134
- export function workspaceProfileDirectory(playbook) {
135
- return playbookProfileDirectory(playbook);
136
- }
137
100
  async function isExistingFile(path) {
138
101
  try {
139
102
  return (await stat(path)).isFile();
@@ -39,8 +39,7 @@ const skillsSchema = z
39
39
  .strict();
40
40
  const profileDocumentSchema = z
41
41
  .object({
42
- playbook: z.string().min(1).optional(),
43
- workspace: z.string().min(1).optional(),
42
+ playbook: z.string().min(1),
44
43
  runtime: z.literal("pi"),
45
44
  model: z
46
45
  .object({
@@ -60,34 +59,7 @@ const profileDocumentSchema = z
60
59
  .optional(),
61
60
  skills: skillsSchema.optional(),
62
61
  })
63
- .strict()
64
- .superRefine((document, context) => {
65
- if (!document.playbook && !document.workspace) {
66
- context.addIssue({
67
- code: "custom",
68
- message: "Profile must declare playbook (legacy key: workspace)",
69
- path: ["playbook"],
70
- });
71
- return;
72
- }
73
- if (document.playbook &&
74
- document.workspace &&
75
- document.playbook !== document.workspace) {
76
- context.addIssue({
77
- code: "custom",
78
- message: "Profile playbook and legacy workspace must match when both are set",
79
- path: ["playbook"],
80
- });
81
- }
82
- })
83
- .transform((document) => {
84
- const playbook = document.playbook ?? document.workspace;
85
- if (!playbook) {
86
- throw new Error("Profile must declare playbook (legacy key: workspace)");
87
- }
88
- const { workspace: _legacy, playbook: _preferred, ...rest } = document;
89
- return { ...rest, playbook };
90
- });
62
+ .strict();
91
63
  function toNotifyView(notify) {
92
64
  if (!notify)
93
65
  return null;
@@ -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
- const code = error instanceof PiRuntimeError
370
- ? error.code
371
- : isMissingReplyOutcomeError(error)
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, error, submitted, submittedObservability, settle);
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.trim();
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", `Pi Run timed out after ${timeoutMs}ms${stderr.trim() ? `: ${stderr.trim()}` : ""}`)), timeoutMs);
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;
@@ -44,24 +44,7 @@ const inputSchema = z.discriminatedUnion("kind", [
44
44
  .strict(),
45
45
  z.object({ kind: z.literal("manual"), text: z.string().min(1) }).strict(),
46
46
  ]);
47
- const runSchema = z.preprocess((value) => {
48
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
49
- return value;
50
- }
51
- const record = value;
52
- if (typeof record.playbook === "string") {
53
- if ("workspace" in record) {
54
- const { workspace: _legacy, ...rest } = record;
55
- return rest;
56
- }
57
- return record;
58
- }
59
- if (typeof record.workspace === "string") {
60
- const { workspace, ...rest } = record;
61
- return { ...rest, playbook: workspace };
62
- }
63
- return record;
64
- }, z
47
+ const runSchema = z
65
48
  .object({
66
49
  runId: z.string().min(1),
67
50
  sessionId: z
@@ -84,7 +67,7 @@ const runSchema = z.preprocess((value) => {
84
67
  systemPrompt: z.string().min(1).optional(),
85
68
  failure: failureSchema.optional(),
86
69
  })
87
- .strict());
70
+ .strict();
88
71
  const outcomeSchema = z.union([
89
72
  z
90
73
  .object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nettee/beacon",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "A single-host bridge from messaging and schedules to isolated agent runs",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {