@agentshouse/kit 0.1.0-alpha.28 → 0.1.0-alpha.29
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 +88 -42
- package/bootstrap.json +3 -3
- package/package.json +1 -1
- package/src/binding.js +11 -0
- package/src/blocks.js +2 -26
- package/src/bridge.js +23 -13
- package/src/contract.js +3 -7
- package/src/draft.js +4 -82
- package/src/house.js +63 -47
- package/src/kit-work-stream.js +9 -1
- package/src/launcher.js +21 -6
- package/src/pacing.js +41 -0
- package/src/plan.js +37 -45
- package/src/telegram-view.js +214 -0
- package/src/telegram.js +124 -53
- package/src/turn-frames.js +69 -0
- package/src/worker.js +45 -37
package/src/house.js
CHANGED
|
@@ -5,7 +5,6 @@ import { dirname, join } from "node:path";
|
|
|
5
5
|
import { assertAcquiredCommandAdapter } from "./adapter.js";
|
|
6
6
|
import { readFrames, writeFrame } from "./channel.js";
|
|
7
7
|
import {
|
|
8
|
-
assistantDraftsPath,
|
|
9
8
|
assistantTextPath,
|
|
10
9
|
ADAPTER_KINDS,
|
|
11
10
|
INTERACTION_CORRELATION_MAX,
|
|
@@ -45,7 +44,6 @@ const RESIDENT = "Resident";
|
|
|
45
44
|
const LEASE_ACTIVE = "environment_control_lease_active";
|
|
46
45
|
const UPGRADE_REQUIRED = "kit_upgrade_required";
|
|
47
46
|
const UPDATE_DRAINING = "kit_update_draining";
|
|
48
|
-
const ASSISTANT_DRAFT_STATES = ["streamed", "unsupported"];
|
|
49
47
|
const ITEM_OUTCOMES = ["accepted", "duplicate", "rejected"];
|
|
50
48
|
|
|
51
49
|
async function readCredential(directory) {
|
|
@@ -174,12 +172,13 @@ async function residentRelay(session, request, signal, draining) {
|
|
|
174
172
|
}
|
|
175
173
|
}
|
|
176
174
|
|
|
177
|
-
export async function serveResident(session, directory, signal, drain, push, capture) {
|
|
175
|
+
export async function serveResident(session, directory, signal, drain, push, capture, stream = null) {
|
|
178
176
|
const socket = residentSocket(directory);
|
|
179
177
|
await mkdir(dirname(socket), { recursive: true, mode: 0o700 });
|
|
180
178
|
await rm(socket, { force: true });
|
|
181
179
|
const connections = new Set();
|
|
182
180
|
let holder = null;
|
|
181
|
+
let listening = null;
|
|
183
182
|
let draining = false;
|
|
184
183
|
function answerDrain(connection) {
|
|
185
184
|
draining = true;
|
|
@@ -200,7 +199,10 @@ export async function serveResident(session, directory, signal, drain, push, cap
|
|
|
200
199
|
}
|
|
201
200
|
holder = connection;
|
|
202
201
|
renewControl(session, signal).then(
|
|
203
|
-
() =>
|
|
202
|
+
() => {
|
|
203
|
+
writeFrame(connection, { held: true });
|
|
204
|
+
if (holder === connection) listening = connection;
|
|
205
|
+
},
|
|
204
206
|
(error) => {
|
|
205
207
|
writeFrame(connection, { refused: error.code ?? "kit_lease_rejected" });
|
|
206
208
|
connection.end();
|
|
@@ -216,6 +218,7 @@ export async function serveResident(session, directory, signal, drain, push, cap
|
|
|
216
218
|
connection.on("close", () => {
|
|
217
219
|
connections.delete(connection);
|
|
218
220
|
if (holder === connection) holder = null;
|
|
221
|
+
if (listening === connection) listening = null;
|
|
219
222
|
for (const cancelled of relayed.values()) cancelled.abort();
|
|
220
223
|
});
|
|
221
224
|
readFrames(connection, {
|
|
@@ -225,6 +228,10 @@ export async function serveResident(session, directory, signal, drain, push, cap
|
|
|
225
228
|
return;
|
|
226
229
|
}
|
|
227
230
|
if (!held) return;
|
|
231
|
+
if (request.stream !== undefined) {
|
|
232
|
+
if (request.stream?.type === "gap") stream?.write(request.stream);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
228
235
|
if (request.cancel === true) {
|
|
229
236
|
relayed.get(request.id)?.abort();
|
|
230
237
|
return;
|
|
@@ -256,6 +263,9 @@ export async function serveResident(session, directory, signal, drain, push, cap
|
|
|
256
263
|
await chmod(socket, 0o600);
|
|
257
264
|
return {
|
|
258
265
|
serving: () => holder !== null,
|
|
266
|
+
forward(frame) {
|
|
267
|
+
if (listening !== null && !listening.destroyed) writeFrame(listening, { stream: frame });
|
|
268
|
+
},
|
|
259
269
|
async close() {
|
|
260
270
|
for (const connection of connections) connection.destroy();
|
|
261
271
|
await new Promise((resolve) => server.close(resolve));
|
|
@@ -284,6 +294,7 @@ export function requestDrain(directory) {
|
|
|
284
294
|
async function residentDelegation(directory, fetcher) {
|
|
285
295
|
const connection = connect(residentSocket(directory));
|
|
286
296
|
const pending = new Map();
|
|
297
|
+
const watchers = new Set();
|
|
287
298
|
let greet;
|
|
288
299
|
const greeting = new Promise((resolve) => {
|
|
289
300
|
greet = resolve;
|
|
@@ -300,7 +311,11 @@ async function residentDelegation(directory, fetcher) {
|
|
|
300
311
|
for (const id of [...pending.keys()]) settle(id, unavailable());
|
|
301
312
|
});
|
|
302
313
|
readFrames(connection, {
|
|
303
|
-
onFrame: (frame) =>
|
|
314
|
+
onFrame: (frame) => {
|
|
315
|
+
if (frame.id !== undefined) settle(frame.id, frame);
|
|
316
|
+
else if (frame.stream !== undefined) for (const heard of watchers) heard.viewed?.(frame.stream);
|
|
317
|
+
else greet(frame);
|
|
318
|
+
},
|
|
304
319
|
onRefusal: () => connection.destroy(),
|
|
305
320
|
});
|
|
306
321
|
writeFrame(connection, { hold: true });
|
|
@@ -365,7 +380,25 @@ async function residentDelegation(directory, fetcher) {
|
|
|
365
380
|
headers: answer.type === null ? {} : { "content-type": answer.type },
|
|
366
381
|
});
|
|
367
382
|
}
|
|
368
|
-
|
|
383
|
+
function relay(signal, heard) {
|
|
384
|
+
return new Promise((resolve) => {
|
|
385
|
+
const end = () => {
|
|
386
|
+
signal.removeEventListener("abort", end);
|
|
387
|
+
connection.removeListener("close", end);
|
|
388
|
+
if (!watchers.delete(heard)) return;
|
|
389
|
+
heard.closed?.();
|
|
390
|
+
resolve();
|
|
391
|
+
};
|
|
392
|
+
watchers.add(heard);
|
|
393
|
+
heard.opened?.((frame) => {
|
|
394
|
+
if (!connection.destroyed) writeFrame(connection, { stream: frame });
|
|
395
|
+
});
|
|
396
|
+
signal.addEventListener("abort", end, { once: true });
|
|
397
|
+
connection.once("close", end);
|
|
398
|
+
if (signal.aborted || connection.destroyed) end();
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
return { connection, fetch: delegated, push, capture, relay };
|
|
369
402
|
}
|
|
370
403
|
|
|
371
404
|
export async function openSession(options, dependencies = {}) {
|
|
@@ -386,6 +419,7 @@ export async function openSession(options, dependencies = {}) {
|
|
|
386
419
|
session.resident = resident.connection;
|
|
387
420
|
session.push = resident.push;
|
|
388
421
|
session.capture = resident.capture;
|
|
422
|
+
session.relay = resident.relay;
|
|
389
423
|
session.dependencies = { ...dependencies, fetch: resident.fetch };
|
|
390
424
|
return session;
|
|
391
425
|
}
|
|
@@ -942,14 +976,17 @@ export async function commitRun(session, run) {
|
|
|
942
976
|
return answer;
|
|
943
977
|
}
|
|
944
978
|
|
|
945
|
-
export async function reportRunOutcome(session, run, reported) {
|
|
979
|
+
export async function reportRunOutcome(session, run, reported, plan = null) {
|
|
946
980
|
const answer = await houseRequest(
|
|
947
981
|
session.house,
|
|
948
982
|
await heldControl(session),
|
|
949
983
|
runOutcomePath(run),
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
984
|
+
withPlan(
|
|
985
|
+
reported.cause === null
|
|
986
|
+
? { operation_id: reported.operation, outcome: reported.outcome }
|
|
987
|
+
: { operation_id: reported.operation, outcome: reported.outcome, cause: reported.cause },
|
|
988
|
+
plan,
|
|
989
|
+
),
|
|
953
990
|
session.dependencies,
|
|
954
991
|
session.signal,
|
|
955
992
|
);
|
|
@@ -982,27 +1019,16 @@ export async function reportSessions(session, held) {
|
|
|
982
1019
|
return sessions;
|
|
983
1020
|
}
|
|
984
1021
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
session.house,
|
|
988
|
-
await heldControl(session),
|
|
989
|
-
assistantDraftsPath(workerSession),
|
|
990
|
-
{ blocks },
|
|
991
|
-
session.dependencies,
|
|
992
|
-
session.signal,
|
|
993
|
-
);
|
|
994
|
-
if (!ASSISTANT_DRAFT_STATES.includes(answer?.state)) {
|
|
995
|
-
throw new KitRefusal("kit_assistant_draft_rejected", workerSession);
|
|
996
|
-
}
|
|
997
|
-
return { state: answer.state };
|
|
1022
|
+
function withPlan(body, plan) {
|
|
1023
|
+
return plan === null || plan === undefined ? body : { ...body, plan };
|
|
998
1024
|
}
|
|
999
1025
|
|
|
1000
|
-
export async function acceptAssistantText(session, workerSession, sealed) {
|
|
1026
|
+
export async function acceptAssistantText(session, workerSession, sealed, plan = null) {
|
|
1001
1027
|
const answer = await houseRequest(
|
|
1002
1028
|
session.house,
|
|
1003
1029
|
await heldControl(session),
|
|
1004
1030
|
assistantTextPath(workerSession),
|
|
1005
|
-
{ operation_id: sealed.operation, blocks: sealed.blocks },
|
|
1031
|
+
withPlan({ operation_id: sealed.operation, blocks: sealed.blocks }, plan),
|
|
1006
1032
|
session.dependencies,
|
|
1007
1033
|
session.signal,
|
|
1008
1034
|
);
|
|
@@ -1017,27 +1043,12 @@ export async function acceptAssistantText(session, workerSession, sealed) {
|
|
|
1017
1043
|
return { turn: answer.turn, delivery: answer.delivery };
|
|
1018
1044
|
}
|
|
1019
1045
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
session.house,
|
|
1023
|
-
await heldControl(session),
|
|
1024
|
-
`/kit/sessions/${encodeURIComponent(workerSession)}/plans`,
|
|
1025
|
-
snapshot,
|
|
1026
|
-
session.dependencies,
|
|
1027
|
-
session.signal,
|
|
1028
|
-
);
|
|
1029
|
-
if (
|
|
1030
|
-
answer?.session !== workerSession ||
|
|
1031
|
-
typeof answer.plan !== "string" ||
|
|
1032
|
-
!["live", "frozen"].includes(answer.state)
|
|
1033
|
-
) {
|
|
1034
|
-
throw new KitRefusal("kit_plan_rejected", workerSession);
|
|
1035
|
-
}
|
|
1036
|
-
return { plan: answer.plan, state: answer.state };
|
|
1046
|
+
function named(value) {
|
|
1047
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
1037
1048
|
}
|
|
1038
1049
|
|
|
1039
1050
|
export async function observeSessionStart(session, workerSession) {
|
|
1040
|
-
|
|
1051
|
+
const answer = await houseRequest(
|
|
1041
1052
|
session.house,
|
|
1042
1053
|
await heldControl(session),
|
|
1043
1054
|
`/kit/sessions/${encodeURIComponent(workerSession)}/started`,
|
|
@@ -1045,14 +1056,19 @@ export async function observeSessionStart(session, workerSession) {
|
|
|
1045
1056
|
session.dependencies,
|
|
1046
1057
|
session.signal,
|
|
1047
1058
|
);
|
|
1059
|
+
const conversation = named(answer?.conversation_id);
|
|
1060
|
+
const turn = named(answer?.turn_id);
|
|
1061
|
+
return conversation === null || turn === null
|
|
1062
|
+
? null
|
|
1063
|
+
: { conversation_id: conversation, turn_id: turn, attempt_id: workerSession };
|
|
1048
1064
|
}
|
|
1049
1065
|
|
|
1050
|
-
export async function observeSessionStop(session, workerSession, turnCompleted = false) {
|
|
1066
|
+
export async function observeSessionStop(session, workerSession, turnCompleted = false, plan = null) {
|
|
1051
1067
|
return houseRequest(
|
|
1052
1068
|
session.house,
|
|
1053
1069
|
await heldControl(session),
|
|
1054
1070
|
`/kit/sessions/${encodeURIComponent(workerSession)}/stopped`,
|
|
1055
|
-
turnCompleted ? { turn_completed: true } : {},
|
|
1071
|
+
withPlan(turnCompleted ? { turn_completed: true } : {}, plan),
|
|
1056
1072
|
session.dependencies,
|
|
1057
1073
|
session.signal,
|
|
1058
1074
|
);
|
|
@@ -1286,12 +1302,12 @@ export async function reportInteractionOutcome(session, interaction, outcome) {
|
|
|
1286
1302
|
return { disposition: answer.disposition ?? null };
|
|
1287
1303
|
}
|
|
1288
1304
|
|
|
1289
|
-
export async function reportInterruptOutcome(session, interrupt, outcome) {
|
|
1305
|
+
export async function reportInterruptOutcome(session, interrupt, outcome, plan = null) {
|
|
1290
1306
|
const answer = await houseRequest(
|
|
1291
1307
|
session.house,
|
|
1292
1308
|
await heldControl(session),
|
|
1293
1309
|
`/kit/interrupts/${encodeURIComponent(interrupt)}/outcome`,
|
|
1294
|
-
{ outcome },
|
|
1310
|
+
withPlan({ outcome }, outcome === "interrupted" ? plan : null),
|
|
1295
1311
|
session.dependencies,
|
|
1296
1312
|
session.signal,
|
|
1297
1313
|
);
|
package/src/kit-work-stream.js
CHANGED
|
@@ -12,6 +12,7 @@ import { KitRefusal } from "./refusal.js";
|
|
|
12
12
|
const PROTOCOL = "house.kit.stream.1";
|
|
13
13
|
const WORK_SUBJECTS = ["runs", "commands", "agents", "working_copy", "sources"];
|
|
14
14
|
const FINAL_REFUSALS = ["kit_upgrade_required", "kit_stream_replaced"];
|
|
15
|
+
const VIEW_FRAMES = ["draft", "plan"];
|
|
15
16
|
|
|
16
17
|
function streamAddress(house) {
|
|
17
18
|
const url = new URL("/kit/stream", house);
|
|
@@ -43,6 +44,7 @@ async function held(session, signal, heard, dependencies) {
|
|
|
43
44
|
if (finished) return;
|
|
44
45
|
finished = true;
|
|
45
46
|
signal.removeEventListener("abort", stop);
|
|
47
|
+
heard.closed?.();
|
|
46
48
|
if (refusal === null) resolve(reason);
|
|
47
49
|
else reject(refusal);
|
|
48
50
|
};
|
|
@@ -51,13 +53,19 @@ async function held(session, signal, heard, dependencies) {
|
|
|
51
53
|
finish();
|
|
52
54
|
};
|
|
53
55
|
signal.addEventListener("abort", stop, { once: true });
|
|
54
|
-
socket.on("open", () =>
|
|
56
|
+
socket.on("open", () => {
|
|
57
|
+
if (finished) return;
|
|
58
|
+
heard.opened?.((frame) => socket.send(JSON.stringify(frame), () => undefined));
|
|
59
|
+
heard.available(null);
|
|
60
|
+
});
|
|
55
61
|
socket.on("message", (data) => {
|
|
56
62
|
const frame = frameOf(data);
|
|
57
63
|
if (frame?.type === "work_available" && WORK_SUBJECTS.includes(frame.subject)) {
|
|
58
64
|
heard.available(frame.subject);
|
|
59
65
|
} else if (frame?.type === "entries") {
|
|
60
66
|
heard.changed();
|
|
67
|
+
} else if (VIEW_FRAMES.includes(frame?.type)) {
|
|
68
|
+
heard.viewed?.(frame);
|
|
61
69
|
}
|
|
62
70
|
});
|
|
63
71
|
socket.on("unexpected-response", (_request, response) => {
|
package/src/launcher.js
CHANGED
|
@@ -155,11 +155,23 @@ async function runTurn(channel, start, bridge, turn) {
|
|
|
155
155
|
);
|
|
156
156
|
const said = [];
|
|
157
157
|
const message = { text: "" };
|
|
158
|
-
const
|
|
158
|
+
const sequences = { draft: 0, plan: 0 };
|
|
159
|
+
const drafts = observedAssistantDrafts({
|
|
160
|
+
send: (text) => channel.send({ type: "draft", text, sequence: (sequences.draft += 1) }),
|
|
161
|
+
});
|
|
159
162
|
const plans = observedPlans({
|
|
160
|
-
send: (
|
|
161
|
-
channel.send({
|
|
163
|
+
send: (steps) =>
|
|
164
|
+
channel.send({
|
|
165
|
+
type: "plan",
|
|
166
|
+
provider_turn: turn.providerTurn,
|
|
167
|
+
sequence: (sequences.plan += 1),
|
|
168
|
+
steps,
|
|
169
|
+
}),
|
|
162
170
|
});
|
|
171
|
+
turn.plan = () => {
|
|
172
|
+
const steps = plans.latest();
|
|
173
|
+
return steps === null ? null : { provider_turn: turn.providerTurn, steps };
|
|
174
|
+
};
|
|
163
175
|
const wholeSoFar = () => [...said, message.text].filter((text) => text !== "").join("\n\n");
|
|
164
176
|
let loading = false;
|
|
165
177
|
const flush = () => {
|
|
@@ -167,7 +179,7 @@ async function runTurn(channel, start, bridge, turn) {
|
|
|
167
179
|
channel.send({ type: "assistant", text: message.text });
|
|
168
180
|
said.push(message.text);
|
|
169
181
|
message.text = "";
|
|
170
|
-
drafts.
|
|
182
|
+
drafts.observe(wholeSoFar());
|
|
171
183
|
};
|
|
172
184
|
const handlers = {
|
|
173
185
|
onPermission: (params) => resolvedPermission(params, start.execution_authority),
|
|
@@ -218,16 +230,16 @@ async function runTurn(channel, start, bridge, turn) {
|
|
|
218
230
|
});
|
|
219
231
|
await new Promise((resolve) => setImmediate(resolve));
|
|
220
232
|
flush();
|
|
221
|
-
plans.complete();
|
|
222
233
|
channel.send({
|
|
223
234
|
type: "turn_ended",
|
|
224
235
|
stop_outcome: turnOutcome(contract, prompted?.stopReason),
|
|
225
236
|
provider_continuation: contract.sessionLoad,
|
|
226
237
|
provider_session_id: providerSessionId,
|
|
238
|
+
plan: turn.plan(),
|
|
227
239
|
});
|
|
228
240
|
} finally {
|
|
229
241
|
turn.cancel = null;
|
|
230
|
-
plans.
|
|
242
|
+
plans.close();
|
|
231
243
|
abandonQuestions(channel, turn);
|
|
232
244
|
drafts.close();
|
|
233
245
|
stopCommandAdapter(adapter);
|
|
@@ -240,6 +252,7 @@ export async function launchWorkerSession(bridge) {
|
|
|
240
252
|
providerTurn: randomUUID(),
|
|
241
253
|
questions: new Map(),
|
|
242
254
|
cancel: null,
|
|
255
|
+
plan: () => null,
|
|
243
256
|
};
|
|
244
257
|
const peer = bridgePeer({
|
|
245
258
|
socket: bridge.socket,
|
|
@@ -267,6 +280,7 @@ export async function launchWorkerSession(bridge) {
|
|
|
267
280
|
type: "interrupt_applied",
|
|
268
281
|
interrupt: frame.interrupt,
|
|
269
282
|
outcome: "interrupted",
|
|
283
|
+
plan: turn.plan(),
|
|
270
284
|
}),
|
|
271
285
|
() =>
|
|
272
286
|
peer.send({
|
|
@@ -289,6 +303,7 @@ export async function launchWorkerSession(bridge) {
|
|
|
289
303
|
type: "failed",
|
|
290
304
|
code: refusal instanceof KitRefusal ? refusal.code : "kit_adapter_failed",
|
|
291
305
|
detail: refusal instanceof Error ? refusal.message : null,
|
|
306
|
+
plan: turn.plan(),
|
|
292
307
|
});
|
|
293
308
|
}
|
|
294
309
|
peer.send({ type: "exiting" });
|
package/src/pacing.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const PACING_INTERVAL_MS = 500;
|
|
2
|
+
|
|
3
|
+
export function pacedLatest(options) {
|
|
4
|
+
const intervalMs = options.intervalMs ?? PACING_INTERVAL_MS;
|
|
5
|
+
const now = options.now ?? (() => Date.now());
|
|
6
|
+
let timer = null;
|
|
7
|
+
let sentAt = null;
|
|
8
|
+
let sent = null;
|
|
9
|
+
let latest = null;
|
|
10
|
+
let closed = false;
|
|
11
|
+
|
|
12
|
+
function write() {
|
|
13
|
+
timer = null;
|
|
14
|
+
const serialized = JSON.stringify(latest);
|
|
15
|
+
if (latest === null || serialized === sent) return;
|
|
16
|
+
sent = serialized;
|
|
17
|
+
sentAt = now();
|
|
18
|
+
options.send(latest);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
observe(value) {
|
|
23
|
+
latest = value;
|
|
24
|
+
if (closed || timer !== null) return;
|
|
25
|
+
const wait = sentAt === null ? 0 : sentAt + intervalMs - now();
|
|
26
|
+
if (wait <= 0) {
|
|
27
|
+
write();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
timer = setTimeout(write, wait);
|
|
31
|
+
timer.unref?.();
|
|
32
|
+
},
|
|
33
|
+
latest: () => latest,
|
|
34
|
+
close() {
|
|
35
|
+
closed = true;
|
|
36
|
+
if (timer === null) return;
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
timer = null;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
package/src/plan.js
CHANGED
|
@@ -1,57 +1,49 @@
|
|
|
1
|
-
|
|
1
|
+
import { pacedLatest } from "./pacing.js";
|
|
2
2
|
|
|
3
|
-
function
|
|
4
|
-
if (
|
|
5
|
-
|
|
3
|
+
function plannedStep(step) {
|
|
4
|
+
if (typeof step !== "object" || step === null || Array.isArray(step)) return null;
|
|
5
|
+
if (typeof step.label !== "string" || typeof step.status !== "string") return null;
|
|
6
|
+
return {
|
|
7
|
+
label: step.label,
|
|
8
|
+
...(typeof step.priority === "string" ? { priority: step.priority } : {}),
|
|
9
|
+
status: step.status,
|
|
10
|
+
};
|
|
6
11
|
}
|
|
7
12
|
|
|
8
|
-
export function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
export function planSnapshot(value) {
|
|
14
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
15
|
+
if (typeof value.provider_turn !== "string" || value.provider_turn === "") return null;
|
|
16
|
+
if (!Array.isArray(value.steps) || value.steps.length === 0) return null;
|
|
17
|
+
const steps = value.steps.map(plannedStep);
|
|
18
|
+
if (steps.includes(null)) return null;
|
|
19
|
+
return { provider_turn: value.provider_turn, steps };
|
|
20
|
+
}
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
22
|
+
export function finalPlan(snapshot) {
|
|
23
|
+
if (snapshot === null || snapshot === undefined) return null;
|
|
24
|
+
return {
|
|
25
|
+
provider_turn: snapshot.provider_turn,
|
|
26
|
+
steps: snapshot.steps.map((step) => ({ label: step.label, status: step.status })),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
function steps(entries) {
|
|
31
|
+
if (!Array.isArray(entries) || entries.length === 0) return null;
|
|
32
|
+
return entries.map((entry) => ({
|
|
33
|
+
label: entry.content,
|
|
34
|
+
priority: entry.priority,
|
|
35
|
+
status: entry.status,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
31
38
|
|
|
39
|
+
export function observedPlans(options) {
|
|
40
|
+
const paced = pacedLatest(options);
|
|
32
41
|
return {
|
|
33
42
|
observe(entries) {
|
|
34
|
-
if (completed) return;
|
|
35
43
|
const observed = steps(entries);
|
|
36
|
-
if (observed
|
|
37
|
-
latest = observed;
|
|
38
|
-
if (timer !== null) return;
|
|
39
|
-
const wait = sentAt === null ? 0 : sentAt + intervalMs - now();
|
|
40
|
-
if (wait <= 0) {
|
|
41
|
-
write(false);
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
timer = setTimeout(() => {
|
|
45
|
-
timer = null;
|
|
46
|
-
write(false);
|
|
47
|
-
}, wait);
|
|
48
|
-
timer.unref?.();
|
|
49
|
-
},
|
|
50
|
-
complete() {
|
|
51
|
-
if (completed) return;
|
|
52
|
-
completed = true;
|
|
53
|
-
cancel();
|
|
54
|
-
write(true);
|
|
44
|
+
if (observed !== null) paced.observe(observed);
|
|
55
45
|
},
|
|
46
|
+
latest: paced.latest,
|
|
47
|
+
close: paced.close,
|
|
56
48
|
};
|
|
57
49
|
}
|