@vincemakes/kiso-server 0.41.1 → 0.42.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/README.md +19 -2
- package/dist/http.d.ts +11 -2
- package/dist/http.js +13 -4
- package/dist/index.d.ts +1 -1
- package/dist/service.d.ts +18 -0
- package/dist/service.js +10 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -66,6 +66,20 @@ await service.close(5_000); // abort what is live,
|
|
|
66
66
|
product that must guarantee no prose reaches a client supplies its own
|
|
67
67
|
`sanitize` in the projection options.
|
|
68
68
|
|
|
69
|
+
## Shutting down, and one thing not to call
|
|
70
|
+
|
|
71
|
+
The order that keeps every run resumable: `service.drain(graceMs)` (no
|
|
72
|
+
new runs; wait for executing tools; the report says what was parked or
|
|
73
|
+
cut), then stop the listener, then `store.closeAll()`, then exit. Do not
|
|
74
|
+
call `service.close()` for that — it ABORTS what is live, including a
|
|
75
|
+
run parked at an approval, which voids the draft its answer would have
|
|
76
|
+
continued. And do not call `agent.close()` on a factory's agent when the
|
|
77
|
+
store is shared: in the runtime it closes the whole store.
|
|
78
|
+
|
|
79
|
+
`hooks.onSettled` is the product's "what next" after a run: a follow-up
|
|
80
|
+
turn, a resume when `uncertainRemaining` reaches zero, a notification.
|
|
81
|
+
The service never starts a run on its own.
|
|
82
|
+
|
|
69
83
|
## What a host still writes
|
|
70
84
|
|
|
71
85
|
The transport (routes, SSE framing, `Last-Event-ID`, keepalive, status
|
|
@@ -82,5 +96,8 @@ here once: `id: <seq>` / `event: <type>` / `data: <WireEvent>`, an `: open`
|
|
|
82
96
|
preamble, `Last-Event-ID` and `?after`, a keepalive; `in_flight` and
|
|
83
97
|
`open_run` are 409, `draining` 503, `forbidden` 403. Wire events are the
|
|
84
98
|
projection in `@vincemakes/kiso-protocol` — never the durable event.
|
|
85
|
-
`authorize` is required: there is no default that allows.
|
|
86
|
-
|
|
99
|
+
`authorize` is required: there is no default that allows. `prepareInput`
|
|
100
|
+
may answer a request itself — write the product's own status and body to
|
|
101
|
+
the response it receives and return `{ handled: true }`; nothing is opened
|
|
102
|
+
and no run starts (a gate, a quota, an attachment check, in the product's
|
|
103
|
+
own shape).
|
package/dist/http.d.ts
CHANGED
|
@@ -33,14 +33,23 @@ export interface HttpHandlerOptions {
|
|
|
33
33
|
readonly authorize: (req: IncomingMessage, sessionId: string) => Promise<boolean> | boolean;
|
|
34
34
|
/** Frames a product adds beside a wire event on the same connection. */
|
|
35
35
|
readonly augment?: (event: Event, sessionId: string) => readonly WireFrame[] | Promise<readonly WireFrame[]>;
|
|
36
|
-
/** The product's turn preparation; default: `body.input` as given.
|
|
37
|
-
|
|
36
|
+
/** The product's turn preparation; default: `body.input` as given.
|
|
37
|
+
* 0.42.0: it may instead ANSWER the request itself — write the
|
|
38
|
+
* product's own status and body to `res` and return `{ handled: true }`;
|
|
39
|
+
* the handler then returns without opening a run, a session, or a 400
|
|
40
|
+
* (a gate, a quota, an attachment check, an empty message — the
|
|
41
|
+
* product's refusals in the product's shape). */
|
|
42
|
+
readonly prepareInput?: (body: Readonly<Record<string, unknown>>, req: IncomingMessage, sessionId: string, res: ServerResponse) => WireInput | Handled | Promise<WireInput | Handled>;
|
|
38
43
|
readonly projection?: ProjectionOptions;
|
|
39
44
|
/** `: keepalive` comments on an idle stream. Default 15 s; 0 disables. */
|
|
40
45
|
readonly keepaliveMs?: number;
|
|
41
46
|
/** Request-body cap in bytes. Default 1 MiB. */
|
|
42
47
|
readonly bodyLimit?: number;
|
|
43
48
|
}
|
|
49
|
+
/** What `prepareInput` returns when it answered the request itself. */
|
|
50
|
+
export interface Handled {
|
|
51
|
+
readonly handled: true;
|
|
52
|
+
}
|
|
44
53
|
export interface HttpHandler {
|
|
45
54
|
/** True when the request was an agent route and has been answered. */
|
|
46
55
|
readonly handle: (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
package/dist/http.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { DrainingError, InFlightError, OpenRunError } from "./errors.js";
|
|
2
2
|
import { toWireEvent } from "./wire.js";
|
|
3
|
-
|
|
3
|
+
/** The store's own rule (`^[A-Za-z0-9._-]+$`), with a length cap. 0.41.2:
|
|
4
|
+
* the first version also required a leading letter or digit, which the
|
|
5
|
+
* store never did — a nanoid can start with `_` or `-` (about 1 in 32 do)
|
|
6
|
+
* and every such session answered 404. */
|
|
7
|
+
const SESSION_ID = /^[A-Za-z0-9._-]{1,128}$/;
|
|
4
8
|
const ACTIONS = new Set(["", "state", "events", "replay", "run", "resume", "abort", "approve", "uncertain"]);
|
|
5
9
|
export function createHttpHandler(service, options) {
|
|
6
10
|
const prefix = (options.prefix ?? "/v1/sessions").replace(/\/+$/, "");
|
|
@@ -50,9 +54,9 @@ export function createHttpHandler(service, options) {
|
|
|
50
54
|
throw new BadRequest("body must be a JSON object");
|
|
51
55
|
return parsed;
|
|
52
56
|
};
|
|
53
|
-
const inputOf = async (body, req, sessionId) => {
|
|
57
|
+
const inputOf = async (body, req, sessionId, res) => {
|
|
54
58
|
if (options.prepareInput !== undefined)
|
|
55
|
-
return options.prepareInput(body, req, sessionId);
|
|
59
|
+
return options.prepareInput(body, req, sessionId, res);
|
|
56
60
|
const input = body["input"];
|
|
57
61
|
if (typeof input === "string") {
|
|
58
62
|
if (input.trim() === "")
|
|
@@ -187,7 +191,9 @@ export function createHttpHandler(service, options) {
|
|
|
187
191
|
if (action === "run" || action === "resume") {
|
|
188
192
|
let handle;
|
|
189
193
|
if (action === "run") {
|
|
190
|
-
const input = await inputOf(body, req, sessionId);
|
|
194
|
+
const input = await inputOf(body, req, sessionId, res);
|
|
195
|
+
if (isHandled(input))
|
|
196
|
+
return true; // the product answered; nothing opened
|
|
191
197
|
const source = body["source"];
|
|
192
198
|
handle = await service.run(sessionId, input, {
|
|
193
199
|
...(source === "user" || source === "suggestion" || source === "tool_result" ? { source: source } : {}),
|
|
@@ -241,6 +247,9 @@ export function createHttpHandler(service, options) {
|
|
|
241
247
|
};
|
|
242
248
|
return { handle };
|
|
243
249
|
}
|
|
250
|
+
function isHandled(v) {
|
|
251
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && v.handled === true;
|
|
252
|
+
}
|
|
244
253
|
class BadRequest extends Error {
|
|
245
254
|
constructor(message) {
|
|
246
255
|
super(message);
|
package/dist/index.d.ts
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 type { AbortOutcome, DrainReport, OpenRun, RunHandle, RunOptions, SessionServiceOptions } from "./service.js";
|
|
10
|
+
export type { AbortOutcome, DrainReport, OpenRun, RunHandle, RunOptions, SessionServiceOptions, SettledRun } from "./service.js";
|
|
11
11
|
export { DrainingError, InFlightError, OpenRunError, StoreMismatchError } from "./errors.js";
|
|
12
12
|
export { EXECUTION_ENDED, executionDelta } from "./execution.js";
|
|
13
13
|
export { tail } from "./tail.js";
|
package/dist/service.d.ts
CHANGED
|
@@ -48,8 +48,26 @@ export interface SessionServiceOptions {
|
|
|
48
48
|
/** After `abort()` has aborted a run: the product's cleanup, e.g.
|
|
49
49
|
* finishing paid jobs the run left in flight. */
|
|
50
50
|
readonly onAbort?: (sessionId: string, runId: string) => Promise<void> | void;
|
|
51
|
+
/** After a run has settled — its terminal delivered to every
|
|
52
|
+
* listener, the session idle again. The product's "what next": a
|
|
53
|
+
* follow-up turn (`service.run`), a resume when the last uncertain
|
|
54
|
+
* verdict is in (`uncertainRemaining === 0`), a notification. The
|
|
55
|
+
* service itself never starts a run on its own. Runs after the
|
|
56
|
+
* store check, so a mismatched session never reaches it. */
|
|
57
|
+
readonly onSettled?: (settled: SettledRun) => Promise<void> | void;
|
|
51
58
|
};
|
|
52
59
|
}
|
|
60
|
+
/** What `hooks.onSettled` receives. */
|
|
61
|
+
export interface SettledRun {
|
|
62
|
+
readonly sessionId: string;
|
|
63
|
+
readonly runId: string;
|
|
64
|
+
/** The terminal's outcome kind: completed | aborted | error | max_turns | max_tokens | hook_stopped. */
|
|
65
|
+
readonly outcome: string;
|
|
66
|
+
/** Executions still awaiting a human verdict after this run. */
|
|
67
|
+
readonly uncertainRemaining: number;
|
|
68
|
+
/** The highest seq this run delivered. */
|
|
69
|
+
readonly highWater: number;
|
|
70
|
+
}
|
|
53
71
|
export interface RunHandle {
|
|
54
72
|
readonly runId: string;
|
|
55
73
|
/** Settles when the run has reached its terminal and the last event has
|
package/dist/service.js
CHANGED
|
@@ -81,6 +81,16 @@ export class SessionService {
|
|
|
81
81
|
held.mismatch = new StoreMismatchError(held.id, run.runId, held.highWater);
|
|
82
82
|
throw held.mismatch;
|
|
83
83
|
}
|
|
84
|
+
if (this.#hooks.onSettled !== undefined) {
|
|
85
|
+
const terminal = [...held.session.log.all].reverse().find((e) => e.type === "terminal");
|
|
86
|
+
await this.#hooks.onSettled({
|
|
87
|
+
sessionId: held.id,
|
|
88
|
+
runId: run.runId,
|
|
89
|
+
outcome: terminal !== undefined && terminal.type === "terminal" ? terminal.outcome.kind : "unknown",
|
|
90
|
+
uncertainRemaining: held.session.uncertainExecutions().length,
|
|
91
|
+
highWater: held.highWater,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
84
94
|
})();
|
|
85
95
|
held.pumping = pumping;
|
|
86
96
|
void pumping.catch(() => { }); // a host that does not await `done` gets no unhandled rejection
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
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.42.0",
|
|
29
|
+
"@vincemakes/kiso-protocol": "0.42.0",
|
|
30
|
+
"@vincemakes/kiso-runtime": "0.42.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@vincemakes/kiso-evals": "0.
|
|
33
|
+
"@vincemakes/kiso-evals": "0.42.0",
|
|
34
34
|
"@types/node": "^26.1.2",
|
|
35
35
|
"typescript": "^5.7.2",
|
|
36
36
|
"vitest": "^3.0.0"
|