@vincemakes/kiso-runtime 0.47.3 → 0.49.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/dist/compose.js +16 -1
- package/dist/provider/metadata.js +9 -0
- package/dist/run.js +6 -2
- package/dist/session.d.ts +5 -1
- package/dist/session.js +6 -0
- package/dist/tasks/delivery.d.ts +27 -3
- package/dist/tasks/delivery.js +172 -59
- package/dist/tasks/handoff.d.ts +44 -0
- package/dist/tasks/handoff.js +81 -0
- package/dist/tasks/journal.d.ts +14 -0
- package/dist/tasks/manager.d.ts +26 -2
- package/dist/tasks/manager.js +80 -6
- package/dist/trace/guard.js +1 -0
- package/dist/trace/rent.d.ts +16 -6
- package/dist/trace/rent.js +14 -5
- package/package.json +6 -6
package/dist/compose.js
CHANGED
|
@@ -28,7 +28,22 @@ export function composeToolTable(registry, rules = []) {
|
|
|
28
28
|
"- end your turn with your findings — never end on a bare tool result",
|
|
29
29
|
...tools.flatMap((t) => (t.promptSnippet === undefined ? [] : [`- ${t.promptSnippet}`])),
|
|
30
30
|
];
|
|
31
|
-
|
|
31
|
+
// Plan B: one statement per fact — a guideline body several tools carry
|
|
32
|
+
// prints ONCE, naming every tool that carries it, in first-seen order.
|
|
33
|
+
// The names stay: a host's or an extension's guideline may need its
|
|
34
|
+
// tool's name to read unambiguously.
|
|
35
|
+
const byBody = new Map();
|
|
36
|
+
for (const t of tools) {
|
|
37
|
+
for (const g of t.promptGuidelines ?? []) {
|
|
38
|
+
const body = g.trim();
|
|
39
|
+
const names = byBody.get(body);
|
|
40
|
+
if (names === undefined)
|
|
41
|
+
byBody.set(body, [t.name]);
|
|
42
|
+
else if (!names.includes(t.name))
|
|
43
|
+
names.push(t.name);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const guidelines = [...byBody].map(([body, names]) => `- ${names.join(", ")}: ${body}`);
|
|
32
47
|
if (guidelines.length > 0)
|
|
33
48
|
lines.push("Active tool guidelines:", ...guidelines);
|
|
34
49
|
return lines.join("\n");
|
|
@@ -167,6 +167,14 @@ const chatgptRow = (model, opts = {}) => ({
|
|
|
167
167
|
const OPENAI_MODELS_ASOF_2 = "2026-09-09";
|
|
168
168
|
const CHATGPT_PRESETS_SOURCE_2 = "https://github.com/openai/codex/blob/634ebc1865c6ac840ed3ba118f040d527bf4b55d/codex-rs/models-manager/models.json";
|
|
169
169
|
const ULTRA_LADDER = ["low", "medium", "high", "xhigh", "max", "ultra"];
|
|
170
|
+
/** 0.48.0 (finding 0473-F2): the presets at a later commit, read 2026-10-09.
|
|
171
|
+
* The owner's `sol` profile moved to `gpt-6.1-sol` on 2026-10-08, and with
|
|
172
|
+
* no row kiso sent it no effort: the remembered `high` never reached the
|
|
173
|
+
* wire and no reasoning summary came back. The id answered on the
|
|
174
|
+
* subscription backend that night (the 0.47.3 live sessions). No
|
|
175
|
+
* first-party row: no model page was read for it. */
|
|
176
|
+
const OPENAI_MODELS_ASOF_3 = "2026-10-09";
|
|
177
|
+
const CHATGPT_PRESETS_SOURCE_3 = "https://github.com/openai/codex/blob/d63a9b8344cfe58bc78bbe319b560378fc8756ef/codex-rs/models-manager/models.json";
|
|
170
178
|
/** The v1 table. Nulls outnumber numbers ON PURPOSE: only values with a
|
|
171
179
|
* named source enter; everything else waits for one. */
|
|
172
180
|
const ENTRIES = [
|
|
@@ -409,6 +417,7 @@ const ENTRIES = [
|
|
|
409
417
|
openaiRow("gpt-5.6-sol", "medium", { inputPerM: 4, outputPerM: 20, cacheReadPerM: 0.4 }, "https://developers.openai.com/api/docs/models/gpt-5.6-sol", { levels: ["none", "low", "medium", "high", "xhigh", "max"], asOf: OPENAI_MODELS_ASOF_2 }),
|
|
410
418
|
chatgptRow("gpt-6-astra", { levels: ULTRA_LADDER, default: "low", source: CHATGPT_PRESETS_SOURCE_2, asOf: OPENAI_MODELS_ASOF_2 }),
|
|
411
419
|
chatgptRow("gpt-5.6-sol", { levels: ULTRA_LADDER, default: "low", source: CHATGPT_PRESETS_SOURCE_2, asOf: OPENAI_MODELS_ASOF_2 }),
|
|
420
|
+
chatgptRow("gpt-6.1-sol", { levels: ULTRA_LADDER, default: "low", source: CHATGPT_PRESETS_SOURCE_3, asOf: OPENAI_MODELS_ASOF_3 }),
|
|
412
421
|
// The compat table's SECOND row (roadmap R3.2 §1a step 1): GLM 5.3 Flash
|
|
413
422
|
// through OpenRouter, read from OpenRouter's models API on 2026-09-09. The
|
|
414
423
|
// id is OpenRouter's, so the row is keyed to that origin; the
|
package/dist/run.js
CHANGED
|
@@ -7,7 +7,7 @@ import { denialResult, loop, validateArgs } from "@vincemakes/kiso-core";
|
|
|
7
7
|
import { ABORTED, MergedSignal, abortable, openRunId } from "./recovery.js";
|
|
8
8
|
import { resolveReasoning } from "./provider/metadata.js";
|
|
9
9
|
import { deriveRecoveryPlan, invocationSeqOf } from "./recovery-plan.js";
|
|
10
|
-
import { appendOf, composeApprovalChain, joinPrompt, promptPerAttempt, runBasePrompt } from "./compose.js";
|
|
10
|
+
import { appendOf, composeApprovalChain, composeToolTable, joinPrompt, promptPerAttempt, runBasePrompt } from "./compose.js";
|
|
11
11
|
import { truncationGuard } from "./truncation-guard.js";
|
|
12
12
|
import { overflowBelt } from "./overflow-belt.js";
|
|
13
13
|
import { DEFAULT_STREAM_IDLE_MS, idleGuard } from "./idle-guard.js";
|
|
@@ -212,7 +212,11 @@ export class Run {
|
|
|
212
212
|
// that layer, records the bytes the model was sent. The in-band
|
|
213
213
|
// summary reads the last attempt's snapshot (the prefix the run
|
|
214
214
|
// last sent) or takes one if no attempt has happened yet.
|
|
215
|
-
|
|
215
|
+
// RG-F2: the generated table is a ledger line of its own — the
|
|
216
|
+
// same text runBasePrompt placed between the base and the appends
|
|
217
|
+
// (one registry read at the same instant, so it cannot differ).
|
|
218
|
+
const table = (this.#config.toolTable ?? "on") === "on" ? composeToolTable(this.#config.registry, this.#config.toolRules ?? []) : "";
|
|
219
|
+
const rentParts = { ...(this.#config.systemPrompt !== undefined ? { base: this.#config.systemPrompt } : {}), ...(table !== "" ? { table } : {}), appends: [] };
|
|
216
220
|
let lastPrompt;
|
|
217
221
|
let taken = false;
|
|
218
222
|
const snapshot = () => {
|
package/dist/session.d.ts
CHANGED
|
@@ -130,7 +130,7 @@ export declare class AgentSession {
|
|
|
130
130
|
/** Deliver this session's task transitions to the model: into a live
|
|
131
131
|
* run at its next safe point, or — idle — held for the next run, or one
|
|
132
132
|
* continuation run through `onWake` (off with `wake: false`). */
|
|
133
|
-
useTasks(manager: TaskManager, options?: Pick<TaskDeliveryOptions, "wake" | "onWake" | "windowMs" | "onLost"> & {
|
|
133
|
+
useTasks(manager: TaskManager, options?: Pick<TaskDeliveryOptions, "wake" | "onWake" | "windowMs" | "maxWakes" | "onLost" | "onBudgetSpent"> & {
|
|
134
134
|
readonly autoDetachMinAgeMs?: number;
|
|
135
135
|
}): () => void;
|
|
136
136
|
/** ADR-0057 §5 (3e D1) — a person's steer arrived: the foreground
|
|
@@ -256,6 +256,10 @@ export declare class AgentSession {
|
|
|
256
256
|
* binding, restored from the durable profile on open. The status row
|
|
257
257
|
* reads THIS, so the row and the request can never disagree. */
|
|
258
258
|
get model(): string;
|
|
259
|
+
/** 0.49.0 C1: the config profile that named the live binding — null for
|
|
260
|
+
* a direct provider/model or an SDK caller. Moves with every binding,
|
|
261
|
+
* so a delegated child can run on the conversation's own profile. */
|
|
262
|
+
get profileName(): string | null;
|
|
259
263
|
/** A1a: the parts of the NEXT request as the kernel would assemble them —
|
|
260
264
|
* the system prompt, the tool table (the same snapshot the loop sends:
|
|
261
265
|
* live extension tools included), the projected messages with their
|
package/dist/session.js
CHANGED
|
@@ -654,6 +654,12 @@ export class AgentSession {
|
|
|
654
654
|
get model() {
|
|
655
655
|
return this.#model;
|
|
656
656
|
}
|
|
657
|
+
/** 0.49.0 C1: the config profile that named the live binding — null for
|
|
658
|
+
* a direct provider/model or an SDK caller. Moves with every binding,
|
|
659
|
+
* so a delegated child can run on the conversation's own profile. */
|
|
660
|
+
get profileName() {
|
|
661
|
+
return this.#profileName;
|
|
662
|
+
}
|
|
657
663
|
/** A1a: the parts of the NEXT request as the kernel would assemble them —
|
|
658
664
|
* the system prompt, the tool table (the same snapshot the loop sends:
|
|
659
665
|
* live extension tools included), the projected messages with their
|
package/dist/tasks/delivery.d.ts
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* Exactly once, from the log: a transition counts as delivered when a
|
|
16
16
|
* `user_input` whose `via.items` names it is in the session's log (ADR-0051
|
|
17
17
|
* Amendment 8). The set is a cache rebuilt from the log, never a second
|
|
18
|
-
* truth.
|
|
19
|
-
*
|
|
18
|
+
* truth. The chain budget from the log too (ADR-0058 Amendment 9): the wake
|
|
19
|
+
* runs since the last run a person started are counted, never kept.
|
|
20
20
|
*
|
|
21
21
|
* The summary snapshot is what the MODEL was told, never the live journal:
|
|
22
22
|
* a compaction must not reveal a transition that has not been delivered.
|
|
@@ -37,18 +37,31 @@
|
|
|
37
37
|
* 16 KiB together; members already delivered are named as reported, and
|
|
38
38
|
* only the rest are receipted. A failure goes into a live run at once and
|
|
39
39
|
* never wakes on its own; idle, it waits for its group.
|
|
40
|
+
*
|
|
41
|
+
* 0.49.0 A: a delegate call that waits for its children (the join)
|
|
42
|
+
* CLAIMS each one that ended while it waited, and its own result carries
|
|
43
|
+
* that child's handoff. Such a member is delivered once the claiming
|
|
44
|
+
* call's result is durable — an error result included, since it carries
|
|
45
|
+
* the handoffs too — and the group's notice names it as reported. A claim
|
|
46
|
+
* whose call never returned (a crash: the kernel records no result, only
|
|
47
|
+
* an uncertain execution) delivered nothing, so the member rides its
|
|
48
|
+
* group's notice in full: never in both places, never in neither.
|
|
40
49
|
*/
|
|
41
50
|
import { type Event, type UserInputVia } from "@vincemakes/kiso-core";
|
|
42
51
|
import { type Run } from "../run.js";
|
|
43
52
|
import { type TaskInfo, type TaskManager } from "./manager.js";
|
|
44
53
|
export interface TaskDeliveryOptions {
|
|
45
54
|
readonly manager: TaskManager;
|
|
46
|
-
/** The session's durable events: receipts and
|
|
55
|
+
/** The session's durable events: receipts and the chain are read here. */
|
|
47
56
|
readonly events: () => readonly Event[];
|
|
48
57
|
/** The session's live run, when one is running. */
|
|
49
58
|
readonly liveRun: () => Run | undefined;
|
|
50
59
|
/** False turns every wake into a notify (the switch, ADR-0058 §8.4). */
|
|
51
60
|
readonly wake?: boolean;
|
|
61
|
+
/** ADR-0058 Amendment 9 — the chain budget: how many autonomous wakes may follow
|
|
62
|
+
* one person's input before a terminal delivers as a notify instead.
|
|
63
|
+
* Replaces ADR-0058 §8 guard 3 (lineage depth 1). Default 20. */
|
|
64
|
+
readonly maxWakes?: number;
|
|
52
65
|
/** Start ONE continuation run whose first input is this notice. */
|
|
53
66
|
readonly onWake?: (input: {
|
|
54
67
|
readonly content: string;
|
|
@@ -60,11 +73,22 @@ export interface TaskDeliveryOptions {
|
|
|
60
73
|
* startup or as it is heard — the person can be told at once, while
|
|
61
74
|
* the model's notice keeps its own turn. A claimed loss never comes. */
|
|
62
75
|
readonly onLost?: (taskIds: readonly string[]) => void;
|
|
76
|
+
/** Finding 0480-F9: the chain budget is spent — a task's end that would
|
|
77
|
+
* have woken the session only notifies. Called ONCE per exhaustion (the
|
|
78
|
+
* first such end since the last run a person started; a resume whose log
|
|
79
|
+
* already carries the sentence is not told again), so the person can be
|
|
80
|
+
* told why the chain went quiet. The model's notice says it either way. */
|
|
81
|
+
readonly onBudgetSpent?: (info: {
|
|
82
|
+
readonly wakes: number;
|
|
83
|
+
readonly maxWakes: number;
|
|
84
|
+
readonly taskIds: readonly string[];
|
|
85
|
+
}) => void;
|
|
63
86
|
/** How much of a failed task's output rides its notice. Default 2 KB. */
|
|
64
87
|
readonly tailBytes?: number;
|
|
65
88
|
/** Read a task's output tail (injectable for tests). */
|
|
66
89
|
readonly readTail?: (task: TaskInfo, bytes: number) => string;
|
|
67
90
|
}
|
|
91
|
+
export declare const DEFAULT_MAX_WAKES = 20;
|
|
68
92
|
export declare class TaskDelivery {
|
|
69
93
|
#private;
|
|
70
94
|
constructor(options: TaskDeliveryOptions);
|
package/dist/tasks/delivery.js
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* Exactly once, from the log: a transition counts as delivered when a
|
|
16
16
|
* `user_input` whose `via.items` names it is in the session's log (ADR-0051
|
|
17
17
|
* Amendment 8). The set is a cache rebuilt from the log, never a second
|
|
18
|
-
* truth.
|
|
19
|
-
*
|
|
18
|
+
* truth. The chain budget from the log too (ADR-0058 Amendment 9): the wake
|
|
19
|
+
* runs since the last run a person started are counted, never kept.
|
|
20
20
|
*
|
|
21
21
|
* The summary snapshot is what the MODEL was told, never the live journal:
|
|
22
22
|
* a compaction must not reveal a transition that has not been delivered.
|
|
@@ -37,16 +37,30 @@
|
|
|
37
37
|
* 16 KiB together; members already delivered are named as reported, and
|
|
38
38
|
* only the rest are receipted. A failure goes into a live run at once and
|
|
39
39
|
* never wakes on its own; idle, it waits for its group.
|
|
40
|
+
*
|
|
41
|
+
* 0.49.0 A: a delegate call that waits for its children (the join)
|
|
42
|
+
* CLAIMS each one that ended while it waited, and its own result carries
|
|
43
|
+
* that child's handoff. Such a member is delivered once the claiming
|
|
44
|
+
* call's result is durable — an error result included, since it carries
|
|
45
|
+
* the handoffs too — and the group's notice names it as reported. A claim
|
|
46
|
+
* whose call never returned (a crash: the kernel records no result, only
|
|
47
|
+
* an uncertain execution) delivered nothing, so the member rides its
|
|
48
|
+
* group's notice in full: never in both places, never in neither.
|
|
40
49
|
*/
|
|
41
50
|
import { closeSync, existsSync, fstatSync, openSync, readFileSync, readSync } from "node:fs";
|
|
42
51
|
import { dirname, join } from "node:path";
|
|
43
52
|
import { isRuntimeInput } from "@vincemakes/kiso-core";
|
|
44
53
|
import { NOTICE_FOOTER as FOOTER } from "../run.js";
|
|
54
|
+
import { CHILD_HANDOFF_BYTES, GROUP_HANDOFF_BYTES, handoffBody, handoffRecordOf, TAIL_BYTES } from "./handoff.js";
|
|
45
55
|
import { endTransitionOf } from "./manager.js";
|
|
46
56
|
const key = (i) => `${i.taskId}:${i.transition}`;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
57
|
+
export const DEFAULT_MAX_WAKES = 20;
|
|
58
|
+
/** The model's sentence for a spent budget (the text 0.48.0 shipped). */
|
|
59
|
+
const spentSentence = (wakes) => `chain budget spent: ${wakes} autonomous wakes since the person's last message — continue when they speak`;
|
|
60
|
+
/** ADR-0058 3d (D3): a child's excerpt, and a group's excerpts together —
|
|
61
|
+
* the handoff format's budgets (0.49.0 C3). */
|
|
62
|
+
const CHILD_EXCERPT_BYTES = CHILD_HANDOFF_BYTES;
|
|
63
|
+
const GROUP_EXCERPT_BYTES = GROUP_HANDOFF_BYTES;
|
|
50
64
|
export class TaskDelivery {
|
|
51
65
|
#o;
|
|
52
66
|
#receipts = new Set();
|
|
@@ -61,6 +75,8 @@ export class TaskDelivery {
|
|
|
61
75
|
/** ended agent tasks waiting for their group (restart: heard at startup) */
|
|
62
76
|
#ended = new Map();
|
|
63
77
|
#recheck = null;
|
|
78
|
+
/** the person-run the budget was last said spent in (0480-F9) */
|
|
79
|
+
#spentEpoch = null;
|
|
64
80
|
#unsubscribe;
|
|
65
81
|
constructor(options) {
|
|
66
82
|
this.#o = options;
|
|
@@ -74,8 +90,10 @@ export class TaskDelivery {
|
|
|
74
90
|
const item = itemOf(task);
|
|
75
91
|
if (item === null || item.transition === "ready" || this.#receipts.has(key(item)))
|
|
76
92
|
continue;
|
|
77
|
-
// a tool result reported it: the claim alone decides (Amendment 7)
|
|
78
|
-
|
|
93
|
+
// a tool result reported it: the claim alone decides (Amendment 7) —
|
|
94
|
+
// for an agent task, once its claiming call's result is durable (0.49.0 A)
|
|
95
|
+
const claim = task.claims?.find((c) => c.transition === item.transition);
|
|
96
|
+
if (claim !== undefined && (task.agent === undefined || this.#toldByResult(claim.executionId)))
|
|
79
97
|
continue;
|
|
80
98
|
if (item.transition === "unknown")
|
|
81
99
|
lost.push(task.id);
|
|
@@ -166,7 +184,8 @@ export class TaskDelivery {
|
|
|
166
184
|
const mode = this.#modeOf(task, item);
|
|
167
185
|
if (mode === "silent")
|
|
168
186
|
return;
|
|
169
|
-
this.#
|
|
187
|
+
const spent = mode === "notify" && this.#eligible(task, item) ? this.#spent([task.id]) : null;
|
|
188
|
+
this.#buffer.push({ item, line: spent !== null ? `${this.#line(task, item)}\n${spent}` : this.#line(task, item), mode });
|
|
170
189
|
if (this.#timer === null)
|
|
171
190
|
this.#timer = setTimeout(() => this.#flush(), this.#o.windowMs ?? 1_000);
|
|
172
191
|
}
|
|
@@ -221,7 +240,7 @@ export class TaskDelivery {
|
|
|
221
240
|
}
|
|
222
241
|
this.#scanned = events.length;
|
|
223
242
|
}
|
|
224
|
-
/** ADR-0058 §8 defaults, then the guards: the switch, and
|
|
243
|
+
/** ADR-0058 §8 defaults, then the guards: the switch, and the chain budget. */
|
|
225
244
|
#modeOf(task, item) {
|
|
226
245
|
if (item.transition === "ready" || item.transition === "stopped" || item.transition === "unknown")
|
|
227
246
|
return "notify";
|
|
@@ -232,17 +251,82 @@ export class TaskDelivery {
|
|
|
232
251
|
return this.#wakes(item) ? "wake" : "notify";
|
|
233
252
|
}
|
|
234
253
|
#wakes(item) {
|
|
235
|
-
if (this.#o.wake === false)
|
|
236
|
-
return false;
|
|
237
|
-
if (item.transition !== "exited" && item.transition !== "failed")
|
|
238
|
-
return false;
|
|
239
254
|
const task = this.#o.manager.get(item.taskId);
|
|
240
|
-
if (task === undefined || task
|
|
255
|
+
if (task === undefined || !this.#eligible(task, item))
|
|
241
256
|
return false;
|
|
242
257
|
// a child wakes only with its whole group (D2): never on its own
|
|
243
258
|
if (task.agent !== undefined && !this.#complete(this.#groupOf(task)))
|
|
244
259
|
return false;
|
|
245
|
-
return
|
|
260
|
+
return this.#underBudget();
|
|
261
|
+
}
|
|
262
|
+
/** The transition could wake, budget aside: the switch, a terminal, an
|
|
263
|
+
* execution's task, not a service. */
|
|
264
|
+
#eligible(task, item) {
|
|
265
|
+
if (this.#o.wake === false)
|
|
266
|
+
return false;
|
|
267
|
+
if (item.transition !== "exited" && item.transition !== "failed")
|
|
268
|
+
return false;
|
|
269
|
+
if (task.executionId === undefined || task.profile === "service")
|
|
270
|
+
return false;
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
/** ADR-0058 Amendment 9: the chain budget, derived from the log — never a
|
|
274
|
+
* counter. The count is the wake runs (first input a runtime notice)
|
|
275
|
+
* since the last run a person started. */
|
|
276
|
+
#underBudget() {
|
|
277
|
+
return this.#wakesSinceLastPerson() < (this.#o.maxWakes ?? DEFAULT_MAX_WAKES);
|
|
278
|
+
}
|
|
279
|
+
/** 0480-F9: the sentence when the budget is spent (null while it is not),
|
|
280
|
+
* and the person's hook, once per exhaustion. */
|
|
281
|
+
#spent(taskIds) {
|
|
282
|
+
if (this.#underBudget())
|
|
283
|
+
return null;
|
|
284
|
+
const wakes = this.#wakesSinceLastPerson();
|
|
285
|
+
const epoch = this.#personEpoch();
|
|
286
|
+
if (this.#spentEpoch !== epoch) {
|
|
287
|
+
this.#spentEpoch = epoch;
|
|
288
|
+
if (!this.#spentInLog(epoch))
|
|
289
|
+
this.#o.onBudgetSpent?.({ wakes, maxWakes: this.#o.maxWakes ?? DEFAULT_MAX_WAKES, taskIds });
|
|
290
|
+
}
|
|
291
|
+
return spentSentence(wakes);
|
|
292
|
+
}
|
|
293
|
+
/** The index of the last run's first input a person started (−1: none). */
|
|
294
|
+
#personEpoch() {
|
|
295
|
+
const events = this.#o.events();
|
|
296
|
+
let epoch = -1;
|
|
297
|
+
let firstOfRun = true;
|
|
298
|
+
for (let i = 0; i < events.length; i++) {
|
|
299
|
+
const e = events[i];
|
|
300
|
+
if (e.type === "terminal") {
|
|
301
|
+
firstOfRun = true;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (e.type !== "user_input" || !firstOfRun)
|
|
305
|
+
continue;
|
|
306
|
+
firstOfRun = false;
|
|
307
|
+
if (!isRuntimeInput(e))
|
|
308
|
+
epoch = i;
|
|
309
|
+
}
|
|
310
|
+
return epoch;
|
|
311
|
+
}
|
|
312
|
+
/** A resume: the log already carries the sentence after that run. */
|
|
313
|
+
#spentInLog(epoch) {
|
|
314
|
+
return this.#o.events().slice(epoch + 1).some((e) => e.type === "user_input" && typeof e.content === "string" && e.content.includes("chain budget spent:"));
|
|
315
|
+
}
|
|
316
|
+
#wakesSinceLastPerson() {
|
|
317
|
+
let wakes = 0;
|
|
318
|
+
let firstOfRun = true;
|
|
319
|
+
for (const e of this.#o.events()) {
|
|
320
|
+
if (e.type === "terminal") {
|
|
321
|
+
firstOfRun = true;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (e.type !== "user_input" || !firstOfRun)
|
|
325
|
+
continue;
|
|
326
|
+
firstOfRun = false;
|
|
327
|
+
wakes = isRuntimeInput(e) ? wakes + 1 : 0;
|
|
328
|
+
}
|
|
329
|
+
return wakes;
|
|
246
330
|
}
|
|
247
331
|
/** ADR-0058 3d: an agent task has ended — a failure goes into a live run
|
|
248
332
|
* at once; every end waits for its group. */
|
|
@@ -318,9 +402,11 @@ export class TaskDelivery {
|
|
|
318
402
|
return { closed: to < events.length && (runEnded || [...calls].every((c) => answered.has(c))), members: members.includes(task.id) ? members : [...members, task.id] };
|
|
319
403
|
}
|
|
320
404
|
#complete(group) {
|
|
405
|
+
// 0.49.0 B: a member's END, not its process's — a writer being
|
|
406
|
+
// collected has not ended, so its group is not complete yet
|
|
321
407
|
return group.closed && group.members.every((id) => {
|
|
322
|
-
const
|
|
323
|
-
return
|
|
408
|
+
const task = this.#o.manager.get(id);
|
|
409
|
+
return task === undefined || endTransitionOf(task) !== null;
|
|
324
410
|
});
|
|
325
411
|
}
|
|
326
412
|
/** ONE notice for a complete group: every member's line, excerpts within
|
|
@@ -332,10 +418,19 @@ export class TaskDelivery {
|
|
|
332
418
|
const tasks = members.map((id) => this.#o.manager.get(id)).filter((t) => t !== undefined);
|
|
333
419
|
const fresh = [];
|
|
334
420
|
const lines = [];
|
|
421
|
+
const unreturned = new Set();
|
|
335
422
|
for (const task of tasks) {
|
|
336
423
|
const item = itemOf(task);
|
|
337
424
|
if (item === null)
|
|
338
425
|
continue;
|
|
426
|
+
// 0.49.0 A: its delegate call's own result carried it
|
|
427
|
+
const claim = task.claims?.find((c) => c.transition === item.transition);
|
|
428
|
+
if (claim !== undefined && this.#toldByResult(claim.executionId)) {
|
|
429
|
+
lines.push(`<kiso-task id="${task.id}" kind="agent" role="${task.agent?.role ?? ""}" status="${item.transition}" reported="earlier"/>`);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (claim !== undefined)
|
|
433
|
+
unreturned.add(task.id);
|
|
339
434
|
if (this.#receipts.has(key(item)) || this.#inFlight.has(key(item))) {
|
|
340
435
|
lines.push(`<kiso-task id="${task.id}" kind="agent" role="${task.agent?.role ?? ""}" status="${item.transition}" reported="earlier"/>`);
|
|
341
436
|
continue;
|
|
@@ -349,11 +444,16 @@ export class TaskDelivery {
|
|
|
349
444
|
for (const { task, item } of fresh) {
|
|
350
445
|
const excerpt = excerptOf(task, Math.min(CHILD_EXCERPT_BYTES, budget));
|
|
351
446
|
budget -= excerpt.bytes;
|
|
352
|
-
lines.push(`${this.#agentTag(task, item)}${excerpt.text}`);
|
|
447
|
+
lines.push(`${this.#agentTag(task, item, unreturned.has(task.id))}${excerpt.text}`);
|
|
353
448
|
}
|
|
354
449
|
const items = fresh.map((f) => f.item);
|
|
355
450
|
const notice = { lines, items };
|
|
356
451
|
const wake = !restart && items.some((i) => this.#wakes(i));
|
|
452
|
+
// 0480-F9: a group that could have woken but for the budget says why,
|
|
453
|
+
// as a single task's end does (0.48.0 delivered it silently)
|
|
454
|
+
const spent = !restart && !wake && fresh.some(({ task, item }) => this.#eligible(task, item)) ? this.#spent(fresh.map((f) => f.task.id)) : null;
|
|
455
|
+
if (spent !== null)
|
|
456
|
+
lines.push(spent);
|
|
357
457
|
const run = this.#o.liveRun();
|
|
358
458
|
if (run !== undefined && this.#hand(run, notice, wake))
|
|
359
459
|
return;
|
|
@@ -361,6 +461,12 @@ export class TaskDelivery {
|
|
|
361
461
|
this.#pending.push({ notice, wake });
|
|
362
462
|
this.#pump();
|
|
363
463
|
}
|
|
464
|
+
/** 0.49.0 A: the call that claimed a child returned — its tool_result is
|
|
465
|
+
* durable (error or not: it carries the handoffs either way). A crashed
|
|
466
|
+
* call has none: the kernel records an uncertain execution instead. */
|
|
467
|
+
#toldByResult(executionId) {
|
|
468
|
+
return this.#o.events().some((e) => e.type === "tool_result" && e.executionId === executionId);
|
|
469
|
+
}
|
|
364
470
|
/** handed to a run, or held for one, or already in the log */
|
|
365
471
|
#known(k) {
|
|
366
472
|
return this.#receipts.has(k) || this.#inFlight.has(k) || this.#pending.some((p) => p.notice.items.some((i) => key(i) === k));
|
|
@@ -370,8 +476,10 @@ export class TaskDelivery {
|
|
|
370
476
|
#unpend(k) {
|
|
371
477
|
this.#pending = this.#pending.filter((p) => !(p.notice.items.length === 1 && key(p.notice.items[0]) === k));
|
|
372
478
|
}
|
|
373
|
-
#agentTag(task, item) {
|
|
479
|
+
#agentTag(task, item, unreturned = false) {
|
|
374
480
|
const attrs = [`id="${task.id}"`, `kind="agent"`, `role="${task.agent?.role ?? ""}"`, `status="${item.transition}"`];
|
|
481
|
+
if (unreturned)
|
|
482
|
+
attrs.push(`note="its delegate call never returned"`);
|
|
375
483
|
const outcome = outcomeOf(task);
|
|
376
484
|
if (outcome !== undefined)
|
|
377
485
|
attrs.push(`outcome="${outcome}"`);
|
|
@@ -379,26 +487,11 @@ export class TaskDelivery {
|
|
|
379
487
|
attrs.push(`session="${task.agent.session}"`);
|
|
380
488
|
if (task.endedAt !== undefined)
|
|
381
489
|
attrs.push(`duration="${duration(task.endedAt - task.startedAt)}"`);
|
|
490
|
+
// I5: the answer is named only once the record commits it
|
|
382
491
|
const result = resultPath(task);
|
|
383
|
-
attrs.push(existsSync(result) ? `result="${result}"` : `output="${task.outputPath}"`);
|
|
492
|
+
attrs.push(recordOf(task) !== null && existsSync(result) ? `result="${result}"` : `output="${task.outputPath}"`);
|
|
384
493
|
return `<kiso-task ${attrs.join(" ")}/>`;
|
|
385
494
|
}
|
|
386
|
-
/** Lineage depth 1: the run that started the task began with a notice. */
|
|
387
|
-
#startedInWakeRun(executionId) {
|
|
388
|
-
const events = this.#o.events();
|
|
389
|
-
const at = events.findIndex((e) => e.type === "tool_execution_started" && e.executionId === executionId);
|
|
390
|
-
if (at < 0)
|
|
391
|
-
return false;
|
|
392
|
-
let start = 0;
|
|
393
|
-
for (let n = at - 1; n >= 0; n--) {
|
|
394
|
-
if (events[n].type === "terminal") {
|
|
395
|
-
start = n + 1;
|
|
396
|
-
break;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
const first = events.slice(start, at).find((e) => e.type === "user_input");
|
|
400
|
-
return isRuntimeInput(first);
|
|
401
|
-
}
|
|
402
495
|
#line(task, item) {
|
|
403
496
|
const attrs = [`id="${task.id}"`, `status="${item.transition}"`];
|
|
404
497
|
if (task.state.kind === "ended" && task.state.exitCode !== null)
|
|
@@ -433,42 +526,62 @@ function itemOf(task, transition) {
|
|
|
433
526
|
return end === null ? null : { taskId: task.id, transition: end };
|
|
434
527
|
}
|
|
435
528
|
const resultPath = (task) => join(dirname(task.outputPath), "result.md");
|
|
436
|
-
/** The child's outcome, as
|
|
529
|
+
/** The child's outcome, as its result record commits it. */
|
|
437
530
|
function outcomeOf(task) {
|
|
531
|
+
return recordOf(task)?.outcome;
|
|
532
|
+
}
|
|
533
|
+
/** The child's result record (result.json) — the commit of its result (I5). */
|
|
534
|
+
function recordOf(task) {
|
|
535
|
+
let raw;
|
|
438
536
|
try {
|
|
439
|
-
|
|
440
|
-
return typeof parsed.outcome === "string" ? parsed.outcome : undefined;
|
|
537
|
+
raw = readFileSync(join(dirname(task.outputPath), "result.json"), "utf8");
|
|
441
538
|
}
|
|
442
539
|
catch {
|
|
443
|
-
|
|
540
|
+
raw = null;
|
|
444
541
|
}
|
|
542
|
+
return handoffRecordOf(raw);
|
|
445
543
|
}
|
|
446
|
-
/** A child's
|
|
447
|
-
*
|
|
544
|
+
/** A child's handoff (the format in handoff.ts): its error, then its
|
|
545
|
+
* answer (result.md, read only beside a valid result.json — I5) cut to
|
|
546
|
+
* `bytes`, or — with no answer — the tail of what it printed. `bytes`
|
|
547
|
+
* counts what is shown, never the pointer. */
|
|
448
548
|
function excerptOf(task, bytes) {
|
|
449
|
-
const
|
|
450
|
-
let answer;
|
|
451
|
-
|
|
452
|
-
|
|
549
|
+
const record = recordOf(task);
|
|
550
|
+
let answer = "";
|
|
551
|
+
if (record !== null) {
|
|
552
|
+
try {
|
|
553
|
+
answer = readFileSync(resultPath(task), "utf8");
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
answer = "";
|
|
557
|
+
}
|
|
453
558
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
return tail === "" ? { text: "", bytes: 0 } : { text: `\n${tail}`, bytes: Buffer.byteLength(tail) };
|
|
457
|
-
}
|
|
458
|
-
if (answer === "")
|
|
459
|
-
return { text: "", bytes: 0 };
|
|
460
|
-
const size = Buffer.byteLength(answer);
|
|
461
|
-
if (size <= bytes)
|
|
462
|
-
return { text: `\n${answer}`, bytes: size };
|
|
463
|
-
if (bytes <= 0)
|
|
464
|
-
return { text: `\n[the whole answer: ${path}]`, bytes: 0 };
|
|
465
|
-
const cut = Buffer.from(answer).subarray(0, bytes).toString("utf8").replace(/\uFFFD+$/, "");
|
|
466
|
-
return { text: `\n${cut}\n… [truncated; the whole answer: ${path}]`, bytes: Buffer.byteLength(cut) };
|
|
559
|
+
const tail = rawTail(task.outputPath, TAIL_BYTES);
|
|
560
|
+
return handoffBody({ record, answer, tail: tail.text, tailCut: tail.cut, resultPath: resultPath(task) }, bytes);
|
|
467
561
|
}
|
|
468
562
|
function duration(ms) {
|
|
469
563
|
const s = Math.max(0, Math.round(ms / 1000));
|
|
470
564
|
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
|
|
471
565
|
}
|
|
566
|
+
/** The last `bytes` of a file as read, and whether it held more. */
|
|
567
|
+
function rawTail(path, bytes) {
|
|
568
|
+
try {
|
|
569
|
+
const fd = openSync(path, "r");
|
|
570
|
+
try {
|
|
571
|
+
const size = fstatSync(fd).size;
|
|
572
|
+
const n = Math.min(size, bytes);
|
|
573
|
+
const buf = Buffer.alloc(n);
|
|
574
|
+
readSync(fd, buf, 0, n, size - n);
|
|
575
|
+
return { text: buf.toString("utf8"), cut: size > n };
|
|
576
|
+
}
|
|
577
|
+
finally {
|
|
578
|
+
closeSync(fd);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
return { text: "", cut: false };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
472
585
|
function readTail(task, bytes) {
|
|
473
586
|
try {
|
|
474
587
|
const fd = openSync(task.outputPath, "r");
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The handoff format (ADR-0058, 0.49.0 C3/I7): what the model reads of one
|
|
3
|
+
* child, byte for byte the same whether the parent waited for it (the
|
|
4
|
+
* delegate tool's section, rendered by the subagent extension) or heard of
|
|
5
|
+
* it later (the group notice, rendered here). The extension carries its own
|
|
6
|
+
* copy, since it has no runtime dependency. The corpus in
|
|
7
|
+
* tests/fixtures/handoff holds both copies to the same expected bytes.
|
|
8
|
+
*
|
|
9
|
+
* The result record (result.json) commits a child's result (I5): the
|
|
10
|
+
* answer (result.md) is read only beside a valid record. Without one, the
|
|
11
|
+
* child's output tail is all there is (I6: nothing is inferred from its
|
|
12
|
+
* session's runs).
|
|
13
|
+
*
|
|
14
|
+
* Budgets count UTF-8 bytes of what is shown, never the pointers:
|
|
15
|
+
* the error line at most ERROR_BYTES, the tail at most TAIL_BYTES, and all
|
|
16
|
+
* of it within the budget the caller passes (CHILD_HANDOFF_BYTES, or what
|
|
17
|
+
* is left of GROUP_HANDOFF_BYTES).
|
|
18
|
+
*/
|
|
19
|
+
export declare const CHILD_HANDOFF_BYTES = 4096;
|
|
20
|
+
export declare const GROUP_HANDOFF_BYTES = 16384;
|
|
21
|
+
export declare const TAIL_BYTES = 2048;
|
|
22
|
+
export declare const ERROR_BYTES = 1024;
|
|
23
|
+
export interface HandoffRecord {
|
|
24
|
+
readonly outcome: string;
|
|
25
|
+
readonly error?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface HandoffInput {
|
|
28
|
+
/** result.json, parsed; null when it is absent or unreadable */
|
|
29
|
+
readonly record: HandoffRecord | null;
|
|
30
|
+
/** result.md's text ("" when absent) — shown only beside a record */
|
|
31
|
+
readonly answer: string;
|
|
32
|
+
/** the last bytes of the child's output, as read (at most TAIL_BYTES) */
|
|
33
|
+
readonly tail: string;
|
|
34
|
+
/** whether the output held more than `tail` */
|
|
35
|
+
readonly tailCut: boolean;
|
|
36
|
+
readonly resultPath: string;
|
|
37
|
+
}
|
|
38
|
+
/** The record a result.json holds, or null — the commit test (I5). */
|
|
39
|
+
export declare function handoffRecordOf(raw: string | null): HandoffRecord | null;
|
|
40
|
+
/** One child's handoff body: lines each led by "\n", and the bytes shown. */
|
|
41
|
+
export declare function handoffBody(input: HandoffInput, budget: number): {
|
|
42
|
+
readonly text: string;
|
|
43
|
+
readonly bytes: number;
|
|
44
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The handoff format (ADR-0058, 0.49.0 C3/I7): what the model reads of one
|
|
3
|
+
* child, byte for byte the same whether the parent waited for it (the
|
|
4
|
+
* delegate tool's section, rendered by the subagent extension) or heard of
|
|
5
|
+
* it later (the group notice, rendered here). The extension carries its own
|
|
6
|
+
* copy, since it has no runtime dependency. The corpus in
|
|
7
|
+
* tests/fixtures/handoff holds both copies to the same expected bytes.
|
|
8
|
+
*
|
|
9
|
+
* The result record (result.json) commits a child's result (I5): the
|
|
10
|
+
* answer (result.md) is read only beside a valid record. Without one, the
|
|
11
|
+
* child's output tail is all there is (I6: nothing is inferred from its
|
|
12
|
+
* session's runs).
|
|
13
|
+
*
|
|
14
|
+
* Budgets count UTF-8 bytes of what is shown, never the pointers:
|
|
15
|
+
* the error line at most ERROR_BYTES, the tail at most TAIL_BYTES, and all
|
|
16
|
+
* of it within the budget the caller passes (CHILD_HANDOFF_BYTES, or what
|
|
17
|
+
* is left of GROUP_HANDOFF_BYTES).
|
|
18
|
+
*/
|
|
19
|
+
export const CHILD_HANDOFF_BYTES = 4_096;
|
|
20
|
+
export const GROUP_HANDOFF_BYTES = 16_384;
|
|
21
|
+
export const TAIL_BYTES = 2_048;
|
|
22
|
+
export const ERROR_BYTES = 1_024;
|
|
23
|
+
/** The record a result.json holds, or null — the commit test (I5). */
|
|
24
|
+
export function handoffRecordOf(raw) {
|
|
25
|
+
if (raw === null)
|
|
26
|
+
return null;
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(raw);
|
|
29
|
+
if (typeof parsed.outcome !== "string" || parsed.outcome === "")
|
|
30
|
+
return null;
|
|
31
|
+
return typeof parsed.error === "string" && parsed.error !== "" ? { outcome: parsed.outcome, error: parsed.error } : { outcome: parsed.outcome };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** One child's handoff body: lines each led by "\n", and the bytes shown. */
|
|
38
|
+
export function handoffBody(input, budget) {
|
|
39
|
+
let text = "";
|
|
40
|
+
let used = 0;
|
|
41
|
+
const room = () => Math.max(0, budget - used);
|
|
42
|
+
if (input.record?.error !== undefined) {
|
|
43
|
+
const line = headOf(`error: ${input.record.error.replace(/\s*\n\s*/g, " ").trim()}`, Math.min(ERROR_BYTES, room()));
|
|
44
|
+
if (line.text !== "") {
|
|
45
|
+
text += `\n${line.text}${line.cut ? "…" : ""}`;
|
|
46
|
+
used += Buffer.byteLength(line.text);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const answer = input.record === null ? "" : input.answer.trimEnd();
|
|
50
|
+
if (answer !== "") {
|
|
51
|
+
if (Buffer.byteLength(answer) <= room())
|
|
52
|
+
return { text: `${text}\n${answer}`, bytes: used + Buffer.byteLength(answer) };
|
|
53
|
+
if (room() === 0)
|
|
54
|
+
return { text: `${text}\n[the whole answer: ${input.resultPath}]`, bytes: used };
|
|
55
|
+
const cut = headOf(answer, room()).text;
|
|
56
|
+
return { text: `${text}\n${cut}\n… [truncated; the whole answer: ${input.resultPath}]`, bytes: used + Buffer.byteLength(cut) };
|
|
57
|
+
}
|
|
58
|
+
const tail = input.tail.trimEnd();
|
|
59
|
+
if (tail !== "") {
|
|
60
|
+
const kept = tailOf(tail, Math.min(TAIL_BYTES, room()));
|
|
61
|
+
if (kept.text !== "") {
|
|
62
|
+
text += `\n${input.tailCut || kept.cut ? "…" : ""}${kept.text}`;
|
|
63
|
+
used += Buffer.byteLength(kept.text);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { text, bytes: used };
|
|
67
|
+
}
|
|
68
|
+
/** The first `bytes` of `s`, never splitting a character. */
|
|
69
|
+
function headOf(s, bytes) {
|
|
70
|
+
const buf = Buffer.from(s, "utf8");
|
|
71
|
+
if (buf.length <= bytes)
|
|
72
|
+
return { text: s, cut: false };
|
|
73
|
+
return { text: buf.subarray(0, Math.max(0, bytes)).toString("utf8").replace(/�+$/, ""), cut: true };
|
|
74
|
+
}
|
|
75
|
+
/** The last `bytes` of `s`, never splitting a character. */
|
|
76
|
+
function tailOf(s, bytes) {
|
|
77
|
+
const buf = Buffer.from(s, "utf8");
|
|
78
|
+
if (buf.length <= bytes)
|
|
79
|
+
return { text: s, cut: false };
|
|
80
|
+
return { text: buf.subarray(buf.length - Math.max(0, bytes)).toString("utf8").replace(/^�+/, ""), cut: true };
|
|
81
|
+
}
|
package/dist/tasks/journal.d.ts
CHANGED
|
@@ -27,7 +27,13 @@ export interface TaskLaunch {
|
|
|
27
27
|
export interface TaskAgent {
|
|
28
28
|
readonly role: string;
|
|
29
29
|
readonly session: string;
|
|
30
|
+
/** 0.49.0 B: a writer — its end is its COLLECTION. The process ending
|
|
31
|
+
* is not the result: the parent collects what it left (a patch, an
|
|
32
|
+
* acceptance) and records `collected`, and only then has it ended. */
|
|
33
|
+
readonly collect?: true;
|
|
30
34
|
}
|
|
35
|
+
/** 0.49.0 B: how a writer's collection ended. */
|
|
36
|
+
export type CollectionOutcome = "collected" | "unchanged" | "stopped" | "failed";
|
|
31
37
|
export type TaskRecord = {
|
|
32
38
|
readonly type: "planned";
|
|
33
39
|
readonly ts: number;
|
|
@@ -89,6 +95,14 @@ export type TaskRecord = {
|
|
|
89
95
|
readonly ts: number;
|
|
90
96
|
readonly transition: ClaimedTransition;
|
|
91
97
|
readonly executionId: string;
|
|
98
|
+
}
|
|
99
|
+
/** 0.49.0 B: the parent's collection of a writer finished — the LAST
|
|
100
|
+
* record of its end; until it is written the writer has not ended. */
|
|
101
|
+
| {
|
|
102
|
+
readonly type: "collected";
|
|
103
|
+
readonly ts: number;
|
|
104
|
+
readonly outcome: CollectionOutcome;
|
|
105
|
+
readonly reason?: string;
|
|
92
106
|
};
|
|
93
107
|
/** A transition as the model is told it — a notice's or a result's name. */
|
|
94
108
|
export type ClaimedTransition = "ready" | "exited" | "failed" | "stopped" | "unknown";
|
package/dist/tasks/manager.d.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* an idle session wakes (3c).
|
|
22
22
|
*/
|
|
23
23
|
import type { AbortSignalLike } from "@vincemakes/kiso-core";
|
|
24
|
-
import { type ClaimedTransition, type TaskAgent, type TaskProfile, type TaskState } from "./journal.js";
|
|
24
|
+
import { type ClaimedTransition, type CollectionOutcome, type TaskAgent, type TaskProfile, type TaskState } from "./journal.js";
|
|
25
25
|
/** Whether a recorded runner is still that process (ADR-0058 §6):
|
|
26
26
|
* "verified" — the pid is live AND its OS start time is the recorded one;
|
|
27
27
|
* "gone" — no such process, or another process holds the pid;
|
|
@@ -86,7 +86,15 @@ export interface TaskInfo {
|
|
|
86
86
|
readonly transition: ClaimedTransition;
|
|
87
87
|
readonly executionId: string;
|
|
88
88
|
}[];
|
|
89
|
+
/** 0.49.0 B: a writer's collection, once recorded (absent until then). */
|
|
90
|
+
readonly collection?: {
|
|
91
|
+
readonly outcome: CollectionOutcome;
|
|
92
|
+
readonly reason?: string;
|
|
93
|
+
};
|
|
89
94
|
}
|
|
95
|
+
/** 0.49.0 B: a writer whose process ended and whose collection is not yet
|
|
96
|
+
* recorded — not ended: never announced, never settled, never complete. */
|
|
97
|
+
export declare function isCollecting(task: TaskInfo): boolean;
|
|
90
98
|
/** The name a task's end (or its runner's vanishing, or a start that never
|
|
91
99
|
* ran) has for the model — as a notice says it, and as a claim records it. */
|
|
92
100
|
export declare function endTransitionOf(task: TaskInfo): Exclude<ClaimedTransition, "ready"> | null;
|
|
@@ -144,6 +152,12 @@ export interface TaskManagerOptions {
|
|
|
144
152
|
readonly backend: TaskBackend;
|
|
145
153
|
/** A first listener — the same as `subscribe` right after construction. */
|
|
146
154
|
readonly onTransition?: TaskListener;
|
|
155
|
+
/** 0.49.0 B: the host's collector for a writer that ended — called once
|
|
156
|
+
* per collecting task (by the watcher, a wait, or `collectPending`). It
|
|
157
|
+
* records `collected` itself, last; a collector that throws leaves the
|
|
158
|
+
* record to the manager (`failed`, with the reason), so a writer's end
|
|
159
|
+
* is never stuck. */
|
|
160
|
+
readonly collect?: (task: TaskInfo) => Promise<void>;
|
|
147
161
|
/** How often live tasks are re-read. Default 500 ms. */
|
|
148
162
|
readonly pollMs?: number;
|
|
149
163
|
/** How long a live runner's identity is reused before it is checked
|
|
@@ -185,10 +199,16 @@ export declare class TaskManager {
|
|
|
185
199
|
* durable first, then the transition counts as seen, so it is never
|
|
186
200
|
* announced. A timeout, an abort, no execution id, an agent task (its
|
|
187
201
|
* end belongs to its group, 3d) or a claim that cannot be written
|
|
188
|
-
* claim nothing: the transition is announced as before.
|
|
202
|
+
* claim nothing: the transition is announced as before.
|
|
203
|
+
*
|
|
204
|
+
* 0.49.0 A: `agentJoin` — the delegate call that started an agent task
|
|
205
|
+
* and waits for it (the join) claims its end, so its own result reports
|
|
206
|
+
* it and the group's notice names it as reported (I2: only a claim made
|
|
207
|
+
* durable counts). While it waits, the watcher never announces it (I1). */
|
|
189
208
|
awaitSettled(id: string, until: "end" | "ready", ms: number, opts?: {
|
|
190
209
|
readonly executionId?: string;
|
|
191
210
|
readonly signal?: AbortSignalLike;
|
|
211
|
+
readonly agentJoin?: boolean;
|
|
192
212
|
}): Promise<TaskSettled>;
|
|
193
213
|
/** 3e: a running foreground command that can become a task now —
|
|
194
214
|
* registered by its owner while it runs; returns the unregister. Every
|
|
@@ -216,4 +236,8 @@ export declare class TaskManager {
|
|
|
216
236
|
observe(): void;
|
|
217
237
|
/** Stop watching. Tasks keep running. */
|
|
218
238
|
close(): void;
|
|
239
|
+
/** 0.49.0 B: collect every writer that ended uncollected — at a session's
|
|
240
|
+
* open, BEFORE its delivery is built, so a restart delivers a writer as
|
|
241
|
+
* it delivers anything that ended while nobody listened (never a wake). */
|
|
242
|
+
collectPending(): Promise<void>;
|
|
219
243
|
}
|
package/dist/tasks/manager.js
CHANGED
|
@@ -23,10 +23,17 @@
|
|
|
23
23
|
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
24
24
|
import { dirname, join } from "node:path";
|
|
25
25
|
import { appendRecord, fsyncDir, readRecords, runnerOf, verdictOf } from "./journal.js";
|
|
26
|
+
/** 0.49.0 B: a writer whose process ended and whose collection is not yet
|
|
27
|
+
* recorded — not ended: never announced, never settled, never complete. */
|
|
28
|
+
export function isCollecting(task) {
|
|
29
|
+
return task.agent?.collect === true && task.state.kind === "ended" && task.collection === undefined;
|
|
30
|
+
}
|
|
26
31
|
/** The name a task's end (or its runner's vanishing, or a start that never
|
|
27
32
|
* ran) has for the model — as a notice says it, and as a claim records it. */
|
|
28
33
|
export function endTransitionOf(task) {
|
|
29
34
|
const s = task.state;
|
|
35
|
+
if (isCollecting(task))
|
|
36
|
+
return null;
|
|
30
37
|
if (s.kind === "ended") {
|
|
31
38
|
if (task.stoppedBy !== undefined)
|
|
32
39
|
return "stopped";
|
|
@@ -58,9 +65,13 @@ export class TaskManager {
|
|
|
58
65
|
#detachable = new Map();
|
|
59
66
|
/** id → how many `awaitSettled` calls wait on it: the watcher skips it */
|
|
60
67
|
#waiting = new Map();
|
|
68
|
+
/** 0.49.0 B: the host's collector, and the writers being collected now */
|
|
69
|
+
#collect;
|
|
70
|
+
#collecting = new Map();
|
|
61
71
|
constructor(options) {
|
|
62
72
|
this.root = options.root;
|
|
63
73
|
this.#backend = options.backend;
|
|
74
|
+
this.#collect = options.collect;
|
|
64
75
|
if (options.onTransition !== undefined)
|
|
65
76
|
this.#listeners.add(options.onTransition);
|
|
66
77
|
this.#pollMs = options.pollMs ?? 500;
|
|
@@ -234,7 +245,12 @@ export class TaskManager {
|
|
|
234
245
|
* durable first, then the transition counts as seen, so it is never
|
|
235
246
|
* announced. A timeout, an abort, no execution id, an agent task (its
|
|
236
247
|
* end belongs to its group, 3d) or a claim that cannot be written
|
|
237
|
-
* claim nothing: the transition is announced as before.
|
|
248
|
+
* claim nothing: the transition is announced as before.
|
|
249
|
+
*
|
|
250
|
+
* 0.49.0 A: `agentJoin` — the delegate call that started an agent task
|
|
251
|
+
* and waits for it (the join) claims its end, so its own result reports
|
|
252
|
+
* it and the group's notice names it as reported (I2: only a claim made
|
|
253
|
+
* durable counts). While it waits, the watcher never announces it (I1). */
|
|
238
254
|
async awaitSettled(id, until, ms, opts = {}) {
|
|
239
255
|
if (this.get(id) === undefined)
|
|
240
256
|
throw new Error(`no task ${id} in this session`);
|
|
@@ -250,9 +266,10 @@ export class TaskManager {
|
|
|
250
266
|
const deadline = Date.now() + ms;
|
|
251
267
|
for (;;) {
|
|
252
268
|
const info = this.#info(id);
|
|
269
|
+
this.#maybeCollect(info); // a writer it waits for ends only once collected
|
|
253
270
|
const reached = endTransitionOf(info) ?? (until === "ready" && info.state.kind === "running" && info.state.ready ? "ready" : null);
|
|
254
271
|
if (reached !== null)
|
|
255
|
-
return { info, settled: true, claimed: this.#claim(info, reached, opts.executionId) };
|
|
272
|
+
return { info, settled: true, claimed: this.#claim(info, reached, opts.executionId, opts.agentJoin === true) };
|
|
256
273
|
const left = deadline - Date.now();
|
|
257
274
|
if (aborted || left <= 0)
|
|
258
275
|
return { info, settled: false, claimed: false };
|
|
@@ -277,8 +294,8 @@ export class TaskManager {
|
|
|
277
294
|
/** The frozen order: the claim durable, THEN seen — an append that fails
|
|
278
295
|
* leaves the transition unseen, to be announced (a duplicate is the
|
|
279
296
|
* lesser evil next to a lost transition). */
|
|
280
|
-
#claim(info, transition, executionId) {
|
|
281
|
-
if (executionId === undefined || info.agent !== undefined)
|
|
297
|
+
#claim(info, transition, executionId, agentJoin = false) {
|
|
298
|
+
if (executionId === undefined || (info.agent !== undefined && !agentJoin))
|
|
282
299
|
return false;
|
|
283
300
|
try {
|
|
284
301
|
appendRecord(join(this.root, info.id, JOURNAL), { type: "result_claimed", ts: Date.now(), transition, executionId });
|
|
@@ -378,9 +395,19 @@ export class TaskManager {
|
|
|
378
395
|
}
|
|
379
396
|
#poll() {
|
|
380
397
|
for (const id of this.#ids()) {
|
|
381
|
-
// a tool call is waiting on it: what it sees, its result reports
|
|
382
|
-
|
|
398
|
+
// a tool call is waiting on it: what it sees, its result reports —
|
|
399
|
+
// but a writer it waits for is still collected (0.49.0 B)
|
|
400
|
+
if (this.#waiting.has(id)) {
|
|
401
|
+
if (this.#collect !== undefined) {
|
|
402
|
+
try {
|
|
403
|
+
this.#maybeCollect(this.#info(id));
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// a corrupt journal: read loudly where it is read on purpose
|
|
407
|
+
}
|
|
408
|
+
}
|
|
383
409
|
continue;
|
|
410
|
+
}
|
|
384
411
|
const before = this.#seen.get(id);
|
|
385
412
|
// a lost (`unknown`) task is still read: if its end is recorded
|
|
386
413
|
// later, that end is announced (Amendment 8)
|
|
@@ -393,6 +420,7 @@ export class TaskManager {
|
|
|
393
420
|
catch {
|
|
394
421
|
continue; // a corrupt journal: reported where it is read, never here in the background
|
|
395
422
|
}
|
|
423
|
+
this.#maybeCollect(info);
|
|
396
424
|
const now = this.#kindOf(info);
|
|
397
425
|
if (now === before)
|
|
398
426
|
continue;
|
|
@@ -404,8 +432,52 @@ export class TaskManager {
|
|
|
404
432
|
}
|
|
405
433
|
}
|
|
406
434
|
#kindOf(info) {
|
|
435
|
+
if (isCollecting(info))
|
|
436
|
+
return "collecting";
|
|
407
437
|
return info.state.kind === "running" && info.state.ready ? "ready" : info.state.kind;
|
|
408
438
|
}
|
|
439
|
+
/** 0.49.0 B: start a writer's collection once — never twice at a time. A
|
|
440
|
+
* collector that throws (or ends without its record) is recorded as a
|
|
441
|
+
* failed collection, so the writer's end is never stuck. */
|
|
442
|
+
#maybeCollect(info) {
|
|
443
|
+
if (this.#collect === undefined || !isCollecting(info) || this.#collecting.has(info.id))
|
|
444
|
+
return;
|
|
445
|
+
const collect = this.#collect;
|
|
446
|
+
const journal = join(this.root, info.id, JOURNAL);
|
|
447
|
+
const done = (async () => {
|
|
448
|
+
let why;
|
|
449
|
+
try {
|
|
450
|
+
await collect(info);
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
why = err instanceof Error ? err.message : String(err);
|
|
454
|
+
}
|
|
455
|
+
if (!readRecords(journal).some((r) => r.type === "collected")) {
|
|
456
|
+
appendRecord(journal, { type: "collected", ts: Date.now(), outcome: "failed", reason: why ?? "the collector recorded nothing" });
|
|
457
|
+
}
|
|
458
|
+
})().finally(() => this.#collecting.delete(info.id));
|
|
459
|
+
this.#collecting.set(info.id, done);
|
|
460
|
+
}
|
|
461
|
+
/** 0.49.0 B: collect every writer that ended uncollected — at a session's
|
|
462
|
+
* open, BEFORE its delivery is built, so a restart delivers a writer as
|
|
463
|
+
* it delivers anything that ended while nobody listened (never a wake). */
|
|
464
|
+
async collectPending() {
|
|
465
|
+
for (const id of this.#ids()) {
|
|
466
|
+
let info;
|
|
467
|
+
try {
|
|
468
|
+
info = this.#info(id);
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (info === undefined)
|
|
474
|
+
continue;
|
|
475
|
+
this.#maybeCollect(info);
|
|
476
|
+
const pending = this.#collecting.get(id);
|
|
477
|
+
if (pending !== undefined)
|
|
478
|
+
await pending;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
409
481
|
#info(id) {
|
|
410
482
|
const records = readRecords(join(this.root, id, JOURNAL));
|
|
411
483
|
const planned = records.find((r) => r.type === "planned");
|
|
@@ -419,6 +491,7 @@ export class TaskManager {
|
|
|
419
491
|
const claims = records
|
|
420
492
|
.filter((r) => r.type === "result_claimed")
|
|
421
493
|
.map((r) => ({ transition: r.transition, executionId: r.executionId }));
|
|
494
|
+
const collected = records.find((r) => r.type === "collected");
|
|
422
495
|
return {
|
|
423
496
|
id,
|
|
424
497
|
command: planned?.command ?? "",
|
|
@@ -432,6 +505,7 @@ export class TaskManager {
|
|
|
432
505
|
startedAt: planned?.ts ?? 0,
|
|
433
506
|
...(terminal !== undefined ? { endedAt: terminal.ts } : {}),
|
|
434
507
|
...(claims.length > 0 ? { claims } : {}),
|
|
508
|
+
...(collected !== undefined ? { collection: { outcome: collected.outcome, ...(collected.reason !== undefined ? { reason: collected.reason } : {}) } } : {}),
|
|
435
509
|
};
|
|
436
510
|
}
|
|
437
511
|
#ids() {
|
package/dist/trace/guard.js
CHANGED
|
@@ -176,6 +176,7 @@ export class RequestTracer {
|
|
|
176
176
|
const rent = buildRentLedger({
|
|
177
177
|
model: options.model,
|
|
178
178
|
...(rentParts?.base !== undefined ? { base: rentParts.base } : {}),
|
|
179
|
+
...(rentParts?.table !== undefined ? { table: rentParts.table } : {}),
|
|
179
180
|
...(rentParts?.appends !== undefined ? { appends: rentParts.appends } : {}),
|
|
180
181
|
...(tools !== undefined ? { tools } : {}),
|
|
181
182
|
});
|
package/dist/trace/rent.d.ts
CHANGED
|
@@ -11,10 +11,17 @@
|
|
|
11
11
|
*
|
|
12
12
|
* - system:base the session's own prompt as the CLI handed it
|
|
13
13
|
* (built-in constant + project instructions — the
|
|
14
|
-
* runtime cannot split those, R3).
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* runtime cannot split those, R3).
|
|
15
|
+
* - system:tools the runtime's generated tool table (compose.ts,
|
|
16
|
+
* composeToolTable): the routing rows, the fixed
|
|
17
|
+
* directives, each tool's snippet and guidelines,
|
|
18
|
+
* between the base and the appends. OVERTURNS the
|
|
19
|
+
* E3 note that it "stays out of the ledger (counts
|
|
20
|
+
* live in tool:<name> lines)": the table is prose
|
|
21
|
+
* the tool specs do not contain (1,686 chars at
|
|
22
|
+
* f42a0edc), so the ledger was short by that much
|
|
23
|
+
* on every request (finding RG-F2, kiso-doc
|
|
24
|
+
* plan-rent-gate rev 2).
|
|
18
25
|
* - system:ext:<name> one line per extension with a non-empty
|
|
19
26
|
* systemPrompt.append, measured on the append
|
|
20
27
|
* string, in load order.
|
|
@@ -44,7 +51,7 @@
|
|
|
44
51
|
*/
|
|
45
52
|
import type { ToolSpec } from "@vincemakes/kiso-core";
|
|
46
53
|
export interface RentLine {
|
|
47
|
-
/** "system:base" | "system:ext:<name>" | "tool:<name>" | "envelope" */
|
|
54
|
+
/** "system:base" | "system:tools" | "system:ext:<name>" | "tool:<name>" | "envelope" */
|
|
48
55
|
surface: string;
|
|
49
56
|
/** length of the serialized surface, measured, never a copy */
|
|
50
57
|
chars: number;
|
|
@@ -59,6 +66,9 @@ export declare const RENT_LINE_FIELDS: readonly ["surface", "chars", "estTokens"
|
|
|
59
66
|
* systemPrompt is the result — the parts are the ledger's inputs). */
|
|
60
67
|
export interface RentParts {
|
|
61
68
|
base?: string;
|
|
69
|
+
/** RG-F2: the generated tool table's text, as composed for the run
|
|
70
|
+
* (absent when the table is off or the registry is empty). */
|
|
71
|
+
table?: string;
|
|
62
72
|
appends?: readonly {
|
|
63
73
|
name: string;
|
|
64
74
|
text: string;
|
|
@@ -68,7 +78,7 @@ export interface RentInput extends RentParts {
|
|
|
68
78
|
model: string;
|
|
69
79
|
tools?: readonly ToolSpec[];
|
|
70
80
|
}
|
|
71
|
-
/** The one ledger: system:base, then system:ext:* (load order), then
|
|
81
|
+
/** The one ledger: system:base, then system:tools, then system:ext:* (load order), then
|
|
72
82
|
* tool:* (the array's order — the registry's toSpecs() order), then the
|
|
73
83
|
* envelope. Absent surfaces contribute no lines (R9). */
|
|
74
84
|
export declare function buildRentLedger(input: RentInput): RentLine[];
|
package/dist/trace/rent.js
CHANGED
|
@@ -11,10 +11,17 @@
|
|
|
11
11
|
*
|
|
12
12
|
* - system:base the session's own prompt as the CLI handed it
|
|
13
13
|
* (built-in constant + project instructions — the
|
|
14
|
-
* runtime cannot split those, R3).
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* runtime cannot split those, R3).
|
|
15
|
+
* - system:tools the runtime's generated tool table (compose.ts,
|
|
16
|
+
* composeToolTable): the routing rows, the fixed
|
|
17
|
+
* directives, each tool's snippet and guidelines,
|
|
18
|
+
* between the base and the appends. OVERTURNS the
|
|
19
|
+
* E3 note that it "stays out of the ledger (counts
|
|
20
|
+
* live in tool:<name> lines)": the table is prose
|
|
21
|
+
* the tool specs do not contain (1,686 chars at
|
|
22
|
+
* f42a0edc), so the ledger was short by that much
|
|
23
|
+
* on every request (finding RG-F2, kiso-doc
|
|
24
|
+
* plan-rent-gate rev 2).
|
|
18
25
|
* - system:ext:<name> one line per extension with a non-empty
|
|
19
26
|
* systemPrompt.append, measured on the append
|
|
20
27
|
* string, in load order.
|
|
@@ -49,13 +56,15 @@ const line = (surface, text) => ({
|
|
|
49
56
|
chars: text.length,
|
|
50
57
|
estTokens: Math.ceil(text.length / 4),
|
|
51
58
|
});
|
|
52
|
-
/** The one ledger: system:base, then system:ext:* (load order), then
|
|
59
|
+
/** The one ledger: system:base, then system:tools, then system:ext:* (load order), then
|
|
53
60
|
* tool:* (the array's order — the registry's toSpecs() order), then the
|
|
54
61
|
* envelope. Absent surfaces contribute no lines (R9). */
|
|
55
62
|
export function buildRentLedger(input) {
|
|
56
63
|
const lines = [];
|
|
57
64
|
if (input.base !== undefined)
|
|
58
65
|
lines.push(line("system:base", input.base));
|
|
66
|
+
if (input.table !== undefined && input.table !== "")
|
|
67
|
+
lines.push(line("system:tools", input.table));
|
|
59
68
|
for (const append of input.appends ?? [])
|
|
60
69
|
lines.push(line(`system:ext:${append.name}`, append.text));
|
|
61
70
|
for (const tool of input.tools ?? []) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"description": "kiso runtime \u2014 durable multi-turn agent sessions: AgentDefinition, AgentRuntime, AgentSession, Run, append-only JSONL store.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"test": "vitest run"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@vincemakes/kiso-core": "0.
|
|
28
|
+
"@vincemakes/kiso-core": "0.49.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@vincemakes/kiso-provider-anthropic": "0.
|
|
32
|
-
"@vincemakes/kiso-provider-openai": "0.
|
|
33
|
-
"@vincemakes/kiso-provider-openai-responses": "0.
|
|
31
|
+
"@vincemakes/kiso-provider-anthropic": "0.49.0",
|
|
32
|
+
"@vincemakes/kiso-provider-openai": "0.49.0",
|
|
33
|
+
"@vincemakes/kiso-provider-openai-responses": "0.49.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependenciesMeta": {
|
|
36
36
|
"@vincemakes/kiso-provider-anthropic": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@vincemakes/kiso-evals": "0.
|
|
47
|
+
"@vincemakes/kiso-evals": "0.49.0",
|
|
48
48
|
"@types/node": "^26.1.2",
|
|
49
49
|
"typescript": "^5.7.2",
|
|
50
50
|
"vitest": "^3.0.0"
|