@nettee/beacon 0.2.0 → 0.2.2

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,6 +3,7 @@ import { mkdtemp, readdir, readFile, realpath, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { basename, dirname, isAbsolute, join, sep } from "node:path";
5
5
  import { promisify } from "node:util";
6
+ import { fillExportedHtmlSystemPrompt, pickExportedSystemPrompt, } from "./session-prompt.js";
6
7
  const execFileAsync = promisify(execFile);
7
8
  function isInside(root, candidate) {
8
9
  return candidate === root || candidate.startsWith(`${root}${sep}`);
@@ -42,7 +43,11 @@ export async function exportSessionHtml(request) {
42
43
  timeout: request.timeoutMs ?? 60_000,
43
44
  windowsHide: true,
44
45
  });
45
- return await readFile(outputPath, "utf8");
46
+ const html = await readFile(outputPath, "utf8");
47
+ const systemPrompt = pickExportedSystemPrompt(await readFile(jsonlPath, "utf8"), request.systemPrompt);
48
+ return systemPrompt
49
+ ? fillExportedHtmlSystemPrompt(html, systemPrompt)
50
+ : html;
46
51
  }
47
52
  finally {
48
53
  await rm(directory, { recursive: true, force: true });
@@ -47,6 +47,7 @@ function summarize(value, hasSessionFile) {
47
47
  deliveryState: asString(delivery?.state),
48
48
  sessionPath: asString(run?.sessionPath),
49
49
  hasSessionFile,
50
+ systemPrompt: asString(run?.systemPrompt),
50
51
  };
51
52
  }
