@vzlentin/harness 0.2.3 → 0.3.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/README.md +11 -8
- package/dist/answer.js +2 -2
- package/dist/children.d.ts +5 -0
- package/dist/children.js +14 -4
- package/dist/cli.js +38 -16
- package/dist/client.d.ts +11 -7
- package/dist/client.js +14 -9
- package/dist/context.js +2 -1
- package/dist/herdr.d.ts +25 -23
- package/dist/herdr.js +93 -38
- package/dist/host.d.ts +1 -1
- package/dist/host.js +42 -219
- package/dist/load-entries.d.ts +18 -0
- package/dist/load-entries.js +91 -0
- package/dist/mirror.d.ts +95 -0
- package/dist/mirror.js +291 -0
- package/dist/protocol.d.ts +70 -44
- package/dist/protocol.js +35 -11
- package/dist/setup.d.ts +2 -0
- package/dist/setup.js +3 -2
- package/dist/targets.d.ts +67 -0
- package/dist/targets.js +140 -0
- package/dist/tool.js +1 -1
- package/dist/view-change.d.ts +38 -0
- package/dist/view-change.js +55 -0
- package/dist/view-state.d.ts +22 -6
- package/dist/view-state.js +45 -7
- package/package.json +2 -1
- package/pi/attachment.ts +303 -0
- package/pi/chat.ts +286 -0
- package/pi/index.ts +10 -8
- package/pi/mirror.ts +218 -0
- package/pi/model-sync.ts +102 -0
- package/pi/package.json +3 -2
- package/pi/program-status.ts +66 -0
- package/pi/usage-line.ts +59 -0
- package/dist/chat.d.ts +0 -48
- package/dist/chat.js +0 -208
- package/dist/chrome.d.ts +0 -18
- package/dist/chrome.js +0 -74
- package/dist/picker.d.ts +0 -22
- package/dist/picker.js +0 -70
- package/dist/theme.d.ts +0 -3
- package/dist/theme.js +0 -19
- package/dist/viewer.d.ts +0 -20
- package/dist/viewer.js +0 -307
package/dist/host.js
CHANGED
|
@@ -1,62 +1,34 @@
|
|
|
1
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
1
|
import { randomBytes } from "node:crypto";
|
|
10
2
|
import { once } from "node:events";
|
|
11
|
-
import {
|
|
12
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
13
4
|
import { createServer } from "node:net";
|
|
14
|
-
import { join
|
|
5
|
+
import { join } from "node:path";
|
|
15
6
|
import { DatabaseSync } from "node:sqlite";
|
|
16
|
-
import { pathToFileURL } from "node:url";
|
|
17
7
|
import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/chord/context";
|
|
18
8
|
import * as ai from "@earendil-works/pi-ai";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
9
|
+
import { CodemodeSandbox } from "@earendil-works/pi-codemode";
|
|
10
|
+
import { getAgentDir, ProjectTrustStore, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
21
11
|
import * as durable from "@earendil-works/pi-durable";
|
|
22
|
-
import { AgentDoc, createRegistry,
|
|
12
|
+
import { AgentDoc, createRegistry, Harness, } from "@earendil-works/pi-durable";
|
|
13
|
+
import { createCodemodeExtension } from "@earendil-works/pi-durable/codemode";
|
|
23
14
|
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
24
15
|
import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
|
|
25
16
|
import { Type } from "typebox";
|
|
26
17
|
import { Value } from "typebox/value";
|
|
27
|
-
import {
|
|
18
|
+
import { resolveModel } from "./children.js";
|
|
28
19
|
import { ChildContext } from "./context.js";
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
20
|
+
import { loadEntries } from "./load-entries.js";
|
|
21
|
+
import { errorMessage, onLines, PROTOCOL_VERSION, RequestLine, } from "./protocol.js";
|
|
22
|
+
import { Children, childInfo, childOf, childRecord, childTarget, library, ownRecord, recordOf, target, } from "./targets.js";
|
|
31
23
|
import { abortTask, startableTasks, startTask, taskInfo, waitTask } from "./tasks.js";
|
|
32
24
|
import { subagentTool } from "./tool.js";
|
|
25
|
+
import { viewChange } from "./view-change.js";
|
|
33
26
|
/** The host exits after this long with no connection, no live task and no request. */
|
|
34
27
|
const IDLE_EXIT_MS = 5 * 60_000;
|
|
35
28
|
const IDLE_CHECK_MS = 60_000;
|
|
36
29
|
const context = BACKGROUND_CONTEXT;
|
|
37
30
|
/** The `detail` of a failed turn that is an error object. */
|
|
38
31
|
const ErrorDetail = Type.Object({ message: Type.String() });
|
|
39
|
-
/**
|
|
40
|
-
* The children by handle, and the handle of each spawn request ID. Version 2 leaves each child's last request to the
|
|
41
|
-
* children library; a store with version 1 has children that the library does not know.
|
|
42
|
-
*/
|
|
43
|
-
const Children = defineDoc({
|
|
44
|
-
kind: "subagent.children",
|
|
45
|
-
version: 2,
|
|
46
|
-
scope: "session",
|
|
47
|
-
initial: () => ({ children: {}, requests: {} }),
|
|
48
|
-
});
|
|
49
|
-
/** The request ID of the project agent's newest input, which its `error` state comes from. */
|
|
50
|
-
const AgentState = defineDoc({
|
|
51
|
-
kind: "harness.agent",
|
|
52
|
-
version: 1,
|
|
53
|
-
scope: "conversation",
|
|
54
|
-
history: "latest",
|
|
55
|
-
fork: "initial",
|
|
56
|
-
initial: () => ({ lastRequest: null }),
|
|
57
|
-
});
|
|
58
|
-
/** The children library, on this process's pi-durable. */
|
|
59
|
-
const library = createChildren(durable);
|
|
60
32
|
/**
|
|
61
33
|
* Runs the host of `project` in this process: the store in `stateDir`, the children's models from `models`, the
|
|
62
34
|
* project agent in `cwd`, and the socket. It is undefined when another host holds the lock.
|
|
@@ -74,7 +46,7 @@ export async function runHost(options) {
|
|
|
74
46
|
const settings = SettingsManager.create(stateDir, agentDir, {
|
|
75
47
|
projectTrusted: new ProjectTrustStore(agentDir).get(stateDir) === true,
|
|
76
48
|
});
|
|
77
|
-
const warn = (cause) => process.stderr.write(`warning: ${
|
|
49
|
+
const warn = (cause) => process.stderr.write(`warning: ${errorMessage(cause)}\n`);
|
|
78
50
|
// The store is there once the harness is open; entries get `place` before, and use the store in their tasks.
|
|
79
51
|
let harness;
|
|
80
52
|
const place = {
|
|
@@ -88,7 +60,7 @@ export async function runHost(options) {
|
|
|
88
60
|
return registry.snapshot();
|
|
89
61
|
},
|
|
90
62
|
};
|
|
91
|
-
const entries = await loadEntries(stateDir, agentDir, settings, place, warn).catch((cause) => {
|
|
63
|
+
const entries = await loadEntries(stateDir, agentDir, settings, place, library, warn).catch((cause) => {
|
|
92
64
|
lock.close();
|
|
93
65
|
throw cause;
|
|
94
66
|
});
|
|
@@ -107,7 +79,9 @@ export async function runHost(options) {
|
|
|
107
79
|
caller: async (api) => String(api.conversationId),
|
|
108
80
|
send: (request, signal) => handleRequest(request, { ...base, signal }),
|
|
109
81
|
});
|
|
110
|
-
|
|
82
|
+
// Each script runs in a worker thread, in a VM of at most 256 MB, as in Pi.
|
|
83
|
+
const sandbox = new CodemodeSandbox({ memoryLimitBytes: 256 * 1024 * 1024 });
|
|
84
|
+
const selected = [ChildContext, tool, createCodemodeExtension({ sandbox }), ...entries.extensions];
|
|
111
85
|
try {
|
|
112
86
|
for (const extension of selected)
|
|
113
87
|
registry.install(extension);
|
|
@@ -148,7 +122,7 @@ export async function runHost(options) {
|
|
|
148
122
|
}
|
|
149
123
|
catch (error) {
|
|
150
124
|
await harness.close(context);
|
|
151
|
-
const message =
|
|
125
|
+
const message = errorMessage(error);
|
|
152
126
|
if (!message.includes("(subagent.children) requires migration from version"))
|
|
153
127
|
throw error;
|
|
154
128
|
throw new Error(`${join(stateDir, "durable.sqlite")} was written by an older pi-subagent (${message}). Stop the host, remove the file and run the command again.`);
|
|
@@ -188,7 +162,7 @@ export async function runHost(options) {
|
|
|
188
162
|
};
|
|
189
163
|
}
|
|
190
164
|
catch (error) {
|
|
191
|
-
reply = { ok: false, error:
|
|
165
|
+
reply = { ok: false, error: errorMessage(error) };
|
|
192
166
|
}
|
|
193
167
|
if (socket.writable)
|
|
194
168
|
socket.write(`${JSON.stringify(reply)}\n`);
|
|
@@ -210,6 +184,7 @@ export async function runHost(options) {
|
|
|
210
184
|
});
|
|
211
185
|
}).catch(async (cause) => {
|
|
212
186
|
await entries.close();
|
|
187
|
+
await sandbox.close();
|
|
213
188
|
await harness.close(context);
|
|
214
189
|
lock.close();
|
|
215
190
|
throw cause;
|
|
@@ -224,8 +199,10 @@ export async function runHost(options) {
|
|
|
224
199
|
for (const abort of aborts)
|
|
225
200
|
abort.abort();
|
|
226
201
|
try {
|
|
227
|
-
// Entries close first: an entry may still need the open harness.
|
|
202
|
+
// Entries close first: an entry may still need the open harness. The sandbox closes before the harness, so a
|
|
203
|
+
// script that runs gets no outcome and continues after a restart.
|
|
228
204
|
await entries.close();
|
|
205
|
+
await sandbox.close();
|
|
229
206
|
await harness.close(context);
|
|
230
207
|
for (const env of envs.values())
|
|
231
208
|
await env.cleanup(context);
|
|
@@ -270,7 +247,8 @@ async function handleRequest(request, handler) {
|
|
|
270
247
|
}
|
|
271
248
|
}
|
|
272
249
|
case "input":
|
|
273
|
-
|
|
250
|
+
// A conversation takes input only while it works, so its input is always a steer.
|
|
251
|
+
return sendTo(request.conversation === undefined ? request : { ...request, followUp: false }, await target(handler, request));
|
|
274
252
|
case "wait":
|
|
275
253
|
return waitChild(request, handler);
|
|
276
254
|
case "status":
|
|
@@ -279,12 +257,10 @@ async function handleRequest(request, handler) {
|
|
|
279
257
|
return abortChild(request, handler);
|
|
280
258
|
case "configure":
|
|
281
259
|
return configureChild(request, handler);
|
|
282
|
-
case "models":
|
|
283
|
-
return modelInfos(handler.place.models);
|
|
284
260
|
case "watch":
|
|
285
261
|
if (handler.connection === undefined)
|
|
286
262
|
throw new Error("A watch needs a connection");
|
|
287
|
-
return serveWatch(handler, request
|
|
263
|
+
return serveWatch(handler, request, handler.connection);
|
|
288
264
|
case "start":
|
|
289
265
|
// A child that a task opens without a model takes the project agent's, so the agent is set up first. Without
|
|
290
266
|
// a model for the agent, the task still starts: its children may name their own.
|
|
@@ -452,7 +428,7 @@ async function waitChild(request, handler) {
|
|
|
452
428
|
/**
|
|
453
429
|
* The requests of `sent` from `read` on that a wait for `main` gives before the answer to `main`, and the new `read`.
|
|
454
430
|
* When `main` is a request of the caller, they are the ones before it, and `main` counts as read; a send after it
|
|
455
|
-
* stays unread. Otherwise, such as for
|
|
431
|
+
* stays unread. Otherwise, such as for the input of a mirror session, they are the caller's requests known when the wait started.
|
|
456
432
|
*/
|
|
457
433
|
function unreadBefore(sent, read, main, atStart) {
|
|
458
434
|
const index = main === null ? -1 : sent.indexOf(main);
|
|
@@ -488,79 +464,18 @@ async function markSent(harness, handle, requestId, add) {
|
|
|
488
464
|
}, context);
|
|
489
465
|
}
|
|
490
466
|
async function abortChild(request, handler) {
|
|
491
|
-
const found = await target(handler, request
|
|
467
|
+
const found = await target(handler, request);
|
|
492
468
|
// Without `background`, an abort of the project agent does not reach the anchors of the children, which keep running.
|
|
493
469
|
await found.conversation.abort(context);
|
|
494
470
|
return found.info();
|
|
495
471
|
}
|
|
496
472
|
async function configureChild(request, handler) {
|
|
497
|
-
|
|
473
|
+
// Only the handle: the schema of `configure` does not refuse an extra `conversation` key.
|
|
474
|
+
const found = await target(handler, { handle: request.handle });
|
|
498
475
|
const agent = await found.conversation.agent(context);
|
|
499
476
|
await found.conversation.configure(resolveModel(handler.place.models, agent, request), context);
|
|
500
477
|
return found.info();
|
|
501
478
|
}
|
|
502
|
-
/**
|
|
503
|
-
* The conversation of `handle`, a child or the project agent, with its info, its input, and the record of a child.
|
|
504
|
-
*/
|
|
505
|
-
async function target(handler, handle) {
|
|
506
|
-
if (handle !== AGENT_HANDLE)
|
|
507
|
-
return childTarget(handler, handle, await childRecord(handler.harness, handle));
|
|
508
|
-
const root = await handler.agent();
|
|
509
|
-
const { harness, place, project } = handler;
|
|
510
|
-
const turn = library.turn(place, root.id, root, async (readContext) => (await harness.snapshot(AgentState, root.id, readContext))?.lastRequest, async (tx, requestId) => {
|
|
511
|
-
(await tx.doc(AgentState, root.id)).lastRequest = requestId;
|
|
512
|
-
});
|
|
513
|
-
return {
|
|
514
|
-
conversation: root,
|
|
515
|
-
info: async () => ({
|
|
516
|
-
...(await agentFields(root)),
|
|
517
|
-
handle,
|
|
518
|
-
name: `${project} agent`,
|
|
519
|
-
state: await turn.state(context),
|
|
520
|
-
}),
|
|
521
|
-
send: (input) => turn.send(input, context),
|
|
522
|
-
record: undefined,
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
/** The target of the child `handle`, whose record is `record`. */
|
|
526
|
-
async function childTarget(handler, handle, record) {
|
|
527
|
-
const child = await childOf(handler, record);
|
|
528
|
-
return {
|
|
529
|
-
conversation: child.conversation,
|
|
530
|
-
info: () => childInfo(handler, handle, record),
|
|
531
|
-
send: (input) => child.send(input, context),
|
|
532
|
-
record,
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
async function modelInfos(models) {
|
|
536
|
-
return (await models.getAvailable()).map((model) => ({
|
|
537
|
-
provider: model.provider,
|
|
538
|
-
id: model.id,
|
|
539
|
-
name: model.name,
|
|
540
|
-
contextWindow: model.contextWindow,
|
|
541
|
-
thinkingLevels: getSupportedThinkingLevels(model),
|
|
542
|
-
}));
|
|
543
|
-
}
|
|
544
|
-
async function childRecord(harness, handle) {
|
|
545
|
-
const state = await harness.snapshot(Children, context);
|
|
546
|
-
const record = recordOf(state, handle);
|
|
547
|
-
if (record === undefined)
|
|
548
|
-
throw new Error(`Unknown subagent: ${handle}`);
|
|
549
|
-
return record;
|
|
550
|
-
}
|
|
551
|
-
/** The record of `handle`, which must belong to `caller`. */
|
|
552
|
-
async function ownRecord(harness, handle, caller) {
|
|
553
|
-
const record = await childRecord(harness, handle);
|
|
554
|
-
if (record.caller !== caller)
|
|
555
|
-
throw new Error(`Subagent ${handle} belongs to another session`);
|
|
556
|
-
return record;
|
|
557
|
-
}
|
|
558
|
-
async function childOf(handler, record) {
|
|
559
|
-
const child = await library.find(handler.place, handler.owner, { id: String(record.conversationId) }, context);
|
|
560
|
-
if (child === undefined)
|
|
561
|
-
throw new Error(`Missing conversation ${record.conversationId}`);
|
|
562
|
-
return child;
|
|
563
|
-
}
|
|
564
479
|
function failureText(handle, reason, detail) {
|
|
565
480
|
if (reason === "aborted")
|
|
566
481
|
return `${handle} was aborted`;
|
|
@@ -571,27 +486,6 @@ function failureText(handle, reason, detail) {
|
|
|
571
486
|
: undefined;
|
|
572
487
|
return text === undefined ? `${handle} failed: ${reason}` : `${handle} failed: ${text}`;
|
|
573
488
|
}
|
|
574
|
-
/** The info of `handle`. A caller that already has the record gives it as `record`. */
|
|
575
|
-
async function childInfo(handler, handle, record) {
|
|
576
|
-
record ??= await childRecord(handler.harness, handle);
|
|
577
|
-
const child = await childOf(handler, record);
|
|
578
|
-
const childState = await child.state(context);
|
|
579
|
-
// A closed child is idle: the watch can still send it before its record goes and the `stopped` line follows.
|
|
580
|
-
const state = childState === "closed" ? "idle" : childState;
|
|
581
|
-
const fields = await agentFields(child.conversation);
|
|
582
|
-
return record.name === undefined ? { handle, state, ...fields } : { handle, name: record.name, state, ...fields };
|
|
583
|
-
}
|
|
584
|
-
/** The model, thinking level and directory of the agent of `conversation`, for its info. */
|
|
585
|
-
async function agentFields(conversation) {
|
|
586
|
-
const agent = await conversation.agent(context);
|
|
587
|
-
return {
|
|
588
|
-
provider: agent.model?.provider ?? "",
|
|
589
|
-
model: agent.model?.modelId ?? "",
|
|
590
|
-
thinking: agent.thinkingLevel,
|
|
591
|
-
// `spawnChild` refuses a spawn without `cwd`; the fallback only satisfies pi-durable's optional type.
|
|
592
|
-
cwd: agent.cwd ?? "",
|
|
593
|
-
};
|
|
594
|
-
}
|
|
595
489
|
/**
|
|
596
490
|
* The root conversation as the agent of `project`. The first time, it gets Pi's default model when it has a login, else
|
|
597
491
|
* the first model with a login, Pi's default thinking level, and the directory `cwd`. Without a model with a login, it
|
|
@@ -619,13 +513,14 @@ async function setUpAgent(root, models, settings, project, cwd) {
|
|
|
619
513
|
}
|
|
620
514
|
/**
|
|
621
515
|
* Opens a watch of `handle` on `connection` and returns the reply, with the view and the child. After the reply, each
|
|
622
|
-
* commit to the child's conversation
|
|
623
|
-
* to drain, so pi-durable replaces the frames it holds back with one frame
|
|
624
|
-
* socket closes, when setup fails, or when the connection ends; a wait for
|
|
625
|
-
* goes with it. When the child stops, the
|
|
516
|
+
* commit to the child's conversation gives one `{ event: WatchLine }` line with its change, and each rename one with
|
|
517
|
+
* an empty change. It waits for a slow socket to drain, so pi-durable replaces the frames it holds back with one frame
|
|
518
|
+
* of the whole view. The watches stop when the socket closes, when setup fails, or when the connection ends; a wait for
|
|
519
|
+
* `drain` then stays on the closed socket and goes with it. When the child stops, the client gets a `stopped` event and the connection ends; when the conversation
|
|
626
520
|
* watch ends for another reason, the connection ends.
|
|
627
521
|
*/
|
|
628
|
-
async function serveWatch(handler,
|
|
522
|
+
async function serveWatch(handler, ref, connection) {
|
|
523
|
+
const handle = ref.conversation ?? ref.handle;
|
|
629
524
|
const { harness } = handler;
|
|
630
525
|
const { socket } = connection;
|
|
631
526
|
const write = (event) => socket.write(`${JSON.stringify({ event })}\n`);
|
|
@@ -642,8 +537,9 @@ async function serveWatch(handler, handle, connection) {
|
|
|
642
537
|
let conversation;
|
|
643
538
|
let last;
|
|
644
539
|
try {
|
|
645
|
-
found = await target(handler,
|
|
646
|
-
// The project agent
|
|
540
|
+
found = await target(handler, ref);
|
|
541
|
+
// The project agent and a conversation target have no record and no `Children` watch: they are never renamed
|
|
542
|
+
// or stopped.
|
|
647
543
|
if (found.record !== undefined) {
|
|
648
544
|
// The `Children` watch opens before the info, so it gets each later rename and stop of the child.
|
|
649
545
|
children = await harness.watchDoc(Children, watchContext);
|
|
@@ -668,14 +564,14 @@ async function serveWatch(handler, handle, connection) {
|
|
|
668
564
|
// pi-durable runs the callbacks of one watch in order, but not those of two watches. Both watches write through
|
|
669
565
|
// this one chain, so a line never has older info than the line before it.
|
|
670
566
|
let queue = Promise.resolve();
|
|
671
|
-
const send = (
|
|
567
|
+
const send = (change) => {
|
|
672
568
|
queue = queue.then(async () => {
|
|
673
569
|
// Some store reads per commit; cache the agent fields if streaming gets slow. A store failure drops this
|
|
674
570
|
// line. A stopped child still gives `idle` here, and its `stopped` line follows.
|
|
675
571
|
const child = await found.info().catch(() => undefined);
|
|
676
572
|
if (child === undefined || !socket.writable)
|
|
677
573
|
return;
|
|
678
|
-
if (!write({
|
|
574
|
+
if (!write({ change, child }))
|
|
679
575
|
await once(socket, "drain").catch(() => { });
|
|
680
576
|
});
|
|
681
577
|
return queue;
|
|
@@ -690,17 +586,13 @@ async function serveWatch(handler, handle, connection) {
|
|
|
690
586
|
}
|
|
691
587
|
else if (next.name !== found.record?.name) {
|
|
692
588
|
found = { ...found, record: next, info: () => childInfo(handler, handle, next) };
|
|
693
|
-
await send(
|
|
589
|
+
await send({});
|
|
694
590
|
}
|
|
695
591
|
});
|
|
696
|
-
conversation.start((
|
|
592
|
+
conversation.start((view, ops) => send(viewChange(view, ops)));
|
|
697
593
|
});
|
|
698
594
|
return { view: conversation.value, child: last };
|
|
699
595
|
}
|
|
700
|
-
/** The record of `handle` in the `Children` value `state`. */
|
|
701
|
-
function recordOf(state, handle) {
|
|
702
|
-
return state != null && Object.hasOwn(state.children, handle) ? state.children[handle] : undefined;
|
|
703
|
-
}
|
|
704
596
|
/**
|
|
705
597
|
* The lock of the host, a write transaction on `lock.sqlite`, held until `close()`. The system drops it when the
|
|
706
598
|
* process ends. It is undefined when another process holds it. `IMMEDIATE`, not `EXCLUSIVE`: two hosts that start at
|
|
@@ -756,72 +648,3 @@ function liveTasks(tasks) {
|
|
|
756
648
|
}
|
|
757
649
|
return tasks.length - dead.size;
|
|
758
650
|
}
|
|
759
|
-
/** A `package.json`, which is a JSON object. */
|
|
760
|
-
const PackageJson = Type.Object({});
|
|
761
|
-
/** The part of a package's `package.json` that names its entries. */
|
|
762
|
-
const Manifest = Type.Object({
|
|
763
|
-
piDurable: Type.Optional(Type.Object({ extensions: Type.Optional(Type.Array(Type.String())) })),
|
|
764
|
-
});
|
|
765
|
-
/**
|
|
766
|
-
* The `piDurable` entries of the installed packages in Pi's `settingsManager` for `cwd`, then the entries of
|
|
767
|
-
* `entries.json` (see `readEntries`), in one shared event bus, as one entry whose `close()` closes each of them and
|
|
768
|
-
* passes its errors to `warn`. When an entry fails to load, the entries loaded before it close. Missing packages are
|
|
769
|
-
* skipped, not installed, and so is an entry module that does not exist, such as one of an uninstalled package.
|
|
770
|
-
*/
|
|
771
|
-
async function loadEntries(cwd, agentDir, settingsManager, place, warn) {
|
|
772
|
-
const modules = new Set();
|
|
773
|
-
const roots = new DefaultPackageManager({ cwd, agentDir, settingsManager })
|
|
774
|
-
.listConfiguredPackages()
|
|
775
|
-
.flatMap((found) => found.installedPath ?? []);
|
|
776
|
-
for (const root of new Set(roots)) {
|
|
777
|
-
const path = join(root, "package.json");
|
|
778
|
-
const manifest = JSON.parse(await readFile(path, "utf8").catch(() => "{}"));
|
|
779
|
-
if (!Value.Check(PackageJson, manifest)) {
|
|
780
|
-
warn(new Error(`${path} is not a valid package.json`));
|
|
781
|
-
continue;
|
|
782
|
-
}
|
|
783
|
-
if (!Value.Check(Manifest, manifest)) {
|
|
784
|
-
warn(new Error(`${path} has an invalid piDurable field`));
|
|
785
|
-
continue;
|
|
786
|
-
}
|
|
787
|
-
for (const path of manifest.piDurable?.extensions ?? [])
|
|
788
|
-
modules.add(resolvePath(root, path));
|
|
789
|
-
}
|
|
790
|
-
for (const module of Object.values(readEntries()))
|
|
791
|
-
if (existsSync(module))
|
|
792
|
-
modules.add(module);
|
|
793
|
-
const events = createEventBus();
|
|
794
|
-
const entries = [];
|
|
795
|
-
const close = async () => {
|
|
796
|
-
for (const result of await Promise.allSettled(entries.map((entry) => entry.close()))) {
|
|
797
|
-
if (result.status === "rejected")
|
|
798
|
-
warn(result.reason);
|
|
799
|
-
}
|
|
800
|
-
};
|
|
801
|
-
try {
|
|
802
|
-
for (const module of modules) {
|
|
803
|
-
const entry = await importEntry(module);
|
|
804
|
-
entries.push(await entry.default({ durable, ai, events, children: library, place }));
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
catch (error) {
|
|
808
|
-
await close();
|
|
809
|
-
throw error;
|
|
810
|
-
}
|
|
811
|
-
return {
|
|
812
|
-
extensions: entries.flatMap((entry) => entry.extensions),
|
|
813
|
-
tasks: entries.flatMap((entry) => entry.tasks ?? []),
|
|
814
|
-
close,
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
/**
|
|
818
|
-
* Imports the entry `module`. Node does not strip types under `node_modules`, where Pi installs npm packages, so jiti
|
|
819
|
-
* loads a TypeScript entry there.
|
|
820
|
-
*/
|
|
821
|
-
async function importEntry(module) {
|
|
822
|
-
if (module.endsWith(".ts") && module.split(sep).includes("node_modules")) {
|
|
823
|
-
const { createJiti } = await import("jiti");
|
|
824
|
-
return createJiti(import.meta.url).import(module);
|
|
825
|
-
}
|
|
826
|
-
return import(__rewriteRelativeImportExtension(pathToFileURL(module).href));
|
|
827
|
-
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AnyTask, Extension } from "@earendil-works/pi-durable";
|
|
3
|
+
import type { createChildren, Place } from "./children.ts";
|
|
4
|
+
type ChildrenLibrary = ReturnType<typeof createChildren>;
|
|
5
|
+
/** What the default export of an entry returns: its extensions, the tasks that `start` runs, and `close`. */
|
|
6
|
+
export type Entry = {
|
|
7
|
+
readonly extensions: readonly Extension[];
|
|
8
|
+
readonly tasks?: readonly AnyTask[];
|
|
9
|
+
close(): Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The `piDurable` entries of the installed packages in Pi's `settingsManager` for `cwd`, then the entries of
|
|
13
|
+
* `entries.json` (see `readEntries`), with the children library `children`, in one shared event bus, as one entry whose `close()` closes each of them and
|
|
14
|
+
* passes its errors to `warn`. When an entry fails to load, the entries loaded before it close. Missing packages are
|
|
15
|
+
* skipped, not installed, and so is an entry module that does not exist, such as one of an uninstalled package.
|
|
16
|
+
*/
|
|
17
|
+
export declare function loadEntries(cwd: string, agentDir: string, settingsManager: SettingsManager, place: Place, children: ChildrenLibrary, warn: (cause: unknown) => void): Promise<Required<Entry>>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The entries that the host loads: the `piDurable` entries of Pi's packages and the entries of `entries.json`. Each
|
|
11
|
+
* entry is a module whose default export gives extensions and tasks to the host.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { readFile } from "node:fs/promises";
|
|
15
|
+
import { join, resolve as resolvePath, sep } from "node:path";
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
import * as ai from "@earendil-works/pi-ai";
|
|
18
|
+
import { createEventBus, DefaultPackageManager, } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import * as durable from "@earendil-works/pi-durable";
|
|
20
|
+
import { Type } from "typebox";
|
|
21
|
+
import { Value } from "typebox/value";
|
|
22
|
+
import { readEntries } from "./entries.js";
|
|
23
|
+
/** A `package.json`, which is a JSON object. */
|
|
24
|
+
const PackageJson = Type.Object({});
|
|
25
|
+
/** The part of a package's `package.json` that names its entries. */
|
|
26
|
+
const Manifest = Type.Object({
|
|
27
|
+
piDurable: Type.Optional(Type.Object({ extensions: Type.Optional(Type.Array(Type.String())) })),
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* The `piDurable` entries of the installed packages in Pi's `settingsManager` for `cwd`, then the entries of
|
|
31
|
+
* `entries.json` (see `readEntries`), with the children library `children`, in one shared event bus, as one entry whose `close()` closes each of them and
|
|
32
|
+
* passes its errors to `warn`. When an entry fails to load, the entries loaded before it close. Missing packages are
|
|
33
|
+
* skipped, not installed, and so is an entry module that does not exist, such as one of an uninstalled package.
|
|
34
|
+
*/
|
|
35
|
+
export async function loadEntries(cwd, agentDir, settingsManager, place, children, warn) {
|
|
36
|
+
const modules = new Set();
|
|
37
|
+
const roots = new DefaultPackageManager({ cwd, agentDir, settingsManager })
|
|
38
|
+
.listConfiguredPackages()
|
|
39
|
+
.flatMap((found) => found.installedPath ?? []);
|
|
40
|
+
for (const root of new Set(roots)) {
|
|
41
|
+
const path = join(root, "package.json");
|
|
42
|
+
const manifest = JSON.parse(await readFile(path, "utf8").catch(() => "{}"));
|
|
43
|
+
if (!Value.Check(PackageJson, manifest)) {
|
|
44
|
+
warn(new Error(`${path} is not a valid package.json`));
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (!Value.Check(Manifest, manifest)) {
|
|
48
|
+
warn(new Error(`${path} has an invalid piDurable field`));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
for (const path of manifest.piDurable?.extensions ?? [])
|
|
52
|
+
modules.add(resolvePath(root, path));
|
|
53
|
+
}
|
|
54
|
+
for (const module of Object.values(readEntries()))
|
|
55
|
+
if (existsSync(module))
|
|
56
|
+
modules.add(module);
|
|
57
|
+
const events = createEventBus();
|
|
58
|
+
const entries = [];
|
|
59
|
+
const close = async () => {
|
|
60
|
+
for (const result of await Promise.allSettled(entries.map((entry) => entry.close()))) {
|
|
61
|
+
if (result.status === "rejected")
|
|
62
|
+
warn(result.reason);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
for (const module of modules) {
|
|
67
|
+
const entry = await importEntry(module);
|
|
68
|
+
entries.push(await entry.default({ durable, ai, events, children, place }));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
await close();
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
extensions: entries.flatMap((entry) => entry.extensions),
|
|
77
|
+
tasks: entries.flatMap((entry) => entry.tasks ?? []),
|
|
78
|
+
close,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Imports the entry `module`. Node does not strip types under `node_modules`, where Pi installs npm packages, so jiti
|
|
83
|
+
* loads a TypeScript entry there.
|
|
84
|
+
*/
|
|
85
|
+
async function importEntry(module) {
|
|
86
|
+
if (module.endsWith(".ts") && module.split(sep).includes("node_modules")) {
|
|
87
|
+
const { createJiti } = await import("jiti");
|
|
88
|
+
return createJiti(import.meta.url).import(module);
|
|
89
|
+
}
|
|
90
|
+
return import(__rewriteRelativeImportExtension(pathToFileURL(module).href));
|
|
91
|
+
}
|
package/dist/mirror.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ConversationView } from "@earendil-works/pi-durable";
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
|
+
import { type ChildInfo, type Request } from "./protocol.ts";
|
|
4
|
+
/** The `customType` of the session entry that holds the target of a mirror session. */
|
|
5
|
+
export declare const MIRROR_ENTRY = "harness-mirror";
|
|
6
|
+
/**
|
|
7
|
+
* The conversation that a mirror session shows, in the host of `project`: the child or project agent `handle`, or the
|
|
8
|
+
* conversation `conversation` that the children library opened. Each refuses the other key.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MirrorTarget: Type.TUnion<[Type.TObject<{
|
|
11
|
+
project: Type.TString;
|
|
12
|
+
handle: Type.TString;
|
|
13
|
+
conversation: Type.TOptional<Type.TNever>;
|
|
14
|
+
}>, Type.TObject<{
|
|
15
|
+
project: Type.TString;
|
|
16
|
+
conversation: Type.TString;
|
|
17
|
+
handle: Type.TOptional<Type.TNever>;
|
|
18
|
+
}>]>;
|
|
19
|
+
export type MirrorTarget = Static<typeof MirrorTarget>;
|
|
20
|
+
/** Whether the data of a session entry is a mirror target. */
|
|
21
|
+
export declare function isMirrorTarget(data: unknown): data is MirrorTarget;
|
|
22
|
+
/**
|
|
23
|
+
* The Pi flags that open a mirror session of a target: the project, and the child or the conversation in place of the
|
|
24
|
+
* project agent.
|
|
25
|
+
*/
|
|
26
|
+
export declare const PI_FLAGS: {
|
|
27
|
+
readonly project: "harness";
|
|
28
|
+
readonly child: "harness-child";
|
|
29
|
+
readonly conversation: "harness-conversation";
|
|
30
|
+
};
|
|
31
|
+
/** The arguments of `pi` that open a mirror session of `target`. */
|
|
32
|
+
export declare function piArgs(target: MirrorTarget): string[];
|
|
33
|
+
/**
|
|
34
|
+
* The arguments of the `harness` CLI that open `target` in Pi: `<project>` for the project agent, `--project <project>
|
|
35
|
+
* attach <handle>` for a child, and `--project <project> attach --conversation <id>` for a conversation.
|
|
36
|
+
*/
|
|
37
|
+
export declare function harnessArgs(target: MirrorTarget): string[];
|
|
38
|
+
/** The title of the agent of `target` before the host gives its name: the handle or the conversation ID of a child. */
|
|
39
|
+
export declare function targetTitle(target: MirrorTarget): string;
|
|
40
|
+
/** A change of the model or the thinking level of the mirrored conversation. */
|
|
41
|
+
export type MirrorConfigure = Omit<Extract<Request, {
|
|
42
|
+
type: "configure";
|
|
43
|
+
}>, "type" | "handle">;
|
|
44
|
+
/**
|
|
45
|
+
* The connection of a mirror: before the first one, open, connecting again after the host closed it, or failed for
|
|
46
|
+
* good, with the reason.
|
|
47
|
+
*/
|
|
48
|
+
export type MirrorStatus = {
|
|
49
|
+
readonly kind: "connecting" | "open" | "reconnecting";
|
|
50
|
+
} | {
|
|
51
|
+
readonly kind: "failed";
|
|
52
|
+
readonly reason: string;
|
|
53
|
+
};
|
|
54
|
+
export type MirrorOptions = {
|
|
55
|
+
/** The waits before the tries to connect again after the host closed the connection. Default 0, 2 and 5 s. */
|
|
56
|
+
readonly retryDelaysMs?: readonly number[];
|
|
57
|
+
/** Whether a connection starts the host when none runs. Default true. */
|
|
58
|
+
readonly start?: boolean;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* A live copy of one conversation of a harness host, and the requests that act on it. It connects at once, which starts
|
|
62
|
+
* the host when none runs, and connects again when the host closes the connection, with one try after each of
|
|
63
|
+
* `retryDelaysMs`. It fails for good when the first connection fails, when all the tries fail, when the child is
|
|
64
|
+
* stopped, and when the host closes a connection that was opened again less than 10 s before, so that a host that fails
|
|
65
|
+
* at each start does not start again and again. `onChange` runs after each change of `status`, `view` and `child`, with
|
|
66
|
+
* the status before the change.
|
|
67
|
+
*/
|
|
68
|
+
export declare class Mirror {
|
|
69
|
+
#private;
|
|
70
|
+
readonly target: MirrorTarget;
|
|
71
|
+
constructor(target: MirrorTarget, onChange: (previous: MirrorStatus) => void, options?: MirrorOptions);
|
|
72
|
+
get status(): MirrorStatus;
|
|
73
|
+
/** The conversation, as the last connection gave it. It is undefined before the first connection. */
|
|
74
|
+
get view(): ConversationView | undefined;
|
|
75
|
+
/** The child or the project agent, as the last connection gave it. It is undefined before the first connection. */
|
|
76
|
+
get child(): ChildInfo | undefined;
|
|
77
|
+
/** Whether the conversation works, as an open connection tells. */
|
|
78
|
+
get busy(): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Sends `message`: a new turn when the conversation is idle, else a steer, or a follow-up with `followUp`. It resolves
|
|
81
|
+
* once the host has the message. The message waits while the mirror connects. When its connection closes before the
|
|
82
|
+
* host answers, it goes again on the next connection with the same request ID, so that the host takes it once. It
|
|
83
|
+
* rejects when the host refuses the message, and when the mirror fails or closes before the host has it.
|
|
84
|
+
*/
|
|
85
|
+
send(message: string, followUp: boolean): Promise<void>;
|
|
86
|
+
/** Aborts the turn, and gives the texts of the queued messages that the host withdraws, steers first. */
|
|
87
|
+
abort(): Promise<string[]>;
|
|
88
|
+
/**
|
|
89
|
+
* Changes the model or the thinking level. The host refuses a model it does not know and a level it does not have. A
|
|
90
|
+
* conversation of the children library refuses it without a request.
|
|
91
|
+
*/
|
|
92
|
+
configure(change: MirrorConfigure): Promise<void>;
|
|
93
|
+
/** Closes the connection and stops connecting. The conversation keeps running in the host. */
|
|
94
|
+
close(): void;
|
|
95
|
+
}
|