@nylorun/runtime 0.4.0-beta → 0.5.0-beta
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/CHANGELOG.md +25 -0
- package/README.md +29 -15
- package/dist/adapters/media.d.ts +2 -18
- package/dist/adapters/media.js +2 -52
- package/dist/adapters/observe.js +1 -1
- package/dist/config.d.ts +17 -10
- package/dist/contracts.d.ts +3 -166
- package/dist/index.d.ts +8 -8
- package/dist/index.js +5 -5
- package/dist/launcher.js +6 -1
- package/dist/media.d.ts +29 -0
- package/dist/media.js +53 -0
- package/dist/model/defaults.d.ts +17 -0
- package/dist/model/defaults.js +21 -0
- package/dist/model/http-model.d.ts +12 -0
- package/dist/model/http-model.js +299 -0
- package/dist/model/pi-model.d.ts +2 -1
- package/dist/model/pi-model.js +52 -7
- package/dist/node/index.d.ts +5 -0
- package/dist/node/index.js +5 -0
- package/dist/node/local-sessions.d.ts +5 -0
- package/dist/node/local-sessions.js +157 -0
- package/dist/redact.d.ts +1 -0
- package/dist/redact.js +14 -0
- package/dist/server/ag-ui.d.ts +1 -1
- package/dist/server/delivery.d.ts +24 -0
- package/dist/server/delivery.js +107 -0
- package/dist/server/host.d.ts +11 -6
- package/dist/server/host.js +236 -305
- package/dist/sessions/host.d.ts +29 -0
- package/dist/sessions/host.js +291 -0
- package/dist/sessions/store.d.ts +40 -0
- package/dist/sessions/store.js +30 -0
- package/package.json +10 -4
- package/dist/adapters/journal.d.ts +0 -35
- package/dist/adapters/journal.js +0 -130
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { BuiltAgent, ExecutionInput, ModelAdapter, RunResult } from "@nylorun/harness";
|
|
2
|
+
import type { CanonicalEvent, SessionStore, StoredSession, SessionSummary } from "./store.js";
|
|
3
|
+
export interface SubmitOptions {
|
|
4
|
+
readonly runId?: string;
|
|
5
|
+
readonly info?: unknown;
|
|
6
|
+
readonly signal?: AbortSignal;
|
|
7
|
+
readonly onModelCall: ModelAdapter;
|
|
8
|
+
readonly secrets?: readonly string[];
|
|
9
|
+
readonly onEvent?: (event: CanonicalEvent) => void;
|
|
10
|
+
readonly started?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
/** The built-in single-process coordinator. Storage adapters do not implement execution. */
|
|
13
|
+
export declare class SessionHost {
|
|
14
|
+
readonly store: SessionStore;
|
|
15
|
+
private readonly queues;
|
|
16
|
+
private readonly generations;
|
|
17
|
+
private readonly active;
|
|
18
|
+
private readonly listeners;
|
|
19
|
+
private closed;
|
|
20
|
+
constructor(store: SessionStore);
|
|
21
|
+
private key;
|
|
22
|
+
read(agentId: string, sessionId: string): Promise<StoredSession | undefined>;
|
|
23
|
+
list(agentId: string): Promise<readonly SessionSummary[]>;
|
|
24
|
+
subscribe(agentId: string, sessionId: string, listener: (event: CanonicalEvent) => void): () => void;
|
|
25
|
+
submit(agent: BuiltAgent<any, any>, sessionId: string, input: ExecutionInput, options: SubmitOptions): Promise<RunResult<any>>;
|
|
26
|
+
cancel(agent: BuiltAgent<any, any>, sessionId: string): Promise<void>;
|
|
27
|
+
interrupt(agent: BuiltAgent<any, any>, sessionId: string, input: ExecutionInput, options: SubmitOptions): Promise<RunResult<any>>;
|
|
28
|
+
close(): Promise<void>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { isHarnessError } from "@nylorun/harness";
|
|
2
|
+
import { scrub } from "../redact.js";
|
|
3
|
+
/** The built-in single-process coordinator. Storage adapters do not implement execution. */
|
|
4
|
+
export class SessionHost {
|
|
5
|
+
store;
|
|
6
|
+
queues = new Map();
|
|
7
|
+
generations = new Map();
|
|
8
|
+
active = new Map();
|
|
9
|
+
listeners = new Map();
|
|
10
|
+
closed = false;
|
|
11
|
+
constructor(store) {
|
|
12
|
+
this.store = store;
|
|
13
|
+
}
|
|
14
|
+
key(agentId, sessionId) {
|
|
15
|
+
return JSON.stringify([agentId, sessionId]);
|
|
16
|
+
}
|
|
17
|
+
async read(agentId, sessionId) {
|
|
18
|
+
const document = await this.store.get(agentId, sessionId);
|
|
19
|
+
if (document?.active && !this.active.has(this.key(agentId, sessionId)))
|
|
20
|
+
return { ...document, status: "interrupted" };
|
|
21
|
+
return document;
|
|
22
|
+
}
|
|
23
|
+
async list(agentId) {
|
|
24
|
+
return (await this.store.list(agentId)).map((session) => session.status === "running" &&
|
|
25
|
+
!this.active.has(this.key(agentId, session.session))
|
|
26
|
+
? { ...session, status: "interrupted" }
|
|
27
|
+
: session);
|
|
28
|
+
}
|
|
29
|
+
subscribe(agentId, sessionId, listener) {
|
|
30
|
+
const key = this.key(agentId, sessionId);
|
|
31
|
+
let listeners = this.listeners.get(key);
|
|
32
|
+
if (!listeners) {
|
|
33
|
+
listeners = new Set();
|
|
34
|
+
this.listeners.set(key, listeners);
|
|
35
|
+
}
|
|
36
|
+
listeners.add(listener);
|
|
37
|
+
return () => {
|
|
38
|
+
listeners.delete(listener);
|
|
39
|
+
if (!listeners.size)
|
|
40
|
+
this.listeners.delete(key);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
submit(agent, sessionId, input, options) {
|
|
44
|
+
const key = this.key(agent.id, sessionId);
|
|
45
|
+
const generation = this.generations.get(key) ?? 0;
|
|
46
|
+
const prior = this.queues.get(key) ?? Promise.resolve();
|
|
47
|
+
const work = prior
|
|
48
|
+
.catch(() => { })
|
|
49
|
+
.then(async () => {
|
|
50
|
+
if ((this.generations.get(key) ?? 0) !== generation)
|
|
51
|
+
throw new Error("Queued input cancelled by interruption");
|
|
52
|
+
if (this.closed)
|
|
53
|
+
throw new Error("Runtime is closed");
|
|
54
|
+
options.signal?.throwIfAborted();
|
|
55
|
+
const stored = await this.store.get(agent.id, sessionId);
|
|
56
|
+
// Cancellation/shutdown can arrive while an asynchronous store is loading.
|
|
57
|
+
if ((this.generations.get(key) ?? 0) !== generation)
|
|
58
|
+
throw new Error("Queued input cancelled by interruption");
|
|
59
|
+
if (this.closed)
|
|
60
|
+
throw new Error("Runtime is closed");
|
|
61
|
+
options.signal?.throwIfAborted();
|
|
62
|
+
if (stored?.active ||
|
|
63
|
+
stored?.status === "interrupted" ||
|
|
64
|
+
stored?.status === "archived")
|
|
65
|
+
throw new Error("This session is interrupted or archived. Reconcile external effects or start a new session.");
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
this.active.set(key, controller);
|
|
68
|
+
const abort = () => controller.abort(options.signal?.reason);
|
|
69
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
70
|
+
if (options.signal?.aborted)
|
|
71
|
+
abort();
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
const runId = options.runId ?? crypto.randomUUID();
|
|
74
|
+
let document = stored ?? {
|
|
75
|
+
version: 1,
|
|
76
|
+
id: sessionId,
|
|
77
|
+
agentId: agent.id,
|
|
78
|
+
status: "running",
|
|
79
|
+
startedAt: now,
|
|
80
|
+
updatedAt: now,
|
|
81
|
+
events: [],
|
|
82
|
+
};
|
|
83
|
+
const title = typeof input === "string"
|
|
84
|
+
? input
|
|
85
|
+
: "text" in input
|
|
86
|
+
? input.text
|
|
87
|
+
: "content" in input
|
|
88
|
+
? input.content
|
|
89
|
+
.filter((part) => part.type === "text")
|
|
90
|
+
.map((part) => part.text)
|
|
91
|
+
.join(" ")
|
|
92
|
+
: undefined;
|
|
93
|
+
const events = [...document.events];
|
|
94
|
+
let sequence = events.at(-1)?.seq ?? 0;
|
|
95
|
+
const add = (type, payload, publish = true) => {
|
|
96
|
+
const event = {
|
|
97
|
+
session: sessionId,
|
|
98
|
+
seq: ++sequence,
|
|
99
|
+
ts: new Date().toISOString(),
|
|
100
|
+
type,
|
|
101
|
+
payload: scrub({ ...payload, runId }, options.secrets ?? []),
|
|
102
|
+
};
|
|
103
|
+
events.push(event);
|
|
104
|
+
if (publish)
|
|
105
|
+
publishEvent(event);
|
|
106
|
+
return event;
|
|
107
|
+
};
|
|
108
|
+
const publishEvent = (event) => {
|
|
109
|
+
for (const listener of [
|
|
110
|
+
options.onEvent,
|
|
111
|
+
...(this.listeners.get(key) ?? []),
|
|
112
|
+
]) {
|
|
113
|
+
try {
|
|
114
|
+
listener?.(event);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/* Delivery cannot acknowledge or block persistence. */
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const save = async () => {
|
|
122
|
+
document = {
|
|
123
|
+
...document,
|
|
124
|
+
updatedAt: Date.now(),
|
|
125
|
+
events: [...events],
|
|
126
|
+
};
|
|
127
|
+
await this.store.put(agent.id, sessionId, document);
|
|
128
|
+
};
|
|
129
|
+
try {
|
|
130
|
+
document = {
|
|
131
|
+
...document,
|
|
132
|
+
status: "running",
|
|
133
|
+
active: { runId, input },
|
|
134
|
+
...(document.title === undefined && title
|
|
135
|
+
? { title: title.slice(0, 120) }
|
|
136
|
+
: {}),
|
|
137
|
+
};
|
|
138
|
+
add("session.run.started", options.started ?? {
|
|
139
|
+
input_kind: typeof input === "string"
|
|
140
|
+
? "user-message"
|
|
141
|
+
: "kind" in input
|
|
142
|
+
? input.kind
|
|
143
|
+
: "user-message",
|
|
144
|
+
...(typeof input === "string" ? { input } : {}),
|
|
145
|
+
});
|
|
146
|
+
await save();
|
|
147
|
+
const result = await agent.run({
|
|
148
|
+
state: document.state,
|
|
149
|
+
input,
|
|
150
|
+
info: options.info,
|
|
151
|
+
signal: controller.signal,
|
|
152
|
+
onModelCall: options.onModelCall,
|
|
153
|
+
onEvent: (event) => {
|
|
154
|
+
add(event.type, event);
|
|
155
|
+
},
|
|
156
|
+
record: async (state) => {
|
|
157
|
+
document = { ...document, state };
|
|
158
|
+
await save();
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
const { active: _, ...rest } = document;
|
|
162
|
+
document = {
|
|
163
|
+
...rest,
|
|
164
|
+
state: result.state,
|
|
165
|
+
status: result.status === "paused" ? "waiting" : result.status,
|
|
166
|
+
};
|
|
167
|
+
const terminal = result.status === "completed"
|
|
168
|
+
? add("final", { output: result.output }, false)
|
|
169
|
+
: result.status === "paused"
|
|
170
|
+
? add("interaction.required", {
|
|
171
|
+
pending: result.pending,
|
|
172
|
+
...(result.pending.find((item) => item.interaction)
|
|
173
|
+
?.interaction
|
|
174
|
+
? {
|
|
175
|
+
interaction: result.pending.find((item) => item.interaction).interaction,
|
|
176
|
+
}
|
|
177
|
+
: {}),
|
|
178
|
+
}, false)
|
|
179
|
+
: add(result.status === "failed" ? "error" : "cancelled", result.status === "failed" ? { ...result.error } : {}, false);
|
|
180
|
+
await save();
|
|
181
|
+
publishEvent(terminal);
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (isHarnessError(error) &&
|
|
186
|
+
[
|
|
187
|
+
"execution.invalid-input",
|
|
188
|
+
"execution.invalid-state",
|
|
189
|
+
"execution.incompatible",
|
|
190
|
+
].includes(error.code)) {
|
|
191
|
+
// Keep the prior continuation, but commit the rejected attempt so event
|
|
192
|
+
// sequence numbers already observed by subscribers are never reused.
|
|
193
|
+
const rejected = add("error", {
|
|
194
|
+
message: error instanceof Error ? error.message : String(error),
|
|
195
|
+
}, false);
|
|
196
|
+
const { active: _, ...rest } = document;
|
|
197
|
+
await this.store.put(agent.id, sessionId, {
|
|
198
|
+
...(stored ?? { ...rest, status: "failed" }),
|
|
199
|
+
updatedAt: Date.now(),
|
|
200
|
+
events: [...events],
|
|
201
|
+
});
|
|
202
|
+
publishEvent(rejected);
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
// A failed commit must leave the durable active marker intact. Never replay it.
|
|
206
|
+
add("error", {
|
|
207
|
+
message: error instanceof Error ? error.message : String(error),
|
|
208
|
+
});
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
options.signal?.removeEventListener("abort", abort);
|
|
213
|
+
this.active.delete(key);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
this.queues.set(key, work);
|
|
217
|
+
void work
|
|
218
|
+
.finally(() => {
|
|
219
|
+
if (this.queues.get(key) === work)
|
|
220
|
+
this.queues.delete(key);
|
|
221
|
+
})
|
|
222
|
+
.catch(() => { });
|
|
223
|
+
return work;
|
|
224
|
+
}
|
|
225
|
+
async cancel(agent, sessionId) {
|
|
226
|
+
const key = this.key(agent.id, sessionId);
|
|
227
|
+
this.generations.set(key, (this.generations.get(key) ?? 0) + 1);
|
|
228
|
+
this.active.get(key)?.abort(new Error("Run cancelled"));
|
|
229
|
+
const prior = this.queues.get(key) ?? Promise.resolve();
|
|
230
|
+
const work = prior
|
|
231
|
+
.catch(() => { })
|
|
232
|
+
.then(async () => {
|
|
233
|
+
const stored = await this.store.get(agent.id, sessionId);
|
|
234
|
+
if (stored?.active || stored?.status === "interrupted")
|
|
235
|
+
throw new Error("This session was interrupted; reconcile its external effects before cancellation.");
|
|
236
|
+
if (stored?.state?.status !== "paused")
|
|
237
|
+
return;
|
|
238
|
+
const result = await agent.run({
|
|
239
|
+
state: stored.state,
|
|
240
|
+
input: { kind: "continue" },
|
|
241
|
+
signal: AbortSignal.abort(new Error("Paused execution cancelled")),
|
|
242
|
+
onModelCall: async () => {
|
|
243
|
+
throw new Error("Cancelled execution cannot call a model");
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
const event = {
|
|
247
|
+
session: sessionId,
|
|
248
|
+
seq: (stored.events.at(-1)?.seq ?? 0) + 1,
|
|
249
|
+
ts: new Date().toISOString(),
|
|
250
|
+
type: "cancelled",
|
|
251
|
+
payload: { executionId: result.state.executionId },
|
|
252
|
+
};
|
|
253
|
+
await this.store.put(agent.id, sessionId, {
|
|
254
|
+
...stored,
|
|
255
|
+
state: result.state,
|
|
256
|
+
status: "cancelled",
|
|
257
|
+
updatedAt: Date.now(),
|
|
258
|
+
events: [...stored.events, event],
|
|
259
|
+
});
|
|
260
|
+
for (const listener of this.listeners.get(key) ?? []) {
|
|
261
|
+
try {
|
|
262
|
+
listener(event);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
/* Observation cannot change committed cancellation. */
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
this.queues.set(key, work);
|
|
270
|
+
try {
|
|
271
|
+
await work;
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
if (this.queues.get(key) === work)
|
|
275
|
+
this.queues.delete(key);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async interrupt(agent, sessionId, input, options) {
|
|
279
|
+
await this.cancel(agent, sessionId);
|
|
280
|
+
return this.submit(agent, sessionId, input, options);
|
|
281
|
+
}
|
|
282
|
+
async close() {
|
|
283
|
+
this.closed = true;
|
|
284
|
+
for (const controller of this.active.values())
|
|
285
|
+
controller.abort(new Error("Runtime closing"));
|
|
286
|
+
await Promise.allSettled(this.queues.values());
|
|
287
|
+
this.listeners.clear();
|
|
288
|
+
if ("close" in this.store && typeof this.store.close === "function")
|
|
289
|
+
await this.store.close();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ExecutionInput, ExecutionState } from "@nylorun/harness";
|
|
2
|
+
export interface CanonicalEvent {
|
|
3
|
+
readonly session: string;
|
|
4
|
+
readonly seq: number;
|
|
5
|
+
readonly ts: string;
|
|
6
|
+
readonly type: string;
|
|
7
|
+
readonly payload: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface SessionSummary {
|
|
10
|
+
readonly session: string;
|
|
11
|
+
readonly title?: string;
|
|
12
|
+
readonly status: StoredSession["status"];
|
|
13
|
+
readonly startedAt: number;
|
|
14
|
+
readonly endedAt?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface StoredSession {
|
|
17
|
+
readonly version: 1;
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly agentId: string;
|
|
20
|
+
readonly state?: ExecutionState;
|
|
21
|
+
readonly status: "running" | "waiting" | "completed" | "cancelled" | "failed" | "interrupted" | "archived";
|
|
22
|
+
readonly startedAt: number;
|
|
23
|
+
readonly updatedAt: number;
|
|
24
|
+
readonly title?: string;
|
|
25
|
+
readonly active?: {
|
|
26
|
+
readonly runId: string;
|
|
27
|
+
readonly input: ExecutionInput;
|
|
28
|
+
};
|
|
29
|
+
readonly events: readonly CanonicalEvent[];
|
|
30
|
+
}
|
|
31
|
+
export interface SessionStore {
|
|
32
|
+
get(agentId: string, sessionId: string): Promise<StoredSession | undefined>;
|
|
33
|
+
put(agentId: string, sessionId: string, session: StoredSession): Promise<void>;
|
|
34
|
+
list(agentId: string): Promise<readonly SessionSummary[]>;
|
|
35
|
+
}
|
|
36
|
+
export interface ManagedSessionStore extends SessionStore {
|
|
37
|
+
close(): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
export declare function sessionSummary(session: StoredSession): SessionSummary;
|
|
40
|
+
export declare function memorySessions(): SessionStore;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function sessionSummary(session) {
|
|
2
|
+
return {
|
|
3
|
+
session: session.id,
|
|
4
|
+
status: session.status,
|
|
5
|
+
startedAt: session.startedAt,
|
|
6
|
+
...(session.title === undefined ? {} : { title: session.title }),
|
|
7
|
+
...(session.status === "completed" ? { endedAt: session.updatedAt } : {}),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function memorySessions() {
|
|
11
|
+
const documents = new Map();
|
|
12
|
+
const key = (agentId, sessionId) => JSON.stringify([agentId, sessionId]);
|
|
13
|
+
return {
|
|
14
|
+
async get(agentId, sessionId) {
|
|
15
|
+
const found = documents.get(key(agentId, sessionId));
|
|
16
|
+
return found === undefined ? undefined : structuredClone(found);
|
|
17
|
+
},
|
|
18
|
+
async put(agentId, sessionId, session) {
|
|
19
|
+
if (session.id !== sessionId || session.agentId !== agentId)
|
|
20
|
+
throw new Error("Session identity mismatch");
|
|
21
|
+
documents.set(key(agentId, sessionId), structuredClone(session));
|
|
22
|
+
},
|
|
23
|
+
async list(agentId) {
|
|
24
|
+
return [...documents.values()]
|
|
25
|
+
.filter((session) => session.agentId === agentId)
|
|
26
|
+
.map(sessionSummary)
|
|
27
|
+
.sort((a, b) => b.startedAt - a.startedAt);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nylorun/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-beta",
|
|
4
4
|
"description": "Portable agent runtime, Hono protocol router, model providers, and the Nylorun CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
".": {
|
|
20
20
|
"types": "./dist/index.d.ts",
|
|
21
21
|
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./node": {
|
|
24
|
+
"types": "./dist/node/index.d.ts",
|
|
25
|
+
"import": "./dist/node/index.js"
|
|
22
26
|
}
|
|
23
27
|
},
|
|
24
28
|
"files": [
|
|
@@ -32,15 +36,17 @@
|
|
|
32
36
|
"provenance": true
|
|
33
37
|
},
|
|
34
38
|
"scripts": {
|
|
35
|
-
"build": "node ../scripts/check-typescript-version.mjs && tsc -p tsconfig.json",
|
|
39
|
+
"build": "npm run clean && node ../scripts/check-typescript-version.mjs && tsc -p tsconfig.json",
|
|
36
40
|
"check": "npm run build && npm test && node scripts/check-package.mjs",
|
|
37
41
|
"typecheck": "node ../scripts/check-typescript-version.mjs && tsc -p tsconfig.json --noEmit",
|
|
38
42
|
"test": "vitest run",
|
|
39
|
-
"prepack": "npm run build"
|
|
43
|
+
"prepack": "npm run build",
|
|
44
|
+
"clean": "node -e \"import('node:fs').then(({rmSync})=>rmSync('dist',{recursive:true,force:true}))\""
|
|
40
45
|
},
|
|
41
46
|
"dependencies": {
|
|
42
47
|
"@earendil-works/pi-ai": "0.85.1",
|
|
43
|
-
"@hono/node-server": "^2.1.1"
|
|
48
|
+
"@hono/node-server": "^2.1.1",
|
|
49
|
+
"@nylorun/harness": "0.13.0-beta"
|
|
44
50
|
},
|
|
45
51
|
"peerDependencies": {
|
|
46
52
|
"hono": "^4.13.7"
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export type CanonicalEvent = Readonly<{
|
|
2
|
-
session: string;
|
|
3
|
-
seq: number;
|
|
4
|
-
ts: string;
|
|
5
|
-
type: string;
|
|
6
|
-
payload: Record<string, unknown>;
|
|
7
|
-
}>;
|
|
8
|
-
export type SessionSummary = Readonly<{
|
|
9
|
-
session: string;
|
|
10
|
-
title?: string;
|
|
11
|
-
status: "idle" | "waiting";
|
|
12
|
-
startedAt: number;
|
|
13
|
-
endedAt?: number;
|
|
14
|
-
}>;
|
|
15
|
-
/** Explicit local-only JSONL durability. Raw provider payloads never enter this service. */
|
|
16
|
-
export declare class JsonlJournal {
|
|
17
|
-
private readonly root;
|
|
18
|
-
private readonly secrets;
|
|
19
|
-
constructor(root: string, secrets: readonly string[]);
|
|
20
|
-
append(agentId: string, event: CanonicalEvent): Promise<void>;
|
|
21
|
-
events(agentId: string, sessionId: string): Promise<readonly CanonicalEvent[]>;
|
|
22
|
-
list(agentId: string): Promise<readonly SessionSummary[]>;
|
|
23
|
-
private file;
|
|
24
|
-
}
|
|
25
|
-
export declare function scrub(value: unknown, secrets: readonly string[]): unknown;
|
|
26
|
-
export interface RuntimeDurability {
|
|
27
|
-
append(agentId: string, event: CanonicalEvent): Promise<void>;
|
|
28
|
-
events(agentId: string, sessionId: string): Promise<readonly CanonicalEvent[]>;
|
|
29
|
-
list(agentId: string): Promise<readonly SessionSummary[]>;
|
|
30
|
-
}
|
|
31
|
-
export declare function localJsonl(options?: {
|
|
32
|
-
root?: string;
|
|
33
|
-
secrets?: readonly string[];
|
|
34
|
-
}): RuntimeDurability;
|
|
35
|
-
export declare function memoryHistory(): RuntimeDurability;
|
package/dist/adapters/journal.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { appendFile, mkdir, readFile, readdir } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
/** Explicit local-only JSONL durability. Raw provider payloads never enter this service. */
|
|
4
|
-
export class JsonlJournal {
|
|
5
|
-
root;
|
|
6
|
-
secrets;
|
|
7
|
-
constructor(root, secrets) {
|
|
8
|
-
this.root = root;
|
|
9
|
-
this.secrets = secrets;
|
|
10
|
-
}
|
|
11
|
-
async append(agentId, event) {
|
|
12
|
-
const file = this.file(agentId, event.session);
|
|
13
|
-
await mkdir(join(this.root, agentId, event.session), { recursive: true });
|
|
14
|
-
await appendFile(file, `${JSON.stringify(scrub(event, this.secrets))}\n`);
|
|
15
|
-
}
|
|
16
|
-
async events(agentId, sessionId) {
|
|
17
|
-
try {
|
|
18
|
-
return Object.freeze((await readFile(this.file(agentId, sessionId), "utf8"))
|
|
19
|
-
.split("\n")
|
|
20
|
-
.filter(Boolean)
|
|
21
|
-
.flatMap((line) => {
|
|
22
|
-
try {
|
|
23
|
-
return [JSON.parse(line)];
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return [];
|
|
27
|
-
}
|
|
28
|
-
}));
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
return Object.freeze([]);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
async list(agentId) {
|
|
35
|
-
try {
|
|
36
|
-
const ids = await readdir(join(this.root, agentId));
|
|
37
|
-
const summaries = await Promise.all(ids.map(async (sessionId) => {
|
|
38
|
-
const events = await this.events(agentId, sessionId);
|
|
39
|
-
const final = events.findLast((event) => event.type === "final");
|
|
40
|
-
const title = sessionTitle(events);
|
|
41
|
-
return {
|
|
42
|
-
session: sessionId,
|
|
43
|
-
...(title === undefined ? {} : { title }),
|
|
44
|
-
status: sessionStatus(events),
|
|
45
|
-
startedAt: events[0] ? Date.parse(events[0].ts) : 0,
|
|
46
|
-
...(final === undefined ? {} : { endedAt: Date.parse(final.ts) }),
|
|
47
|
-
};
|
|
48
|
-
}));
|
|
49
|
-
return Object.freeze(summaries.sort((a, b) => b.startedAt - a.startedAt));
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
return Object.freeze([]);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
file(agentId, sessionId) {
|
|
56
|
-
return join(this.root, safe(agentId), safe(sessionId), "events.jsonl");
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
function sessionTitle(events) {
|
|
60
|
-
for (const event of events) {
|
|
61
|
-
if (event.type !== "session.run.started" ||
|
|
62
|
-
typeof event.payload.input !== "string")
|
|
63
|
-
continue;
|
|
64
|
-
const title = event.payload.input.replace(/\s+/gu, " ").trim();
|
|
65
|
-
if (title)
|
|
66
|
-
return title;
|
|
67
|
-
}
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
70
|
-
function sessionStatus(events) {
|
|
71
|
-
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
72
|
-
const event = events[index];
|
|
73
|
-
if (event.type === "final")
|
|
74
|
-
return "idle";
|
|
75
|
-
if (event.type === "interaction.required")
|
|
76
|
-
return "waiting";
|
|
77
|
-
if (event.type === "session.run.started" &&
|
|
78
|
-
typeof event.payload.approved === "boolean")
|
|
79
|
-
return "idle";
|
|
80
|
-
}
|
|
81
|
-
return "idle";
|
|
82
|
-
}
|
|
83
|
-
function safe(value) {
|
|
84
|
-
if (value === "." || value === ".." || !/^[a-zA-Z0-9._-]+$/u.test(value)) {
|
|
85
|
-
throw new Error("Refusing a path-shaped ID.");
|
|
86
|
-
}
|
|
87
|
-
return value;
|
|
88
|
-
}
|
|
89
|
-
export function scrub(value, secrets) {
|
|
90
|
-
if (typeof value === "string")
|
|
91
|
-
return secrets.reduce((text, secret) => secret.length >= 8 ? text.split(secret).join("[redacted]") : text, value.replace(/data:image\/[a-z0-9.+-]+;base64,[A-Za-z0-9+/=]+/giu, "[inline image data redacted]"));
|
|
92
|
-
if (Array.isArray(value))
|
|
93
|
-
return value.map((item) => scrub(item, secrets));
|
|
94
|
-
if (value && typeof value === "object")
|
|
95
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
96
|
-
key,
|
|
97
|
-
/^(authorization|api[_-]?key|(?:access|refresh|id|auth)[_-]?token|token|secret|password|cookie|credentials?)$/iu.test(key)
|
|
98
|
-
? "[redacted]"
|
|
99
|
-
: scrub(item, secrets),
|
|
100
|
-
]));
|
|
101
|
-
return value;
|
|
102
|
-
}
|
|
103
|
-
export function localJsonl(options = {}) {
|
|
104
|
-
return new JsonlJournal(options.root ?? join(process.cwd(), ".data", "sessions"), options.secrets ?? []);
|
|
105
|
-
}
|
|
106
|
-
export function memoryHistory() {
|
|
107
|
-
const agents = new Map();
|
|
108
|
-
return {
|
|
109
|
-
async append(agentId, event) {
|
|
110
|
-
const sessions = agents.get(agentId) ?? new Map();
|
|
111
|
-
agents.set(agentId, sessions);
|
|
112
|
-
const events = sessions.get(event.session) ?? [];
|
|
113
|
-
events.push(event);
|
|
114
|
-
sessions.set(event.session, events);
|
|
115
|
-
},
|
|
116
|
-
async events(agentId, sessionId) {
|
|
117
|
-
return agents.get(agentId)?.get(sessionId) ?? [];
|
|
118
|
-
},
|
|
119
|
-
async list(agentId) {
|
|
120
|
-
return [...(agents.get(agentId) ?? [])]
|
|
121
|
-
.map(([session, events]) => ({
|
|
122
|
-
session,
|
|
123
|
-
title: sessionTitle(events),
|
|
124
|
-
status: sessionStatus(events),
|
|
125
|
-
startedAt: Date.parse(events[0].ts),
|
|
126
|
-
}))
|
|
127
|
-
.sort((a, b) => b.startedAt - a.startedAt);
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
}
|