@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,299 @@
|
|
|
1
|
+
/** Fetch-only adapter for OpenAI-compatible and Anthropic HTTP endpoints. */
|
|
2
|
+
export function httpModel(options = {}) {
|
|
3
|
+
return async (call, context) => {
|
|
4
|
+
const env = options.environment ??
|
|
5
|
+
(typeof process === "undefined" ? {} : process.env);
|
|
6
|
+
const provider = options.provider ?? env.MODEL_PROVIDER;
|
|
7
|
+
const model = call.model?.id ?? options.model ?? env.MODEL;
|
|
8
|
+
const baseUrl = options.baseUrl ?? env.MODEL_PROVIDER_BASE_URL;
|
|
9
|
+
if (!provider || !model)
|
|
10
|
+
throw new Error("Set both MODEL_PROVIDER and MODEL, or supply a model adapter.");
|
|
11
|
+
if (!["openai", "custom", "anthropic"].includes(provider))
|
|
12
|
+
throw new Error(`The portable HTTP adapter does not support '${provider}'. Supply onModelCall or use the Node piModel adapter.`);
|
|
13
|
+
if (provider === "custom" && !baseUrl)
|
|
14
|
+
throw new Error("MODEL_PROVIDER=custom requires MODEL_PROVIDER_BASE_URL.");
|
|
15
|
+
const apiKey = options.apiKey ??
|
|
16
|
+
env.MODEL_PROVIDER_API_KEY ??
|
|
17
|
+
(provider === "anthropic" ? env.ANTHROPIC_API_KEY : env.OPENAI_API_KEY);
|
|
18
|
+
if (!apiKey)
|
|
19
|
+
throw new Error("Set MODEL_PROVIDER_API_KEY or the provider-native API key variable.");
|
|
20
|
+
const text = (parts) => parts
|
|
21
|
+
.flatMap((part) => (part.type === "text" ? [part.text] : []))
|
|
22
|
+
.join("\n");
|
|
23
|
+
if (call.prompt.some((item) => item.content.some((part) => part.type === "media")))
|
|
24
|
+
throw new Error("Supply a media-aware model adapter for media inputs.");
|
|
25
|
+
context.signal.throwIfAborted();
|
|
26
|
+
let body;
|
|
27
|
+
let url;
|
|
28
|
+
let headers;
|
|
29
|
+
const streaming = provider !== "anthropic" && options.onPreview !== undefined;
|
|
30
|
+
if (provider === "anthropic") {
|
|
31
|
+
const messages = call.prompt
|
|
32
|
+
.filter((item) => item.kind !== "instructions")
|
|
33
|
+
.map((item) => {
|
|
34
|
+
if (item.kind === "tool-result")
|
|
35
|
+
return {
|
|
36
|
+
role: "user",
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: "tool_result",
|
|
40
|
+
tool_use_id: item.toolCallId,
|
|
41
|
+
content: text(item.content),
|
|
42
|
+
is_error: item.status !== "completed",
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
role: item.role === "assistant" ? "assistant" : "user",
|
|
48
|
+
content: item.content.flatMap((part) => part.type === "tool-call"
|
|
49
|
+
? [
|
|
50
|
+
{
|
|
51
|
+
type: "tool_use",
|
|
52
|
+
id: part.id,
|
|
53
|
+
name: part.name,
|
|
54
|
+
input: part.args,
|
|
55
|
+
},
|
|
56
|
+
]
|
|
57
|
+
: part.type === "text"
|
|
58
|
+
? [{ type: "text", text: part.text }]
|
|
59
|
+
: []),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
body = {
|
|
63
|
+
model,
|
|
64
|
+
messages,
|
|
65
|
+
max_tokens: call.model?.controls?.maxOutputTokens ?? 4096,
|
|
66
|
+
system: call.prompt
|
|
67
|
+
.filter((item) => item.kind === "instructions")
|
|
68
|
+
.map((item) => text(item.content))
|
|
69
|
+
.join("\n"),
|
|
70
|
+
...(call.tools.length
|
|
71
|
+
? {
|
|
72
|
+
tools: call.tools.map((tool) => ({
|
|
73
|
+
name: tool.name,
|
|
74
|
+
description: tool.description ?? "",
|
|
75
|
+
input_schema: tool.inputSchema,
|
|
76
|
+
})),
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
79
|
+
...(call.outputSchema
|
|
80
|
+
? {
|
|
81
|
+
output_config: {
|
|
82
|
+
format: { type: "json_schema", schema: call.outputSchema },
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
: {}),
|
|
86
|
+
...(call.model?.controls?.temperature === undefined
|
|
87
|
+
? {}
|
|
88
|
+
: { temperature: call.model.controls.temperature }),
|
|
89
|
+
};
|
|
90
|
+
url = `${(baseUrl ?? "https://api.anthropic.com").replace(/\/$/, "")}/v1/messages`;
|
|
91
|
+
headers = {
|
|
92
|
+
"content-type": "application/json",
|
|
93
|
+
"x-api-key": apiKey,
|
|
94
|
+
"anthropic-version": "2023-06-01",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const messages = call.prompt.map((item) => {
|
|
99
|
+
if (item.kind === "tool-result")
|
|
100
|
+
return {
|
|
101
|
+
role: "tool",
|
|
102
|
+
tool_call_id: item.toolCallId,
|
|
103
|
+
content: text(item.content),
|
|
104
|
+
};
|
|
105
|
+
const calls = item.content
|
|
106
|
+
.filter((part) => part.type === "tool-call")
|
|
107
|
+
.map((part) => ({
|
|
108
|
+
id: part.id,
|
|
109
|
+
type: "function",
|
|
110
|
+
function: { name: part.name, arguments: JSON.stringify(part.args) },
|
|
111
|
+
}));
|
|
112
|
+
return {
|
|
113
|
+
role: item.kind === "instructions" ? "system" : item.role,
|
|
114
|
+
content: text(item.content),
|
|
115
|
+
...(calls.length ? { tool_calls: calls } : {}),
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
body = {
|
|
119
|
+
model,
|
|
120
|
+
messages,
|
|
121
|
+
stream: streaming,
|
|
122
|
+
...(call.tools.length
|
|
123
|
+
? {
|
|
124
|
+
tools: call.tools.map((tool) => ({
|
|
125
|
+
type: "function",
|
|
126
|
+
function: {
|
|
127
|
+
name: tool.name,
|
|
128
|
+
description: tool.description,
|
|
129
|
+
parameters: tool.inputSchema,
|
|
130
|
+
},
|
|
131
|
+
})),
|
|
132
|
+
}
|
|
133
|
+
: {}),
|
|
134
|
+
...(call.outputSchema
|
|
135
|
+
? {
|
|
136
|
+
response_format: {
|
|
137
|
+
type: "json_schema",
|
|
138
|
+
json_schema: {
|
|
139
|
+
name: "agent_output",
|
|
140
|
+
strict: true,
|
|
141
|
+
schema: call.outputSchema,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
: {}),
|
|
146
|
+
...(call.model?.controls?.temperature === undefined
|
|
147
|
+
? {}
|
|
148
|
+
: { temperature: call.model.controls.temperature }),
|
|
149
|
+
...(call.model?.controls?.maxOutputTokens === undefined
|
|
150
|
+
? {}
|
|
151
|
+
: { max_completion_tokens: call.model.controls.maxOutputTokens }),
|
|
152
|
+
};
|
|
153
|
+
url = `${(baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "")}/chat/completions`;
|
|
154
|
+
headers = {
|
|
155
|
+
"content-type": "application/json",
|
|
156
|
+
authorization: `Bearer ${apiKey}`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (!["https:", "http:"].includes(new URL(url).protocol))
|
|
160
|
+
throw new Error("Model endpoint must use HTTP(S).");
|
|
161
|
+
context.reportPreparedCall?.({
|
|
162
|
+
adapter: "runtime.http",
|
|
163
|
+
call: JSON.parse(JSON.stringify(body)),
|
|
164
|
+
});
|
|
165
|
+
const response = await (options.fetch ?? fetch)(url, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers,
|
|
168
|
+
body: JSON.stringify(body),
|
|
169
|
+
signal: context.signal,
|
|
170
|
+
});
|
|
171
|
+
if (!response.ok)
|
|
172
|
+
throw new Error(`Model provider returned HTTP ${response.status}. Check the selected model, credentials, and endpoint.`);
|
|
173
|
+
let result;
|
|
174
|
+
if (streaming)
|
|
175
|
+
result = await readOpenAIStream(response, (value) => {
|
|
176
|
+
try {
|
|
177
|
+
void Promise.resolve(options.onPreview?.({
|
|
178
|
+
invocationId: context.invocationId,
|
|
179
|
+
text: value,
|
|
180
|
+
})).catch(() => { });
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
/* Preview delivery cannot affect generation. */
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
else
|
|
187
|
+
result = await response.json();
|
|
188
|
+
context.signal.throwIfAborted();
|
|
189
|
+
const output = [];
|
|
190
|
+
if (provider === "anthropic") {
|
|
191
|
+
for (const part of result.content ?? []) {
|
|
192
|
+
if (part.type === "text")
|
|
193
|
+
output.push(call.outputSchema &&
|
|
194
|
+
!(result.content ?? []).some((item) => item.type === "tool_use")
|
|
195
|
+
? { type: "json", value: JSON.parse(part.text) }
|
|
196
|
+
: { type: "text", text: part.text });
|
|
197
|
+
if (part.type === "tool_use")
|
|
198
|
+
output.push({
|
|
199
|
+
type: "tool-call",
|
|
200
|
+
id: part.id,
|
|
201
|
+
name: part.name,
|
|
202
|
+
args: part.input,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const message = result.choices?.[0]?.message;
|
|
208
|
+
if (!message)
|
|
209
|
+
throw new Error("Model provider returned no message.");
|
|
210
|
+
if (message.content)
|
|
211
|
+
output.push(call.outputSchema && !message.tool_calls?.length
|
|
212
|
+
? { type: "json", value: JSON.parse(message.content) }
|
|
213
|
+
: { type: "text", text: message.content });
|
|
214
|
+
for (const tool of message.tool_calls ?? [])
|
|
215
|
+
output.push({
|
|
216
|
+
type: "tool-call",
|
|
217
|
+
id: tool.id,
|
|
218
|
+
name: tool.function.name,
|
|
219
|
+
args: JSON.parse(tool.function.arguments),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return { output, evidence: { resolvedModel: result.model ?? model } };
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
async function readOpenAIStream(response, preview) {
|
|
226
|
+
if (!response.body)
|
|
227
|
+
throw new Error("Model provider returned no stream.");
|
|
228
|
+
const reader = response.body.getReader();
|
|
229
|
+
const decoder = new TextDecoder();
|
|
230
|
+
let buffer = "", content = "", complete = false;
|
|
231
|
+
const calls = new Map();
|
|
232
|
+
const line = (value) => {
|
|
233
|
+
if (!value.startsWith("data:"))
|
|
234
|
+
return;
|
|
235
|
+
const data = value.slice(5).trim();
|
|
236
|
+
if (data === "[DONE]") {
|
|
237
|
+
complete = true;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (!data)
|
|
241
|
+
return;
|
|
242
|
+
const chunk = JSON.parse(data);
|
|
243
|
+
if (chunk.error)
|
|
244
|
+
throw new Error("Model provider reported a streaming error.");
|
|
245
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
246
|
+
if (typeof delta?.content === "string") {
|
|
247
|
+
content += delta.content;
|
|
248
|
+
preview(delta.content);
|
|
249
|
+
}
|
|
250
|
+
for (const tool of delta?.tool_calls ?? []) {
|
|
251
|
+
const target = calls.get(tool.index) ?? {
|
|
252
|
+
id: "",
|
|
253
|
+
type: "function",
|
|
254
|
+
function: { name: "", arguments: "" },
|
|
255
|
+
};
|
|
256
|
+
if (tool.id)
|
|
257
|
+
target.id = tool.id;
|
|
258
|
+
if (tool.function?.name)
|
|
259
|
+
target.function.name += tool.function.name;
|
|
260
|
+
if (tool.function?.arguments)
|
|
261
|
+
target.function.arguments += tool.function.arguments;
|
|
262
|
+
calls.set(tool.index, target);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
try {
|
|
266
|
+
while (!complete) {
|
|
267
|
+
const next = await reader.read();
|
|
268
|
+
buffer += decoder.decode(next.value, { stream: !next.done });
|
|
269
|
+
let end;
|
|
270
|
+
while ((end = buffer.indexOf("\n")) >= 0) {
|
|
271
|
+
line(buffer.slice(0, end).replace(/\r$/, ""));
|
|
272
|
+
buffer = buffer.slice(end + 1);
|
|
273
|
+
}
|
|
274
|
+
if (next.done) {
|
|
275
|
+
if (buffer)
|
|
276
|
+
line(buffer);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (!complete)
|
|
281
|
+
throw new Error("Model stream ended before completion.");
|
|
282
|
+
return {
|
|
283
|
+
choices: [
|
|
284
|
+
{
|
|
285
|
+
message: {
|
|
286
|
+
content,
|
|
287
|
+
tool_calls: [...calls]
|
|
288
|
+
.sort(([a], [b]) => a - b)
|
|
289
|
+
.map(([, value]) => value),
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
await reader.cancel().catch(() => { });
|
|
297
|
+
reader.releaseLock();
|
|
298
|
+
}
|
|
299
|
+
}
|
package/dist/model/pi-model.d.ts
CHANGED
|
@@ -3,8 +3,9 @@ import type { RuntimeMedia } from "../adapters/media.js";
|
|
|
3
3
|
import { type Selection } from "./models.js";
|
|
4
4
|
export interface PiModelOptions {
|
|
5
5
|
readonly root?: string;
|
|
6
|
+
readonly onPreview?: (preview: import("./defaults.js").ModelPreview) => void;
|
|
6
7
|
readonly selection?: Selection;
|
|
7
8
|
readonly media?: Pick<RuntimeMedia, "dataUrl">;
|
|
8
9
|
}
|
|
9
|
-
/**
|
|
10
|
+
/** Node model adapter. Local provider configuration is read only when invoked. */
|
|
10
11
|
export declare function piModel(options?: PiModelOptions): RuntimeModelAdapter;
|
package/dist/model/pi-model.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { scrub } from "../
|
|
2
|
+
import { scrub } from "../redact.js";
|
|
3
3
|
import { ProjectCredentialStore } from "./auth-store.js";
|
|
4
4
|
import { modelsFor } from "./models.js";
|
|
5
5
|
import { modelSelection, projectSecrets } from "./settings.js";
|
|
@@ -11,12 +11,19 @@ const emptyUsage = () => ({
|
|
|
11
11
|
cacheWrite: 0,
|
|
12
12
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
13
13
|
});
|
|
14
|
-
/**
|
|
14
|
+
/** Node model adapter. Local provider configuration is read only when invoked. */
|
|
15
15
|
export function piModel(options = {}) {
|
|
16
16
|
return async (call, context) => {
|
|
17
17
|
context.signal.throwIfAborted();
|
|
18
18
|
const root = options.root ?? process.cwd();
|
|
19
|
-
const
|
|
19
|
+
const configured = options.selection ?? modelSelection(root);
|
|
20
|
+
const requested = call.model?.id;
|
|
21
|
+
const selection = {
|
|
22
|
+
...configured,
|
|
23
|
+
model: requested?.startsWith(`${configured.provider}/`)
|
|
24
|
+
? requested.slice(configured.provider.length + 1)
|
|
25
|
+
: (requested ?? configured.model),
|
|
26
|
+
};
|
|
20
27
|
const registry = modelsFor(selection, new ProjectCredentialStore(join(root, ".nylorun", "auth.json"), join(root, ".env", "auth.json")));
|
|
21
28
|
const selected = registry.getModel(selection.provider, selection.model);
|
|
22
29
|
if (!selected)
|
|
@@ -65,7 +72,9 @@ export function piModel(options = {}) {
|
|
|
65
72
|
typeof ref.agentId !== "string" ||
|
|
66
73
|
typeof ref.assetId !== "string")
|
|
67
74
|
throw new Error("Expected a local media reference.");
|
|
68
|
-
const asset = await options.media?.dataUrl({ agentId: ref.agentId, assetId: ref.assetId },
|
|
75
|
+
const asset = await options.media?.dataUrl({ agentId: ref.agentId, assetId: ref.assetId }, "sessionId" in ref && typeof ref.sessionId === "string"
|
|
76
|
+
? ref.sessionId
|
|
77
|
+
: call.executionId);
|
|
69
78
|
if (!asset)
|
|
70
79
|
throw new Error("Media is unavailable; pass the shared media adapter to piModel({ media }).");
|
|
71
80
|
const comma = asset.url.indexOf(",");
|
|
@@ -156,7 +165,14 @@ export function piModel(options = {}) {
|
|
|
156
165
|
parameters: { ...tool.inputSchema },
|
|
157
166
|
}));
|
|
158
167
|
const request = {
|
|
159
|
-
systemPrompt:
|
|
168
|
+
systemPrompt: [
|
|
169
|
+
...instructions,
|
|
170
|
+
...(call.outputSchema
|
|
171
|
+
? [
|
|
172
|
+
`Return the final answer as JSON matching this schema: ${JSON.stringify(call.outputSchema)}. Use tools when needed before returning the final JSON.`,
|
|
173
|
+
]
|
|
174
|
+
: []),
|
|
175
|
+
].join("\n"),
|
|
160
176
|
messages,
|
|
161
177
|
tools,
|
|
162
178
|
};
|
|
@@ -167,14 +183,34 @@ export function piModel(options = {}) {
|
|
|
167
183
|
call: scrub(call, secrets),
|
|
168
184
|
});
|
|
169
185
|
try {
|
|
170
|
-
const
|
|
186
|
+
const invocationOptions = {
|
|
171
187
|
signal: context.signal,
|
|
172
188
|
temperature: call.model?.controls?.temperature,
|
|
173
189
|
maxTokens: call.model?.controls?.maxOutputTokens,
|
|
174
190
|
...(call.model?.config
|
|
175
191
|
? { samplingParams: { ...call.model.config } }
|
|
176
192
|
: {}),
|
|
177
|
-
}
|
|
193
|
+
};
|
|
194
|
+
let response;
|
|
195
|
+
if (options.onPreview) {
|
|
196
|
+
const stream = registry.streamSimple(selected, request, invocationOptions);
|
|
197
|
+
for await (const event of stream) {
|
|
198
|
+
if (event.type === "text_delta") {
|
|
199
|
+
try {
|
|
200
|
+
void Promise.resolve(options.onPreview({
|
|
201
|
+
invocationId: context.invocationId,
|
|
202
|
+
text: event.delta,
|
|
203
|
+
})).catch(() => { });
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
/* Preview delivery is independent. */
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
response = await stream.result();
|
|
211
|
+
}
|
|
212
|
+
else
|
|
213
|
+
response = await registry.complete(selected, request, invocationOptions);
|
|
178
214
|
context.signal.throwIfAborted();
|
|
179
215
|
if (response.stopReason === "error" ||
|
|
180
216
|
response.stopReason === "aborted" ||
|
|
@@ -203,6 +239,15 @@ export function piModel(options = {}) {
|
|
|
203
239
|
...metadataFor(part.thoughtSignature),
|
|
204
240
|
});
|
|
205
241
|
}
|
|
242
|
+
if (call.outputSchema &&
|
|
243
|
+
!output.some((part) => part.type === "tool-call")) {
|
|
244
|
+
const text = output
|
|
245
|
+
.filter((part) => part.type === "text")
|
|
246
|
+
.map((part) => part.text)
|
|
247
|
+
.join("");
|
|
248
|
+
const value = JSON.parse(text);
|
|
249
|
+
output.splice(0, output.length, { type: "json", value });
|
|
250
|
+
}
|
|
206
251
|
return {
|
|
207
252
|
output,
|
|
208
253
|
finishReason: response.stopReason === "toolUse"
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { localSessions } from "./local-sessions.js";
|
|
2
|
+
export { localMedia, MediaStore } from "../adapters/media.js";
|
|
3
|
+
export { jsonlObserver } from "../adapters/observe.js";
|
|
4
|
+
export { piModel, type PiModelOptions } from "../model/pi-model.js";
|
|
5
|
+
export { projectAsset } from "../assets.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { localSessions } from "./local-sessions.js";
|
|
2
|
+
export { localMedia, MediaStore } from "../adapters/media.js";
|
|
3
|
+
export { jsonlObserver } from "../adapters/observe.js";
|
|
4
|
+
export { piModel } from "../model/pi-model.js";
|
|
5
|
+
export { projectAsset } from "../assets.js";
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { mkdir, open, readFile, readdir, rename, rm, realpath, } from "node:fs/promises";
|
|
2
|
+
import { resolve, join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { sessionSummary, } from "../sessions/store.js";
|
|
5
|
+
/** Single-owner local disk store. No lease stealing, replay, or shared-filesystem guarantee. */
|
|
6
|
+
export function localSessions(options) {
|
|
7
|
+
let closed = false;
|
|
8
|
+
let directory;
|
|
9
|
+
const token = randomUUID();
|
|
10
|
+
const acquired = (async () => {
|
|
11
|
+
await mkdir(resolve(options.root), { recursive: true, mode: 0o700 });
|
|
12
|
+
directory = await realpath(resolve(options.root));
|
|
13
|
+
const path = join(directory, ".owner.lock");
|
|
14
|
+
let handle;
|
|
15
|
+
try {
|
|
16
|
+
handle = await open(path, "wx", 0o600);
|
|
17
|
+
}
|
|
18
|
+
catch (cause) {
|
|
19
|
+
throw new Error(`Session store ${directory} already has an owner. Stop the owner before opening it again. After a crash, confirm it has stopped and manually remove .owner.lock.`, { cause });
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
await handle.writeFile(JSON.stringify({ token, pid: process.pid }));
|
|
23
|
+
await handle.sync();
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
await handle.close();
|
|
27
|
+
}
|
|
28
|
+
})();
|
|
29
|
+
void acquired.catch(() => { });
|
|
30
|
+
let operations = Promise.resolve();
|
|
31
|
+
const check = async () => {
|
|
32
|
+
await acquired;
|
|
33
|
+
if (closed)
|
|
34
|
+
throw new Error("Session store is closed");
|
|
35
|
+
const owner = JSON.parse(await readFile(join(directory, ".owner.lock"), "utf8"));
|
|
36
|
+
if (owner.token !== token)
|
|
37
|
+
throw new Error("Session store ownership was lost; refusing further writes");
|
|
38
|
+
};
|
|
39
|
+
const pathFor = (agentId, sessionId) => join(directory, safe(agentId), `${safe(sessionId)}.json`);
|
|
40
|
+
const get = async (agentId, sessionId) => {
|
|
41
|
+
await check();
|
|
42
|
+
try {
|
|
43
|
+
const value = JSON.parse(await readFile(pathFor(agentId, sessionId), "utf8"));
|
|
44
|
+
if (value.version !== 1 ||
|
|
45
|
+
value.id !== sessionId ||
|
|
46
|
+
value.agentId !== agentId ||
|
|
47
|
+
!Array.isArray(value.events))
|
|
48
|
+
throw new Error("Invalid stored session document");
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (!isMissing(error))
|
|
53
|
+
throw error;
|
|
54
|
+
try {
|
|
55
|
+
const contents = await readFile(join(directory, safe(agentId), safe(sessionId), "events.jsonl"), "utf8");
|
|
56
|
+
const events = contents
|
|
57
|
+
.split("\n")
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.map((line) => JSON.parse(line));
|
|
60
|
+
return {
|
|
61
|
+
version: 1,
|
|
62
|
+
id: sessionId,
|
|
63
|
+
agentId,
|
|
64
|
+
status: "archived",
|
|
65
|
+
startedAt: events[0] ? Date.parse(events[0].ts) : 0,
|
|
66
|
+
updatedAt: 0,
|
|
67
|
+
events,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (legacyError) {
|
|
71
|
+
if (isMissing(legacyError))
|
|
72
|
+
return undefined;
|
|
73
|
+
throw legacyError;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
get,
|
|
79
|
+
put(agentId, sessionId, session) {
|
|
80
|
+
const contents = JSON.stringify(session);
|
|
81
|
+
const operation = operations.then(async () => {
|
|
82
|
+
await check();
|
|
83
|
+
if (session.id !== sessionId || session.agentId !== agentId)
|
|
84
|
+
throw new Error("Session identity mismatch");
|
|
85
|
+
const target = pathFor(agentId, sessionId);
|
|
86
|
+
await mkdir(join(directory, safe(agentId)), {
|
|
87
|
+
recursive: true,
|
|
88
|
+
mode: 0o700,
|
|
89
|
+
});
|
|
90
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
91
|
+
try {
|
|
92
|
+
const file = await open(temporary, "wx", 0o600);
|
|
93
|
+
try {
|
|
94
|
+
await file.writeFile(contents);
|
|
95
|
+
await file.sync();
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
await file.close();
|
|
99
|
+
}
|
|
100
|
+
await check();
|
|
101
|
+
await rename(temporary, target);
|
|
102
|
+
const parent = await open(join(directory, safe(agentId)), "r");
|
|
103
|
+
try {
|
|
104
|
+
await parent.sync();
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await parent.close();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
await rm(temporary, { force: true });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
operations = operation.catch(() => { });
|
|
115
|
+
return operation;
|
|
116
|
+
},
|
|
117
|
+
async list(agentId) {
|
|
118
|
+
await check();
|
|
119
|
+
let names;
|
|
120
|
+
try {
|
|
121
|
+
names = await readdir(join(directory, safe(agentId)), {
|
|
122
|
+
withFileTypes: true,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (isMissing(error))
|
|
127
|
+
return [];
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
const sessions = await Promise.all(names
|
|
131
|
+
.filter((item) => item.isDirectory() || item.name.endsWith(".json"))
|
|
132
|
+
.map((item) => get(agentId, item.isDirectory() ? item.name : item.name.slice(0, -5))));
|
|
133
|
+
return sessions
|
|
134
|
+
.flatMap((session) => (session ? [sessionSummary(session)] : []))
|
|
135
|
+
.sort((a, b) => b.startedAt - a.startedAt);
|
|
136
|
+
},
|
|
137
|
+
async close() {
|
|
138
|
+
await operations;
|
|
139
|
+
if (closed)
|
|
140
|
+
return;
|
|
141
|
+
await check();
|
|
142
|
+
closed = true;
|
|
143
|
+
await rm(join(directory, ".owner.lock"));
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function safe(value) {
|
|
148
|
+
if (value === "." || value === ".." || !/^[a-zA-Z0-9._-]+$/.test(value))
|
|
149
|
+
throw new Error("Invalid session path identifier");
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
function isMissing(error) {
|
|
153
|
+
return (!!error &&
|
|
154
|
+
typeof error === "object" &&
|
|
155
|
+
"code" in error &&
|
|
156
|
+
error.code === "ENOENT");
|
|
157
|
+
}
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function scrub(value: unknown, secrets: readonly string[]): unknown;
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function scrub(value, secrets) {
|
|
2
|
+
if (typeof value === "string")
|
|
3
|
+
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]"));
|
|
4
|
+
if (Array.isArray(value))
|
|
5
|
+
return value.map((item) => scrub(item, secrets));
|
|
6
|
+
if (value && typeof value === "object")
|
|
7
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
8
|
+
key,
|
|
9
|
+
/^(authorization|api[_-]?key|(?:access|refresh|id|auth)[_-]?token|token|secret|password|cookie|credentials?)$/iu.test(key)
|
|
10
|
+
? "[redacted]"
|
|
11
|
+
: scrub(item, secrets),
|
|
12
|
+
]));
|
|
13
|
+
return value;
|
|
14
|
+
}
|
package/dist/server/ag-ui.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CanonicalEvent } from "../
|
|
1
|
+
import type { CanonicalEvent } from "../sessions/store.js";
|
|
2
2
|
/** Minimal truthful AG-UI projection from the same canonical events Studio displays. */
|
|
3
3
|
export declare function agUiEvents(events: readonly CanonicalEvent[], threadId: string, runId: string): readonly Record<string, unknown>[];
|
|
4
4
|
/**
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Bounded per-request delivery. Generation never waits for a subscriber. */
|
|
2
|
+
export declare class EventDelivery {
|
|
3
|
+
private readonly abort;
|
|
4
|
+
private readonly limits;
|
|
5
|
+
readonly response: Response;
|
|
6
|
+
private controller;
|
|
7
|
+
private readonly queue;
|
|
8
|
+
private readonly suppressed;
|
|
9
|
+
private previewBytes;
|
|
10
|
+
private readonly previewTotals;
|
|
11
|
+
private eventBytes;
|
|
12
|
+
private eventCount;
|
|
13
|
+
private ended;
|
|
14
|
+
private closed;
|
|
15
|
+
constructor(abort: () => void, limits?: {
|
|
16
|
+
previewBytes?: number;
|
|
17
|
+
eventBytes?: number;
|
|
18
|
+
eventCount?: number;
|
|
19
|
+
}, inherited?: HeadersInit);
|
|
20
|
+
private waiting;
|
|
21
|
+
private flush;
|
|
22
|
+
push(event: Record<string, unknown>, preview?: string): void;
|
|
23
|
+
end(): void;
|
|
24
|
+
}
|