@vincemakes/kiso-server 0.41.0 → 0.41.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 +16 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +14 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/service.d.ts +23 -6
- package/dist/service.js +30 -10
- package/dist/wire.d.ts +9 -4
- package/dist/wire.js +17 -6
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -50,6 +50,22 @@ await service.close(5_000); // abort what is live,
|
|
|
50
50
|
- **Product frames on the stream** (billing, estimates, couriers) are the
|
|
51
51
|
transport's business; the service delivers the durable events only.
|
|
52
52
|
|
|
53
|
+
## What the host owns
|
|
54
|
+
|
|
55
|
+
- **The store.** `createSessionService({ store })` takes the store the
|
|
56
|
+
factory's agents write to; the service never closes it — `close()`
|
|
57
|
+
aborts runs and forgets sessions, and the host calls `store.closeAll()`
|
|
58
|
+
when its process ends. Everything about an open session is read from
|
|
59
|
+
that session's own log (the object the run writes), so replay and run
|
|
60
|
+
share one truth by construction; a factory that binds its agents to a
|
|
61
|
+
DIFFERENT store is detected — the first run to settle on such a session
|
|
62
|
+
ends with `StoreMismatchError` and the session is refused from then on.
|
|
63
|
+
- **The sanitizer's guarantee.** The default `sanitizeToolArgs` strips
|
|
64
|
+
prose keys and truncates long strings at every depth. It is a UI-noise
|
|
65
|
+
and casual-leak filter for a tool card, not a privacy boundary: a
|
|
66
|
+
product that must guarantee no prose reaches a client supplies its own
|
|
67
|
+
`sanitize` in the projection options.
|
|
68
|
+
|
|
53
69
|
## What a host still writes
|
|
54
70
|
|
|
55
71
|
The transport (routes, SSE framing, `Last-Event-ID`, keepalive, status
|
package/dist/errors.d.ts
CHANGED
|
@@ -19,6 +19,15 @@ export declare class OpenRunError extends Error {
|
|
|
19
19
|
readonly runId: string;
|
|
20
20
|
constructor(sessionId: string, runId: string);
|
|
21
21
|
}
|
|
22
|
+
/** The factory's agent writes to a different store than the service was
|
|
23
|
+
* given: a run settled and its terminal is not on the service's store.
|
|
24
|
+
* Two stores are two truths, which kiso does not allow — the session is
|
|
25
|
+
* refused from here on (the host's wiring is wrong, not the run). */
|
|
26
|
+
export declare class StoreMismatchError extends Error {
|
|
27
|
+
readonly sessionId: string;
|
|
28
|
+
readonly runId: string;
|
|
29
|
+
constructor(sessionId: string, runId: string, expectedSeq: number);
|
|
30
|
+
}
|
|
22
31
|
/** drain() was called: no new run or resume starts in this process. */
|
|
23
32
|
export declare class DrainingError extends Error {
|
|
24
33
|
constructor();
|
package/dist/errors.js
CHANGED
|
@@ -29,6 +29,20 @@ export class OpenRunError extends Error {
|
|
|
29
29
|
this.runId = runId;
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
/** The factory's agent writes to a different store than the service was
|
|
33
|
+
* given: a run settled and its terminal is not on the service's store.
|
|
34
|
+
* Two stores are two truths, which kiso does not allow — the session is
|
|
35
|
+
* refused from here on (the host's wiring is wrong, not the run). */
|
|
36
|
+
export class StoreMismatchError extends Error {
|
|
37
|
+
sessionId;
|
|
38
|
+
runId;
|
|
39
|
+
constructor(sessionId, runId, expectedSeq) {
|
|
40
|
+
super(`session ${sessionId}: run ${runId} settled at seq ${expectedSeq} but the service's store does not hold it — the agent factory's store is not the service's store`);
|
|
41
|
+
this.name = "StoreMismatchError";
|
|
42
|
+
this.sessionId = sessionId;
|
|
43
|
+
this.runId = runId;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
32
46
|
/** drain() was called: no new run or resume starts in this process. */
|
|
33
47
|
export class DrainingError extends Error {
|
|
34
48
|
constructor() {
|
package/dist/index.d.ts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export { createSessionService, SessionService } from "./service.js";
|
|
10
10
|
export type { AbortOutcome, DrainReport, OpenRun, RunHandle, RunOptions, SessionServiceOptions } from "./service.js";
|
|
11
|
-
export { DrainingError, InFlightError, OpenRunError } from "./errors.js";
|
|
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";
|
|
14
|
-
export { MAX_ARG_VALUE_CHARS, STRIPPED_ARG_KEYS, sanitizeToolArgs, toWireEvent } from "./wire.js";
|
|
14
|
+
export { MAX_ARG_DEPTH, MAX_ARG_VALUE_CHARS, STRIPPED_ARG_KEYS, sanitizeToolArgs, toWireEvent } from "./wire.js";
|
|
15
15
|
export type { ProjectionOptions } from "./wire.js";
|
|
16
16
|
export type { Listener } from "./tail.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 } from "./errors.js";
|
|
10
|
+
export { DrainingError, InFlightError, OpenRunError, StoreMismatchError } from "./errors.js";
|
|
11
11
|
export { EXECUTION_ENDED, executionDelta } from "./execution.js";
|
|
12
12
|
export { tail } from "./tail.js";
|
|
13
|
-
export { MAX_ARG_VALUE_CHARS, STRIPPED_ARG_KEYS, sanitizeToolArgs, toWireEvent } from "./wire.js";
|
|
13
|
+
export { MAX_ARG_DEPTH, MAX_ARG_VALUE_CHARS, STRIPPED_ARG_KEYS, sanitizeToolArgs, toWireEvent } from "./wire.js";
|
package/dist/service.d.ts
CHANGED
|
@@ -20,9 +20,19 @@ import { type Listener } from "./tail.js";
|
|
|
20
20
|
* loop of its own.
|
|
21
21
|
*/
|
|
22
22
|
export interface SessionServiceOptions {
|
|
23
|
-
/** The store every session in this process lives on — the
|
|
24
|
-
* factory's agents write to. The service reads it for
|
|
25
|
-
*
|
|
23
|
+
/** The store every session in this process lives on — the SAME one the
|
|
24
|
+
* factory's agents write to. The service reads it for boot recovery
|
|
25
|
+
* (`openRuns()`) and for cold reads of sessions it has not opened;
|
|
26
|
+
* everything about an open session is read from that session's own
|
|
27
|
+
* log, so the run that writes and the replay that reads share one
|
|
28
|
+
* object by construction. The remaining way to get two stores — a
|
|
29
|
+
* factory that binds its agents to another store — is DETECTED: the
|
|
30
|
+
* first run to settle on such a session ends with StoreMismatchError
|
|
31
|
+
* and the session is refused from then on.
|
|
32
|
+
*
|
|
33
|
+
* Ownership: the host owns the store. `close()` aborts runs and
|
|
34
|
+
* forgets sessions; it never closes the store — the host calls
|
|
35
|
+
* `store.closeAll()` when its process ends. */
|
|
26
36
|
readonly store: SessionStore;
|
|
27
37
|
/** The product's agent factory, called once per session id (and again by
|
|
28
38
|
* `reopen`). Everything product-specific — prompt, tools, extensions,
|
|
@@ -121,9 +131,12 @@ export declare class SessionService {
|
|
|
121
131
|
readonly remaining: number;
|
|
122
132
|
}>;
|
|
123
133
|
/** Every event after `after` — the ones on disk now, then the live ones
|
|
124
|
-
* as they land — exactly once each, in order. Returns the unsubscribe.
|
|
134
|
+
* as they land — exactly once each, in order. Returns the unsubscribe.
|
|
135
|
+
* The replay reads the OPEN session's own log, never a second copy. */
|
|
125
136
|
subscribe(sessionId: string, after: number, listener: Listener): Promise<() => void>;
|
|
126
|
-
/** The durable log after `after
|
|
137
|
+
/** The durable log after `after`. An open session answers from its own
|
|
138
|
+
* log; a session this process has not opened is read from the store
|
|
139
|
+
* (a cold read, nothing opened). */
|
|
127
140
|
events(sessionId: string, after?: number): Event[];
|
|
128
141
|
isRunning(sessionId: string): boolean;
|
|
129
142
|
/** The highest seq this process has delivered for the session, −1 before any. */
|
|
@@ -133,6 +146,8 @@ export declare class SessionService {
|
|
|
133
146
|
/** The run a previous process left without a terminal on this session,
|
|
134
147
|
* or null. The plain read the transport's snapshot needs. */
|
|
135
148
|
openRun(sessionId: string): string | null;
|
|
149
|
+
/** True once a settled run proved the factory's store is not this one. */
|
|
150
|
+
mismatched(sessionId: string): boolean;
|
|
136
151
|
/** Boot recovery: every session on the store whose last run has no
|
|
137
152
|
* terminal. The host decides what to do with them (resume, or leave
|
|
138
153
|
* for the next request to find). */
|
|
@@ -144,7 +159,9 @@ export declare class SessionService {
|
|
|
144
159
|
/** Abort every live run, wait up to `graceMs` for each to settle (a
|
|
145
160
|
* tool that ignores the abort keeps its run open until it returns),
|
|
146
161
|
* then forget every session. Whatever did not settle is resumable by
|
|
147
|
-
* the next process — persist-first means nothing delivered is lost.
|
|
162
|
+
* the next process — persist-first means nothing delivered is lost.
|
|
163
|
+
* The STORE is not closed here: the host owns it and calls
|
|
164
|
+
* `store.closeAll()` when its process ends. */
|
|
148
165
|
close(graceMs?: number): Promise<void>;
|
|
149
166
|
}
|
|
150
167
|
export declare function createSessionService(options: SessionServiceOptions): SessionService;
|
package/dist/service.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { openRunId } from "@vincemakes/kiso-runtime/internal";
|
|
2
|
-
import { DrainingError, InFlightError, OpenRunError } from "./errors.js";
|
|
2
|
+
import { DrainingError, InFlightError, 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 };
|
|
27
|
+
const held = { id: sessionId, agent, session, listeners: new Set(), live: null, pumping: null, executing: 0, highWater: -1, mismatch: null };
|
|
28
28
|
this.#held.set(sessionId, held);
|
|
29
29
|
return held;
|
|
30
30
|
})();
|
|
@@ -73,6 +73,14 @@ export class SessionService {
|
|
|
73
73
|
held.executing = 0;
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
+
// the one-truth check: what this run wrote must be on the store the
|
|
77
|
+
// service was given. Persist-first means the terminal is on disk
|
|
78
|
+
// before it was yielded above; a store that does not hold it is a
|
|
79
|
+
// different store.
|
|
80
|
+
if (held.highWater >= 0 && !this.#store.load(held.id).some((r) => r.event.seq === held.highWater)) {
|
|
81
|
+
held.mismatch = new StoreMismatchError(held.id, run.runId, held.highWater);
|
|
82
|
+
throw held.mismatch;
|
|
83
|
+
}
|
|
76
84
|
})();
|
|
77
85
|
held.pumping = pumping;
|
|
78
86
|
void pumping.catch(() => { }); // a host that does not await `done` gets no unhandled rejection
|
|
@@ -85,6 +93,8 @@ export class SessionService {
|
|
|
85
93
|
if (this.#draining)
|
|
86
94
|
throw new DrainingError();
|
|
87
95
|
const held = await this.#hold(sessionId);
|
|
96
|
+
if (held.mismatch !== null)
|
|
97
|
+
throw held.mismatch;
|
|
88
98
|
if (held.live !== null)
|
|
89
99
|
throw new InFlightError(sessionId, held.live.runId);
|
|
90
100
|
const open = openRunId(this.#store.load(sessionId));
|
|
@@ -102,6 +112,8 @@ export class SessionService {
|
|
|
102
112
|
if (this.#draining)
|
|
103
113
|
throw new DrainingError();
|
|
104
114
|
const held = await this.#hold(sessionId);
|
|
115
|
+
if (held.mismatch !== null)
|
|
116
|
+
throw held.mismatch;
|
|
105
117
|
if (held.live !== null)
|
|
106
118
|
throw new InFlightError(sessionId, held.live.runId);
|
|
107
119
|
return this.#pump(held, held.session.resume());
|
|
@@ -155,17 +167,19 @@ export class SessionService {
|
|
|
155
167
|
}
|
|
156
168
|
// ---- observation ----------------------------------------------------------
|
|
157
169
|
/** Every event after `after` — the ones on disk now, then the live ones
|
|
158
|
-
* as they land — exactly once each, in order. Returns the unsubscribe.
|
|
170
|
+
* as they land — exactly once each, in order. Returns the unsubscribe.
|
|
171
|
+
* The replay reads the OPEN session's own log, never a second copy. */
|
|
159
172
|
async subscribe(sessionId, after, listener) {
|
|
160
173
|
const held = await this.#hold(sessionId);
|
|
161
|
-
return tail(() =>
|
|
174
|
+
return tail(() => held.session.log.all.filter((e) => e.seq > after), held.listeners, after, listener);
|
|
162
175
|
}
|
|
163
|
-
/** The durable log after `after
|
|
176
|
+
/** The durable log after `after`. An open session answers from its own
|
|
177
|
+
* log; a session this process has not opened is read from the store
|
|
178
|
+
* (a cold read, nothing opened). */
|
|
164
179
|
events(sessionId, after = -1) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
.filter((e) => e.seq > after);
|
|
180
|
+
const held = this.#held.get(sessionId);
|
|
181
|
+
const events = held !== undefined ? held.session.log.all : this.#store.load(sessionId).map((r) => r.event);
|
|
182
|
+
return events.filter((e) => e.seq > after);
|
|
169
183
|
}
|
|
170
184
|
isRunning(sessionId) {
|
|
171
185
|
return this.#held.get(sessionId)?.live !== null && this.#held.get(sessionId)?.live !== undefined;
|
|
@@ -188,6 +202,10 @@ export class SessionService {
|
|
|
188
202
|
openRun(sessionId) {
|
|
189
203
|
return openRunId(this.#store.load(sessionId)) ?? null;
|
|
190
204
|
}
|
|
205
|
+
/** True once a settled run proved the factory's store is not this one. */
|
|
206
|
+
mismatched(sessionId) {
|
|
207
|
+
return this.#held.get(sessionId)?.mismatch !== null && this.#held.get(sessionId)?.mismatch !== undefined;
|
|
208
|
+
}
|
|
191
209
|
/** Boot recovery: every session on the store whose last run has no
|
|
192
210
|
* terminal. The host decides what to do with them (resume, or leave
|
|
193
211
|
* for the next request to find). */
|
|
@@ -237,7 +255,9 @@ export class SessionService {
|
|
|
237
255
|
/** Abort every live run, wait up to `graceMs` for each to settle (a
|
|
238
256
|
* tool that ignores the abort keeps its run open until it returns),
|
|
239
257
|
* then forget every session. Whatever did not settle is resumable by
|
|
240
|
-
* the next process — persist-first means nothing delivered is lost.
|
|
258
|
+
* the next process — persist-first means nothing delivered is lost.
|
|
259
|
+
* The STORE is not closed here: the host owns it and calls
|
|
260
|
+
* `store.closeAll()` when its process ends. */
|
|
241
261
|
async close(graceMs = 0) {
|
|
242
262
|
this.#draining = true;
|
|
243
263
|
const settling = [];
|
package/dist/wire.d.ts
CHANGED
|
@@ -22,8 +22,13 @@ export interface ProjectionOptions {
|
|
|
22
22
|
export declare const STRIPPED_ARG_KEYS: ReadonlySet<string>;
|
|
23
23
|
/** Longer string values are cut here; the card shows a prefix. */
|
|
24
24
|
export declare const MAX_ARG_VALUE_CHARS = 200;
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
/** Nesting beyond this is replaced by a marker rather than walked. */
|
|
26
|
+
export declare const MAX_ARG_DEPTH = 8;
|
|
27
|
+
/** The default sanitizer: drops the prose keys and truncates long strings
|
|
28
|
+
* AT EVERY DEPTH — objects and arrays are walked (0.41.1; the 0.41.0
|
|
29
|
+
* version was shallow, so `{ options: { prompt } }` passed the prompt
|
|
30
|
+
* through). What it is: a UI-noise and casual-leak filter for a tool
|
|
31
|
+
* card. What it is not: a privacy boundary — a product that must
|
|
32
|
+
* guarantee no prose reaches a client supplies its own `sanitize`. */
|
|
33
|
+
export declare function sanitizeToolArgs(input: unknown, depth?: number): unknown;
|
|
29
34
|
export declare function toWireEvent(event: Event, options?: ProjectionOptions): WireEvent | null;
|
package/dist/wire.js
CHANGED
|
@@ -5,17 +5,28 @@ import { DURABLE_TO_WIRE, SANITIZED_FIELDS, WIRE_FIELDS } from "@vincemakes/kiso
|
|
|
5
5
|
export const STRIPPED_ARG_KEYS = new Set(["prompt", "description", "text", "body", "content", "instructions", "system_prompt", "input_text"]);
|
|
6
6
|
/** Longer string values are cut here; the card shows a prefix. */
|
|
7
7
|
export const MAX_ARG_VALUE_CHARS = 200;
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
/** Nesting beyond this is replaced by a marker rather than walked. */
|
|
9
|
+
export const MAX_ARG_DEPTH = 8;
|
|
10
|
+
/** The default sanitizer: drops the prose keys and truncates long strings
|
|
11
|
+
* AT EVERY DEPTH — objects and arrays are walked (0.41.1; the 0.41.0
|
|
12
|
+
* version was shallow, so `{ options: { prompt } }` passed the prompt
|
|
13
|
+
* through). What it is: a UI-noise and casual-leak filter for a tool
|
|
14
|
+
* card. What it is not: a privacy boundary — a product that must
|
|
15
|
+
* guarantee no prose reaches a client supplies its own `sanitize`. */
|
|
16
|
+
export function sanitizeToolArgs(input, depth = 0) {
|
|
17
|
+
if (typeof input === "string")
|
|
18
|
+
return input.length > MAX_ARG_VALUE_CHARS ? `${input.slice(0, MAX_ARG_VALUE_CHARS)}…` : input;
|
|
19
|
+
if (input === null || typeof input !== "object")
|
|
13
20
|
return input;
|
|
21
|
+
if (depth >= MAX_ARG_DEPTH)
|
|
22
|
+
return Array.isArray(input) ? "[…]" : "{…}";
|
|
23
|
+
if (Array.isArray(input))
|
|
24
|
+
return input.map((v) => sanitizeToolArgs(v, depth + 1));
|
|
14
25
|
const out = {};
|
|
15
26
|
for (const [key, value] of Object.entries(input)) {
|
|
16
27
|
if (STRIPPED_ARG_KEYS.has(key))
|
|
17
28
|
continue;
|
|
18
|
-
out[key] =
|
|
29
|
+
out[key] = sanitizeToolArgs(value, depth + 1);
|
|
19
30
|
}
|
|
20
31
|
return out;
|
|
21
32
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-server",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.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.41.
|
|
29
|
-
"@vincemakes/kiso-protocol": "0.41.
|
|
30
|
-
"@vincemakes/kiso-runtime": "0.41.
|
|
28
|
+
"@vincemakes/kiso-core": "0.41.1",
|
|
29
|
+
"@vincemakes/kiso-protocol": "0.41.1",
|
|
30
|
+
"@vincemakes/kiso-runtime": "0.41.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@vincemakes/kiso-evals": "0.41.
|
|
33
|
+
"@vincemakes/kiso-evals": "0.41.1",
|
|
34
34
|
"@types/node": "^26.1.2",
|
|
35
35
|
"typescript": "^5.7.2",
|
|
36
36
|
"vitest": "^3.0.0"
|