@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.
- 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 +104 -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,58 @@ 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
|
-
"Returns
|
|
66
|
-
"
|
|
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
|
|
72
|
-
const
|
|
73
|
-
const
|
|
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
|
|
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
|
|
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 =
|
|
132
|
-
|
|
133
|
-
return textToolResult(repeated ? `${text}\nResults are
|
|
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
|
|
136
|
-
name: "
|
|
105
|
+
const delegate = {
|
|
106
|
+
name: "agents_delegate",
|
|
137
107
|
namespace: SUBAGENTS_NAMESPACE,
|
|
138
|
-
label: "
|
|
139
|
-
description:
|
|
140
|
-
parameters:
|
|
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(
|
|
148
|
-
throw new Error("Invalid
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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.
|
|
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: "
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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: "
|
|
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
|
|
213
|
-
if (isOperationError(
|
|
214
|
-
throw new Error(
|
|
215
|
-
return textToolResult(
|
|
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
|
-
|
|
220
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|
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
|
-
}
|