@hheei/pi-subagents 0.7.0 → 0.8.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/LICENSES/Apache-2.0.txt +202 -0
- package/NOTICE +10 -0
- package/README.md +13 -13
- package/dist/child-control.d.ts +0 -8
- package/dist/child-control.js +40 -8
- package/dist/commands.js +1 -1
- package/dist/controller-ipc.d.ts +38 -0
- package/dist/controller-ipc.js +292 -0
- package/dist/controller-main.d.ts +1 -0
- package/dist/controller-main.js +26 -0
- package/dist/controller-role.d.ts +40 -0
- package/dist/controller-role.js +37 -0
- package/dist/controller-socket.d.ts +6 -0
- package/dist/controller-socket.js +43 -0
- package/dist/controller.d.ts +25 -0
- package/dist/controller.js +217 -0
- package/dist/delegation.d.ts +73 -0
- package/dist/delegation.js +71 -0
- package/dist/domain.d.ts +20 -18
- package/dist/domain.js +6 -1
- package/dist/extension.js +43 -26
- package/dist/host-adapter.js +1 -1
- package/dist/launch-spec.d.ts +1 -3
- package/dist/launch-spec.js +38 -15
- package/dist/lease.d.ts +0 -1
- package/dist/lease.js +8 -11
- package/dist/mailbox.js +110 -34
- package/dist/manager.d.ts +52 -6
- package/dist/manager.js +568 -32
- package/dist/mcp/app.html +948 -0
- package/dist/mcp/delegate.d.ts +30 -0
- package/dist/mcp/delegate.js +44 -0
- package/dist/mcp/views.d.ts +5 -0
- package/dist/mcp/views.js +12 -0
- package/dist/mcp-server.d.ts +6 -0
- package/dist/mcp-server.js +287 -0
- package/dist/mcp-stdio.d.ts +2 -0
- package/dist/mcp-stdio.js +111 -0
- package/dist/process-lock.d.ts +1 -1
- package/dist/process-lock.js +4 -2
- package/dist/protocol.d.ts +5 -5
- package/dist/protocol.js +3 -4
- package/dist/receipts.d.ts +80 -0
- package/dist/receipts.js +326 -0
- package/dist/runtime.d.ts +5 -26
- package/dist/runtime.js +15 -57
- package/dist/session-bootstrap.js +2 -6
- package/dist/state-db.d.ts +11 -4
- package/dist/state-db.js +102 -24
- package/dist/state.js +12 -3
- package/dist/tool-activation.d.ts +2 -2
- package/dist/tool-activation.js +5 -5
- package/dist/tool-presentation.d.ts +2 -6
- package/dist/tool-presentation.js +75 -122
- package/dist/tools.d.ts +0 -3
- package/dist/tools.js +105 -132
- package/package.json +12 -5
- package/dist/json-lines.d.ts +0 -14
- 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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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: "
|
|
21
|
+
description: "Work to perform in the selected agent session.",
|
|
11
22
|
}),
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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: "
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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: "
|
|
60
|
+
name: "agents",
|
|
45
61
|
aliases: ["agent", "agents", "subagent", "subagents"],
|
|
46
|
-
description: "
|
|
62
|
+
description: "Agent orchestration and delegation tools",
|
|
47
63
|
};
|
|
48
64
|
export function formatAvailableAgents(agents) {
|
|
49
65
|
const available = agents
|
|
@@ -59,124 +75,59 @@ export function formatAvailableAgents(agents) {
|
|
|
59
75
|
"</available_agents>",
|
|
60
76
|
].join("\n");
|
|
61
77
|
}
|
|
62
|
-
function
|
|
78
|
+
function getDelegateDescription(agents) {
|
|
63
79
|
return [
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
"Related
|
|
80
|
+
"Delegate work to an agent. New: omit id and supply agent. Continue: use an existing id; agent/cwd are ignored. Unknown or blank ids fail.",
|
|
81
|
+
'Create: agents_delegate({ agent: "worker", task: "..." }). Continue: agents_delegate({ id: "agent-2", task: "..." }).',
|
|
82
|
+
"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.",
|
|
83
|
+
"Related agent tools: agents_get, agents_stop, agents_dashboard.",
|
|
68
84
|
formatAvailableAgents(agents),
|
|
69
85
|
].join("\n");
|
|
70
86
|
}
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const
|
|
87
|
+
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.";
|
|
88
|
+
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.";
|
|
89
|
+
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
90
|
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
91
|
const CONTACT_SNIPPET = "Report a blocker to the parent. Do NOT call on success: write normal text to finish.";
|
|
76
92
|
const CONTACT_GUIDELINES = [
|
|
77
93
|
"Use contact_parent ONLY if you are blocked or urgently require a parent decision midway.",
|
|
78
94
|
"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
|
|
95
|
+
"After reporting a blocker, wait for a parent agents_delegate instruction.",
|
|
80
96
|
];
|
|
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
97
|
export function registerParentTools(pi, manager, agents) {
|
|
128
98
|
const renderers = parentToolRenderers(pi);
|
|
129
|
-
const
|
|
99
|
+
const inspected = new Set();
|
|
100
|
+
/** The same inspection twice in a row is polling, and polling never gets the answer faster. */
|
|
130
101
|
function inspectionResult(id, text, details) {
|
|
131
|
-
const repeated =
|
|
132
|
-
|
|
133
|
-
return textToolResult(repeated ? `${text}\nResults are
|
|
102
|
+
const repeated = inspected.has(id);
|
|
103
|
+
inspected.add(id);
|
|
104
|
+
return textToolResult(repeated ? `${text}\nResults are delivered automatically; do not poll agents_get.` : text, details);
|
|
134
105
|
}
|
|
135
|
-
const
|
|
136
|
-
name: "
|
|
106
|
+
const delegate = {
|
|
107
|
+
name: "agents_delegate",
|
|
137
108
|
namespace: SUBAGENTS_NAMESPACE,
|
|
138
|
-
label: "
|
|
139
|
-
description:
|
|
140
|
-
parameters:
|
|
109
|
+
label: "Delegate to agent",
|
|
110
|
+
description: getDelegateDescription(agents),
|
|
111
|
+
parameters: delegateSchema,
|
|
141
112
|
exposure: "deferred",
|
|
142
113
|
defaultActive: false,
|
|
143
114
|
annotations: {
|
|
144
115
|
openWorldHint: true,
|
|
145
116
|
},
|
|
146
|
-
async execute(_id, params) {
|
|
147
|
-
if (!Value.Check(
|
|
148
|
-
throw new Error("Invalid
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return textToolResult(JSON.stringify(details), details);
|
|
117
|
+
async execute(_id, params, signal, onUpdate) {
|
|
118
|
+
if (!Value.Check(delegateSchema, params))
|
|
119
|
+
throw new Error("Invalid agents_delegate parameters");
|
|
120
|
+
const view = await manager.delegate(params, signal, (accepted) => {
|
|
121
|
+
onUpdate?.(textToolResult(JSON.stringify(accepted), accepted));
|
|
122
|
+
});
|
|
123
|
+
if (isOperationError(view))
|
|
124
|
+
throw new Error(operationErrorText(view));
|
|
125
|
+
return textToolResult(JSON.stringify(view), view);
|
|
156
126
|
},
|
|
157
|
-
...renderers.
|
|
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,
|
|
127
|
+
...renderers.delegate,
|
|
177
128
|
};
|
|
178
129
|
const get = {
|
|
179
|
-
name: "
|
|
130
|
+
name: "agents_get",
|
|
180
131
|
namespace: SUBAGENTS_NAMESPACE,
|
|
181
132
|
label: "Get agent",
|
|
182
133
|
description: GET_DESCRIPTION,
|
|
@@ -188,17 +139,21 @@ export function registerParentTools(pi, manager, agents) {
|
|
|
188
139
|
idempotentHint: true,
|
|
189
140
|
},
|
|
190
141
|
async execute(_id, params) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
142
|
+
if (!Value.Check(getSchema, params))
|
|
143
|
+
throw new Error("Invalid agents_get parameters");
|
|
144
|
+
const view = await manager.inspect({
|
|
145
|
+
id: params.id,
|
|
146
|
+
...(params.revision === undefined ? {} : { revision: params.revision }),
|
|
147
|
+
includeOutput: params.includeOutput === true,
|
|
148
|
+
});
|
|
149
|
+
if (isOperationError(view))
|
|
150
|
+
throw new Error(operationErrorText(view));
|
|
196
151
|
return inspectionResult(params.id, JSON.stringify(view), view);
|
|
197
152
|
},
|
|
198
153
|
...renderers.get,
|
|
199
154
|
};
|
|
200
155
|
const stop = {
|
|
201
|
-
name: "
|
|
156
|
+
name: "agents_stop",
|
|
202
157
|
namespace: SUBAGENTS_NAMESPACE,
|
|
203
158
|
label: "Stop agent",
|
|
204
159
|
description: STOP_DESCRIPTION,
|
|
@@ -209,17 +164,35 @@ export function registerParentTools(pi, manager, agents) {
|
|
|
209
164
|
destructiveHint: true,
|
|
210
165
|
},
|
|
211
166
|
async execute(_id, params) {
|
|
212
|
-
const
|
|
213
|
-
if (isOperationError(
|
|
214
|
-
throw new Error(
|
|
215
|
-
return textToolResult(
|
|
167
|
+
const view = await manager.stopAgent(params.id);
|
|
168
|
+
if (isOperationError(view))
|
|
169
|
+
throw new Error(operationErrorText(view));
|
|
170
|
+
return textToolResult(`Agent ${view.id} stopped; revision ${view.revision} is ${view.status}.`, view);
|
|
216
171
|
},
|
|
217
172
|
...renderers.stop,
|
|
218
173
|
};
|
|
219
|
-
|
|
220
|
-
|
|
174
|
+
const dashboard = {
|
|
175
|
+
name: "agents_dashboard",
|
|
176
|
+
namespace: SUBAGENTS_NAMESPACE,
|
|
177
|
+
label: "Agent dashboard",
|
|
178
|
+
description: DASHBOARD_DESCRIPTION,
|
|
179
|
+
parameters: dashboardSchema,
|
|
180
|
+
exposure: "deferred",
|
|
181
|
+
defaultActive: false,
|
|
182
|
+
annotations: {
|
|
183
|
+
readOnlyHint: true,
|
|
184
|
+
idempotentHint: true,
|
|
185
|
+
},
|
|
186
|
+
async execute() {
|
|
187
|
+
const view = await manager.dashboard();
|
|
188
|
+
return textToolResult(`${view.total} agents · running ${view.running} · blocked ${view.blocked} · completed ${view.completed} · failed ${view.failed}`, view);
|
|
189
|
+
},
|
|
190
|
+
...renderers.dashboard,
|
|
191
|
+
};
|
|
192
|
+
pi.registerTool(delegate);
|
|
221
193
|
pi.registerTool(get);
|
|
222
194
|
pi.registerTool(stop);
|
|
195
|
+
pi.registerTool(dashboard);
|
|
223
196
|
}
|
|
224
197
|
export function registerChildTools(pi, identity, options) {
|
|
225
198
|
const tool = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hheei/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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.
|
|
43
|
+
"@hheei/pi-ext-core": "^0.7.1",
|
|
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
|
}
|
package/dist/json-lines.d.ts
DELETED
|
@@ -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>;
|
package/dist/json-lines.js
DELETED
|
@@ -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
|
-
}
|