@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 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
- readonly prepareInput?: (body: Readonly<Record<string, unknown>>, req: IncomingMessage, sessionId: string) => WireInput | Promise<WireInput>;
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
- const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
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.41.1",
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.41.1",
29
- "@vincemakes/kiso-protocol": "0.41.1",
30
- "@vincemakes/kiso-runtime": "0.41.1"
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.41.1",
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"