@hheei/pi-subagents 0.7.0 → 0.8.0

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.
Files changed (59) hide show
  1. package/LICENSES/Apache-2.0.txt +202 -0
  2. package/NOTICE +10 -0
  3. package/README.md +13 -13
  4. package/dist/child-control.d.ts +0 -8
  5. package/dist/child-control.js +40 -8
  6. package/dist/commands.js +1 -1
  7. package/dist/controller-ipc.d.ts +38 -0
  8. package/dist/controller-ipc.js +292 -0
  9. package/dist/controller-main.d.ts +1 -0
  10. package/dist/controller-main.js +26 -0
  11. package/dist/controller-role.d.ts +40 -0
  12. package/dist/controller-role.js +37 -0
  13. package/dist/controller-socket.d.ts +6 -0
  14. package/dist/controller-socket.js +43 -0
  15. package/dist/controller.d.ts +25 -0
  16. package/dist/controller.js +217 -0
  17. package/dist/delegation.d.ts +73 -0
  18. package/dist/delegation.js +71 -0
  19. package/dist/domain.d.ts +20 -18
  20. package/dist/domain.js +6 -1
  21. package/dist/extension.js +43 -26
  22. package/dist/host-adapter.js +1 -1
  23. package/dist/launch-spec.d.ts +1 -3
  24. package/dist/launch-spec.js +38 -15
  25. package/dist/lease.d.ts +0 -1
  26. package/dist/lease.js +8 -11
  27. package/dist/mailbox.js +110 -34
  28. package/dist/manager.d.ts +52 -6
  29. package/dist/manager.js +568 -32
  30. package/dist/mcp/app.html +948 -0
  31. package/dist/mcp/delegate.d.ts +30 -0
  32. package/dist/mcp/delegate.js +44 -0
  33. package/dist/mcp/views.d.ts +5 -0
  34. package/dist/mcp/views.js +12 -0
  35. package/dist/mcp-server.d.ts +6 -0
  36. package/dist/mcp-server.js +287 -0
  37. package/dist/mcp-stdio.d.ts +2 -0
  38. package/dist/mcp-stdio.js +111 -0
  39. package/dist/process-lock.d.ts +1 -1
  40. package/dist/process-lock.js +4 -2
  41. package/dist/protocol.d.ts +5 -5
  42. package/dist/protocol.js +3 -4
  43. package/dist/receipts.d.ts +80 -0
  44. package/dist/receipts.js +326 -0
  45. package/dist/runtime.d.ts +5 -26
  46. package/dist/runtime.js +15 -57
  47. package/dist/session-bootstrap.js +2 -6
  48. package/dist/state-db.d.ts +11 -4
  49. package/dist/state-db.js +102 -24
  50. package/dist/state.js +12 -3
  51. package/dist/tool-activation.d.ts +2 -2
  52. package/dist/tool-activation.js +5 -5
  53. package/dist/tool-presentation.d.ts +2 -6
  54. package/dist/tool-presentation.js +75 -122
  55. package/dist/tools.d.ts +0 -3
  56. package/dist/tools.js +104 -132
  57. package/package.json +12 -5
  58. package/dist/json-lines.d.ts +0 -14
  59. package/dist/json-lines.js +0 -95
package/dist/tools.js CHANGED
@@ -2,48 +2,64 @@ import { textToolResult } from "@hheei/pi-ext-core";
2
2
  import { Type } from "typebox";
3
3
  import { Value } from "typebox/value";
4
4
  import { isOperationError } from "./domain.js";
