@vincemakes/kiso-server 0.44.0 → 0.45.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +10 -0
- package/dist/http.d.ts +1 -0
- package/dist/http.js +21 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/service.d.ts +17 -0
- package/dist/service.js +25 -2
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ const service = createSessionService({
|
|
|
23
23
|
|
|
24
24
|
const { runId, done } = await service.run("s1", "hello"); // InFlightError | OpenRunError | DrainingError
|
|
25
25
|
const off = await service.subscribe("s1", lastSeqSeen, (ev) => send(ev)); // replay, then live, exactly once
|
|
26
|
+
service.steer("s1", "only the editor tests"); // → { runId } — NotRunningError | RunClosedError: run() it instead
|
|
26
27
|
await service.approve("s1", decisionId, true); // → { needsResume }
|
|
27
28
|
await service.abort("s1"); // → idle | parked (who is waited for) | stopped
|
|
28
29
|
await service.drain(20_000); // → { waitedFor, parked, interrupted, timedOut }
|
|
@@ -42,6 +43,13 @@ await service.close(5_000); // abort what is live,
|
|
|
42
43
|
does nothing; `{ force: true }` aborts anyway. "stopped" carries a
|
|
43
44
|
`settled` promise: the kernel does not kill a tool, so a tool that
|
|
44
45
|
ignores the abort keeps its run open until it returns.
|
|
46
|
+
- **Steer, and what never landed** (ADR-0057). `steer()` hands the live
|
|
47
|
+
run a person's input; the run admits it at its next quiescent boundary.
|
|
48
|
+
With no run live it refuses (`NotRunningError`); once the run has
|
|
49
|
+
decided to end, the runtime refuses (`RunClosedError`). Input accepted
|
|
50
|
+
and never admitted is on `SettledRun.unadmitted`, and the HTTP stream
|
|
51
|
+
names it in an `unadmitted` frame after the terminal; the service never
|
|
52
|
+
starts the follow-up run itself — the host does, usually with it.
|
|
45
53
|
- **After the last uncertain verdict** the service does not resume;
|
|
46
54
|
`resolveUncertain` returns `{ remaining }` and the host resumes at zero.
|
|
47
55
|
- **Executing tools** are counted from `tool_execution_started` and the
|
package/dist/errors.d.ts
CHANGED
|
@@ -32,3 +32,9 @@ export declare class StoreMismatchError extends Error {
|
|
|
32
32
|
export declare class DrainingError extends Error {
|
|
33
33
|
constructor();
|
|
34
34
|
}
|
|
35
|
+
/** ADR-0057: a steer for a session with no run in flight. The host starts a
|
|
36
|
+
* run with the input instead. */
|
|
37
|
+
export declare class NotRunningError extends Error {
|
|
38
|
+
readonly sessionId: string;
|
|
39
|
+
constructor(sessionId: string);
|
|
40
|
+
}
|
package/dist/errors.js
CHANGED
|
@@ -50,3 +50,13 @@ export class DrainingError extends Error {
|
|
|
50
50
|
this.name = "DrainingError";
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
/** ADR-0057: a steer for a session with no run in flight. The host starts a
|
|
54
|
+
* run with the input instead. */
|
|
55
|
+
export class NotRunningError extends Error {
|
|
56
|
+
sessionId;
|
|
57
|
+
constructor(sessionId) {
|
|
58
|
+
super(`session ${sessionId} has no run in flight — start a run with this input`);
|
|
59
|
+
this.name = "NotRunningError";
|
|
60
|
+
this.sessionId = sessionId;
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/http.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { type ProjectionOptions } from "./wire.js";
|
|
|
17
17
|
* GET {prefix}/:id/replay every wire event on the log + the snapshot
|
|
18
18
|
* POST {prefix}/:id/run 202 { runId } — or the run's own stream with ?stream=1
|
|
19
19
|
* POST {prefix}/:id/resume 202 { runId } — or the stream with ?stream=1
|
|
20
|
+
* POST {prefix}/:id/steer 202 { runId } — 409 idle | closed (ADR-0057)
|
|
20
21
|
* POST {prefix}/:id/abort 200 idle | stopped — 409 parked (who is waited for)
|
|
21
22
|
* POST {prefix}/:id/approve 200 { needsResume }
|
|
22
23
|
* POST {prefix}/:id/uncertain 200 { remaining }
|
package/dist/http.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RunClosedError } from "@vincemakes/kiso-runtime";
|
|
2
|
+
import { DrainingError, InFlightError, NotRunningError, OpenRunError } from "./errors.js";
|
|
2
3
|
import { toWireEvent } from "./wire.js";
|
|
3
4
|
/** The store's own rule (`^[A-Za-z0-9._-]+$`), with a length cap. 0.41.2:
|
|
4
5
|
* the first version also required a leading letter or digit, which the
|
|
5
6
|
* store never did — a nanoid can start with `_` or `-` (about 1 in 32 do)
|
|
6
7
|
* and every such session answered 404. */
|
|
7
8
|
const SESSION_ID = /^[A-Za-z0-9._-]{1,128}$/;
|
|
8
|
-
const ACTIONS = new Set(["", "state", "events", "replay", "run", "resume", "abort", "approve", "uncertain"]);
|
|
9
|
+
const ACTIONS = new Set(["", "state", "events", "replay", "run", "resume", "steer", "abort", "approve", "uncertain"]);
|
|
9
10
|
export function createHttpHandler(service, options) {
|
|
10
11
|
const prefix = (options.prefix ?? "/v1/sessions").replace(/\/+$/, "");
|
|
11
12
|
const keepaliveMs = options.keepaliveMs ?? 15_000;
|
|
@@ -27,6 +28,10 @@ export function createHttpHandler(service, options) {
|
|
|
27
28
|
return fail(res, 409, "open_run", err.message, err.runId);
|
|
28
29
|
if (err instanceof DrainingError)
|
|
29
30
|
return fail(res, 503, "draining", err.message);
|
|
31
|
+
if (err instanceof NotRunningError)
|
|
32
|
+
return fail(res, 409, "idle", err.message);
|
|
33
|
+
if (err instanceof RunClosedError)
|
|
34
|
+
return fail(res, 409, "closed", err.message, err.runId);
|
|
30
35
|
if (err instanceof BadRequest)
|
|
31
36
|
return fail(res, 400, "bad_request", err.message);
|
|
32
37
|
fail(res, 500, "internal", err instanceof Error ? err.message : String(err));
|
|
@@ -84,6 +89,13 @@ export function createHttpHandler(service, options) {
|
|
|
84
89
|
const wire = toWireEvent(event, projection);
|
|
85
90
|
if (wire !== null)
|
|
86
91
|
out += `id: ${wire.seq}\nevent: ${wire.type}\ndata: ${JSON.stringify(wire)}\n\n`;
|
|
92
|
+
// ADR-0057: a 202 steer always has a visible fate — admitted (its
|
|
93
|
+
// user_input is on the stream) or named here, right after the terminal.
|
|
94
|
+
if (event.type === "terminal") {
|
|
95
|
+
const left = service.unadmittedAt(sessionId, event.seq);
|
|
96
|
+
if (left !== null)
|
|
97
|
+
out += `event: unadmitted\ndata: ${JSON.stringify(left)}\n\n`;
|
|
98
|
+
}
|
|
87
99
|
if (options.augment !== undefined) {
|
|
88
100
|
for (const frame of await options.augment(event, sessionId))
|
|
89
101
|
out += `event: ${frame.event}\ndata: ${JSON.stringify(frame.data)}\n\n`;
|
|
@@ -219,6 +231,13 @@ export function createHttpHandler(service, options) {
|
|
|
219
231
|
json(res, 202, { runId: handle.runId });
|
|
220
232
|
return true;
|
|
221
233
|
}
|
|
234
|
+
if (action === "steer") {
|
|
235
|
+
const input = await inputOf(body, req, sessionId, res);
|
|
236
|
+
if (isHandled(input))
|
|
237
|
+
return true; // the product answered; nothing steered
|
|
238
|
+
json(res, 202, service.steer(sessionId, input));
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
222
241
|
if (action === "abort") {
|
|
223
242
|
const outcome = await service.abort(sessionId, body["force"] === true ? { force: true } : {});
|
|
224
243
|
const reply = outcome.kind === "stopped" ? { kind: "stopped", runId: outcome.runId } : outcome;
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export { createSessionService, SessionService } from "./service.js";
|
|
10
10
|
export type { AbortOutcome, DrainReport, OpenRun, RunHandle, RunOptions, SessionServiceOptions, SettledRun } from "./service.js";
|
|
11
|
-
export { DrainingError, InFlightError, OpenRunError, StoreMismatchError } from "./errors.js";
|
|
11
|
+
export { DrainingError, InFlightError, NotRunningError, OpenRunError, StoreMismatchError } from "./errors.js";
|
|
12
12
|
export { EXECUTION_ENDED, executionDelta } from "./execution.js";
|
|
13
13
|
export { tail } from "./tail.js";
|
|
14
14
|
export { MAX_ARG_DEPTH, MAX_ARG_VALUE_CHARS, STRIPPED_ARG_KEYS, sanitizeToolArgs, toWireEvent } from "./wire.js";
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* the agent factory, and every run is the runtime's own `Session.run()`.
|
|
8
8
|
*/
|
|
9
9
|
export { createSessionService, SessionService } from "./service.js";
|
|
10
|
-
export { DrainingError, InFlightError, OpenRunError, StoreMismatchError } from "./errors.js";
|
|
10
|
+
export { DrainingError, InFlightError, NotRunningError, OpenRunError, StoreMismatchError } from "./errors.js";
|
|
11
11
|
export { EXECUTION_ENDED, executionDelta } from "./execution.js";
|
|
12
12
|
export { tail } from "./tail.js";
|
|
13
13
|
export { MAX_ARG_DEPTH, MAX_ARG_VALUE_CHARS, STRIPPED_ARG_KEYS, sanitizeToolArgs, toWireEvent } from "./wire.js";
|
package/dist/service.d.ts
CHANGED
|
@@ -67,6 +67,9 @@ export interface SettledRun {
|
|
|
67
67
|
readonly uncertainRemaining: number;
|
|
68
68
|
/** The highest seq this run delivered. */
|
|
69
69
|
readonly highWater: number;
|
|
70
|
+
/** ADR-0057: input a steer handed this run that it never admitted — the
|
|
71
|
+
* run sealed first. The host's "what next" is usually a new run with it. */
|
|
72
|
+
readonly unadmitted: readonly (string | readonly ContentBlock[])[];
|
|
70
73
|
}
|
|
71
74
|
export interface RunHandle {
|
|
72
75
|
readonly runId: string;
|
|
@@ -116,6 +119,20 @@ export declare class SessionService {
|
|
|
116
119
|
* changed (its tool set, its prompt) and the next run should see it.
|
|
117
120
|
* Observers are carried over. Refuses while a run is in flight. */
|
|
118
121
|
reopen(sessionId: string): Promise<void>;
|
|
122
|
+
/** ADR-0057 — hand the session's live run a person's input; it is
|
|
123
|
+
* admitted at the run's next Safe Admission boundary. Refuses with
|
|
124
|
+
* `NotRunningError` when no run is live, and with the runtime's
|
|
125
|
+
* `RunClosedError` once the run's ingress has sealed — either way the
|
|
126
|
+
* host starts a new run with the input. */
|
|
127
|
+
steer(sessionId: string, input: string | readonly ContentBlock[]): {
|
|
128
|
+
readonly runId: string;
|
|
129
|
+
};
|
|
130
|
+
/** ADR-0057 — what the run that ended at `terminalSeq` accepted and never
|
|
131
|
+
* admitted; null when nothing, or when that is not the latest terminal. */
|
|
132
|
+
unadmittedAt(sessionId: string, terminalSeq: number): {
|
|
133
|
+
readonly runId: string;
|
|
134
|
+
readonly items: readonly (string | readonly ContentBlock[])[];
|
|
135
|
+
} | null;
|
|
119
136
|
/** Start a turn. Refuses with `InFlightError` (one run per session),
|
|
120
137
|
* `OpenRunError` (a previous process died inside a run — see
|
|
121
138
|
* `resumeFirst`) or `DrainingError`. */
|
package/dist/service.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { openRunId } from "@vincemakes/kiso-runtime/internal";
|
|
2
|
-
import { DrainingError, InFlightError, OpenRunError, StoreMismatchError } from "./errors.js";
|
|
2
|
+
import { DrainingError, InFlightError, NotRunningError, OpenRunError, StoreMismatchError } from "./errors.js";
|
|
3
3
|
import { executionDelta } from "./execution.js";
|
|
4
4
|
import { tail } from "./tail.js";
|
|
5
5
|
export class SessionService {
|
|
@@ -24,7 +24,7 @@ export class SessionService {
|
|
|
24
24
|
const opening = (async () => {
|
|
25
25
|
const agent = await this.#open(sessionId);
|
|
26
26
|
const session = await agent.session({ id: sessionId });
|
|
27
|
-
const held = { id: sessionId, agent, session, listeners: new Set(), live: null, pumping: null, executing: 0, highWater: -1, mismatch: null };
|
|
27
|
+
const held = { id: sessionId, agent, session, listeners: new Set(), live: null, pumping: null, executing: 0, highWater: -1, mismatch: null, unadmitted: null };
|
|
28
28
|
this.#held.set(sessionId, held);
|
|
29
29
|
return held;
|
|
30
30
|
})();
|
|
@@ -54,6 +54,10 @@ export class SessionService {
|
|
|
54
54
|
const pumping = (async () => {
|
|
55
55
|
try {
|
|
56
56
|
for await (const event of run) {
|
|
57
|
+
// ADR-0057: the ingress sealed before the terminal was appended,
|
|
58
|
+
// so what never landed is known by the time it is delivered.
|
|
59
|
+
if (event.type === "terminal")
|
|
60
|
+
held.unadmitted = { runId: run.runId, terminalSeq: event.seq, items: run.unadmitted() };
|
|
57
61
|
held.executing = Math.max(0, held.executing + executionDelta(event));
|
|
58
62
|
held.highWater = Math.max(held.highWater, event.seq);
|
|
59
63
|
for (const listener of [...held.listeners]) {
|
|
@@ -89,6 +93,7 @@ export class SessionService {
|
|
|
89
93
|
outcome: terminal !== undefined && terminal.type === "terminal" ? terminal.outcome.kind : "unknown",
|
|
90
94
|
uncertainRemaining: held.session.uncertainExecutions().length,
|
|
91
95
|
highWater: held.highWater,
|
|
96
|
+
unadmitted: run.unadmitted(),
|
|
92
97
|
});
|
|
93
98
|
}
|
|
94
99
|
})();
|
|
@@ -96,6 +101,24 @@ export class SessionService {
|
|
|
96
101
|
void pumping.catch(() => { }); // a host that does not await `done` gets no unhandled rejection
|
|
97
102
|
return { runId: run.runId, done: pumping };
|
|
98
103
|
}
|
|
104
|
+
/** ADR-0057 — hand the session's live run a person's input; it is
|
|
105
|
+
* admitted at the run's next Safe Admission boundary. Refuses with
|
|
106
|
+
* `NotRunningError` when no run is live, and with the runtime's
|
|
107
|
+
* `RunClosedError` once the run's ingress has sealed — either way the
|
|
108
|
+
* host starts a new run with the input. */
|
|
109
|
+
steer(sessionId, input) {
|
|
110
|
+
const live = this.#held.get(sessionId)?.live ?? null;
|
|
111
|
+
if (live === null)
|
|
112
|
+
throw new NotRunningError(sessionId);
|
|
113
|
+
live.steer(input);
|
|
114
|
+
return { runId: live.runId };
|
|
115
|
+
}
|
|
116
|
+
/** ADR-0057 — what the run that ended at `terminalSeq` accepted and never
|
|
117
|
+
* admitted; null when nothing, or when that is not the latest terminal. */
|
|
118
|
+
unadmittedAt(sessionId, terminalSeq) {
|
|
119
|
+
const u = this.#held.get(sessionId)?.unadmitted ?? null;
|
|
120
|
+
return u !== null && u.terminalSeq === terminalSeq && u.items.length > 0 ? { runId: u.runId, items: u.items } : null;
|
|
121
|
+
}
|
|
99
122
|
/** Start a turn. Refuses with `InFlightError` (one run per session),
|
|
100
123
|
* `OpenRunError` (a previous process died inside a run — see
|
|
101
124
|
* `resumeFirst`) or `DrainingError`. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.1",
|
|
4
4
|
"description": "kiso server — the hosted-session service: many durable sessions in one process, observers with exact replay from a sequence number, approve / abort / resume routed to the right run, drain and close. No transport; a product supplies the agent factory.",
|
|
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.
|
|
29
|
-
"@vincemakes/kiso-protocol": "0.
|
|
30
|
-
"@vincemakes/kiso-runtime": "0.
|
|
28
|
+
"@vincemakes/kiso-core": "0.45.1",
|
|
29
|
+
"@vincemakes/kiso-protocol": "0.45.1",
|
|
30
|
+
"@vincemakes/kiso-runtime": "0.45.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@vincemakes/kiso-evals": "0.
|
|
33
|
+
"@vincemakes/kiso-evals": "0.45.1",
|
|
34
34
|
"@types/node": "^26.1.2",
|
|
35
35
|
"typescript": "^5.7.2",
|
|
36
36
|
"vitest": "^3.0.0"
|