@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/mirror.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A mirror session is a Pi session that shows a conversation of a harness host and sends what the user types to it.
|
|
3
|
+
* The conversation stays in the host; the Pi session keeps only its target, in one custom entry. This module has no
|
|
4
|
+
* runtime import of Pi, chord or pi-durable, so the Pi extension can load it.
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
8
|
+
import { Type } from "typebox";
|
|
9
|
+
import { Value } from "typebox/value";
|
|
10
|
+
import { connect } from "./client.js";
|
|
11
|
+
import { AGENT_HANDLE, errorMessage, runDisplayName, } from "./protocol.js";
|
|
12
|
+
import { applyChange } from "./view-change.js";
|
|
13
|
+
import { inboxOf, withdrawnTexts } from "./view-state.js";
|
|
14
|
+
/** The `customType` of the session entry that holds the target of a mirror session. */
|
|
15
|
+
export const MIRROR_ENTRY = "harness-mirror";
|
|
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
|
+
]);
|
|
24
|
+
/** Whether the data of a session entry is a mirror target. */
|
|
25
|
+
export function isMirrorTarget(data) {
|
|
26
|
+
return Value.Check(MirrorTarget, data);
|
|
27
|
+
}
|
|
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" };
|
|
33
|
+
/** The arguments of `pi` that open a mirror session of `target`. */
|
|
34
|
+
export function piArgs(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];
|
|
40
|
+
return [`--${PI_FLAGS.project}`, target.project, ...child];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The arguments of the `harness` CLI that open `target` in Pi: `<project>` for the project agent, `--project <project>
|
|
44
|
+
* attach <handle>` for a child, and `--project <project> attach --conversation <id>` for a conversation.
|
|
45
|
+
*/
|
|
46
|
+
export function harnessArgs(target) {
|
|
47
|
+
if (target.conversation !== undefined) {
|
|
48
|
+
return ["--project", target.project, "attach", "--conversation", target.conversation];
|
|
49
|
+
}
|
|
50
|
+
return target.handle === AGENT_HANDLE ? [target.project] : ["--project", target.project, "attach", target.handle];
|
|
51
|
+
}
|
|
52
|
+
/** The title of the agent of `target` before the host gives its name: the handle or the conversation ID of a child. */
|
|
53
|
+
export function targetTitle(target) {
|
|
54
|
+
if (target.conversation !== undefined)
|
|
55
|
+
return target.conversation;
|
|
56
|
+
return target.handle === AGENT_HANDLE ? `${target.project} agent` : target.handle;
|
|
57
|
+
}
|
|
58
|
+
const RETRY_DELAYS_MS = [0, 2000, 5000];
|
|
59
|
+
/** A connection that was opened again and ends sooner than this is not opened again: the host fails at each start. */
|
|
60
|
+
const SHORT_CONNECTION_MS = 10_000;
|
|
61
|
+
const HOST_CLOSED = "The harness host closed the connection";
|
|
62
|
+
/**
|
|
63
|
+
* A live copy of one conversation of a harness host, and the requests that act on it. It connects at once, which starts
|
|
64
|
+
* the host when none runs, and connects again when the host closes the connection, with one try after each of
|
|
65
|
+
* `retryDelaysMs`. It fails for good when the first connection fails, when all the tries fail, when the child is
|
|
66
|
+
* stopped, and when the host closes a connection that was opened again less than 10 s before, so that a host that fails
|
|
67
|
+
* at each start does not start again and again. `onChange` runs after each change of `status`, `view` and `child`, with
|
|
68
|
+
* the status before the change.
|
|
69
|
+
*/
|
|
70
|
+
export class Mirror {
|
|
71
|
+
target;
|
|
72
|
+
/** The target of the requests: the handle or the conversation of `target`, without the project. */
|
|
73
|
+
#ref;
|
|
74
|
+
#onChange;
|
|
75
|
+
#retryDelaysMs;
|
|
76
|
+
#start;
|
|
77
|
+
/** Stops the wait for a host that starts, and the waits between the tries, when the mirror closes. */
|
|
78
|
+
#closing = new AbortController();
|
|
79
|
+
/** The messages that the host does not have yet, in the order of `send`. */
|
|
80
|
+
#outgoing = [];
|
|
81
|
+
#status = { kind: "connecting" };
|
|
82
|
+
#view;
|
|
83
|
+
#child;
|
|
84
|
+
#client;
|
|
85
|
+
#sending = false;
|
|
86
|
+
constructor(target, onChange, options = {}) {
|
|
87
|
+
this.target = target;
|
|
88
|
+
this.#ref = target.conversation === undefined ? { handle: target.handle } : { conversation: target.conversation };
|
|
89
|
+
this.#onChange = onChange;
|
|
90
|
+
this.#retryDelaysMs = options.retryDelaysMs ?? RETRY_DELAYS_MS;
|
|
91
|
+
this.#start = options.start ?? true;
|
|
92
|
+
void this.#run();
|
|
93
|
+
}
|
|
94
|
+
get status() {
|
|
95
|
+
return this.#status;
|
|
96
|
+
}
|
|
97
|
+
/** The conversation, as the last connection gave it. It is undefined before the first connection. */
|
|
98
|
+
get view() {
|
|
99
|
+
return this.#view;
|
|
100
|
+
}
|
|
101
|
+
/** The child or the project agent, as the last connection gave it. It is undefined before the first connection. */
|
|
102
|
+
get child() {
|
|
103
|
+
return this.#child;
|
|
104
|
+
}
|
|
105
|
+
/** Whether the conversation works, as an open connection tells. */
|
|
106
|
+
get busy() {
|
|
107
|
+
return this.#status.kind === "open" && this.#child?.state === "busy";
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Sends `message`: a new turn when the conversation is idle, else a steer, or a follow-up with `followUp`. It resolves
|
|
111
|
+
* once the host has the message. The message waits while the mirror connects. When its connection closes before the
|
|
112
|
+
* host answers, it goes again on the next connection with the same request ID, so that the host takes it once. It
|
|
113
|
+
* rejects when the host refuses the message, and when the mirror fails or closes before the host has it.
|
|
114
|
+
*/
|
|
115
|
+
send(message, followUp) {
|
|
116
|
+
if (this.#status.kind === "failed")
|
|
117
|
+
return Promise.reject(new Error(this.#status.reason));
|
|
118
|
+
if (this.#closing.signal.aborted)
|
|
119
|
+
return Promise.reject(new Error("The mirror is closed"));
|
|
120
|
+
const outgoing = { requestId: randomUUID(), message, followUp, sent: Promise.withResolvers() };
|
|
121
|
+
this.#outgoing.push(outgoing);
|
|
122
|
+
void this.#flush();
|
|
123
|
+
return outgoing.sent.promise;
|
|
124
|
+
}
|
|
125
|
+
/** Aborts the turn, and gives the texts of the queued messages that the host withdraws, steers first. */
|
|
126
|
+
async abort() {
|
|
127
|
+
const { client, view } = this.#connection();
|
|
128
|
+
const texts = withdrawnTexts(inboxOf(view));
|
|
129
|
+
this.#setChild(await client.request({ type: "abort", ...this.#ref }));
|
|
130
|
+
return texts;
|
|
131
|
+
}
|
|
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
|
+
*/
|
|
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
|
+
}
|
|
141
|
+
const { client } = this.#connection();
|
|
142
|
+
this.#setChild(await client.request({ type: "configure", handle, ...change }));
|
|
143
|
+
}
|
|
144
|
+
/** Closes the connection and stops connecting. The conversation keeps running in the host. */
|
|
145
|
+
close() {
|
|
146
|
+
this.#closing.abort();
|
|
147
|
+
this.#client?.close();
|
|
148
|
+
this.#reject(new Error("The mirror is closed"));
|
|
149
|
+
}
|
|
150
|
+
/** Connects, follows the watch, and connects again after the host closed the connection, until a failure or `close()`. */
|
|
151
|
+
async #run() {
|
|
152
|
+
let attempt = await this.#connect([0]);
|
|
153
|
+
let again = false;
|
|
154
|
+
while (!(attempt instanceof Error)) {
|
|
155
|
+
const opened = Date.now();
|
|
156
|
+
this.#client = attempt.client;
|
|
157
|
+
this.#view = attempt.view;
|
|
158
|
+
this.#child = attempt.child;
|
|
159
|
+
this.#setStatus({ kind: "open" });
|
|
160
|
+
void this.#flush();
|
|
161
|
+
const end = await this.#follow(attempt.view, attempt.lines);
|
|
162
|
+
this.#client = undefined;
|
|
163
|
+
attempt.client.close();
|
|
164
|
+
if (this.#closing.signal.aborted)
|
|
165
|
+
return;
|
|
166
|
+
if (end === "stopped") {
|
|
167
|
+
this.#fail(`${runDisplayName(this.#child ?? attempt.child)} was stopped`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (again && Date.now() - opened < SHORT_CONNECTION_MS) {
|
|
171
|
+
this.#fail(`${HOST_CLOSED}. Run /reload to attach again`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
again = true;
|
|
175
|
+
this.#setStatus({ kind: "reconnecting" });
|
|
176
|
+
attempt = await this.#connect(this.#retryDelaysMs);
|
|
177
|
+
}
|
|
178
|
+
if (this.#closing.signal.aborted)
|
|
179
|
+
return;
|
|
180
|
+
this.#fail(again ? `${HOST_CLOSED}. Connecting again failed: ${attempt.message}` : attempt.message);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* A connection with the watch of the target, after one try at each delay of `delays`. It is the error of the last try
|
|
184
|
+
* when all the tries fail, and an error after `close()`.
|
|
185
|
+
*/
|
|
186
|
+
async #connect(delays) {
|
|
187
|
+
const { signal } = this.#closing;
|
|
188
|
+
let error = new Error("Could not connect to the harness host");
|
|
189
|
+
for (const delay of delays) {
|
|
190
|
+
if (delay > 0)
|
|
191
|
+
await sleep(delay, undefined, { signal }).catch(() => { });
|
|
192
|
+
if (signal.aborted)
|
|
193
|
+
break;
|
|
194
|
+
let client;
|
|
195
|
+
try {
|
|
196
|
+
client = await connect({ start: this.#start, project: this.target.project, signal });
|
|
197
|
+
if (client === undefined)
|
|
198
|
+
throw new Error("Could not connect to the harness host");
|
|
199
|
+
const watched = await client.watch(this.#ref);
|
|
200
|
+
if (signal.aborted) {
|
|
201
|
+
client.close();
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
return { ...watched, client };
|
|
205
|
+
}
|
|
206
|
+
catch (cause) {
|
|
207
|
+
client?.close();
|
|
208
|
+
error = new Error(errorMessage(cause));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return signal.aborted ? new Error("The mirror is closed") : error;
|
|
212
|
+
}
|
|
213
|
+
/** Applies each change of the watch until it ends: `stopped` when the child was stopped, else `closed`. */
|
|
214
|
+
async #follow(view, lines) {
|
|
215
|
+
let current = view;
|
|
216
|
+
for await (const line of lines) {
|
|
217
|
+
if (line.stopped)
|
|
218
|
+
return "stopped";
|
|
219
|
+
current = applyChange(current, line.change);
|
|
220
|
+
this.#view = current;
|
|
221
|
+
this.#child = line.child;
|
|
222
|
+
this.#onChange(this.#status);
|
|
223
|
+
}
|
|
224
|
+
return "closed";
|
|
225
|
+
}
|
|
226
|
+
/** Sends the waiting messages on the open connection, one at a time, in order. */
|
|
227
|
+
async #flush() {
|
|
228
|
+
if (this.#sending)
|
|
229
|
+
return;
|
|
230
|
+
this.#sending = true;
|
|
231
|
+
try {
|
|
232
|
+
for (let next = this.#outgoing[0]; next !== undefined; next = this.#outgoing[0]) {
|
|
233
|
+
const client = this.#client;
|
|
234
|
+
if (client === undefined)
|
|
235
|
+
return;
|
|
236
|
+
const { requestId, message, followUp } = next;
|
|
237
|
+
try {
|
|
238
|
+
const reply = await client.request({
|
|
239
|
+
type: "input",
|
|
240
|
+
requestId,
|
|
241
|
+
...this.#ref,
|
|
242
|
+
message,
|
|
243
|
+
followUp,
|
|
244
|
+
});
|
|
245
|
+
// `close()` and a failure settle the waiting messages themselves.
|
|
246
|
+
if (this.#outgoing[0] !== next)
|
|
247
|
+
return;
|
|
248
|
+
this.#outgoing.shift();
|
|
249
|
+
this.#setChild(reply.child);
|
|
250
|
+
next.sent.resolve();
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
// After the end of the connection, the host may have the message or not, so it goes again.
|
|
254
|
+
if (client.ended || this.#outgoing[0] !== next)
|
|
255
|
+
return;
|
|
256
|
+
this.#outgoing.shift();
|
|
257
|
+
next.sent.reject(error);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
this.#sending = false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/** The open connection and its view, or an error without one. */
|
|
266
|
+
#connection() {
|
|
267
|
+
const client = this.#client;
|
|
268
|
+
const view = this.#view;
|
|
269
|
+
if (client === undefined || view === undefined)
|
|
270
|
+
throw new Error("Not connected to the harness host");
|
|
271
|
+
return { client, view };
|
|
272
|
+
}
|
|
273
|
+
#fail(reason) {
|
|
274
|
+
this.#reject(new Error(reason));
|
|
275
|
+
this.#setStatus({ kind: "failed", reason });
|
|
276
|
+
}
|
|
277
|
+
/** Rejects the waiting messages with `error`. */
|
|
278
|
+
#reject(error) {
|
|
279
|
+
for (const outgoing of this.#outgoing.splice(0))
|
|
280
|
+
outgoing.sent.reject(error);
|
|
281
|
+
}
|
|
282
|
+
#setStatus(status) {
|
|
283
|
+
const previous = this.#status;
|
|
284
|
+
this.#status = status;
|
|
285
|
+
this.#onChange(previous);
|
|
286
|
+
}
|
|
287
|
+
#setChild(child) {
|
|
288
|
+
this.#child = child;
|
|
289
|
+
this.#onChange(this.#status);
|
|
290
|
+
}
|
|
291
|
+
}
|
package/dist/protocol.d.ts
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import type { Socket } from "node:net";
|
|
2
2
|
import type { JsonValue } from "@earendil-works/chord";
|
|
3
|
-
import type { Op } from "@earendil-works/chord/delta";
|
|
4
3
|
import type { ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
5
4
|
import type { ConversationView, TaskId, TaskOutcome, TaskState } from "@earendil-works/pi-durable";
|
|
6
5
|
import { type Static, Type } from "typebox";
|
|
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;
|
|
@@ -37,19 +41,21 @@ export type TaskInfo = {
|
|
|
37
41
|
readonly status: TaskState<JsonValue, JsonValue>["status"];
|
|
38
42
|
readonly outcome?: TaskOutcome<JsonValue>;
|
|
39
43
|
};
|
|
40
|
-
/** A model that the host can run, with what the viewer needs to show and change it. */
|
|
41
|
-
export type ModelInfo = {
|
|
42
|
-
readonly provider: string;
|
|
43
|
-
readonly id: string;
|
|
44
|
-
readonly name: string;
|
|
45
|
-
readonly contextWindow: number;
|
|
46
|
-
/** The thinking levels of the model, from `off`. */
|
|
47
|
-
readonly thinkingLevels: readonly ModelThinkingLevel[];
|
|
48
|
-
};
|
|
49
44
|
/** Any JSON value. A request line is JSON, so `start` takes any input. */
|
|
50
45
|
export declare const Json: Type.TCyclic<{
|
|
51
46
|
Json: Type.TUnion<[Type.TNull, Type.TBoolean, Type.TNumber, Type.TString, Type.TArray<Type.TRef<"Json">>, Type.TRecord<"^.*$", Type.TRef<"Json">>]>;
|
|
52
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
|
+
};
|
|
53
59
|
/** One request line. The caller is `PI_SESSION_ID`, or `cli` without one. */
|
|
54
60
|
export type Request = {
|
|
55
61
|
readonly type: "hello";
|
|
@@ -96,25 +102,19 @@ export type Request = {
|
|
|
96
102
|
readonly handle: string;
|
|
97
103
|
}
|
|
98
104
|
/**
|
|
99
|
-
* The
|
|
100
|
-
* conversation and after each rename. Every other line on the connection stays a
|
|
101
|
-
* connection carries one watch.
|
|
105
|
+
* The stream of the child for a mirror session of the Pi extension: the reply, then one `{ event: WatchLine }` line
|
|
106
|
+
* after each commit to the child's conversation and after each rename. Every other line on the connection stays a
|
|
107
|
+
* reply, in request order. A connection carries one watch.
|
|
102
108
|
*/
|
|
103
|
-
| {
|
|
109
|
+
| ({
|
|
104
110
|
readonly type: "watch";
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
| {
|
|
111
|
+
} & Target)
|
|
112
|
+
/** Stops the child's current turn. A mirror session sends it. */
|
|
113
|
+
| ({
|
|
109
114
|
readonly type: "abort";
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
/** The models that have a login, for the viewer's model picker and context window. */
|
|
113
|
-
| {
|
|
114
|
-
readonly type: "models";
|
|
115
|
-
}
|
|
115
|
+
} & Target)
|
|
116
116
|
/**
|
|
117
|
-
*
|
|
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
|
|
118
118
|
* when the model has it, and takes the nearest level that it has otherwise. The host refuses a level that the model
|
|
119
119
|
* does not have.
|
|
120
120
|
*/
|
|
@@ -125,14 +125,13 @@ export type Request = {
|
|
|
125
125
|
readonly model?: string;
|
|
126
126
|
readonly thinking?: ModelThinkingLevel;
|
|
127
127
|
}
|
|
128
|
-
/**
|
|
129
|
-
| {
|
|
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
|
+
| ({
|
|
130
130
|
readonly type: "input";
|
|
131
131
|
readonly requestId: string;
|
|
132
|
-
readonly handle: string;
|
|
133
132
|
readonly message: string;
|
|
134
133
|
readonly followUp: boolean;
|
|
135
|
-
}
|
|
134
|
+
} & Target)
|
|
136
135
|
/**
|
|
137
136
|
* Starts a background task of the kind `kind`, a task of an entry in `entries.json`, in the root conversation.
|
|
138
137
|
* A repeated `key` gives the first task, and its new kind, version and input are ignored. With `version`, a host
|
|
@@ -203,27 +202,45 @@ export declare const RequestLine: Type.TUnion<[Type.TObject<{
|
|
|
203
202
|
type: Type.TLiteral<"stop">;
|
|
204
203
|
caller: Type.TString;
|
|
205
204
|
handle: Type.TString;
|
|
206
|
-
}>, Type.TObject<{
|
|
205
|
+
}>, Type.TUnion<[Type.TObject<{
|
|
207
206
|
type: Type.TLiteral<"watch">;
|
|
208
207
|
handle: Type.TString;
|
|
208
|
+
conversation: Type.TOptional<Type.TNever>;
|
|
209
209
|
}>, Type.TObject<{
|
|
210
|
+
type: Type.TLiteral<"watch">;
|
|
211
|
+
conversation: Type.TString;
|
|
212
|
+
handle: Type.TOptional<Type.TNever>;
|
|
213
|
+
}>]>, Type.TUnion<[Type.TObject<{
|
|
210
214
|
type: Type.TLiteral<"abort">;
|
|
211
215
|
handle: Type.TString;
|
|
216
|
+
conversation: Type.TOptional<Type.TNever>;
|
|
212
217
|
}>, Type.TObject<{
|
|
213
|
-
type: Type.TLiteral<"
|
|
214
|
-
|
|
218
|
+
type: Type.TLiteral<"abort">;
|
|
219
|
+
conversation: Type.TString;
|
|
220
|
+
handle: Type.TOptional<Type.TNever>;
|
|
221
|
+
}>]>, Type.TObject<{
|
|
215
222
|
type: Type.TLiteral<"configure">;
|
|
216
223
|
handle: Type.TString;
|
|
217
224
|
provider: Type.TOptional<Type.TString>;
|
|
218
225
|
model: Type.TOptional<Type.TString>;
|
|
219
226
|
thinking: Type.TOptional<Type.TString>;
|
|
220
|
-
}>, Type.TObject<{
|
|
221
|
-
type: Type.TLiteral<"input">;
|
|
227
|
+
}>, Type.TUnion<[Type.TObject<{
|
|
222
228
|
requestId: Type.TString;
|
|
223
|
-
handle: Type.TString;
|
|
224
229
|
message: Type.TString;
|
|
225
230
|
followUp: Type.TBoolean;
|
|
231
|
+
} & {
|
|
232
|
+
type: Type.TLiteral<"input">;
|
|
233
|
+
handle: Type.TString;
|
|
234
|
+
conversation: Type.TOptional<Type.TNever>;
|
|
226
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<{
|
|
227
244
|
type: Type.TLiteral<"start">;
|
|
228
245
|
kind: Type.TString;
|
|
229
246
|
input: Type.TCyclic<{
|
|
@@ -269,7 +286,6 @@ export type Replies = {
|
|
|
269
286
|
};
|
|
270
287
|
readonly abort: ChildInfo;
|
|
271
288
|
readonly input: Replies["send"];
|
|
272
|
-
readonly models: readonly ModelInfo[];
|
|
273
289
|
readonly configure: ChildInfo;
|
|
274
290
|
readonly start: TaskInfo;
|
|
275
291
|
readonly task: TaskInfo;
|
|
@@ -277,11 +293,11 @@ export type Replies = {
|
|
|
277
293
|
readonly "abort-task": TaskInfo;
|
|
278
294
|
};
|
|
279
295
|
/**
|
|
280
|
-
* An event of a watch after its reply, sent as `{ event: WatchLine }`: the
|
|
281
|
-
* when the child was stopped.
|
|
296
|
+
* An event of a watch after its reply, sent as `{ event: WatchLine }`: the change of one commit, an empty change after a
|
|
297
|
+
* rename, or the end when the child was stopped.
|
|
282
298
|
*/
|
|
283
299
|
export type WatchLine = {
|
|
284
|
-
readonly
|
|
300
|
+
readonly change: ViewChange;
|
|
285
301
|
readonly child: ChildInfo;
|
|
286
302
|
readonly stopped?: undefined;
|
|
287
303
|
} | {
|
|
@@ -308,16 +324,26 @@ export type StatePaths = {
|
|
|
308
324
|
};
|
|
309
325
|
export declare function statePaths(project: string): StatePaths;
|
|
310
326
|
export declare function isValidRunName(value: string): boolean;
|
|
311
|
-
|
|
312
|
-
export declare function childLabel(child: {
|
|
327
|
+
type Labeled = {
|
|
313
328
|
readonly handle: string;
|
|
314
329
|
readonly name?: string | undefined;
|
|
315
|
-
|
|
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;
|
|
316
334
|
export declare function runDisplayName(metadata: {
|
|
317
335
|
readonly handle: string;
|
|
318
336
|
readonly name?: string | undefined;
|
|
319
337
|
}): string;
|
|
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;
|
|
343
|
+
/** The message of a thrown value. */
|
|
344
|
+
export declare function errorMessage(cause: unknown): string;
|
|
320
345
|
/** The line of one child in a list. */
|
|
321
346
|
export declare function childLine(child: ChildInfo): string;
|
|
322
347
|
/** Calls `onLine` with each line that arrives on `socket`, without its newline. */
|
|
323
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,9 +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
|
-
|
|
57
|
-
Type.Object({ type: Type.Literal("models") }),
|
|
75
|
+
targeted("watch", {}),
|
|
76
|
+
targeted("abort", {}),
|
|
58
77
|
Type.Object({
|
|
59
78
|
type: Type.Literal("configure"),
|
|
60
79
|
handle: Type.String(),
|
|
@@ -62,13 +81,7 @@ export const RequestLine = Type.Union([
|
|
|
62
81
|
model: Type.Optional(Type.String()),
|
|
63
82
|
thinking: Type.Optional(Type.String()),
|
|
64
83
|
}),
|
|
65
|
-
Type.
|
|
66
|
-
type: Type.Literal("input"),
|
|
67
|
-
requestId: Type.String(),
|
|
68
|
-
handle: Type.String(),
|
|
69
|
-
message: Type.String(),
|
|
70
|
-
followUp: Type.Boolean(),
|
|
71
|
-
}),
|
|
84
|
+
targeted("input", { requestId: Type.String(), message: Type.String(), followUp: Type.Boolean() }),
|
|
72
85
|
Type.Object({
|
|
73
86
|
type: Type.Literal("start"),
|
|
74
87
|
kind: Type.String(),
|
|
@@ -107,6 +120,17 @@ export function childLabel(child) {
|
|
|
107
120
|
export function runDisplayName(metadata) {
|
|
108
121
|
return metadata.name ? `${metadata.name} (${metadata.handle})` : metadata.handle;
|
|
109
122
|
}
|
|
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
|
+
*/
|
|
127
|
+
export function childTitle(child) {
|
|
128
|
+
return child.handle === AGENT_HANDLE || child.conversation !== undefined ? childLabel(child) : runDisplayName(child);
|
|
129
|
+
}
|
|
130
|
+
/** The message of a thrown value. */
|
|
131
|
+
export function errorMessage(cause) {
|
|
132
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
133
|
+
}
|
|
110
134
|
/** The line of one child in a list. */
|
|
111
135
|
export function childLine(child) {
|
|
112
136
|
return `${runDisplayName(child)} ${child.state.padEnd(8)} ${child.provider}/${child.model} ${child.thinking}`;
|
package/dist/setup.d.ts
CHANGED
|
@@ -10,3 +10,5 @@ export declare function setup(packageDir: string): void;
|
|
|
10
10
|
* throws; an error is a warning.
|
|
11
11
|
*/
|
|
12
12
|
export declare function setupOnce(packageDir: string): void;
|
|
13
|
+
/** The `PATH` without the `node_modules/.bin` folders that npm adds: pi-coding-agent there has its own `pi`. */
|
|
14
|
+
export declare function userPath(): string;
|
package/dist/setup.js
CHANGED
|
@@ -10,6 +10,7 @@ import { basename, delimiter, dirname, join, resolve, sep } from "node:path";
|
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import { Type } from "typebox";
|
|
12
12
|
import { Value } from "typebox/value";
|
|
13
|
+
import { errorMessage } from "./protocol.js";
|
|
13
14
|
const PackageJson = Type.Object({ name: Type.String(), version: Type.String() });
|
|
14
15
|
const PiSettings = Type.Object({
|
|
15
16
|
packages: Type.Optional(Type.Array(Type.Union([Type.String(), Type.Object({ source: Type.String() })]))),
|
|
@@ -42,7 +43,7 @@ export function setupOnce(packageDir) {
|
|
|
42
43
|
writeStamp(installed);
|
|
43
44
|
}
|
|
44
45
|
catch (error) {
|
|
45
|
-
process.stderr.write(`harness: setup failed: ${
|
|
46
|
+
process.stderr.write(`harness: setup failed: ${errorMessage(error)}\n`);
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
@@ -137,7 +138,7 @@ function expandHome(path) {
|
|
|
137
138
|
return path === "~" || path.startsWith("~/") ? join(homedir(), path.slice(1)) : path;
|
|
138
139
|
}
|
|
139
140
|
/** The `PATH` without the `node_modules/.bin` folders that npm adds: pi-coding-agent there has its own `pi`. */
|
|
140
|
-
function userPath() {
|
|
141
|
+
export function userPath() {
|
|
141
142
|
return (process.env.PATH ?? "")
|
|
142
143
|
.split(delimiter)
|
|
143
144
|
.filter((entry) => !entry.endsWith(join("node_modules", ".bin")))
|