5
- import { contactParentRenderers, parentToolRenderers, } from "./tool-presentation.js";
6
- const spawnSchema = Type.Object({
7
- task: Type.String({ minLength: 1 }),
8
- agent: Type.String({
5
+ import { contactParentRenderers, parentToolRenderers } from "./tool-presentation.js";
6
+ /**
7
+ * A failed operation still tells the caller what it may have already done: the identity it was
8
+ * about, whether retrying is safe, and which side effects to check.
9
+ */
10
+ function operationErrorText(error) {
11
+ const details = [
12
+ ...(error.childId === undefined ? [] : [`id ${error.childId}`]),
13
+ `safeToRetry ${error.safeToRetry}`,
14
+ ...(error.sideEffects.length === 0 ? [] : [`sideEffects ${error.sideEffects.join(", ")}`]),
15
+ ];
16
+ return `${error.reason} (${details.join("; ")})`;
17
+ }
18
+ const delegateSchema = Type.Object({
19
+ task: Type.String({
9
20
  minLength: 1,
10
- description: "Agent name from <available_agents> in this tool description.",
21
+ description: "Work to perform in the selected agent session.",
11
22
  }),
12
- cwd: Type.Optional(Type.String()),
13
- title: Type.Optional(Type.String({
14
- maxLength: 60,
15
- description: "Short session title for this child, e.g. 'OVITO properties editor'. It becomes the child's Pi session title, prefixed with a robot marker so delegated sessions stay recognizable; omit it — or leave it blank — to derive one from the agent name and child id.",
23
+ wait: Type.Optional(Type.Boolean({
24
+ description: "Wait for the final result (default true). False returns the accepted delegation immediately, and its result is delivered automatically when it finishes.",
16
25
  })),
17
- }, { additionalProperties: false });
18
- const sendSchema = Type.Object({
19
- id: Type.String({ minLength: 1, description: "Subagent id, e.g. agent-1" }),
20
- message: Type.String({ minLength: 1 }),
21
- mode: Type.Optional(Type.Union([Type.Literal("steer"), Type.Literal("follow_up")], {
22
- description: "steer (the default) enters the child's current turn once its tool calls are in, before the next model request, so it redirects work already in flight; an idle child just starts a run. follow_up waits until the child has no tool calls or steering left, then continues as a new turn. Use follow_up for work that must not interrupt what the child is doing now.",
26
+ id: Type.Optional(Type.String({
27
+ minLength: 1,
28
+ description: "Existing agent id, e.g. agent-1. Takes precedence over agent and reuses its session.",
23
29
  })),
24
- });
25
- const idSchema = Type.Object({
26
- id: Type.String({ minLength: 1, description: "Subagent id, e.g. agent-1" }),
27
- });
30
+ agent: Type.Optional(Type.String({
31
+ minLength: 1,
32
+ description: "Agent name from <available_agents> for a new agent. Ignored with id.",
33
+ })),
34
+ cwd: Type.Optional(Type.String({ description: "Working directory for a new agent; ignored with id." })),
35
+ }, { additionalProperties: false });
28
36
  const getSchema = Type.Object({
29
- id: Type.String({ minLength: 1, description: "Subagent id, e.g. agent-1" }),
30
- keys: Type.Optional(Type.String({
31
- description: "Comma-separated fields to return. Defaults to 'status,progress'. Allowed: status, progress, diagnostic.",
37
+ id: Type.String({ minLength: 1, description: "Agent id, e.g. agent-1" }),
38
+ revision: Type.Optional(Type.Integer({
39
+ minimum: 1,
40
+ description: "Delegation revision of this agent. Defaults to the newest one.",
32
41
  })),
42
+ includeOutput: Type.Optional(Type.Boolean({
43
+ description: "Return the delegation's result body, bounded by the shared output budget. Defaults to false.",
44
+ })),
45
+ }, { additionalProperties: false });
46
+ const idSchema = Type.Object({
47
+ id: Type.String({ minLength: 1, description: "Agent id, e.g. agent-1" }),
33
48
  });
49
+ const dashboardSchema = Type.Object({}, { additionalProperties: false });
34
50
  const contactSchema = Type.Object({
35
51
  message: Type.String({
36
52
  minLength: 1,
37
53
  description: "Explanation of what is blocking progress or the error encountered.",
38
54
  }),
39
55
  reason: Type.Optional(Type.Union([Type.Literal("blocked"), Type.Literal("error")], {
40
- description: "Reason: 'blocked' if waiting for input/guidance (can be resumed via agent_send), or 'error' if the turn failed (you may retry via agent_send or create a new child). Defaults to 'blocked'.",
56
+ description: "Reason: 'blocked' if waiting for input/guidance (can be resumed via agents_delegate with this id), or 'error' if the turn failed (you may retry or create a new agent). Defaults to 'blocked'.",
41
57
  })),
42
58
  });
43
59
  const SUBAGENTS_NAMESPACE = {
44
- name: "agent",
60
+ name: "agents",
45
61
  aliases: ["agent", "agents", "subagent", "subagents"],
46
- description: "Subagent orchestration and delegation tools",
62
+ description: "Agent orchestration and delegation tools",
47
63
  };
48
64
  export function formatAvailableAgents(agents) {
49
65
  const available = agents
@@ -59,124 +75,58 @@ export function formatAvailableAgents(agents) {
59
75
  "</available_agents>",
60
76
  ].join("\n");
61
77
  }
62
- function getSpawnDescription(agents) {
78
+ function getDelegateDescription(agents) {
63
79
  return [
64
- "Spawn an independent background subagent task to work on a goal asynchronously. Choose an agent from <available_agents> below.",
65
- "Returns JSON { id, path }, where path is the absolute child session file path; the first write may still be pending.",
66
- "Results are returned automatically to this parent session when the task finishes. After spawning, do independent work or end your turn; do not poll agent_get.",
67
- "Related subagent tools: agent_send, agent_get, agent_stop.",
80
+ "Delegate work to a new or existing agent. Supply agent to start a new session, or id to continue an existing one; with both, id wins and agent is ignored.",
81
+ "Returns { id, revision, status, hasOutput }: revision identifies this delegation of that agent, and a finished delegation carries its result when wait is true. Results are delivered automatically when the task finishes, so do independent work or end your turn instead of polling agents_get.",
82
+ "Related agent tools: agents_get, agents_stop, agents_dashboard.",
68
83
  formatAvailableAgents(agents),
69
84
  ].join("\n");
70
85
  }
71
- const SEND_DESCRIPTION = "Send input to an existing subagent (e.g. 'agent-1'), resuming its session when it is idle, blocked, done, or errored. A child whose runtime has ended is resumed the same way: send to that id, do not spawn a replacement. Stop it first only when send reports the previous process is still running. Results are returned automatically when the task finishes; do not poll agent_get. Related subagent tools: agent_spawn, agent_get, agent_stop.";
72
- const GET_DESCRIPTION = "Inspect one subagent's status, progress, or diagnostic details by id (e.g. 'agent-1'). Use keys to choose fields; defaults to 'status,progress'. Results are returned automatically; use this for inspection, not completion polling. Related subagent tools: agent_spawn, agent_send, agent_stop.";
73
- const STOP_DESCRIPTION = "Stop a subagent's runtime by id (e.g. 'agent-1'). The child keeps its session, so a later agent_send can resume it. Related subagent tools: agent_spawn, agent_send, agent_get.";
86
+ const GET_DESCRIPTION = "Read one agent delegation: the newest by default, or one revision, with its result body when includeOutput is true. Use it for inspection, not completion polling. Related agent tools: agents_delegate, agents_stop, agents_dashboard.";
87
+ const STOP_DESCRIPTION = "Stop an agent runtime by id (e.g. 'agent-1'). Open delegations end as failed; finished results and the session are kept, so a later agents_delegate with this id resumes it. Related agent tools: agents_delegate, agents_get, agents_dashboard.";
88
+ const DASHBOARD_DESCRIPTION = "List this session's agents with their runtime state and latest delegation. Related agent tools: agents_delegate, agents_get, agents_stop.";
74
89
  const CONTACT_DESCRIPTION = "Notify the parent session when you are BLOCKED and cannot continue without parent decision or intervention. Do NOT call this on success or task completion: simply output your final answer directly as normal text in your response, and the harness will automatically capture and report your result to the parent.";
75
90
  const CONTACT_SNIPPET = "Report a blocker to the parent. Do NOT call on success: write normal text to finish.";
76
91
  const CONTACT_GUIDELINES = [
77
92
  "Use contact_parent ONLY if you are blocked or urgently require a parent decision midway.",
78
93
  "Do NOT use contact_parent to deliver final findings or report success: output normal text instead.",
79
- "After reporting a blocker, wait for a parent agent_send instruction.",
94
+ "After reporting a blocker, wait for a parent agents_delegate instruction.",
80
95
  ];
81
- const GET_KEYS = ["status", "progress", "diagnostic"];
82
- /** Parses the comma-separated `keys` argument; empty means the default view. */
83
- function parseAgentGetKeys(raw) {
84
- if (raw === undefined)
85
- return ["status", "progress"];
86
- const keys = [];
87
- for (const part of raw.split(",")) {
88
- const key = part.trim();
89
- if (key === "")
90
- continue;
91
- if (!GET_KEYS.includes(key)) {
92
- throw new Error(`Unknown agent_get key "${key}". Allowed keys: ${GET_KEYS.join(", ")}.`);
93
- }
94
- if (!keys.includes(key))
95
- keys.push(key);
96
- }
97
- return keys.length > 0 ? keys : ["status", "progress"];
98
- }
99
- function childAgentDetails(child, keys) {
100
- const view = {};
101
- if (keys.includes("status"))
102
- view.status = child.state;
103
- if (keys.includes("progress")) {
104
- view.progress = {
105
- agent: child.agent,
106
- ...(child.path === undefined ? {} : { path: child.path }),
107
- presentation: child.presentation,
108
- freshness: child.freshness,
109
- ...(child.activeTool === undefined ? {} : { activeTool: child.activeTool.slice(0, 200) }),
110
- ...(child.state !== "running" || child.summary === undefined
111
- ? {}
112
- : { summary: child.summary.slice(0, 2000) }),
113
- ...(child.usage === undefined
114
- ? {}
115
- : { turns: child.usage.turns, tokens: child.usage.inputTokens + child.usage.outputTokens }),
116
- };
117
- }
118
- if (keys.includes("diagnostic")) {
119
- const diagnostic = child.interrupted ??
120
- (child.state === "blocked" || child.state === "error" ? child.summary : undefined) ??
121
- (child.freshness === "last_known" ? "Runtime disconnected; state is last known" : undefined);
122
- if (diagnostic !== undefined)
123
- view.diagnostic = diagnostic.slice(0, 2000);
124
- }
125
- return view;
126
- }
127
96
  export function registerParentTools(pi, manager, agents) {
128
97
  const renderers = parentToolRenderers(pi);
129
- const inspectedTasks = new Set();
98
+ const inspected = new Set();
99
+ /** The same inspection twice in a row is polling, and polling never gets the answer faster. */
130
100
  function inspectionResult(id, text, details) {
131
- const repeated = inspectedTasks.has(id);
132
- inspectedTasks.add(id);
133
- return textToolResult(repeated ? `${text}\nResults are returned automatically; do not poll agent_get.` : text, details);
101
+ const repeated = inspected.has(id);
102
+ inspected.add(id);
103
+ return textToolResult(repeated ? `${text}\nResults are delivered automatically; do not poll agents_get.` : text, details);
134
104
  }
135
- const spawn = {
136
- name: "agent_spawn",
105
+ const delegate = {
106
+ name: "agents_delegate",
137
107
  namespace: SUBAGENTS_NAMESPACE,
138
- label: "Spawn agent",
139
- description: getSpawnDescription(agents),
140
- parameters: spawnSchema,
108
+ label: "Delegate to agent",
109
+ description: getDelegateDescription(agents),
110
+ parameters: delegateSchema,
141
111
  exposure: "deferred",
142
112
  defaultActive: false,
143
113
  annotations: {
144
114
  openWorldHint: true,
145
115
  },
146
- async execute(_id, params) {
147
- if (!Value.Check(spawnSchema, params))
148
- throw new Error("Invalid agent_spawn parameters");
149
- const spawned = await manager.spawn(params);
150
- if (isOperationError(spawned))
151
- throw new Error(spawned.reason);
152
- const { id } = spawned.child;
153
- const { path } = spawned;
154
- const details = { id, path };
155
- return textToolResult(JSON.stringify(details), details);
116
+ async execute(_id, params, signal, onUpdate) {
117
+ if (!Value.Check(delegateSchema, params))
118
+ throw new Error("Invalid agents_delegate parameters");
119
+ const view = await manager.delegate(params, signal, (accepted) => {
120
+ onUpdate?.(textToolResult(JSON.stringify(accepted), accepted));
121
+ });
122
+ if (isOperationError(view))
123
+ throw new Error(operationErrorText(view));
124
+ return textToolResult(JSON.stringify(view), view);
156
125
  },
157
- ...renderers.spawn,
158
- };
159
- const send = {
160
- name: "agent_send",
161
- namespace: SUBAGENTS_NAMESPACE,
162
- label: "Send to agent",
163
- description: SEND_DESCRIPTION,
164
- parameters: sendSchema,
165
- exposure: "deferred",
166
- defaultActive: false,
167
- annotations: {
168
- openWorldHint: true,
169
- },
170
- async execute(_id, params, signal) {
171
- const sent = await manager.send(params.id, params.message, params.mode, signal);
172
- if (isOperationError(sent))
173
- throw new Error(sent.reason);
174
- return textToolResult("Message sent.", undefined);
175
- },
176
- ...renderers.send,
126
+ ...renderers.delegate,
177
127
  };
178
128
  const get = {
179
- name: "agent_get",
129
+ name: "agents_get",
180
130
  namespace: SUBAGENTS_NAMESPACE,
181
131
  label: "Get agent",
182
132
  description: GET_DESCRIPTION,
@@ -188,17 +138,21 @@ export function registerParentTools(pi, manager, agents) {
188
138
  idempotentHint: true,
189
139
  },
190
140
  async execute(_id, params) {
191
- const keys = parseAgentGetKeys(params.keys);
192
- const child = await manager.get(params.id);
193
- if (isOperationError(child))
194
- throw new Error(child.reason);
195
- const view = childAgentDetails(child, keys);
141
+ if (!Value.Check(getSchema, params))
142
+ throw new Error("Invalid agents_get parameters");
143
+ const view = await manager.inspect({
144
+ id: params.id,
145
+ ...(params.revision === undefined ? {} : { revision: params.revision }),
146
+ includeOutput: params.includeOutput === true,
147
+ });
148
+ if (isOperationError(view))
149
+ throw new Error(operationErrorText(view));
196
150
  return inspectionResult(params.id, JSON.stringify(view), view);
197
151
  },
198
152
  ...renderers.get,
199
153
  };
200
154
  const stop = {
201
- name: "agent_stop",
155
+ name: "agents_stop",
202
156
  namespace: SUBAGENTS_NAMESPACE,
203
157
  label: "Stop agent",
204
158
  description: STOP_DESCRIPTION,
@@ -209,17 +163,35 @@ export function registerParentTools(pi, manager, agents) {
209
163
  destructiveHint: true,
210
164
  },
211
165
  async execute(_id, params) {
212
- const stopped = await manager.stop(params.id);
213
- if (isOperationError(stopped))
214
- throw new Error(stopped.reason);
215
- return textToolResult("Agent stopped.", undefined);
166
+ const view = await manager.stopAgent(params.id);
167
+ if (isOperationError(view))
168
+ throw new Error(operationErrorText(view));
169
+ return textToolResult(`Agent ${view.id} stopped; revision ${view.revision} is ${view.status}.`, view);
216
170
  },
217
171
  ...renderers.stop,
218
172
  };
219
- pi.registerTool(spawn);
220
- pi.registerTool(send);
173
+ const dashboard = {
174
+ name: "agents_dashboard",
175
+ namespace: SUBAGENTS_NAMESPACE,
176
+ label: "Agent dashboard",
177
+ description: DASHBOARD_DESCRIPTION,
178
+ parameters: dashboardSchema,
179
+ exposure: "deferred",
180
+ defaultActive: false,
181
+ annotations: {
182
+ readOnlyHint: true,
183
+ idempotentHint: true,
184
+ },
185
+ async execute() {
186
+ const view = await manager.dashboard();
187
+ return textToolResult(`${view.total} agents · running ${view.running} · blocked ${view.blocked} · completed ${view.completed} · failed ${view.failed}`, view);
188
+ },
189
+ ...renderers.dashboard,
190
+ };
191
+ pi.registerTool(delegate);
221
192
  pi.registerTool(get);
222
193
  pi.registerTool(stop);
194
+ pi.registerTool(dashboard);
223
195
  }
224
196
  export function registerChildTools(pi, identity, options) {
225
197
  const tool = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hheei/pi-subagents",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Independent background Pi subagent sessions with durable identity and RPC control.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,8 +11,13 @@
11
11
  "files": [
12
12
  "dist",
13
13
  "README.md",
14
- "LICENSE"
14
+ "LICENSE",
15
+ "NOTICE",
16
+ "LICENSES"
15
17
  ],
18
+ "bin": {
19
+ "pi-subagents-mcp": "./dist/mcp-stdio.js"
20
+ },
16
21
  "pi": {
17
22
  "extensions": [
18
23
  "dist/extension.js"
@@ -35,7 +40,8 @@
35
40
  "access": "public"
36
41
  },
37
42
  "dependencies": {
38
- "@hheei/pi-ext-core": "^0.6.4"
43
+ "@hheei/pi-ext-core": "^0.7.0",
44
+ "@modelcontextprotocol/server": "2.4.0"
39
45
  },
40
46
  "peerDependencies": {
41
47
  "@earendil-works/pi-coding-agent": ">=1.3.0",
@@ -44,9 +50,10 @@
44
50
  },
45
51
  "devDependencies": {
46
52
  "@earendil-works/pi-coding-agent": "1.3.0",
47
- "@earendil-works/pi-tui": "1.3.0"
53
+ "@earendil-works/pi-tui": "1.3.0",
54
+ "linkedom": "^0.18.14"
48
55
  },
49
56
  "scripts": {
50
- "build": "node -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json"
57
+ "build": "node -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json && node -e \"import { chmodSync, copyFileSync, mkdirSync } from 'node:fs'; mkdirSync('dist/mcp', { recursive: true }); copyFileSync('src/mcp/app.html', 'dist/mcp/app.html'); chmodSync('dist/mcp-stdio.js', 0o755)\""
51
58
  }
52
59
  }
@@ -1,14 +0,0 @@
1
- import type { Readable, Writable } from "node:stream";
2
- export type JsonLineErrorCode = "frame_too_large" | "malformed_json" | "stream_error";
3
- export declare class JsonLineError extends Error {
4
- readonly code: JsonLineErrorCode;
5
- constructor(code: JsonLineErrorCode, message: string);
6
- }
7
- export interface JsonLineReaderOptions {
8
- readonly maxFrameBytes: number;
9
- readonly onValue: (value: unknown) => void;
10
- readonly onError: (error: JsonLineError) => void;
11
- }
12
- export declare function attachJsonLineReader(stream: Readable, options: JsonLineReaderOptions): () => void;
13
- export declare function serializeJsonLine(value: unknown, maxFrameBytes: number): Buffer;
14
- export declare function writeJsonLine(stream: Writable, value: unknown, maxFrameBytes: number): Promise<void>;
@@ -1,95 +0,0 @@
1
- import { errorMessage } from "@hheei/pi-ext-core";
2
- export class JsonLineError extends Error {
3
- code;
4
- constructor(code, message) {
5
- super(message);
6
- this.name = "JsonLineError";
7
- this.code = code;
8
- }
9
- }
10
- export function attachJsonLineReader(stream, options) {
11
- if (!Number.isSafeInteger(options.maxFrameBytes) || options.maxFrameBytes < 1) {
12
- throw new Error("maxFrameBytes must be a positive safe integer");
13
- }
14
- let buffer = Buffer.alloc(0);
15
- let stopped = false;
16
- const fail = (error) => {
17
- if (stopped)
18
- return;
19
- stopped = true;
20
- cleanup();
21
- options.onError(error);
22
- };
23
- const onData = (chunk) => {
24
- if (stopped)
25
- return;
26
- const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
27
- buffer = buffer.length === 0 ? incoming : Buffer.concat([buffer, incoming]);
28
- while (!stopped) {
29
- const newline = buffer.indexOf(0x0a);
30
- if (newline < 0) {
31
- if (buffer.length > options.maxFrameBytes) {
32
- fail(new JsonLineError("frame_too_large", "JSON line exceeds frame limit"));
33
- }
34
- return;
35
- }
36
- if (newline > options.maxFrameBytes) {
37
- fail(new JsonLineError("frame_too_large", "JSON line exceeds frame limit"));
38
- return;
39
- }
40
- let frame = buffer.subarray(0, newline);
41
- buffer = buffer.subarray(newline + 1);
42
- if (frame.length > 0 && frame[frame.length - 1] === 0x0d) {
43
- frame = frame.subarray(0, frame.length - 1);
44
- }
45
- if (frame.length === 0)
46
- continue;
47
- try {
48
- options.onValue(JSON.parse(frame.toString("utf8")));
49
- }
50
- catch (error) {
51
- fail(new JsonLineError("malformed_json", `Malformed JSON line: ${errorMessage(error)}`));
52
- }
53
- }
54
- };
55
- const onStreamError = (error) => {
56
- fail(new JsonLineError("stream_error", error.message));
57
- };
58
- const cleanup = () => {
59
- stream.off("data", onData);
60
- stream.off("error", onStreamError);
61
- buffer = Buffer.alloc(0);
62
- };
63
- stream.on("data", onData);
64
- stream.on("error", onStreamError);
65
- return () => {
66
- if (stopped)
67
- return;
68
- stopped = true;
69
- cleanup();
70
- };
71
- }
72
- export function serializeJsonLine(value, maxFrameBytes) {
73
- const encoded = Buffer.from(`${JSON.stringify(value)}\n`, "utf8");
74
- if (encoded.length - 1 > maxFrameBytes) {
75
- throw new JsonLineError("frame_too_large", "Serialized JSON line exceeds frame limit");
76
- }
77
- return encoded;
78
- }
79
- export function writeJsonLine(stream, value, maxFrameBytes) {
80
- let encoded;
81
- try {
82
- encoded = serializeJsonLine(value, maxFrameBytes);
83
- }
84
- catch (error) {
85
- return Promise.reject(error);
86
- }
87
- return new Promise((resolve, reject) => {
88
- stream.write(encoded, (error) => {
89
- if (error)
90
- reject(error);
91
- else
92
- resolve();
93
- });
94
- });
95
- }