@vzlentin/harness 0.3.0 → 0.4.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/README.md +13 -11
- package/dist/children.d.ts +5 -0
- package/dist/children.js +14 -4
- package/dist/cli.js +26 -8
- package/dist/client.d.ts +4 -4
- package/dist/client.js +5 -4
- package/dist/herdr.d.ts +14 -0
- package/dist/herdr.js +65 -12
- package/dist/host.js +15 -109
- package/dist/mirror.d.ts +22 -7
- package/dist/mirror.js +40 -12
- package/dist/protocol.d.ts +59 -24
- package/dist/protocol.js +29 -12
- package/dist/setup.d.ts +6 -6
- package/dist/setup.js +28 -23
- package/dist/targets.d.ts +67 -0
- package/dist/targets.js +140 -0
- package/package.json +1 -2
- package/scripts/postinstall.js +4 -4
- package/pi/attachment.ts +0 -296
- package/pi/chat.ts +0 -286
- package/pi/index.ts +0 -170
- package/pi/mirror.ts +0 -206
- package/pi/model-sync.ts +0 -102
- package/pi/package.json +0 -15
- package/pi/usage-line.ts +0 -59
package/dist/mirror.js
CHANGED
|
@@ -13,28 +13,46 @@ import { applyChange } from "./view-change.js";
|
|
|
13
13
|
import { inboxOf, withdrawnTexts } from "./view-state.js";
|
|
14
14
|
/** The `customType` of the session entry that holds the target of a mirror session. */
|
|
15
15
|
export const MIRROR_ENTRY = "harness-mirror";
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The conversation that a mirror session shows, in the host of `project`: the child or project agent `handle`, or the
|
|
18
|
+
* conversation `conversation` that the children library opened. Each refuses the other key.
|
|
19
|
+
*/
|
|
20
|
+
export const MirrorTarget = Type.Union([
|
|
21
|
+
Type.Object({ project: Type.String(), handle: Type.String(), conversation: Type.Optional(Type.Never()) }),
|
|
22
|
+
Type.Object({ project: Type.String(), conversation: Type.String(), handle: Type.Optional(Type.Never()) }),
|
|
23
|
+
]);
|
|
18
24
|
/** Whether the data of a session entry is a mirror target. */
|
|
19
25
|
export function isMirrorTarget(data) {
|
|
20
26
|
return Value.Check(MirrorTarget, data);
|
|
21
27
|
}
|
|
22
|
-
/**
|
|
23
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The Pi flags that open a mirror session of a target: the project, and the child or the conversation in place of the
|
|
30
|
+
* project agent.
|
|
31
|
+
*/
|
|
32
|
+
export const PI_FLAGS = { project: "harness", child: "harness-child", conversation: "harness-conversation" };
|
|
24
33
|
/** The arguments of `pi` that open a mirror session of `target`. */
|
|
25
34
|
export function piArgs(target) {
|
|
26
|
-
const child = target.
|
|
35
|
+
const child = target.conversation !== undefined
|
|
36
|
+
? [`--${PI_FLAGS.conversation}`, target.conversation]
|
|
37
|
+
: target.handle === AGENT_HANDLE
|
|
38
|
+
? []
|
|
39
|
+
: [`--${PI_FLAGS.child}`, target.handle];
|
|
27
40
|
return [`--${PI_FLAGS.project}`, target.project, ...child];
|
|
28
41
|
}
|
|
29
42
|
/**
|
|
30
43
|
* The arguments of the `harness` CLI that open `target` in Pi: `<project>` for the project agent, `--project <project>
|
|
31
|
-
* attach <handle>` for a child.
|
|
44
|
+
* attach <handle>` for a child, and `--project <project> attach --conversation <id>` for a conversation.
|
|
32
45
|
*/
|
|
33
46
|
export function harnessArgs(target) {
|
|
47
|
+
if (target.conversation !== undefined) {
|
|
48
|
+
return ["--project", target.project, "attach", "--conversation", target.conversation];
|
|
49
|
+
}
|
|
34
50
|
return target.handle === AGENT_HANDLE ? [target.project] : ["--project", target.project, "attach", target.handle];
|
|
35
51
|
}
|
|
36
|
-
/** The title of the agent of `target` before the host gives its name. */
|
|
52
|
+
/** The title of the agent of `target` before the host gives its name: the handle or the conversation ID of a child. */
|
|
37
53
|
export function targetTitle(target) {
|
|
54
|
+
if (target.conversation !== undefined)
|
|
55
|
+
return target.conversation;
|
|
38
56
|
return target.handle === AGENT_HANDLE ? `${target.project} agent` : target.handle;
|
|
39
57
|
}
|
|
40
58
|
const RETRY_DELAYS_MS = [0, 2000, 5000];
|
|
@@ -51,6 +69,8 @@ const HOST_CLOSED = "The harness host closed the connection";
|
|
|
51
69
|
*/
|
|
52
70
|
export class Mirror {
|
|
53
71
|
target;
|
|
72
|
+
/** The target of the requests: the handle or the conversation of `target`, without the project. */
|
|
73
|
+
#ref;
|
|
54
74
|
#onChange;
|
|
55
75
|
#retryDelaysMs;
|
|
56
76
|
#start;
|
|
@@ -65,6 +85,7 @@ export class Mirror {
|
|
|
65
85
|
#sending = false;
|
|
66
86
|
constructor(target, onChange, options = {}) {
|
|
67
87
|
this.target = target;
|
|
88
|
+
this.#ref = target.conversation === undefined ? { handle: target.handle } : { conversation: target.conversation };
|
|
68
89
|
this.#onChange = onChange;
|
|
69
90
|
this.#retryDelaysMs = options.retryDelaysMs ?? RETRY_DELAYS_MS;
|
|
70
91
|
this.#start = options.start ?? true;
|
|
@@ -105,13 +126,20 @@ export class Mirror {
|
|
|
105
126
|
async abort() {
|
|
106
127
|
const { client, view } = this.#connection();
|
|
107
128
|
const texts = withdrawnTexts(inboxOf(view));
|
|
108
|
-
this.#setChild(await client.request({ type: "abort",
|
|
129
|
+
this.#setChild(await client.request({ type: "abort", ...this.#ref }));
|
|
109
130
|
return texts;
|
|
110
131
|
}
|
|
111
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* Changes the model or the thinking level. The host refuses a model it does not know and a level it does not have. A
|
|
134
|
+
* conversation of the children library refuses it without a request.
|
|
135
|
+
*/
|
|
112
136
|
async configure(change) {
|
|
137
|
+
const { handle } = this.target;
|
|
138
|
+
if (handle === undefined) {
|
|
139
|
+
throw new Error("A conversation of the children library keeps its model and thinking level");
|
|
140
|
+
}
|
|
113
141
|
const { client } = this.#connection();
|
|
114
|
-
this.#setChild(await client.request({ type: "configure", handle
|
|
142
|
+
this.#setChild(await client.request({ type: "configure", handle, ...change }));
|
|
115
143
|
}
|
|
116
144
|
/** Closes the connection and stops connecting. The conversation keeps running in the host. */
|
|
117
145
|
close() {
|
|
@@ -168,7 +196,7 @@ export class Mirror {
|
|
|
168
196
|
client = await connect({ start: this.#start, project: this.target.project, signal });
|
|
169
197
|
if (client === undefined)
|
|
170
198
|
throw new Error("Could not connect to the harness host");
|
|
171
|
-
const watched = await client.watch(this
|
|
199
|
+
const watched = await client.watch(this.#ref);
|
|
172
200
|
if (signal.aborted) {
|
|
173
201
|
client.close();
|
|
174
202
|
break;
|
|
@@ -210,7 +238,7 @@ export class Mirror {
|
|
|
210
238
|
const reply = await client.request({
|
|
211
239
|
type: "input",
|
|
212
240
|
requestId,
|
|
213
|
-
|
|
241
|
+
...this.#ref,
|
|
214
242
|
message,
|
|
215
243
|
followUp,
|
|
216
244
|
});
|
package/dist/protocol.d.ts
CHANGED
|
@@ -5,16 +5,20 @@ import type { ConversationView, TaskId, TaskOutcome, TaskState } from "@earendil
|
|
|
5
5
|
import { type Static, Type } from "typebox";
|
|
6
6
|
import type { ViewChange } from "./view-change.ts";
|
|
7
7
|
/** The version of the protocol between the CLI and the host. A client refuses a host with another version. */
|
|
8
|
-
export declare const PROTOCOL_VERSION =
|
|
8
|
+
export declare const PROTOCOL_VERSION = 9;
|
|
9
9
|
/**
|
|
10
10
|
* The handle of the project agent, the host's root conversation, in `watch`, `input`, `abort` and `configure`. It is not
|
|
11
11
|
* hex, so no child has it.
|
|
12
12
|
*/
|
|
13
13
|
export declare const AGENT_HANDLE = "agent";
|
|
14
14
|
export type ChildState = "busy" | "idle" | "error";
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* One child as the host reports it. Provider, model and thinking level come from the child's agent. For a `{ conversation }`
|
|
17
|
+
* target, `handle` and `conversation` are the conversation ID, and `name` is the child's lineage key.
|
|
18
|
+
*/
|
|
16
19
|
export type ChildInfo = {
|
|
17
20
|
readonly handle: string;
|
|
21
|
+
readonly conversation?: string;
|
|
18
22
|
readonly name?: string;
|
|
19
23
|
readonly state: ChildState;
|
|
20
24
|
readonly provider: string;
|
|
@@ -41,6 +45,17 @@ export type TaskInfo = {
|
|
|
41
45
|
export declare const Json: Type.TCyclic<{
|
|
42
46
|
Json: Type.TUnion<[Type.TNull, Type.TBoolean, Type.TNumber, Type.TString, Type.TArray<Type.TRef<"Json">>, Type.TRecord<"^.*$", Type.TRef<"Json">>]>;
|
|
43
47
|
}, "Json">;
|
|
48
|
+
/**
|
|
49
|
+
* The target of `watch`, `input` and `abort`: a handle, or the ID of a conversation that the children library opened,
|
|
50
|
+
* such as a child of a task.
|
|
51
|
+
*/
|
|
52
|
+
export type Target = {
|
|
53
|
+
readonly handle: string;
|
|
54
|
+
readonly conversation?: never;
|
|
55
|
+
} | {
|
|
56
|
+
readonly conversation: string;
|
|
57
|
+
readonly handle?: never;
|
|
58
|
+
};
|
|
44
59
|
/** One request line. The caller is `PI_SESSION_ID`, or `cli` without one. */
|
|
45
60
|
export type Request = {
|
|
46
61
|
readonly type: "hello";
|
|
@@ -91,15 +106,13 @@ export type Request = {
|
|
|
91
106
|
* after each commit to the child's conversation and after each rename. Every other line on the connection stays a
|
|
92
107
|
* reply, in request order. A connection carries one watch.
|
|
93
108
|
*/
|
|
94
|
-
| {
|
|
109
|
+
| ({
|
|
95
110
|
readonly type: "watch";
|
|
96
|
-
|
|
97
|
-
}
|
|
111
|
+
} & Target)
|
|
98
112
|
/** Stops the child's current turn. A mirror session sends it. */
|
|
99
|
-
| {
|
|
113
|
+
| ({
|
|
100
114
|
readonly type: "abort";
|
|
101
|
-
|
|
102
|
-
}
|
|
115
|
+
} & Target)
|
|
103
116
|
/**
|
|
104
117
|
* A mirror session's change of the child's model or thinking level. It has no caller. A new model keeps the thinking level
|
|
105
118
|
* when the model has it, and takes the nearest level that it has otherwise. The host refuses a level that the model
|
|
@@ -112,14 +125,13 @@ export type Request = {
|
|
|
112
125
|
readonly model?: string;
|
|
113
126
|
readonly thinking?: ModelThinkingLevel;
|
|
114
127
|
}
|
|
115
|
-
/** A mirror session's message to the child. It has no caller. */
|
|
116
|
-
| {
|
|
128
|
+
/** A mirror session's message to the child. It has no caller. A conversation takes it only while it works, as a steer. */
|
|
129
|
+
| ({
|
|
117
130
|
readonly type: "input";
|
|
118
131
|
readonly requestId: string;
|
|
119
|
-
readonly handle: string;
|
|
120
132
|
readonly message: string;
|
|
121
133
|
readonly followUp: boolean;
|
|
122
|
-
}
|
|
134
|
+
} & Target)
|
|
123
135
|
/**
|
|
124
136
|
* Starts a background task of the kind `kind`, a task of an entry in `entries.json`, in the root conversation.
|
|
125
137
|
* A repeated `key` gives the first task, and its new kind, version and input are ignored. With `version`, a host
|
|
@@ -190,25 +202,45 @@ export declare const RequestLine: Type.TUnion<[Type.TObject<{
|
|
|
190
202
|
type: Type.TLiteral<"stop">;
|
|
191
203
|
caller: Type.TString;
|
|
192
204
|
handle: Type.TString;
|
|
193
|
-
}>, Type.TObject<{
|
|
205
|
+
}>, Type.TUnion<[Type.TObject<{
|
|
194
206
|
type: Type.TLiteral<"watch">;
|
|
195
207
|
handle: Type.TString;
|
|
208
|
+
conversation: Type.TOptional<Type.TNever>;
|
|
196
209
|
}>, Type.TObject<{
|
|
210
|
+
type: Type.TLiteral<"watch">;
|
|
211
|
+
conversation: Type.TString;
|
|
212
|
+
handle: Type.TOptional<Type.TNever>;
|
|
213
|
+
}>]>, Type.TUnion<[Type.TObject<{
|
|
197
214
|
type: Type.TLiteral<"abort">;
|
|
198
215
|
handle: Type.TString;
|
|
216
|
+
conversation: Type.TOptional<Type.TNever>;
|
|
199
217
|
}>, Type.TObject<{
|
|
218
|
+
type: Type.TLiteral<"abort">;
|
|
219
|
+
conversation: Type.TString;
|
|
220
|
+
handle: Type.TOptional<Type.TNever>;
|
|
221
|
+
}>]>, Type.TObject<{
|
|
200
222
|
type: Type.TLiteral<"configure">;
|
|
201
223
|
handle: Type.TString;
|
|
202
224
|
provider: Type.TOptional<Type.TString>;
|
|
203
225
|
model: Type.TOptional<Type.TString>;
|
|
204
226
|
thinking: Type.TOptional<Type.TString>;
|
|
205
|
-
}>, Type.TObject<{
|
|
206
|
-
type: Type.TLiteral<"input">;
|
|
227
|
+
}>, Type.TUnion<[Type.TObject<{
|
|
207
228
|
requestId: Type.TString;
|
|
208
|
-
handle: Type.TString;
|
|
209
229
|
message: Type.TString;
|
|
210
230
|
followUp: Type.TBoolean;
|
|
231
|
+
} & {
|
|
232
|
+
type: Type.TLiteral<"input">;
|
|
233
|
+
handle: Type.TString;
|
|
234
|
+
conversation: Type.TOptional<Type.TNever>;
|
|
211
235
|
}>, Type.TObject<{
|
|
236
|
+
requestId: Type.TString;
|
|
237
|
+
message: Type.TString;
|
|
238
|
+
followUp: Type.TBoolean;
|
|
239
|
+
} & {
|
|
240
|
+
type: Type.TLiteral<"input">;
|
|
241
|
+
conversation: Type.TString;
|
|
242
|
+
handle: Type.TOptional<Type.TNever>;
|
|
243
|
+
}>]>, Type.TObject<{
|
|
212
244
|
type: Type.TLiteral<"start">;
|
|
213
245
|
kind: Type.TString;
|
|
214
246
|
input: Type.TCyclic<{
|
|
@@ -292,23 +324,26 @@ export type StatePaths = {
|
|
|
292
324
|
};
|
|
293
325
|
export declare function statePaths(project: string): StatePaths;
|
|
294
326
|
export declare function isValidRunName(value: string): boolean;
|
|
295
|
-
|
|
296
|
-
export declare function childLabel(child: {
|
|
327
|
+
type Labeled = {
|
|
297
328
|
readonly handle: string;
|
|
298
329
|
readonly name?: string | undefined;
|
|
299
|
-
|
|
330
|
+
readonly conversation?: string | undefined;
|
|
331
|
+
};
|
|
332
|
+
/** The label of a child: its name, or its handle when it has no name. */
|
|
333
|
+
export declare function childLabel(child: Labeled): string;
|
|
300
334
|
export declare function runDisplayName(metadata: {
|
|
301
335
|
readonly handle: string;
|
|
302
336
|
readonly name?: string | undefined;
|
|
303
337
|
}): string;
|
|
304
|
-
/**
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
338
|
+
/**
|
|
339
|
+
* The title of a child in a client: `<name> (<handle>)`, or only the name for the project agent, which says what it is,
|
|
340
|
+
* and for a conversation, whose name is its key.
|
|
341
|
+
*/
|
|
342
|
+
export declare function childTitle(child: Labeled): string;
|
|
309
343
|
/** The message of a thrown value. */
|
|
310
344
|
export declare function errorMessage(cause: unknown): string;
|
|
311
345
|
/** The line of one child in a list. */
|
|
312
346
|
export declare function childLine(child: ChildInfo): string;
|
|
313
347
|
/** Calls `onLine` with each line that arrives on `socket`, without its newline. */
|
|
314
348
|
export declare function onLines(socket: Socket, onLine: (line: string) => void): void;
|
|
349
|
+
export {};
|
package/dist/protocol.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
/** The version of the protocol between the CLI and the host. A client refuses a host with another version. */
|
|
6
|
-
export const PROTOCOL_VERSION =
|
|
6
|
+
export const PROTOCOL_VERSION = 9;
|
|
7
7
|
/**
|
|
8
8
|
* The handle of the project agent, the host's root conversation, in `watch`, `input`, `abort` and `configure`. It is not
|
|
9
9
|
* hex, so no child has it.
|
|
@@ -20,6 +20,26 @@ export const Json = Type.Cyclic({
|
|
|
20
20
|
Type.Record(Type.String(), Type.Ref("Json")),
|
|
21
21
|
]),
|
|
22
22
|
}, "Json");
|
|
23
|
+
/**
|
|
24
|
+
* The request objects of `type` with `fields` and a target: one with `handle`, one with `conversation`. Each refuses the
|
|
25
|
+
* other key, because an object schema accepts extra keys.
|
|
26
|
+
*/
|
|
27
|
+
function targeted(type, fields) {
|
|
28
|
+
return Type.Union([
|
|
29
|
+
Type.Object({
|
|
30
|
+
...fields,
|
|
31
|
+
type: Type.Literal(type),
|
|
32
|
+
handle: Type.String(),
|
|
33
|
+
conversation: Type.Optional(Type.Never()),
|
|
34
|
+
}),
|
|
35
|
+
Type.Object({
|
|
36
|
+
...fields,
|
|
37
|
+
type: Type.Literal(type),
|
|
38
|
+
conversation: Type.String(),
|
|
39
|
+
handle: Type.Optional(Type.Never()),
|
|
40
|
+
}),
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
23
43
|
/**
|
|
24
44
|
* The check of a request line on the host. It is looser than `Request`, so the host answers a spawn without `cwd` and an
|
|
25
45
|
* unknown thinking level with its own errors.
|
|
@@ -52,8 +72,8 @@ export const RequestLine = Type.Union([
|
|
|
52
72
|
Type.Object({ type: Type.Literal("list"), caller: Type.String() }),
|
|
53
73
|
Type.Object({ type: Type.Literal("rename"), caller: Type.String(), handle: Type.String(), name: Type.String() }),
|
|
54
74
|
Type.Object({ type: Type.Literal("stop"), caller: Type.String(), handle: Type.String() }),
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
targeted("watch", {}),
|
|
76
|
+
targeted("abort", {}),
|
|
57
77
|
Type.Object({
|
|
58
78
|
type: Type.Literal("configure"),
|
|
59
79
|
handle: Type.String(),
|
|
@@ -61,13 +81,7 @@ export const RequestLine = Type.Union([
|
|
|
61
81
|
model: Type.Optional(Type.String()),
|
|
62
82
|
thinking: Type.Optional(Type.String()),
|
|
63
83
|
}),
|
|
64
|
-
Type.
|
|
65
|
-
type: Type.Literal("input"),
|
|
66
|
-
requestId: Type.String(),
|
|
67
|
-
handle: Type.String(),
|
|
68
|
-
message: Type.String(),
|
|
69
|
-
followUp: Type.Boolean(),
|
|
70
|
-
}),
|
|
84
|
+
targeted("input", { requestId: Type.String(), message: Type.String(), followUp: Type.Boolean() }),
|
|
71
85
|
Type.Object({
|
|
72
86
|
type: Type.Literal("start"),
|
|
73
87
|
kind: Type.String(),
|
|
@@ -106,9 +120,12 @@ export function childLabel(child) {
|
|
|
106
120
|
export function runDisplayName(metadata) {
|
|
107
121
|
return metadata.name ? `${metadata.name} (${metadata.handle})` : metadata.handle;
|
|
108
122
|
}
|
|
109
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* The title of a child in a client: `<name> (<handle>)`, or only the name for the project agent, which says what it is,
|
|
125
|
+
* and for a conversation, whose name is its key.
|
|
126
|
+
*/
|
|
110
127
|
export function childTitle(child) {
|
|
111
|
-
return child.handle === AGENT_HANDLE ? childLabel(child) : runDisplayName(child);
|
|
128
|
+
return child.handle === AGENT_HANDLE || child.conversation !== undefined ? childLabel(child) : runDisplayName(child);
|
|
112
129
|
}
|
|
113
130
|
/** The message of a thrown value. */
|
|
114
131
|
export function errorMessage(cause) {
|
package/dist/setup.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Registers
|
|
3
|
-
* stderr, and it does not throw for a Pi failure: it writes a
|
|
4
|
-
* that `setupOnce` reads unless `pi` failed, so the next run
|
|
5
|
-
* a user without Pi gets the hint once per version.
|
|
2
|
+
* Registers `npm:<piPackage>@<version>` in Pi when `packageDir` is a top-level global npm install, with the version of
|
|
3
|
+
* `<packageDir>/package.json`. It writes its messages to stderr, and it does not throw for a Pi failure: it writes a
|
|
4
|
+
* warning with the command to run. It writes the stamp that `setupOnce` reads unless `pi` failed, so the next run
|
|
5
|
+
* tries again. Without `pi` on PATH it writes the stamp, so a user without Pi gets the hint once per version.
|
|
6
6
|
*/
|
|
7
|
-
export declare function setup(packageDir: string): void;
|
|
7
|
+
export declare function setup(packageDir: string, piPackage: string): void;
|
|
8
8
|
/**
|
|
9
9
|
* `setup` once per version and path of the package: it does nothing when the stamp of `setup` exists. It never
|
|
10
10
|
* throws; an error is a warning.
|
|
11
11
|
*/
|
|
12
|
-
export declare function setupOnce(packageDir: string): void;
|
|
12
|
+
export declare function setupOnce(packageDir: string, piPackage: string): void;
|
|
13
13
|
/** The `PATH` without the `node_modules/.bin` folders that npm adds: pi-coding-agent there has its own `pi`. */
|
|
14
14
|
export declare function userPath(): string;
|
package/dist/setup.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Registers the Pi
|
|
3
|
-
* run it, since npm 12 does not run install scripts by default. workflows uses it
|
|
2
|
+
* Registers the Pi package of a globally installed package in Pi, as `npm:<piPackage>@<version>`. The `postinstall`
|
|
3
|
+
* script and the CLI of the package run it, since npm 12 does not run install scripts by default. workflows uses it
|
|
4
|
+
* too, as `@vzlentin/harness/setup`, with its own Pi package.
|
|
4
5
|
*/
|
|
5
6
|
import { spawnSync } from "node:child_process";
|
|
6
7
|
import { createHash } from "node:crypto";
|
|
@@ -18,28 +19,28 @@ const PiSettings = Type.Object({
|
|
|
18
19
|
/** The prefixes of the Pi package sources that are not local paths, as Pi reads them. */
|
|
19
20
|
const REMOTE = ["npm:", "git:", "github:", "http:", "https:", "ssh:", "builtin:"];
|
|
20
21
|
/**
|
|
21
|
-
* Registers
|
|
22
|
-
* stderr, and it does not throw for a Pi failure: it writes a
|
|
23
|
-
* that `setupOnce` reads unless `pi` failed, so the next run
|
|
24
|
-
* a user without Pi gets the hint once per version.
|
|
22
|
+
* Registers `npm:<piPackage>@<version>` in Pi when `packageDir` is a top-level global npm install, with the version of
|
|
23
|
+
* `<packageDir>/package.json`. It writes its messages to stderr, and it does not throw for a Pi failure: it writes a
|
|
24
|
+
* warning with the command to run. It writes the stamp that `setupOnce` reads unless `pi` failed, so the next run
|
|
25
|
+
* tries again. Without `pi` on PATH it writes the stamp, so a user without Pi gets the hint once per version.
|
|
25
26
|
*/
|
|
26
|
-
export function setup(packageDir) {
|
|
27
|
+
export function setup(packageDir, piPackage) {
|
|
27
28
|
const installed = readInstalled(packageDir);
|
|
28
29
|
if (installed === undefined || !isTopLevelGlobal(installed))
|
|
29
30
|
return;
|
|
30
|
-
if (registerExtension(installed))
|
|
31
|
+
if (registerExtension(installed, piPackage))
|
|
31
32
|
writeStamp(installed);
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
34
35
|
* `setup` once per version and path of the package: it does nothing when the stamp of `setup` exists. It never
|
|
35
36
|
* throws; an error is a warning.
|
|
36
37
|
*/
|
|
37
|
-
export function setupOnce(packageDir) {
|
|
38
|
+
export function setupOnce(packageDir, piPackage) {
|
|
38
39
|
try {
|
|
39
40
|
const installed = readInstalled(packageDir);
|
|
40
41
|
if (installed === undefined || !isTopLevelGlobal(installed) || existsSync(stampPath(installed)))
|
|
41
42
|
return;
|
|
42
|
-
if (registerExtension(installed))
|
|
43
|
+
if (registerExtension(installed, piPackage))
|
|
43
44
|
writeStamp(installed);
|
|
44
45
|
}
|
|
45
46
|
catch (error) {
|
|
@@ -76,15 +77,15 @@ function writeStamp(installed) {
|
|
|
76
77
|
writeFileSync(path, `${installed.dir}\n${installed.json.version}\n`);
|
|
77
78
|
}
|
|
78
79
|
/**
|
|
79
|
-
*
|
|
80
|
-
* entry
|
|
80
|
+
* Runs `pi install npm:<piPackage>@<version>` unless Pi already lists it. False when `pi install` failed. Pi replaces
|
|
81
|
+
* an entry with the same package name, so an older version or a range is updated in place.
|
|
81
82
|
*/
|
|
82
|
-
function registerExtension({
|
|
83
|
+
function registerExtension({ json }, piPackage) {
|
|
83
84
|
const label = basename(json.name);
|
|
84
|
-
const
|
|
85
|
-
if (piLists(
|
|
85
|
+
const source = `npm:${piPackage}@${json.version}`;
|
|
86
|
+
if (piLists(piPackage, source))
|
|
86
87
|
return true;
|
|
87
|
-
return runPi(label, ["install",
|
|
88
|
+
return runPi(label, ["install", source], `To add the Pi extension of ${label}, run: pi install ${source}`);
|
|
88
89
|
}
|
|
89
90
|
/** Runs `pi`, with a warning when it is missing or fails. False when it failed; true when it worked or is missing. */
|
|
90
91
|
function runPi(label, args, later) {
|
|
@@ -105,10 +106,11 @@ function runPi(label, args, later) {
|
|
|
105
106
|
return true;
|
|
106
107
|
}
|
|
107
108
|
/**
|
|
108
|
-
* Whether the `packages` of Pi's global settings list
|
|
109
|
-
* the agent directory, with `~` or as a
|
|
109
|
+
* Whether the `packages` of Pi's global settings list `source` exactly, or a local folder whose `package.json` has the
|
|
110
|
+
* name `piPackage`, at any version. A local path is absolute or relative to the agent directory, with `~` or as a
|
|
111
|
+
* `file:` URL, as Pi resolves it. Another remote source, such as another `npm:` spec, is not listed.
|
|
110
112
|
*/
|
|
111
|
-
function piLists(
|
|
113
|
+
function piLists(piPackage, source) {
|
|
112
114
|
const env = process.env.PI_CODING_AGENT_DIR;
|
|
113
115
|
const agentDir = env ? expandHome(env) : join(homedir(), ".pi", "agent");
|
|
114
116
|
let settings;
|
|
@@ -121,12 +123,15 @@ function piLists(extension) {
|
|
|
121
123
|
if (!Value.Check(PiSettings, settings))
|
|
122
124
|
return false;
|
|
123
125
|
return (settings.packages ?? []).some((item) => {
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
126
|
+
const entry = (Value.Check(Type.String(), item) ? item : item.source).trim();
|
|
127
|
+
if (entry === source)
|
|
128
|
+
return true;
|
|
129
|
+
if (REMOTE.some((prefix) => entry.startsWith(prefix)))
|
|
126
130
|
return false;
|
|
127
|
-
const path = source.startsWith("file://") ? fileURLToPath(source) : resolve(agentDir, expandHome(source));
|
|
128
131
|
try {
|
|
129
|
-
|
|
132
|
+
const path = entry.startsWith("file://") ? fileURLToPath(entry) : resolve(agentDir, expandHome(entry));
|
|
133
|
+
const json = JSON.parse(readFileSync(join(path, "package.json"), "utf8"));
|
|
134
|
+
return Value.Check(PackageJson, json) && json.name === piPackage;
|
|
130
135
|
}
|
|
131
136
|
catch {
|
|
132
137
|
return false;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as durable from "@earendil-works/pi-durable";
|
|
2
|
+
import { type Conversation, type ConversationId, type Harness } from "@earendil-works/pi-durable";
|
|
3
|
+
import { type Child, type Input, type Owner, type Place } from "./children.ts";
|
|
4
|
+
import { type ChildInfo, type Target } from "./protocol.ts";
|
|
5
|
+
/** What the targets use of the host. */
|
|
6
|
+
export type TargetHost = {
|
|
7
|
+
readonly harness: Harness;
|
|
8
|
+
/** The host's store, models and tools, for the children library. */
|
|
9
|
+
readonly place: Place;
|
|
10
|
+
readonly owner: Owner;
|
|
11
|
+
readonly project: string;
|
|
12
|
+
/** The project agent: the root conversation, with a model and the project directory from its first use. */
|
|
13
|
+
readonly agent: () => Promise<Conversation>;
|
|
14
|
+
};
|
|
15
|
+
export type ChildRecord = {
|
|
16
|
+
name?: string;
|
|
17
|
+
/** `PI_SESSION_ID` of the spawner: a Pi session, the conversation ID of another child, or `cli`. */
|
|
18
|
+
caller: string;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
conversationId: ConversationId;
|
|
21
|
+
/** The request IDs of the spawn and of each `send` of the caller, in order. */
|
|
22
|
+
sent?: string[];
|
|
23
|
+
/** How many requests of `sent` a `wait` gave the answer of. */
|
|
24
|
+
read?: number;
|
|
25
|
+
};
|
|
26
|
+
export type ChildrenState = {
|
|
27
|
+
children: Record<string, ChildRecord>;
|
|
28
|
+
requests: Record<string, string>;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The children by handle, and the handle of each spawn request ID. Version 2 leaves each child's last request to the
|
|
32
|
+
* children library; a store with version 1 has children that the library does not know.
|
|
33
|
+
*/
|
|
34
|
+
export declare const Children: durable.SessionDocToken<ChildrenState>;
|
|
35
|
+
/** The children library, on this process's pi-durable. */
|
|
36
|
+
export declare const library: import("./children.ts").Children;
|
|
37
|
+
/**
|
|
38
|
+
* The conversation of `ref`: a child or the project agent by handle, or a conversation of the children library by ID,
|
|
39
|
+
* with its info, its input, and the record of a child.
|
|
40
|
+
*/
|
|
41
|
+
export declare function target(handler: TargetHost, ref: Target): Promise<{
|
|
42
|
+
conversation: Conversation;
|
|
43
|
+
info: () => Promise<ChildInfo>;
|
|
44
|
+
send: (input: Input) => Promise<void>;
|
|
45
|
+
record: undefined;
|
|
46
|
+
} | {
|
|
47
|
+
conversation: Conversation;
|
|
48
|
+
info: () => Promise<ChildInfo>;
|
|
49
|
+
send: (input: Input) => Promise<void>;
|
|
50
|
+
record: ChildRecord;
|
|
51
|
+
}>;
|
|
52
|
+
export type Found = Awaited<ReturnType<typeof target>>;
|
|
53
|
+
/** The target of the child `handle`, whose record is `record`. */
|
|
54
|
+
export declare function childTarget(handler: TargetHost, handle: string, record: ChildRecord): Promise<{
|
|
55
|
+
conversation: Conversation;
|
|
56
|
+
info: () => Promise<ChildInfo>;
|
|
57
|
+
send: (input: Input) => Promise<void>;
|
|
58
|
+
record: ChildRecord;
|
|
59
|
+
}>;
|
|
60
|
+
export declare function childRecord(harness: Harness, handle: string): Promise<ChildRecord>;
|
|
61
|
+
/** The record of `handle`, which must belong to `caller`. */
|
|
62
|
+
export declare function ownRecord(harness: Harness, handle: string, caller: string): Promise<ChildRecord>;
|
|
63
|
+
export declare function childOf(handler: Pick<TargetHost, "place" | "owner">, record: ChildRecord): Promise<Child>;
|
|
64
|
+
/** The info of `handle`. A caller that already has the record gives it as `record`. */
|
|
65
|
+
export declare function childInfo(handler: Pick<TargetHost, "harness" | "place" | "owner">, handle: string, record?: ChildRecord): Promise<ChildInfo>;
|
|
66
|
+
/** The record of `handle` in the `Children` value `state`. */
|
|
67
|
+
export declare function recordOf(state: Readonly<ChildrenState> | null | undefined, handle: string): ChildRecord | undefined;
|