@hheei/pi-subagents 0.1.2 → 0.2.3
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/README.md +45 -137
- package/dist/agent-resolver.d.ts +20 -0
- package/dist/agent-resolver.js +155 -21
- package/dist/bridge-client.d.ts +55 -0
- package/dist/bridge-client.js +453 -0
- package/dist/bridge-server.d.ts +72 -0
- package/dist/bridge-server.js +490 -0
- package/dist/builtin-agents.d.ts +24 -0
- package/dist/builtin-agents.js +152 -0
- package/dist/child-bridge.d.ts +31 -15
- package/dist/child-bridge.js +95 -117
- package/dist/child-control.d.ts +58 -0
- package/dist/child-control.js +176 -0
- package/dist/child-process.d.ts +21 -0
- package/dist/child-process.js +93 -0
- package/dist/commands.d.ts +2 -2
- package/dist/commands.js +52 -60
- package/dist/diagnostics.d.ts +0 -2
- package/dist/diagnostics.js +0 -8
- package/dist/domain.d.ts +57 -27
- package/dist/domain.js +60 -11
- package/dist/extension.d.ts +26 -1
- package/dist/extension.js +243 -46
- package/dist/host-adapter.d.ts +37 -12
- package/dist/host-adapter.js +214 -63
- package/dist/json-lines.js +2 -1
- package/dist/launch-spec.d.ts +8 -6
- package/dist/launch-spec.js +61 -18
- package/dist/manager.d.ts +93 -49
- package/dist/manager.js +1175 -528
- package/dist/markdown.d.ts +8 -0
- package/dist/markdown.js +21 -0
- package/dist/protocol.d.ts +33 -52
- package/dist/protocol.js +41 -52
- package/dist/registry.d.ts +3 -8
- package/dist/registry.js +41 -163
- package/dist/rpc-events.d.ts +3 -5
- package/dist/rpc-events.js +5 -9
- package/dist/runtime.d.ts +66 -11
- package/dist/runtime.js +182 -208
- package/dist/session-bootstrap.d.ts +23 -7
- package/dist/session-bootstrap.js +79 -15
- package/dist/session-fork.d.ts +16 -0
- package/dist/session-fork.js +67 -0
- package/dist/state.d.ts +7 -0
- package/dist/state.js +60 -25
- package/dist/tool-activation.d.ts +18 -0
- package/dist/tool-activation.js +227 -0
- package/dist/tools.d.ts +6 -3
- package/dist/tools.js +213 -72
- package/dist/widget.d.ts +4 -8
- package/dist/widget.js +55 -64
- package/package.json +51 -52
- package/dist/child-nudge.d.ts +0 -24
- package/dist/child-nudge.js +0 -102
- package/dist/connector.d.ts +0 -53
- package/dist/connector.js +0 -283
- package/dist/pause-gate.d.ts +0 -14
- package/dist/pause-gate.js +0 -64
- package/dist/rpc-adapter.d.ts +0 -40
- package/dist/rpc-adapter.js +0 -194
- package/dist/runner-entry.d.ts +0 -24
- package/dist/runner-entry.js +0 -166
- package/dist/runner.d.ts +0 -52
- package/dist/runner.js +0 -930
package/dist/registry.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { readJsonSettingsRoot, updateJsonSettingsRoot } from "@hheei/pi-ext-core";
|
|
4
|
-
import {
|
|
3
|
+
import { isRecord, readJsonSettingsRoot, updateJsonSettingsRoot } from "@hheei/pi-ext-core";
|
|
4
|
+
import { isLaunchValueSource, isPersistenceState, isPresentation, isSessionId, isSubagentIntent, isSubagentState, isThinkingLevel, normalizeSubagentState, REGISTRY_VERSION, } from "./domain.js";
|
|
5
5
|
export class SubagentRegistryError extends Error {
|
|
6
6
|
code;
|
|
7
7
|
constructor(code, message) {
|
|
@@ -31,15 +31,15 @@ const RECORD_FIELDS = {
|
|
|
31
31
|
initialTask: true,
|
|
32
32
|
intent: true,
|
|
33
33
|
state: true,
|
|
34
|
-
|
|
34
|
+
presentation: true,
|
|
35
35
|
persistence: true,
|
|
36
36
|
launchConfig: true,
|
|
37
37
|
runtime: true,
|
|
38
38
|
latestSummary: true,
|
|
39
|
+
activeTool: true,
|
|
39
40
|
usage: true,
|
|
40
41
|
interrupted: true,
|
|
41
42
|
unacknowledgedInput: true,
|
|
42
|
-
claim: true,
|
|
43
43
|
};
|
|
44
44
|
const LAUNCH_CONFIG_FIELDS = {
|
|
45
45
|
subagentId: true,
|
|
@@ -58,6 +58,10 @@ const LAUNCH_CONFIG_FIELDS = {
|
|
|
58
58
|
prompt: true,
|
|
59
59
|
bridgeExtensionPath: true,
|
|
60
60
|
interactive: true,
|
|
61
|
+
title: true,
|
|
62
|
+
task: true,
|
|
63
|
+
referencedSessionPath: true,
|
|
64
|
+
codemodeOnly: true,
|
|
61
65
|
};
|
|
62
66
|
const AGENT_FIELDS = {
|
|
63
67
|
name: true,
|
|
@@ -71,16 +75,7 @@ const INVOCATION_FIELDS = { command: true, args: true };
|
|
|
71
75
|
const MODEL_FIELDS = { provider: true, id: true, source: true };
|
|
72
76
|
const THINKING_FIELDS = { level: true, source: true };
|
|
73
77
|
const SELECTION_FIELDS = { discovery: true, paths: true };
|
|
74
|
-
const RUNTIME_FIELDS = { runtimeIdentity: true, endpoint: true
|
|
75
|
-
const CLAIM_FIELDS = {
|
|
76
|
-
claimId: true,
|
|
77
|
-
kind: true,
|
|
78
|
-
holderPid: true,
|
|
79
|
-
runtimeIdentity: true,
|
|
80
|
-
endpoint: true,
|
|
81
|
-
controllerTokenHash: true,
|
|
82
|
-
runnerPid: true,
|
|
83
|
-
};
|
|
78
|
+
const RUNTIME_FIELDS = { runtimeIdentity: true, endpoint: true };
|
|
84
79
|
const USAGE_FIELDS = {
|
|
85
80
|
inputTokens: true,
|
|
86
81
|
outputTokens: true,
|
|
@@ -188,26 +183,15 @@ function parseThinking(value, path) {
|
|
|
188
183
|
throw invalid(path, "launchConfig.thinking.source is invalid");
|
|
189
184
|
return Object.freeze({ level, source });
|
|
190
185
|
}
|
|
191
|
-
function
|
|
192
|
-
const raw = expectObject(value,
|
|
193
|
-
expectKeys(raw, SELECTION_FIELDS,
|
|
194
|
-
if (typeof raw.discovery !== "boolean") {
|
|
195
|
-
throw invalid(path, "launchConfig.extensions.discovery must be boolean");
|
|
196
|
-
}
|
|
197
|
-
return Object.freeze({
|
|
198
|
-
discovery: raw.discovery,
|
|
199
|
-
paths: Object.freeze(expectStringArray(raw.paths, "launchConfig.extensions.paths", path)),
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
function parseSkills(value, path) {
|
|
203
|
-
const raw = expectObject(value, "launchConfig.skills", path);
|
|
204
|
-
expectKeys(raw, SELECTION_FIELDS, "launchConfig.skills", path);
|
|
186
|
+
function parseSelection(value, path, field) {
|
|
187
|
+
const raw = expectObject(value, `launchConfig.${field}`, path);
|
|
188
|
+
expectKeys(raw, SELECTION_FIELDS, `launchConfig.${field}`, path);
|
|
205
189
|
if (typeof raw.discovery !== "boolean") {
|
|
206
|
-
throw invalid(path,
|
|
190
|
+
throw invalid(path, `launchConfig.${field}.discovery must be boolean`);
|
|
207
191
|
}
|
|
208
192
|
return Object.freeze({
|
|
209
193
|
discovery: raw.discovery,
|
|
210
|
-
paths: Object.freeze(expectStringArray(raw.paths,
|
|
194
|
+
paths: Object.freeze(expectStringArray(raw.paths, `launchConfig.${field}.paths`, path)),
|
|
211
195
|
});
|
|
212
196
|
}
|
|
213
197
|
function parseUsage(value, path) {
|
|
@@ -235,42 +219,7 @@ function parseRuntime(value, path) {
|
|
|
235
219
|
expectKeys(raw, RUNTIME_FIELDS, "runtime", path);
|
|
236
220
|
const runtimeIdentity = expectString(raw.runtimeIdentity, "runtime.runtimeIdentity", path);
|
|
237
221
|
const endpoint = expectString(raw.endpoint, "runtime.endpoint", path);
|
|
238
|
-
|
|
239
|
-
if (pid === undefined)
|
|
240
|
-
return Object.freeze({ runtimeIdentity, endpoint });
|
|
241
|
-
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
|
|
242
|
-
throw invalid(path, "runtime.pid must be a positive integer");
|
|
243
|
-
}
|
|
244
|
-
return Object.freeze({ runtimeIdentity, endpoint, pid });
|
|
245
|
-
}
|
|
246
|
-
function parseClaim(value, path) {
|
|
247
|
-
const raw = expectObject(value, "claim", path);
|
|
248
|
-
expectKeys(raw, CLAIM_FIELDS, "claim", path);
|
|
249
|
-
if (raw.kind !== "reconnect" && raw.kind !== "replacement") {
|
|
250
|
-
throw invalid(path, "claim.kind must be reconnect or replacement");
|
|
251
|
-
}
|
|
252
|
-
const holderPid = raw.holderPid;
|
|
253
|
-
if (typeof holderPid !== "number" || !Number.isInteger(holderPid) || holderPid <= 0) {
|
|
254
|
-
throw invalid(path, "claim.holderPid must be a positive integer");
|
|
255
|
-
}
|
|
256
|
-
const tokenHash = expectString(raw.controllerTokenHash, "claim.controllerTokenHash", path);
|
|
257
|
-
if (!/^[0-9a-f]{64}$/u.test(tokenHash)) {
|
|
258
|
-
throw invalid(path, "claim.controllerTokenHash must be a SHA-256 hex digest");
|
|
259
|
-
}
|
|
260
|
-
const runnerPid = raw.runnerPid;
|
|
261
|
-
if (runnerPid !== undefined &&
|
|
262
|
-
(typeof runnerPid !== "number" || !Number.isInteger(runnerPid) || runnerPid <= 0)) {
|
|
263
|
-
throw invalid(path, "claim.runnerPid must be a positive integer");
|
|
264
|
-
}
|
|
265
|
-
return Object.freeze({
|
|
266
|
-
claimId: expectString(raw.claimId, "claim.claimId", path),
|
|
267
|
-
kind: raw.kind,
|
|
268
|
-
holderPid,
|
|
269
|
-
runtimeIdentity: expectString(raw.runtimeIdentity, "claim.runtimeIdentity", path),
|
|
270
|
-
endpoint: expectString(raw.endpoint, "claim.endpoint", path),
|
|
271
|
-
controllerTokenHash: tokenHash,
|
|
272
|
-
...(runnerPid === undefined ? {} : { runnerPid }),
|
|
273
|
-
});
|
|
222
|
+
return Object.freeze({ runtimeIdentity, endpoint });
|
|
274
223
|
}
|
|
275
224
|
function parseLaunchConfig(value, path) {
|
|
276
225
|
const raw = expectObject(value, "launchConfig", path);
|
|
@@ -279,7 +228,8 @@ function parseLaunchConfig(value, path) {
|
|
|
279
228
|
if (!isSessionId(sessionId))
|
|
280
229
|
throw invalid(path, "launchConfig.sessionId is not a valid session id");
|
|
281
230
|
const sessionPath = expectOptionalString(raw.sessionPath, "launchConfig.sessionPath", path);
|
|
282
|
-
const
|
|
231
|
+
const referencedSessionPath = expectOptionalString(raw.referencedSessionPath, "launchConfig.referencedSessionPath", path);
|
|
232
|
+
const extensions = parseSelection(raw.extensions, path, "extensions");
|
|
283
233
|
const bridgeExtensionPath = expectString(raw.bridgeExtensionPath, "launchConfig.bridgeExtensionPath", path);
|
|
284
234
|
if (!extensions.paths.includes(bridgeExtensionPath)) {
|
|
285
235
|
throw invalid(path, "launchConfig.bridgeExtensionPath is missing from the extension selection");
|
|
@@ -287,6 +237,7 @@ function parseLaunchConfig(value, path) {
|
|
|
287
237
|
const subagentId = expectString(raw.subagentId, "launchConfig.subagentId", path);
|
|
288
238
|
if (!isSessionId(subagentId))
|
|
289
239
|
throw invalid(path, "launchConfig.subagentId is not a valid child id");
|
|
240
|
+
const title = expectOptionalString(raw.title, "launchConfig.title", path);
|
|
290
241
|
return Object.freeze({
|
|
291
242
|
subagentId,
|
|
292
243
|
invocation: parseInvocation(raw.invocation, path),
|
|
@@ -294,21 +245,33 @@ function parseLaunchConfig(value, path) {
|
|
|
294
245
|
sessionId,
|
|
295
246
|
sessionDir: expectString(raw.sessionDir, "launchConfig.sessionDir", path),
|
|
296
247
|
...(sessionPath === undefined ? {} : { sessionPath }),
|
|
248
|
+
...(referencedSessionPath === undefined ? {} : { referencedSessionPath }),
|
|
297
249
|
agent: parseAgent(raw.agent, path),
|
|
298
250
|
model: parseModel(raw.model, path),
|
|
299
251
|
thinking: parseThinking(raw.thinking, path),
|
|
300
252
|
tools: Object.freeze(expectStringArray(raw.tools, "launchConfig.tools", path)),
|
|
301
253
|
excludeTools: Object.freeze(expectStringArray(raw.excludeTools, "launchConfig.excludeTools", path)),
|
|
302
254
|
extensions,
|
|
303
|
-
skills:
|
|
255
|
+
skills: parseSelection(raw.skills, path, "skills"),
|
|
304
256
|
prompt: expectString(raw.prompt, "launchConfig.prompt", path),
|
|
305
257
|
bridgeExtensionPath,
|
|
306
258
|
interactive: expectOptionalBoolean(raw.interactive, "launchConfig.interactive", path, false),
|
|
259
|
+
...(expectOptionalBoolean(raw.codemodeOnly, "launchConfig.codemodeOnly", path, false)
|
|
260
|
+
? { codemodeOnly: true }
|
|
261
|
+
: {}),
|
|
262
|
+
...(title === undefined ? {} : { title }),
|
|
307
263
|
});
|
|
308
264
|
}
|
|
309
265
|
/** Strictly revalidates one record read from disk; partial or unknown shapes never become state. */
|
|
310
266
|
export function parseSubagentRecord(value, key, parentSessionId, path) {
|
|
311
267
|
const raw = expectObject(value, `record ${key}`, path);
|
|
268
|
+
if (raw.presentation === undefined && raw.mode !== undefined) {
|
|
269
|
+
// A record from the attach era names its presentation `mode`, and that field means something
|
|
270
|
+
// else now (who holds the child's process), so it is never guessed at: the user deletes the
|
|
271
|
+
// record and this version starts the child fresh. Checked before the field list, because the
|
|
272
|
+
// old field is otherwise just "unsupported".
|
|
273
|
+
throw invalid(path, `record ${key} was written by an older version (mode ${JSON.stringify(raw.mode)}); delete this record to use presentation "panel" | "background"`);
|
|
274
|
+
}
|
|
312
275
|
expectKeys(raw, RECORD_FIELDS, `record ${key}`, path);
|
|
313
276
|
const subagentId = expectString(raw.subagentId, "subagentId", path);
|
|
314
277
|
if (subagentId !== key)
|
|
@@ -322,12 +285,13 @@ export function parseSubagentRecord(value, key, parentSessionId, path) {
|
|
|
322
285
|
if (!Number.isInteger(revision) || revision < 1) {
|
|
323
286
|
throw invalid(path, `record ${key} revision must be a positive integer`);
|
|
324
287
|
}
|
|
325
|
-
const
|
|
326
|
-
if (!isSubagentState(
|
|
288
|
+
const rawState = raw.state;
|
|
289
|
+
if (!isSubagentState(rawState))
|
|
327
290
|
throw invalid(path, `record ${key} state is invalid`);
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
291
|
+
const state = normalizeSubagentState(rawState) ?? rawState;
|
|
292
|
+
const presentation = raw.presentation;
|
|
293
|
+
if (!isPresentation(presentation))
|
|
294
|
+
throw invalid(path, `record ${key} presentation is invalid`);
|
|
331
295
|
const intent = raw.intent;
|
|
332
296
|
if (!isSubagentIntent(intent))
|
|
333
297
|
throw invalid(path, `record ${key} intent is invalid`);
|
|
@@ -357,9 +321,9 @@ export function parseSubagentRecord(value, key, parentSessionId, path) {
|
|
|
357
321
|
throw invalid(path, `record ${key} claims a never-flushed session with a session path`);
|
|
358
322
|
}
|
|
359
323
|
const runtime = raw.runtime === undefined ? undefined : parseRuntime(raw.runtime, path);
|
|
360
|
-
const claim = raw.claim === undefined ? undefined : parseClaim(raw.claim, path);
|
|
361
324
|
const usage = raw.usage === undefined ? undefined : parseUsage(raw.usage, path);
|
|
362
325
|
const latestSummary = expectOptionalString(raw.latestSummary, "latestSummary", path);
|
|
326
|
+
const activeTool = expectOptionalString(raw.activeTool, "activeTool", path);
|
|
363
327
|
const interrupted = expectOptionalString(raw.interrupted, "interrupted", path);
|
|
364
328
|
const unacknowledgedInput = expectOptionalString(raw.unacknowledgedInput, "unacknowledgedInput", path);
|
|
365
329
|
return Object.freeze({
|
|
@@ -374,12 +338,12 @@ export function parseSubagentRecord(value, key, parentSessionId, path) {
|
|
|
374
338
|
initialTask: expectString(raw.initialTask, "initialTask", path),
|
|
375
339
|
intent,
|
|
376
340
|
state,
|
|
377
|
-
|
|
341
|
+
presentation,
|
|
378
342
|
persistence,
|
|
379
343
|
launchConfig,
|
|
380
344
|
...(runtime === undefined ? {} : { runtime }),
|
|
381
|
-
...(claim === undefined ? {} : { claim }),
|
|
382
345
|
...(latestSummary === undefined ? {} : { latestSummary }),
|
|
346
|
+
...(activeTool === undefined ? {} : { activeTool }),
|
|
383
347
|
...(usage === undefined ? {} : { usage }),
|
|
384
348
|
...(interrupted === undefined ? {} : { interrupted }),
|
|
385
349
|
...(unacknowledgedInput === undefined ? {} : { unacknowledgedInput }),
|
|
@@ -461,7 +425,7 @@ export function createSubagentRegistry(options) {
|
|
|
461
425
|
if (current === undefined) {
|
|
462
426
|
throw new SubagentRegistryError("unknown_child", `Unknown child ${id} in ${path}`);
|
|
463
427
|
}
|
|
464
|
-
if (current.revision !== expectedRevision) {
|
|
428
|
+
if (expectedRevision !== undefined && current.revision !== expectedRevision) {
|
|
465
429
|
throw new SubagentRegistryError("stale_revision", `Child ${id} is at revision ${current.revision}, not ${expectedRevision}`);
|
|
466
430
|
}
|
|
467
431
|
if (expectedRuntimeIdentity !== undefined &&
|
|
@@ -484,92 +448,6 @@ export function createSubagentRegistry(options) {
|
|
|
484
448
|
throw corrupt(path, `update of ${id} did not persist a record`);
|
|
485
449
|
return stored;
|
|
486
450
|
},
|
|
487
|
-
async claim(id, expectedRevision, candidate, expectedRuntimeIdentity, expectedClaimId, signal) {
|
|
488
|
-
return store.update(id, expectedRevision, (current) => {
|
|
489
|
-
if (current.intent === "stopped") {
|
|
490
|
-
throw new SubagentRegistryError("stopped_child", `Child ${id} is stopped`);
|
|
491
|
-
}
|
|
492
|
-
if ((expectedClaimId === undefined && current.claim !== undefined) ||
|
|
493
|
-
(expectedClaimId !== undefined && current.claim?.claimId !== expectedClaimId)) {
|
|
494
|
-
throw new SubagentRegistryError("claim_conflict", `Child ${id} has another claim`);
|
|
495
|
-
}
|
|
496
|
-
if (candidate.kind === "reconnect") {
|
|
497
|
-
if (current.runtime?.runtimeIdentity !== candidate.runtimeIdentity ||
|
|
498
|
-
current.runtime.endpoint !== candidate.endpoint) {
|
|
499
|
-
throw new SubagentRegistryError("runtime_mismatch", `Child ${id} runtime changed before reconnect claim`);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
else if (expectedRuntimeIdentity === undefined
|
|
503
|
-
? current.runtime !== undefined
|
|
504
|
-
: current.runtime?.runtimeIdentity !== expectedRuntimeIdentity) {
|
|
505
|
-
throw new SubagentRegistryError("runtime_mismatch", `Child ${id} runtime changed before replacement claim`);
|
|
506
|
-
}
|
|
507
|
-
return { ...current, claim: candidate };
|
|
508
|
-
}, undefined, signal);
|
|
509
|
-
},
|
|
510
|
-
async markClaimRunner(id, claimId, runnerPid, signal) {
|
|
511
|
-
const current = await store.get(id, signal);
|
|
512
|
-
if (current === undefined)
|
|
513
|
-
throw new SubagentRegistryError("unknown_child", `Unknown child ${id} in ${path}`);
|
|
514
|
-
return store.update(id, current.revision, (value) => {
|
|
515
|
-
if (value.claim?.claimId !== claimId || value.claim.kind !== "replacement") {
|
|
516
|
-
throw new SubagentRegistryError("claim_mismatch", `Child ${id} claim changed`);
|
|
517
|
-
}
|
|
518
|
-
return { ...value, claim: { ...value.claim, runnerPid } };
|
|
519
|
-
}, undefined, signal);
|
|
520
|
-
},
|
|
521
|
-
async activateClaim(id, claimId, runnerPid, signal) {
|
|
522
|
-
const current = await store.get(id, signal);
|
|
523
|
-
if (current === undefined)
|
|
524
|
-
throw new SubagentRegistryError("unknown_child", `Unknown child ${id} in ${path}`);
|
|
525
|
-
return store.update(id, current.revision, (value) => {
|
|
526
|
-
const claim = value.claim;
|
|
527
|
-
if (claim?.claimId !== claimId ||
|
|
528
|
-
claim.kind !== "replacement" ||
|
|
529
|
-
(claim.runnerPid !== undefined && claim.runnerPid !== runnerPid) ||
|
|
530
|
-
value.intent === "stopped") {
|
|
531
|
-
throw new SubagentRegistryError("claim_mismatch", `Child ${id} replacement claim is no longer valid`);
|
|
532
|
-
}
|
|
533
|
-
const { claim: _claim, ...withoutClaim } = value;
|
|
534
|
-
return {
|
|
535
|
-
...withoutClaim,
|
|
536
|
-
runtime: {
|
|
537
|
-
runtimeIdentity: claim.runtimeIdentity,
|
|
538
|
-
endpoint: claim.endpoint,
|
|
539
|
-
pid: runnerPid,
|
|
540
|
-
},
|
|
541
|
-
};
|
|
542
|
-
}, undefined, signal);
|
|
543
|
-
},
|
|
544
|
-
async consumeReconnectClaim(id, claimId, controllerTokenHash, signal) {
|
|
545
|
-
const current = await store.get(id, signal);
|
|
546
|
-
if (current === undefined)
|
|
547
|
-
throw new SubagentRegistryError("unknown_child", `Unknown child ${id} in ${path}`);
|
|
548
|
-
return store.update(id, current.revision, (value) => {
|
|
549
|
-
const claim = value.claim;
|
|
550
|
-
if (claim?.claimId !== claimId ||
|
|
551
|
-
claim.kind !== "reconnect" ||
|
|
552
|
-
claim.controllerTokenHash !== controllerTokenHash ||
|
|
553
|
-
value.runtime?.runtimeIdentity !== claim.runtimeIdentity ||
|
|
554
|
-
value.intent === "stopped") {
|
|
555
|
-
throw new SubagentRegistryError("claim_mismatch", `Child ${id} reconnect claim is no longer valid`);
|
|
556
|
-
}
|
|
557
|
-
const { claim: _claim, ...withoutClaim } = value;
|
|
558
|
-
return withoutClaim;
|
|
559
|
-
}, current.runtime?.runtimeIdentity, signal);
|
|
560
|
-
},
|
|
561
|
-
async releaseClaim(id, claimId, signal) {
|
|
562
|
-
const current = await store.get(id, signal);
|
|
563
|
-
if (current === undefined)
|
|
564
|
-
throw new SubagentRegistryError("unknown_child", `Unknown child ${id} in ${path}`);
|
|
565
|
-
return store.update(id, current.revision, (value) => {
|
|
566
|
-
if (value.claim?.claimId !== claimId) {
|
|
567
|
-
throw new SubagentRegistryError("claim_mismatch", `Child ${id} claim changed`);
|
|
568
|
-
}
|
|
569
|
-
const { claim: _claim, ...withoutClaim } = value;
|
|
570
|
-
return withoutClaim;
|
|
571
|
-
}, undefined, signal);
|
|
572
|
-
},
|
|
573
451
|
};
|
|
574
452
|
return store;
|
|
575
453
|
}
|
package/dist/rpc-events.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* else (streaming token noise, undocumented internals) is dropped.
|
|
2
|
+
* The Pi events a child forwards to its parent. Everything else — streaming token noise,
|
|
3
|
+
* undocumented internals — is dropped, so the parent's projection only sees turn-level facts.
|
|
5
4
|
*/
|
|
6
|
-
export declare const FORWARDED_PI_EVENT_TYPES: readonly ["agent_start", "agent_end", "turn_start", "turn_end", "tool_execution_start", "tool_execution_update", "tool_execution_end", "auto_retry", "message_update", "agent_settled"];
|
|
5
|
+
export declare const FORWARDED_PI_EVENT_TYPES: readonly ["agent_start", "agent_end", "turn_start", "turn_end", "tool_execution_start", "tool_execution_update", "tool_execution_end", "auto_retry", "auto_retry_start", "auto_retry_end", "message_update", "agent_settled"];
|
|
7
6
|
export declare function shouldForwardPiEvent(event: unknown): boolean;
|
|
8
|
-
export declare function isIdlePiState(value: unknown): boolean;
|
package/dist/rpc-events.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { isRecord } from "
|
|
1
|
+
import { isRecord } from "@hheei/pi-ext-core";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* else (streaming token noise, undocumented internals) is dropped.
|
|
3
|
+
* The Pi events a child forwards to its parent. Everything else — streaming token noise,
|
|
4
|
+
* undocumented internals — is dropped, so the parent's projection only sees turn-level facts.
|
|
6
5
|
*/
|
|
7
6
|
export const FORWARDED_PI_EVENT_TYPES = [
|
|
8
7
|
"agent_start",
|
|
@@ -13,6 +12,8 @@ export const FORWARDED_PI_EVENT_TYPES = [
|
|
|
13
12
|
"tool_execution_update",
|
|
14
13
|
"tool_execution_end",
|
|
15
14
|
"auto_retry",
|
|
15
|
+
"auto_retry_start",
|
|
16
|
+
"auto_retry_end",
|
|
16
17
|
"message_update",
|
|
17
18
|
"agent_settled",
|
|
18
19
|
];
|
|
@@ -22,8 +23,3 @@ export function shouldForwardPiEvent(event) {
|
|
|
22
23
|
return (typeof event.type === "string" &&
|
|
23
24
|
FORWARDED_PI_EVENT_TYPES.includes(event.type));
|
|
24
25
|
}
|
|
25
|
-
export function isIdlePiState(value) {
|
|
26
|
-
if (!isRecord(value))
|
|
27
|
-
return false;
|
|
28
|
-
return (value.isStreaming === false && value.isCompacting === false && value.pendingMessageCount === 0);
|
|
29
|
-
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,22 +1,77 @@
|
|
|
1
|
+
import { type ChildRuntime } from "./child-process.js";
|
|
1
2
|
import type { SubagentRecord } from "./domain.js";
|
|
2
|
-
import type {
|
|
3
|
+
import type { HostAdapter, HostAttachment } from "./host-adapter.js";
|
|
3
4
|
import type { SubagentRegistry } from "./registry.js";
|
|
5
|
+
/** Per-runtime bridge tokens, kept out of the registry because they are credentials. */
|
|
4
6
|
export interface RuntimeTokenStore {
|
|
5
7
|
remember(runtimeIdentity: string, token: string): void;
|
|
6
8
|
get(runtimeIdentity: string): string | undefined;
|
|
7
9
|
forget(runtimeIdentity: string): void;
|
|
8
|
-
clear(): void;
|
|
9
10
|
}
|
|
10
|
-
export interface
|
|
11
|
-
|
|
11
|
+
export interface RuntimeTokenStoreOptions {
|
|
12
|
+
/** The parent session these tokens belong to. */
|
|
13
|
+
readonly parentSessionId: string;
|
|
14
|
+
/** Override for tests. Defaults to this parent's file in the shared runtime directory. */
|
|
15
|
+
readonly file?: string;
|
|
16
|
+
readonly diagnose?: (message: string) => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The one socket a parent session listens on. A child dials this path, so a parent that restarts
|
|
20
|
+
* re-listens in the same place and a surviving child reconnects instead of being rebuilt.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parentBridgeEndpoint(parentSessionId: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Every parent session keeps its own token file. One shared file is rewritten wholesale by whichever
|
|
25
|
+
* parent saved last, so two parent sessions in the same runtime directory would erase each other's
|
|
26
|
+
* credentials and a child that survived a restart could no longer authorize itself.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parentTokenFile(parentSessionId: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Tokens outlive the parent process: a child that survived (a panel child, or one whose parent was
|
|
31
|
+
* killed) reconnects with the token it was launched with, and a restarted parent must be able to
|
|
32
|
+
* authorize it without holding the old process's memory. The store is a 0600 file in the 0700
|
|
33
|
+
* runtime directory; the registry never sees a token.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createRuntimeTokenStore(options: RuntimeTokenStoreOptions): RuntimeTokenStore;
|
|
36
|
+
export interface LaunchChildOptions {
|
|
37
|
+
readonly registry: Pick<SubagentRegistry, "update">;
|
|
12
38
|
readonly record: SubagentRecord;
|
|
13
39
|
readonly tokens: RuntimeTokenStore;
|
|
40
|
+
/** Waits for this child's bridge connection; false when it did not connect in time. */
|
|
41
|
+
readonly waitForBridge: (childId: string, signal?: AbortSignal) => Promise<boolean>;
|
|
14
42
|
readonly signal?: AbortSignal;
|
|
43
|
+
readonly diagnose?: (message: string) => void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Starts one headless child Pi and waits for it to connect. Nothing is driven over its stdio: the
|
|
47
|
+
* process gets the parent's socket path and its own runtime token in the environment, and the
|
|
48
|
+
* bridge is the only control channel. A child that never connects is terminated here rather than
|
|
49
|
+
* left running with no way to reach it.
|
|
50
|
+
*/
|
|
51
|
+
export declare function launchChild(options: LaunchChildOptions): Promise<LaunchOutcome<ChildRuntime>>;
|
|
52
|
+
export interface OpenChildPanelOptions extends LaunchChildOptions {
|
|
53
|
+
readonly host: HostAdapter;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* What a launch left behind. A launch that failed can still have started something — a child that
|
|
57
|
+
* never dialed in, or a process that ignores SIGTERM — and then the caller must keep the handle:
|
|
58
|
+
* dropping it would let the next send start a second runtime for a session the first one may still
|
|
59
|
+
* own. `failure` is what the caller reports.
|
|
60
|
+
*/
|
|
61
|
+
export interface LaunchOutcome<T> {
|
|
62
|
+
readonly handle?: T;
|
|
63
|
+
readonly failure?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Set when something may still own the child's session and this process cannot observe it, so
|
|
66
|
+
* the caller refuses to start a second runtime until an explicit stop.
|
|
67
|
+
*/
|
|
68
|
+
readonly unconfirmed?: boolean;
|
|
15
69
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export declare function
|
|
22
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Opens one child in a host panel and waits for it to connect. The host owns this process, so the
|
|
72
|
+
* parent holds no runtime handle for it; a panel that never yields a bridge is closed again instead
|
|
73
|
+
* of being left as a process nobody can control.
|
|
74
|
+
*/
|
|
75
|
+
export declare function openChildPanel(options: OpenChildPanelOptions): Promise<LaunchOutcome<HostAttachment>>;
|
|
76
|
+
/** Creates the runtime directory if needed; the socket itself is 0600 in a 0700 directory. */
|
|
77
|
+
export declare function prepareRuntimeDirectory(diagnose?: (message: string) => void): Promise<string>;
|