52
53
  async function loadSummary(path) {
@@ -1,6 +1,8 @@
1
1
  import { createServer, } from "node:http";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { loadProfile } from "../config/profile.js";
5
+ import { buildAgentSystemPrompt } from "../runtime/system-prompt.js";
4
6
  import { exportSessionHtml } from "./export.js";
5
7
  import { findRunSummary, listRunSummaries, runIdPattern, toRunListItem, } from "./records.js";
6
8
  import { sendStaticFile } from "./static.js";
@@ -18,6 +20,17 @@ function sendText(response, status, body) {
18
20
  export function defaultUiDirectory() {
19
21
  return join(dirname(fileURLToPath(import.meta.url)), "ui");
20
22
  }
23
+ async function beaconOwnedSystemPrompt(summary, profilesDirectory) {
24
+ if (summary.systemPrompt)
25
+ return summary.systemPrompt;
26
+ try {
27
+ return buildAgentSystemPrompt(await loadProfile(summary.profileId, profilesDirectory));
28
+ }
29
+ catch {
30
+ // Old runs without a stored prompt: reconstruction needs a readable Profile.
31
+ return undefined;
32
+ }
33
+ }
21
34
  export async function startDashboard(options) {
22
35
  const cache = new Map();
23
36
  const inflight = new Map();
@@ -63,6 +76,7 @@ export async function startDashboard(options) {
63
76
  sessionDirectory: options.sessionDirectory,
64
77
  runId: summary.runId,
65
78
  sessionPath: summary.sessionPath,
79
+ systemPrompt: await beaconOwnedSystemPrompt(summary, options.profilesDirectory),
66
80
  timeoutMs: options.exportTimeoutMs,
67
81
  }).finally(() => {
68
82
  inflight.delete(runId);
@@ -0,0 +1,100 @@
1
+ function asRecord(value) {
2
+ return typeof value === "object" && value !== null
3
+ ? value
4
+ : undefined;
5
+ }
6
+ function contentText(content) {
7
+ if (typeof content === "string")
8
+ return content;
9
+ if (!Array.isArray(content))
10
+ return "";
11
+ return content
12
+ .map((block) => {
13
+ const record = asRecord(block);
14
+ return record?.type === "text" && typeof record.text === "string"
15
+ ? record.text
16
+ : "";
17
+ })
18
+ .filter((part) => part.length > 0)
19
+ .join("\n");
20
+ }
21
+ function systemMessageText(content, sections) {
22
+ return [content, ...sections.values()]
23
+ .filter((part) => part.length > 0)
24
+ .join("\n\n");
25
+ }
26
+ function nonempty(value) {
27
+ return value && value.length > 0 ? value : undefined;
28
+ }
29
+ /** Prefer a JSONL system message; otherwise the Beacon-owned `--system-prompt` text. */
30
+ export function pickExportedSystemPrompt(jsonl, beaconOwned) {
31
+ return systemPromptFromJsonl(jsonl) ?? nonempty(beaconOwned);
32
+ }
33
+ export function systemPromptFromJsonl(jsonl) {
34
+ const contents = [];
35
+ const sections = new Map();
36
+ let seen = false;
37
+ for (const line of jsonl.split(/\r?\n/)) {
38
+ if (line.trim().length === 0)
39
+ continue;
40
+ let parsed;
41
+ try {
42
+ parsed = JSON.parse(line);
43
+ }
44
+ catch {
45
+ continue;
46
+ }
47
+ const entry = asRecord(parsed);
48
+ const message = asRecord(entry?.message);
49
+ if (entry?.type !== "message" || message?.role !== "system")
50
+ continue;
51
+ seen = true;
52
+ const text = contentText(message.content);
53
+ if (text.length > 0)
54
+ contents.push(text);
55
+ const named = asRecord(message.sections);
56
+ if (!named)
57
+ continue;
58
+ for (const [name, value] of Object.entries(named)) {
59
+ if (value === null)
60
+ sections.delete(name);
61
+ else if (typeof value === "string")
62
+ sections.set(name, value);
63
+ }
64
+ }
65
+ if (!seen)
66
+ return undefined;
67
+ const prompt = systemMessageText(contents.join("\n\n"), sections);
68
+ return prompt.length > 0 ? prompt : undefined;
69
+ }
70
+ const sessionDataScript = /<script\s+id="session-data"(?:\s+[^>]*)?>([A-Za-z0-9+/=\s]*)<\/script>/i;
71
+ export function fillExportedHtmlSystemPrompt(html, systemPrompt) {
72
+ if (systemPrompt.length === 0)
73
+ return html;
74
+ const match = sessionDataScript.exec(html);
75
+ if (!match || match.index === undefined)
76
+ return html;
77
+ const raw = match[1] ?? "";
78
+ const encoded = raw.replace(/\s+/g, "");
79
+ if (encoded.length === 0)
80
+ return html;
81
+ let data;
82
+ try {
83
+ const parsed = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
84
+ const record = asRecord(parsed);
85
+ if (!record)
86
+ return html;
87
+ data = record;
88
+ }
89
+ catch {
90
+ return html;
91
+ }
92
+ const existing = data.systemPrompt;
93
+ if (typeof existing === "string" && existing.length > 0)
94
+ return html;
95
+ data.systemPrompt = systemPrompt;
96
+ const next = Buffer.from(JSON.stringify(data), "utf8").toString("base64");
97
+ return (html.slice(0, match.index) +
98
+ match[0].replace(raw, next) +
99
+ html.slice(match.index + match[0].length));
100
+ }
@@ -80,6 +80,11 @@ export class RunOrchestrator {
80
80
  throw new Error(`Unknown Trigger ${triggerKey}`);
81
81
  return found;
82
82
  }
83
+ agentSystemPrompt(existing) {
84
+ return existing && existing.length > 0
85
+ ? existing
86
+ : buildAgentSystemPrompt(this.options.profile);
87
+ }
83
88
  newRun(runId, state, failure) {
84
89
  const timestamp = this.timestamp();
85
90
  return {
@@ -95,6 +100,7 @@ export class RunOrchestrator {
95
100
  promptDigest: createHash("sha256")
96
101
  .update(this.options.profile.prompt)
97
102
  .digest("hex"),
103
+ systemPrompt: this.agentSystemPrompt(),
98
104
  ...(failure ? { failure } : {}),
99
105
  };
100
106
  }
@@ -193,6 +199,7 @@ export class RunOrchestrator {
193
199
  sessionId: current.run.runId,
194
200
  sessionPath: join(this.options.sessionDirectory, this.options.profile.id, current.run.runId),
195
201
  };
202
+ const systemPrompt = this.agentSystemPrompt(current.run.systemPrompt);
196
203
  return {
197
204
  ...current,
198
205
  run: {
@@ -200,6 +207,7 @@ export class RunOrchestrator {
200
207
  ...session,
201
208
  state: "starting",
202
209
  startedAt: this.timestamp(),
210
+ systemPrompt,
203
211
  },
204
212
  };
205
213
  });
@@ -214,7 +222,7 @@ export class RunOrchestrator {
214
222
  workspace: this.options.profile.workspace,
215
223
  provider: this.options.profile.model.provider,
216
224
  model: this.options.profile.model.id,
217
- systemPrompt: buildAgentSystemPrompt(this.options.profile),
225
+ systemPrompt: record.run.systemPrompt,
218
226
  outcome: { ...submission.binding, cliPath: this.options.beaconCliPath },
219
227
  session: {
220
228
  id: record.run.sessionId,
@@ -62,6 +62,7 @@ const runSchema = z
62
62
  model: z.string().min(1),
63
63
  workspace: z.string().min(1),
64
64
  promptDigest: z.string().regex(/^[a-f0-9]{64}$/),
65
+ systemPrompt: z.string().min(1).optional(),
65
66
  failure: failureSchema.optional(),
66
67
  })
67
68
  .strict();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nettee/beacon",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "A single-host bridge from messaging and schedules to isolated agent runs",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {