@vincemakes/kiso-runtime 0.45.3 → 0.46.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/dist/agent.d.ts +6 -0
- package/dist/agent.js +5 -1
- package/dist/internal.d.ts +2 -0
- package/dist/internal.js +4 -0
- package/dist/profile.js +13 -7
- package/dist/run.d.ts +22 -3
- package/dist/run.js +74 -22
- package/dist/session.d.ts +19 -0
- package/dist/session.js +66 -5
- package/dist/store.js +14 -4
- package/dist/tasks/delivery.d.ts +82 -0
- package/dist/tasks/delivery.js +482 -0
- package/dist/tasks/journal.d.ts +150 -0
- package/dist/tasks/journal.js +124 -0
- package/dist/tasks/manager.d.ts +219 -0
- package/dist/tasks/manager.js +438 -0
- package/package.json +6 -6
package/dist/agent.d.ts
CHANGED
|
@@ -34,6 +34,12 @@ export interface AgentDefinition {
|
|
|
34
34
|
/** 0.40.0: the config profile that named this binding, recorded per
|
|
35
35
|
* revision for display. Never part of drift. */
|
|
36
36
|
readonly profileName?: string;
|
|
37
|
+
/** B1: a NEW session's initial reasoning (the CLI passes the effort last
|
|
38
|
+
* picked for the profile). Recorded in the session's first profile
|
|
39
|
+
* revision; never read for a session that has a recorded profile — its
|
|
40
|
+
* own wins, and a changed binding resets to defaults as before.
|
|
41
|
+
* `setModelBinding` stays the one way to change a live binding. */
|
|
42
|
+
readonly reasoning?: import("./provider/metadata.js").ReasoningSetting;
|
|
37
43
|
/** `Tool<any>` like the registry: typed tools register without casts. */
|
|
38
44
|
readonly tools: readonly Tool<any>[];
|
|
39
45
|
readonly store: SessionStore;
|
package/dist/agent.js
CHANGED
|
@@ -155,7 +155,11 @@ export class AgentRuntime {
|
|
|
155
155
|
...((restored !== null ? restored.scope : startupScope) !== undefined
|
|
156
156
|
? { continuationScope: (restored !== null ? restored.scope : startupScope) }
|
|
157
157
|
: {}),
|
|
158
|
-
...(restored !== null
|
|
158
|
+
...(restored !== null
|
|
159
|
+
? { reasoning: restored.reasoning }
|
|
160
|
+
: newSession !== null && this.#definition.reasoning !== undefined
|
|
161
|
+
? { reasoning: this.#definition.reasoning }
|
|
162
|
+
: {}),
|
|
159
163
|
...(profilePending ? { profilePending: true } : {}),
|
|
160
164
|
...(newSession !== null ? { newSession } : {}),
|
|
161
165
|
...(this.#definition.profileName !== undefined ? { profileName: this.#definition.profileName } : {}),
|
package/dist/internal.d.ts
CHANGED
|
@@ -31,3 +31,5 @@ export * from "./checkpoint.js";
|
|
|
31
31
|
export * from "./request-budget.js";
|
|
32
32
|
export { readProfile, readSummary, writeSummary, type ExecutionProfile, type ProfileReadResult } from "./profile.js";
|
|
33
33
|
export { SUMMARY_MIGRATION_MARKER, listSessionSidecars, migrateSummaries, sessionSummary, summarizeEvents, summaryMigrationPending, type SessionListing, type SessionSummary } from "./session-summary.js";
|
|
34
|
+
export * from "./tasks/journal.js";
|
|
35
|
+
export * from "./tasks/manager.js";
|
package/dist/internal.js
CHANGED
|
@@ -40,3 +40,7 @@ export * from "./request-budget.js";
|
|
|
40
40
|
// 0.40.0: the listing reads the recorded workspace and profile name.
|
|
41
41
|
export { readProfile, readSummary, writeSummary } from "./profile.js";
|
|
42
42
|
export { SUMMARY_MIGRATION_MARKER, listSessionSidecars, migrateSummaries, sessionSummary, summarizeEvents, summaryMigrationPending } from "./session-summary.js";
|
|
43
|
+
// ADR-0058 §6: the task journal and the TaskManager (internal until the
|
|
44
|
+
// public shape is proven by the shell and delegate steps)
|
|
45
|
+
export * from "./tasks/journal.js";
|
|
46
|
+
export * from "./tasks/manager.js";
|
package/dist/profile.js
CHANGED
|
@@ -90,7 +90,11 @@ function writeTenants(root, sessionId, tenants) {
|
|
|
90
90
|
// this file comes into existence. Existing files are not migrated —
|
|
91
91
|
// R6's choice, kept.
|
|
92
92
|
writeFileSync(tmp, `${JSON.stringify(tenants, null, "\t")}\n`, { mode: 0o600 });
|
|
93
|
-
|
|
93
|
+
// Windows: only a write-capable handle can be flushed, and no
|
|
94
|
+
// directory opens as a file — the rename is not flushed there (a
|
|
95
|
+
// process crash loses nothing; the power-loss contract is P6's)
|
|
96
|
+
const windows = process.platform === "win32";
|
|
97
|
+
const fd = openSync(tmp, windows ? "r+" : "r");
|
|
94
98
|
try {
|
|
95
99
|
fsyncSync(fd);
|
|
96
100
|
}
|
|
@@ -98,12 +102,14 @@ function writeTenants(root, sessionId, tenants) {
|
|
|
98
102
|
closeSync(fd);
|
|
99
103
|
}
|
|
100
104
|
renameSync(tmp, path);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
if (!windows) {
|
|
106
|
+
const dirFd = openSync(dirname(path), "r");
|
|
107
|
+
try {
|
|
108
|
+
fsyncSync(dirFd);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
closeSync(dirFd);
|
|
112
|
+
}
|
|
107
113
|
}
|
|
108
114
|
}
|
|
109
115
|
finally {
|
package/dist/run.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* persistence, the loop drive, the durable recovery state machine), moved
|
|
4
4
|
* verbatim from session.ts.
|
|
5
5
|
*/
|
|
6
|
-
import { type AbortSignalLike, type Adapter, type ContentBlock, type Event } from "@vincemakes/kiso-core";
|
|
6
|
+
import { type AbortSignalLike, type TaskDeliveryItem, type Adapter, type ContentBlock, type Event } from "@vincemakes/kiso-core";
|
|
7
7
|
import type { SessionStore } from "./store.js";
|
|
8
8
|
import { type AgentSession, type SessionConfig } from "./session.js";
|
|
9
9
|
/**
|
|
@@ -21,6 +21,14 @@ export declare class RunClosedError extends Error {
|
|
|
21
21
|
* natural shape; the handle also carries the runId, the abort and the
|
|
22
22
|
* steer.
|
|
23
23
|
*/
|
|
24
|
+
/** ADR-0058 (3c): a task delivery handed to a run — one line per task
|
|
25
|
+
* (the wire marking) and the transitions it carries (the receipt). */
|
|
26
|
+
export interface TaskNotice {
|
|
27
|
+
readonly lines: readonly string[];
|
|
28
|
+
readonly items: readonly TaskDeliveryItem[];
|
|
29
|
+
}
|
|
30
|
+
/** The runtime's words, marked so the model never takes them for a person's. */
|
|
31
|
+
export declare const NOTICE_FOOTER = "Runtime notice \u2014 not the user.";
|
|
24
32
|
export declare class Run implements AsyncIterable<Event> {
|
|
25
33
|
#private;
|
|
26
34
|
runId: string;
|
|
@@ -34,9 +42,20 @@ export declare class Run implements AsyncIterable<Event> {
|
|
|
34
42
|
* {@link RunClosedError} once the run has decided to end.
|
|
35
43
|
*/
|
|
36
44
|
steer(content: string | readonly ContentBlock[]): void;
|
|
37
|
-
/** ADR-
|
|
45
|
+
/** ADR-0058 (3c) — hand this run a task delivery. It is admitted at the
|
|
46
|
+
* next Safe Admission boundary, in arrival order with the person's
|
|
47
|
+
* steers, as a `source: "system"` user_input whose `via` is the receipt;
|
|
48
|
+
* it never passes onUserMessage. False once the run has decided to end
|
|
49
|
+
* — the caller keeps the notice for the next run or a wake. */
|
|
50
|
+
notify(notice: TaskNotice): boolean;
|
|
51
|
+
/** ADR-0057 — take back the person's input that has not been admitted
|
|
52
|
+
* yet. A task notice is not the person's, and stays. */
|
|
38
53
|
retract(): readonly (string | readonly ContentBlock[])[];
|
|
39
|
-
/** ADR-0057 — input that arrived but was not admitted when
|
|
54
|
+
/** ADR-0057 — the person's input that arrived but was not admitted when
|
|
55
|
+
* the run ended. */
|
|
40
56
|
unadmitted(): readonly (string | readonly ContentBlock[])[];
|
|
57
|
+
/** ADR-0058 (3c) — task notices still pending when the run ended: the
|
|
58
|
+
* delivery takes them back, never lost. */
|
|
59
|
+
unadmittedNotices(): readonly TaskNotice[];
|
|
41
60
|
[Symbol.asyncIterator](): AsyncIterator<Event>;
|
|
42
61
|
}
|
package/dist/run.js
CHANGED
|
@@ -26,13 +26,8 @@ export class RunClosedError extends Error {
|
|
|
26
26
|
this.runId = runId;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
* it may admit further input at Safe Admission boundaries).
|
|
32
|
-
* Async-iterable, so `for await (const ev of session.run(x))` is the
|
|
33
|
-
* natural shape; the handle also carries the runId, the abort and the
|
|
34
|
-
* steer.
|
|
35
|
-
*/
|
|
29
|
+
/** The runtime's words, marked so the model never takes them for a person's. */
|
|
30
|
+
export const NOTICE_FOOTER = "Runtime notice — not the user.";
|
|
36
31
|
export class Run {
|
|
37
32
|
runId;
|
|
38
33
|
#store;
|
|
@@ -51,11 +46,14 @@ export class Run {
|
|
|
51
46
|
#externalSignal;
|
|
52
47
|
#decisionIds = [];
|
|
53
48
|
#started = false;
|
|
54
|
-
// ADR-0057 — the ephemeral ingress: a person's input
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
49
|
+
// ADR-0057 — the ephemeral ingress: a person's input AND the runtime's
|
|
50
|
+
// task notices (ADR-0058 3c), ONE queue in arrival order, held in memory
|
|
51
|
+
// until an admission site takes it. The log owns what was admitted;
|
|
52
|
+
// nothing here is durable (a crash before admission loses it — a task
|
|
53
|
+
// notice is re-derived from the journal and the log's receipts).
|
|
54
|
+
#ingress = [];
|
|
58
55
|
#unadmitted = [];
|
|
56
|
+
#unadmittedNotices = [];
|
|
59
57
|
#sealed = false;
|
|
60
58
|
constructor(store, adapter, config, session, input, externalSignal, resume,
|
|
61
59
|
// TV-1B: durable PROVENANCE for the input (e.g. source:"system"
|
|
@@ -89,23 +87,72 @@ export class Run {
|
|
|
89
87
|
steer(content) {
|
|
90
88
|
if (this.#sealed)
|
|
91
89
|
throw new RunClosedError(this.runId);
|
|
92
|
-
this.#
|
|
90
|
+
this.#ingress.push({ kind: "human", content });
|
|
91
|
+
// ADR-0057 §5: a long foreground command no longer holds the steer
|
|
92
|
+
this.#session.detachForSteer();
|
|
93
|
+
}
|
|
94
|
+
/** ADR-0058 (3c) — hand this run a task delivery. It is admitted at the
|
|
95
|
+
* next Safe Admission boundary, in arrival order with the person's
|
|
96
|
+
* steers, as a `source: "system"` user_input whose `via` is the receipt;
|
|
97
|
+
* it never passes onUserMessage. False once the run has decided to end
|
|
98
|
+
* — the caller keeps the notice for the next run or a wake. */
|
|
99
|
+
notify(notice) {
|
|
100
|
+
if (this.#sealed)
|
|
101
|
+
return false;
|
|
102
|
+
this.#ingress.push({ kind: "runtime", notice });
|
|
103
|
+
return true;
|
|
93
104
|
}
|
|
94
|
-
/** ADR-0057 — take back the input that has not been admitted
|
|
105
|
+
/** ADR-0057 — take back the person's input that has not been admitted
|
|
106
|
+
* yet. A task notice is not the person's, and stays. */
|
|
95
107
|
retract() {
|
|
96
|
-
|
|
108
|
+
const taken = [];
|
|
109
|
+
for (let i = this.#ingress.length - 1; i >= 0; i--) {
|
|
110
|
+
const entry = this.#ingress[i];
|
|
111
|
+
if (entry.kind === "human") {
|
|
112
|
+
taken.unshift(entry.content);
|
|
113
|
+
this.#ingress.splice(i, 1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return taken;
|
|
97
117
|
}
|
|
98
|
-
/** ADR-0057 — input that arrived but was not admitted when
|
|
118
|
+
/** ADR-0057 — the person's input that arrived but was not admitted when
|
|
119
|
+
* the run ended. */
|
|
99
120
|
unadmitted() {
|
|
100
121
|
return [...this.#unadmitted];
|
|
101
122
|
}
|
|
102
|
-
/**
|
|
103
|
-
*
|
|
123
|
+
/** ADR-0058 (3c) — task notices still pending when the run ended: the
|
|
124
|
+
* delivery takes them back, never lost. */
|
|
125
|
+
unadmittedNotices() {
|
|
126
|
+
return [...this.#unadmittedNotices];
|
|
127
|
+
}
|
|
128
|
+
/** The kernel's `admit`: the ingress in arrival order, neighbours of one
|
|
129
|
+
* kind merged — a person's lines as ONE human input (a single line as
|
|
130
|
+
* itself, several as text blocks), task notices as ONE runtime input. */
|
|
104
131
|
async #admit(mode) {
|
|
105
|
-
if (mode !== "seal" && this.#
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
132
|
+
if (mode !== "seal" && this.#ingress.length > 0) {
|
|
133
|
+
// group neighbours of one kind, in arrival order
|
|
134
|
+
const groups = [];
|
|
135
|
+
for (const entry of this.#ingress.splice(0)) {
|
|
136
|
+
const last = groups.at(-1);
|
|
137
|
+
if (entry.kind === "human") {
|
|
138
|
+
if (last?.kind === "human")
|
|
139
|
+
last.contents.push(entry.content);
|
|
140
|
+
else
|
|
141
|
+
groups.push({ kind: "human", contents: [entry.content] });
|
|
142
|
+
}
|
|
143
|
+
else if (last?.kind === "runtime")
|
|
144
|
+
last.notices.push(entry.notice);
|
|
145
|
+
else
|
|
146
|
+
groups.push({ kind: "runtime", notices: [entry.notice] });
|
|
147
|
+
}
|
|
148
|
+
return groups.map((g) => {
|
|
149
|
+
if (g.kind === "human") {
|
|
150
|
+
const content = g.contents.length === 1 ? g.contents[0] : g.contents.flatMap((l) => (typeof l === "string" ? [{ type: "text", text: l }] : l));
|
|
151
|
+
return { kind: "human", content, source: "user" };
|
|
152
|
+
}
|
|
153
|
+
const content = [...g.notices.flatMap((n) => n.lines), NOTICE_FOOTER].join("\n");
|
|
154
|
+
return { kind: "runtime", content, source: "system", via: { kind: "tasks", items: g.notices.flatMap((n) => n.items) } };
|
|
155
|
+
});
|
|
109
156
|
}
|
|
110
157
|
if (mode !== "take")
|
|
111
158
|
this.#seal();
|
|
@@ -113,7 +160,12 @@ export class Run {
|
|
|
113
160
|
}
|
|
114
161
|
#seal() {
|
|
115
162
|
this.#sealed = true;
|
|
116
|
-
|
|
163
|
+
for (const entry of this.#ingress.splice(0)) {
|
|
164
|
+
if (entry.kind === "human")
|
|
165
|
+
this.#unadmitted.push(entry.content);
|
|
166
|
+
else
|
|
167
|
+
this.#unadmittedNotices.push(entry.notice);
|
|
168
|
+
}
|
|
117
169
|
}
|
|
118
170
|
async *[Symbol.asyncIterator]() {
|
|
119
171
|
if (this.#started)
|
package/dist/session.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ import { type ContinuationScope } from "@vincemakes/kiso-core";
|
|
|
33
33
|
import { type ReasoningSetting } from "./provider/metadata.js";
|
|
34
34
|
import { type SessionStore, type StoreRecord } from "./store.js";
|
|
35
35
|
import { Run } from "./run.js";
|
|
36
|
+
import { type TaskDeliveryOptions } from "./tasks/delivery.js";
|
|
37
|
+
import type { TaskManager } from "./tasks/manager.js";
|
|
36
38
|
/** TUI2-R3v2 ③ — one off-trajectory model request (session.sideQuery).
|
|
37
39
|
* Deliberately tiny: a purpose to mark it in the trace, its own short
|
|
38
40
|
* system prompt, one user message, and an abort. No tools — a side
|
|
@@ -89,6 +91,9 @@ export interface CompactInfo {
|
|
|
89
91
|
/** The covered content's estimated tokens (the chars/4 proxy). */
|
|
90
92
|
readonly tokens: number;
|
|
91
93
|
}
|
|
94
|
+
/** ADR-0057 §5 (3e D1): a steer detaches a foreground command only once it
|
|
95
|
+
* is this old — a command about to end on its own is left to end. */
|
|
96
|
+
export declare const AUTO_DETACH_MIN_AGE_MS = 2000;
|
|
92
97
|
export declare class AgentSession {
|
|
93
98
|
#private;
|
|
94
99
|
readonly id: string;
|
|
@@ -122,6 +127,19 @@ export declare class AgentSession {
|
|
|
122
127
|
persist(runId: string, event: Event): Promise<void>;
|
|
123
128
|
beginRun(run: Run): void;
|
|
124
129
|
endRun(run: Run): void;
|
|
130
|
+
/** Deliver this session's task transitions to the model: into a live
|
|
131
|
+
* run at its next safe point, or — idle — held for the next run, or one
|
|
132
|
+
* continuation run through `onWake` (off with `wake: false`). */
|
|
133
|
+
useTasks(manager: TaskManager, options?: Pick<TaskDeliveryOptions, "wake" | "onWake" | "windowMs"> & {
|
|
134
|
+
readonly autoDetachMinAgeMs?: number;
|
|
135
|
+
}): () => void;
|
|
136
|
+
/** ADR-0057 §5 (3e D1) — a person's steer arrived: the foreground
|
|
137
|
+
* commands running NOW stop holding it back. Each is detached once it is
|
|
138
|
+
* `autoDetachMinAgeMs` old — at once if it already is — so a command
|
|
139
|
+
* about to end anyway is not turned into a task. Commands started after
|
|
140
|
+
* the steer are never touched; a command that ends first unregistered
|
|
141
|
+
* itself, and its detach is a no-op. Called by the run. */
|
|
142
|
+
detachForSteer(): void;
|
|
125
143
|
/** The conversation so far, as the model sees it. */
|
|
126
144
|
projected(): readonly Message[];
|
|
127
145
|
/** 0.40.0: the context as the last bill measured it, plus what the log
|
|
@@ -309,6 +327,7 @@ export declare class AgentSession {
|
|
|
309
327
|
signal?: AbortSignalLike;
|
|
310
328
|
source?: import("@vincemakes/kiso-core").MessageSource;
|
|
311
329
|
via?: import("@vincemakes/kiso-core").UserInputVia;
|
|
330
|
+
maxTurns?: number;
|
|
312
331
|
}): Run;
|
|
313
332
|
/**
|
|
314
333
|
* Continue the interrupted run (Area 2): apply durable decisions,
|
package/dist/session.js
CHANGED
|
@@ -47,6 +47,7 @@ import { checkpointBoundarySeq } from "./checkpoint.js";
|
|
|
47
47
|
import { ABORTED_BEFORE_EXECUTION, unansweredAbortedCalls } from "./aborted-calls.js";
|
|
48
48
|
import { breakEvenFactor, guardedPruneSeq, KEEP_COMPACTABLE_RESULTS, microcompactBoundarySeq, outputReserve, phaseEnd, runsACheck, tierReason, tiersFor } from "./compaction-policy.js";
|
|
49
49
|
import { Run } from "./run.js";
|
|
50
|
+
import { TaskDelivery } from "./tasks/delivery.js";
|
|
50
51
|
// TUI2-R3v2 ③ — the side query rides the SAME tracer the runs ride; that
|
|
51
52
|
// sameness is the whole point (one ledger, one shape, no second path).
|
|
52
53
|
import { RequestTracer, traceGuard } from "./trace/guard.js";
|
|
@@ -90,6 +91,9 @@ function summaryReasoning(model, baseUrl) {
|
|
|
90
91
|
const r = resolveReasoning(model, { thinking: "disabled", effort: "default" }, baseUrl);
|
|
91
92
|
return r.ok && r.wire.thinking !== undefined ? { reasoning: r.wire } : {};
|
|
92
93
|
}
|
|
94
|
+
/** ADR-0057 §5 (3e D1): a steer detaches a foreground command only once it
|
|
95
|
+
* is this old — a command about to end on its own is left to end. */
|
|
96
|
+
export const AUTO_DETACH_MIN_AGE_MS = 2_000;
|
|
93
97
|
export class AgentSession {
|
|
94
98
|
id;
|
|
95
99
|
log;
|
|
@@ -266,9 +270,61 @@ export class AgentSession {
|
|
|
266
270
|
throw new Error("this session already has an active run — one run at a time");
|
|
267
271
|
}
|
|
268
272
|
this.#activeRuns.add(run);
|
|
273
|
+
this.#delivery?.takeForRun(run); // ADR-0058: pending task notices ride its first admission
|
|
269
274
|
}
|
|
270
275
|
endRun(run) {
|
|
271
276
|
this.#activeRuns.delete(run);
|
|
277
|
+
this.#delivery?.onRunSettled(run); // the slot is free: a wake may follow
|
|
278
|
+
}
|
|
279
|
+
// ── ADR-0058 (3c): the session's tasks reach the model ────────────────
|
|
280
|
+
#delivery;
|
|
281
|
+
#tasks;
|
|
282
|
+
#autoDetachMinAgeMs = AUTO_DETACH_MIN_AGE_MS;
|
|
283
|
+
/** Deliver this session's task transitions to the model: into a live
|
|
284
|
+
* run at its next safe point, or — idle — held for the next run, or one
|
|
285
|
+
* continuation run through `onWake` (off with `wake: false`). */
|
|
286
|
+
useTasks(manager,
|
|
287
|
+
// autoDetachMinAgeMs (3e D1): how old a foreground command must be
|
|
288
|
+
// before a steer detaches it — default AUTO_DETACH_MIN_AGE_MS
|
|
289
|
+
options = {}) {
|
|
290
|
+
this.#delivery?.close();
|
|
291
|
+
const { autoDetachMinAgeMs, ...deliveryOptions } = options;
|
|
292
|
+
const delivery = new TaskDelivery({ ...deliveryOptions, manager, events: () => this.log.all, liveRun: () => [...this.#activeRuns][0] });
|
|
293
|
+
this.#delivery = delivery;
|
|
294
|
+
this.#tasks = manager;
|
|
295
|
+
this.#autoDetachMinAgeMs = autoDetachMinAgeMs ?? AUTO_DETACH_MIN_AGE_MS;
|
|
296
|
+
return () => {
|
|
297
|
+
delivery.close();
|
|
298
|
+
if (this.#delivery === delivery)
|
|
299
|
+
this.#delivery = undefined;
|
|
300
|
+
if (this.#tasks === manager)
|
|
301
|
+
this.#tasks = undefined;
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/** ADR-0057 §5 (3e D1) — a person's steer arrived: the foreground
|
|
305
|
+
* commands running NOW stop holding it back. Each is detached once it is
|
|
306
|
+
* `autoDetachMinAgeMs` old — at once if it already is — so a command
|
|
307
|
+
* about to end anyway is not turned into a task. Commands started after
|
|
308
|
+
* the steer are never touched; a command that ends first unregistered
|
|
309
|
+
* itself, and its detach is a no-op. Called by the run. */
|
|
310
|
+
detachForSteer() {
|
|
311
|
+
const manager = this.#tasks;
|
|
312
|
+
if (manager === undefined)
|
|
313
|
+
return;
|
|
314
|
+
const now = Date.now();
|
|
315
|
+
for (const { executionId, startedAt } of manager.detachable()) {
|
|
316
|
+
const wait = Math.max(0, this.#autoDetachMinAgeMs - (now - startedAt));
|
|
317
|
+
if (wait === 0)
|
|
318
|
+
manager.detach(executionId, "steer");
|
|
319
|
+
else
|
|
320
|
+
setTimeout(() => manager.detach(executionId, "steer"), wait).unref?.();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/** A summary carries the tasks as the model was last told of them —
|
|
324
|
+
* never the live journal (a compaction is not a delivery). */
|
|
325
|
+
#withTasks(summary) {
|
|
326
|
+
const snapshot = this.#delivery?.snapshot(this.log.all) ?? "";
|
|
327
|
+
return snapshot === "" ? summary : `${summary}\n\n${snapshot}`;
|
|
272
328
|
}
|
|
273
329
|
/** The conversation so far, as the model sees it. */
|
|
274
330
|
projected() {
|
|
@@ -414,10 +470,11 @@ export class AgentSession {
|
|
|
414
470
|
// against an estimate). A checkpoint that fails it is discarded:
|
|
415
471
|
// nothing is appended, the run goes on as before, and the failure
|
|
416
472
|
// counts toward the breaker.
|
|
417
|
-
const
|
|
473
|
+
const text = this.#withTasks(result.text);
|
|
474
|
+
const fire = { type: "summarized", coversToSeq: boundary, summary: text };
|
|
418
475
|
const pre = estimateTokens(projectMessages(events));
|
|
419
476
|
const post = estimateTokens(projectMessages([...events, { ...fire, seq: (events.at(-1)?.seq ?? -1) + 1 }]));
|
|
420
|
-
const written = estimateTokens([{ role: "user", content: `${SUMMARY_FRAMING}\n\n${
|
|
477
|
+
const written = estimateTokens([{ role: "user", content: `${SUMMARY_FRAMING}\n\n${text}` }]);
|
|
421
478
|
const shrinks = pre - post >= written;
|
|
422
479
|
// Every in-run fire is recorded — its reason and path are what the
|
|
423
480
|
// measurement counts — and its usage when the provider reported one;
|
|
@@ -734,11 +791,15 @@ export class AgentSession {
|
|
|
734
791
|
* OpenAI-compatible family as `image_url` with a data URI. Only this
|
|
735
792
|
* signature narrowed it to text, which is why no caller could ever
|
|
736
793
|
* send one. */
|
|
737
|
-
run(input,
|
|
794
|
+
run(input,
|
|
795
|
+
// ADR-0058 3d: `maxTurns` bounds THIS run only (a background child's
|
|
796
|
+
// wrap-up gets one request), over the session's own limit
|
|
797
|
+
options) {
|
|
738
798
|
this.ensureHealthy();
|
|
739
799
|
if (this.#profilePending)
|
|
740
800
|
this.#recordProfile(); // XP-1: legacy revision 1, before the first request
|
|
741
|
-
|
|
801
|
+
const config = options?.maxTurns !== undefined ? { ...this.#effectiveConfig(), maxTurns: options.maxTurns } : this.#effectiveConfig();
|
|
802
|
+
return new Run(this.#store, this.#learning(this.#adapter), config, this, input, options?.signal, false, options?.source, options?.via);
|
|
742
803
|
}
|
|
743
804
|
/**
|
|
744
805
|
* Continue the interrupted run (Area 2): apply durable decisions,
|
|
@@ -862,7 +923,7 @@ export class AgentSession {
|
|
|
862
923
|
// adapter returned must NOT persist — "nothing happened".
|
|
863
924
|
if (options.signal !== undefined && options.signal.aborted)
|
|
864
925
|
throw cancelled();
|
|
865
|
-
const full = this.log.append({ type: "summarized", coversToSeq: boundary, summary });
|
|
926
|
+
const full = this.log.append({ type: "summarized", coversToSeq: boundary, summary: this.#withTasks(summary) });
|
|
866
927
|
// The record rides the LAST recorded run's id — a summarized fact
|
|
867
928
|
// must never open a run of its own: the open-run gate keys on
|
|
868
929
|
// terminal-less runIds, and a "compact" runId would block the next
|
package/dist/store.js
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
* - load is strict (A group round 1): a partial final line is the only
|
|
43
43
|
* tolerated damage; everything else throws StoreCorruptionError.
|
|
44
44
|
*/
|
|
45
|
-
import { appendFileSync, closeSync, existsSync, fsyncSync, fstatSync, statSync, ftruncateSync, mkdirSync, openSync, readFileSync, readdirSync, readSync, } from "node:fs";
|
|
45
|
+
import { appendFileSync, closeSync, constants, existsSync, fsyncSync, fstatSync, statSync, ftruncateSync, mkdirSync, openSync, readFileSync, readdirSync, readSync, writeSync, } from "node:fs";
|
|
46
46
|
import { dirname, join } from "node:path";
|
|
47
47
|
import { isKisoEvent } from "@vincemakes/kiso-core";
|
|
48
48
|
import { LockedError, nativeLockAdapter, } from "./lock-adapter.js";
|
|
@@ -135,7 +135,8 @@ function contentText(content) {
|
|
|
135
135
|
export function sessionTitle(records) {
|
|
136
136
|
const asked = records
|
|
137
137
|
.map((r) => r.event)
|
|
138
|
-
|
|
138
|
+
// ADR-0058: a task notice is the runtime's, never the person's words
|
|
139
|
+
.filter((e) => e.type === "user_input" && e.via?.kind !== "tasks")
|
|
139
140
|
// 0.40.0: a skill turn is named by what the person TYPED — its
|
|
140
141
|
// content is a SKILL.md body the person never wrote.
|
|
141
142
|
.map((e) => (typeof e.via?.line === "string" ? e.via.line : contentText(e.content)).trim())
|
|
@@ -312,7 +313,13 @@ export class SessionStore {
|
|
|
312
313
|
if (event.seq !== expected) {
|
|
313
314
|
throw new StaleWriterError(expected, event.seq);
|
|
314
315
|
}
|
|
315
|
-
|
|
316
|
+
const line = `${JSON.stringify({ runId, ts: Date.now(), event })}\n`;
|
|
317
|
+
// Windows: the log is open read-write (see fd()), so the append is a
|
|
318
|
+
// write at the end — the lock makes this the only writer
|
|
319
|
+
if (process.platform === "win32")
|
|
320
|
+
writeSync(fd, line, fstatSync(fd).size);
|
|
321
|
+
else
|
|
322
|
+
appendFileSync(fd, line);
|
|
316
323
|
// A streamed fragment is not a durability boundary: it belongs to a
|
|
317
324
|
// turn that is uncommitted until its stop is durable, and the next
|
|
318
325
|
// synced event (the call's end, the text's end, the stop) flushes
|
|
@@ -340,7 +347,10 @@ export class SessionStore {
|
|
|
340
347
|
const path = this.pathFor(sessionId);
|
|
341
348
|
// R6: the mode applies at CREATION only — an existing log keeps
|
|
342
349
|
// whatever it has, which is the not-migrated rule above.
|
|
343
|
-
|
|
350
|
+
// Windows: a handle opened for append can be neither flushed nor
|
|
351
|
+
// truncated (FlushFileBuffers and SetEndOfFile need the write access
|
|
352
|
+
// append mode drops), so the log opens read-write there
|
|
353
|
+
const fd = openSync(path, process.platform === "win32" ? constants.O_RDWR | constants.O_CREAT : "a+", 0o600);
|
|
344
354
|
repairTornTail(fd);
|
|
345
355
|
fsyncDir(dirname(path));
|
|
346
356
|
this.#fds.set(sessionId, fd);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-0058 §7–§8, step 3c — a task's transitions reach the model.
|
|
3
|
+
*
|
|
4
|
+
* One delivery per session. It hears the TaskManager's transitions,
|
|
5
|
+
* decides each by the §8 defaults (silent | notify | wake), merges what
|
|
6
|
+
* arrives within a window into ONE notice, and hands it on:
|
|
7
|
+
*
|
|
8
|
+
* a live run → `run.notify` — admitted at the next safe point, in
|
|
9
|
+
* arrival order with the person's steers (ADR-0057);
|
|
10
|
+
* an idle session → held for the next run a person starts, or — when a
|
|
11
|
+
* transition asks to wake — ONE continuation run whose
|
|
12
|
+
* first input is the notice (the host starts it, after
|
|
13
|
+
* the previous run has fully settled).
|
|
14
|
+
*
|
|
15
|
+
* Exactly once, from the log: a transition counts as delivered when a
|
|
16
|
+
* `user_input` whose `via.items` names it is in the session's log (ADR-0051
|
|
17
|
+
* Amendment 8). The set is a cache rebuilt from the log, never a second
|
|
18
|
+
* truth. Lineage from the log too: a task started inside a wake run (a run
|
|
19
|
+
* whose first input is a task notice) never wakes — depth 1.
|
|
20
|
+
*
|
|
21
|
+
* The summary snapshot is what the MODEL was told, never the live journal:
|
|
22
|
+
* a compaction must not reveal a transition that has not been delivered.
|
|
23
|
+
*
|
|
24
|
+
* A transition a tool result reports (ADR-0058 Amendment 7) is CLAIMED in
|
|
25
|
+
* the task journal by that execution and never noticed — the manager does
|
|
26
|
+
* not announce it, and a restart does not deliver it. The claim decides
|
|
27
|
+
* only that; what the model knows stays the log's: the summary learns a
|
|
28
|
+
* claimed transition from the claiming execution's durable, successful
|
|
29
|
+
* tool_result, never from the journal alone.
|
|
30
|
+
*
|
|
31
|
+
* Background children (ADR-0058 3d) arrive as a GROUP: the agent tasks
|
|
32
|
+
* started by the calls of one model turn. The group closes once that turn
|
|
33
|
+
* has ended and every call in it has its result — before that, a fast
|
|
34
|
+
* child's end says nothing about the group. A closed group whose members
|
|
35
|
+
* have all ended is delivered ONCE: every member's line, then excerpts of
|
|
36
|
+
* the answers (result.md, which the child wrote) within 4 KiB each and
|
|
37
|
+
* 16 KiB together; members already delivered are named as reported, and
|
|
38
|
+
* only the rest are receipted. A failure goes into a live run at once and
|
|
39
|
+
* never wakes on its own; idle, it waits for its group.
|
|
40
|
+
*/
|
|
41
|
+
import { type Event, type UserInputVia } from "@vincemakes/kiso-core";
|
|
42
|
+
import { type Run } from "../run.js";
|
|
43
|
+
import { type TaskInfo, type TaskManager } from "./manager.js";
|
|
44
|
+
export interface TaskDeliveryOptions {
|
|
45
|
+
readonly manager: TaskManager;
|
|
46
|
+
/** The session's durable events: receipts and lineage are read here. */
|
|
47
|
+
readonly events: () => readonly Event[];
|
|
48
|
+
/** The session's live run, when one is running. */
|
|
49
|
+
readonly liveRun: () => Run | undefined;
|
|
50
|
+
/** False turns every wake into a notify (the switch, ADR-0058 §8.4). */
|
|
51
|
+
readonly wake?: boolean;
|
|
52
|
+
/** Start ONE continuation run whose first input is this notice. */
|
|
53
|
+
readonly onWake?: (input: {
|
|
54
|
+
readonly content: string;
|
|
55
|
+
readonly via: UserInputVia;
|
|
56
|
+
}) => void;
|
|
57
|
+
/** Transitions within this window merge into one notice. Default 1 s. */
|
|
58
|
+
readonly windowMs?: number;
|
|
59
|
+
/** How much of a failed task's output rides its notice. Default 2 KB. */
|
|
60
|
+
readonly tailBytes?: number;
|
|
61
|
+
/** Read a task's output tail (injectable for tests). */
|
|
62
|
+
readonly readTail?: (task: TaskInfo, bytes: number) => string;
|
|
63
|
+
}
|
|
64
|
+
export declare class TaskDelivery {
|
|
65
|
+
#private;
|
|
66
|
+
constructor(options: TaskDeliveryOptions);
|
|
67
|
+
close(): void;
|
|
68
|
+
/** A run a person started: pending notices ride its first admission. */
|
|
69
|
+
takeForRun(run: Run): void;
|
|
70
|
+
/** The run has fully settled (its terminal durable, the slot free):
|
|
71
|
+
* notices it never admitted come back, and a wake may start. */
|
|
72
|
+
onRunSettled(run: Run): void;
|
|
73
|
+
/** The conversation's view of its tasks for a summary: a task the model
|
|
74
|
+
* was told started, at the last transition it was TOLD about. Empty
|
|
75
|
+
* when the model knows of no task. Told started = a durable, successful
|
|
76
|
+
* tool_result for the execution that started it — a started execution
|
|
77
|
+
* with no result (the crash window), or one resolved as not applied,
|
|
78
|
+
* never told the model a task exists. Told a transition = a notice
|
|
79
|
+
* naming it, or (Amendment 7) a durable, successful tool_result of the
|
|
80
|
+
* execution that claimed it — in log order, the last one wins. */
|
|
81
|
+
snapshot(events: readonly Event[]): string;
|
|
82
|
+
}
|