@nylorun/runtime 0.1.1-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 +29 -0
- package/LICENSE +192 -0
- package/README.md +44 -0
- package/dist/adapters/journal.d.ts +35 -0
- package/dist/adapters/journal.js +130 -0
- package/dist/adapters/media.d.ts +40 -0
- package/dist/adapters/media.js +161 -0
- package/dist/assets.d.ts +2 -0
- package/dist/assets.js +10 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +319 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +15 -0
- package/dist/contracts.d.ts +147 -0
- package/dist/contracts.js +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +6 -0
- package/dist/model/auth-store.d.ts +10 -0
- package/dist/model/auth-store.js +85 -0
- package/dist/model/configure.d.ts +12 -0
- package/dist/model/configure.js +115 -0
- package/dist/model/models.d.ts +9 -0
- package/dist/model/models.js +31 -0
- package/dist/model/pi-model.d.ts +10 -0
- package/dist/model/pi-model.js +166 -0
- package/dist/model/settings.d.ts +3 -0
- package/dist/model/settings.js +35 -0
- package/dist/server/ag-ui.d.ts +8 -0
- package/dist/server/ag-ui.js +67 -0
- package/dist/server/digests.d.ts +11 -0
- package/dist/server/digests.js +39 -0
- package/dist/server/host.d.ts +14 -0
- package/dist/server/host.js +589 -0
- package/package.json +53 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { loadEnvFile } from "node:process";
|
|
4
|
+
import { cp, mkdir } from "node:fs/promises";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { resolve, join, relative, isAbsolute, sep } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { serve } from "@hono/node-server";
|
|
9
|
+
import { build, createServer } from "vite";
|
|
10
|
+
import { defineRuntime } from "./config.js";
|
|
11
|
+
import { allowedHost, createRuntime, loopbackHosts } from "./server/host.js";
|
|
12
|
+
import { configureProvider, ConfigurationCancelled, } from "./model/configure.js";
|
|
13
|
+
import { modelSelection } from "./model/settings.js";
|
|
14
|
+
import { modelsFor } from "./model/models.js";
|
|
15
|
+
import { ProjectCredentialStore } from "./model/auth-store.js";
|
|
16
|
+
const usage = `nylorun <dev|studio|configure|inspect|build|start>
|
|
17
|
+
dev [--no-studio] [--no-open] [--port <n>] [--host <address>] [--allowed-hosts <list>]
|
|
18
|
+
studio --agent-url <http(s)-url> [--port <n>] [--no-open]
|
|
19
|
+
start [--port <n>] [--host <address>] [--allowed-hosts <list>]
|
|
20
|
+
Run from the directory containing nylorun.config.ts.
|
|
21
|
+
PORT, HOST and ALLOWED_HOSTS environment variables supply the same settings.`;
|
|
22
|
+
async function studio(agentServerUrl, open, port) {
|
|
23
|
+
let entry;
|
|
24
|
+
try {
|
|
25
|
+
entry = createRequire(join(process.cwd(), "package.json")).resolve("@nylorun/studio");
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error("Install @nylorun/studio in this project, or run nylorun dev --no-studio.");
|
|
29
|
+
}
|
|
30
|
+
const module = await import(pathToFileURL(entry).href);
|
|
31
|
+
return module.startStudio({
|
|
32
|
+
agentServerUrl,
|
|
33
|
+
open,
|
|
34
|
+
...(port === undefined ? {} : { port }),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function port(value, fallback) {
|
|
38
|
+
const result = value === undefined ? fallback : Number(value);
|
|
39
|
+
if (!Number.isInteger(result) || result < 1 || result > 65535)
|
|
40
|
+
throw new Error("Port must be an integer between 1 and 65535.");
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Loopback binds answer only to their own address on the chosen port, so
|
|
45
|
+
* development needs no setup. A published bind or an explicit allowed-host
|
|
46
|
+
* list is the deployment decision; "*" accepts any Host header.
|
|
47
|
+
*/
|
|
48
|
+
function bindAddress(host, allowed, hostPort) {
|
|
49
|
+
const hostname = (host?.trim() || "127.0.0.1").replace(/^\[(.*)\]$/u, "$1");
|
|
50
|
+
const loopback = ["127.0.0.1", "localhost", "::1"].includes(hostname);
|
|
51
|
+
const configured = (allowed ?? "")
|
|
52
|
+
.split(",")
|
|
53
|
+
.map((entry) => entry.trim())
|
|
54
|
+
.filter(Boolean);
|
|
55
|
+
const hosts = configured.length
|
|
56
|
+
? [...(loopback ? loopbackHosts(hostPort) : []), ...configured]
|
|
57
|
+
: loopback
|
|
58
|
+
? loopbackHosts(hostPort)
|
|
59
|
+
: ["*"];
|
|
60
|
+
const unspecified = ["0.0.0.0", "::"].includes(hostname);
|
|
61
|
+
const reachable = unspecified
|
|
62
|
+
? "127.0.0.1"
|
|
63
|
+
: hostname.includes(":")
|
|
64
|
+
? `[${hostname}]`
|
|
65
|
+
: hostname;
|
|
66
|
+
return { hostname, reachable, loopback, hosts };
|
|
67
|
+
}
|
|
68
|
+
async function sourceLoader() {
|
|
69
|
+
const vite = await createServer({
|
|
70
|
+
configFile: false,
|
|
71
|
+
appType: "custom",
|
|
72
|
+
optimizeDeps: { noDiscovery: true, include: [] },
|
|
73
|
+
server: { middlewareMode: true, hmr: false, ws: false },
|
|
74
|
+
ssr: { external: true },
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
vite,
|
|
78
|
+
async load() {
|
|
79
|
+
const module = await vite.ssrLoadModule("/nylorun.config.ts");
|
|
80
|
+
return defineRuntime(module.default);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function main() {
|
|
85
|
+
const [command, ...args] = process.argv.slice(2);
|
|
86
|
+
if (!command || command === "--help" || command === "-h") {
|
|
87
|
+
console.log(usage);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const allowed = {
|
|
91
|
+
dev: ["--no-studio", "--no-open", "--port", "--host", "--allowed-hosts"],
|
|
92
|
+
studio: ["--agent-url", "--port", "--no-open"],
|
|
93
|
+
start: ["--port", "--host", "--allowed-hosts"],
|
|
94
|
+
configure: [],
|
|
95
|
+
inspect: [],
|
|
96
|
+
build: [],
|
|
97
|
+
};
|
|
98
|
+
if (!(command in allowed))
|
|
99
|
+
throw new Error(usage);
|
|
100
|
+
const flags = new Map();
|
|
101
|
+
for (let i = 0; i < args.length; i++) {
|
|
102
|
+
const arg = args[i];
|
|
103
|
+
if (!allowed[command].includes(arg) || flags.has(arg))
|
|
104
|
+
throw new Error(`Invalid option ${arg}\n${usage}`);
|
|
105
|
+
if (["--port", "--agent-url", "--host", "--allowed-hosts"].includes(arg)) {
|
|
106
|
+
const value = args[++i];
|
|
107
|
+
if (!value || value.startsWith("--"))
|
|
108
|
+
throw new Error(`${arg} requires a value.`);
|
|
109
|
+
flags.set(arg, value);
|
|
110
|
+
}
|
|
111
|
+
else
|
|
112
|
+
flags.set(arg, true);
|
|
113
|
+
}
|
|
114
|
+
const requestedPort = flags.get("--port");
|
|
115
|
+
if (command === "configure") {
|
|
116
|
+
const controller = new AbortController();
|
|
117
|
+
const onInt = () => controller.abort(new ConfigurationCancelled("SIGINT"));
|
|
118
|
+
const onTerm = () => controller.abort(new ConfigurationCancelled("SIGTERM"));
|
|
119
|
+
process.on("SIGINT", onInt);
|
|
120
|
+
process.on("SIGTERM", onTerm);
|
|
121
|
+
try {
|
|
122
|
+
const integrations = join(process.cwd(), ".env", "integrations.env");
|
|
123
|
+
if (existsSync(integrations))
|
|
124
|
+
loadEnvFile(integrations);
|
|
125
|
+
await configureProvider({ signal: controller.signal });
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
process.removeListener("SIGINT", onInt);
|
|
129
|
+
process.removeListener("SIGTERM", onTerm);
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
let stopping = false;
|
|
134
|
+
const shutdown = [];
|
|
135
|
+
const stop = async () => {
|
|
136
|
+
if (stopping)
|
|
137
|
+
return;
|
|
138
|
+
stopping = true;
|
|
139
|
+
const results = await Promise.allSettled(shutdown.map((close) => close()));
|
|
140
|
+
if (results.some((result) => result.status === "rejected"))
|
|
141
|
+
process.exitCode = 1;
|
|
142
|
+
};
|
|
143
|
+
process.once("SIGINT", () => void stop());
|
|
144
|
+
process.once("SIGTERM", () => void stop());
|
|
145
|
+
try {
|
|
146
|
+
const integrations = join(process.cwd(), ".env", "integrations.env");
|
|
147
|
+
if (existsSync(integrations))
|
|
148
|
+
loadEnvFile(integrations);
|
|
149
|
+
if (command === "studio") {
|
|
150
|
+
const url = flags.get("--agent-url");
|
|
151
|
+
if (typeof url !== "string")
|
|
152
|
+
throw new Error("--agent-url is required.");
|
|
153
|
+
const dashboard = await studio(url, !flags.has("--no-open"), requestedPort ? port(requestedPort, 0) : undefined);
|
|
154
|
+
shutdown.push(() => dashboard.close());
|
|
155
|
+
console.log(`Studio on ${dashboard.address}`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (command === "build") {
|
|
159
|
+
await build({
|
|
160
|
+
configFile: false,
|
|
161
|
+
build: {
|
|
162
|
+
target: "node22",
|
|
163
|
+
ssr: "nylorun.config.ts",
|
|
164
|
+
outDir: "dist",
|
|
165
|
+
rollupOptions: { output: { entryFileNames: "nylorun.config.js" } },
|
|
166
|
+
},
|
|
167
|
+
ssr: { external: true },
|
|
168
|
+
});
|
|
169
|
+
if (existsSync("agent")) {
|
|
170
|
+
await mkdir("dist/agent", { recursive: true });
|
|
171
|
+
await cp("agent", "dist/agent", {
|
|
172
|
+
recursive: true,
|
|
173
|
+
filter: (source) => !source.split(/[\\/]/).includes("node_modules"),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
let loader;
|
|
179
|
+
let config;
|
|
180
|
+
if (command === "start")
|
|
181
|
+
config = defineRuntime((await import(pathToFileURL(resolve("dist/nylorun.config.js")).href))
|
|
182
|
+
.default);
|
|
183
|
+
else {
|
|
184
|
+
loader = await sourceLoader();
|
|
185
|
+
shutdown.push(() => loader.vite.close());
|
|
186
|
+
config = await loader.load();
|
|
187
|
+
}
|
|
188
|
+
if (command === "inspect") {
|
|
189
|
+
let setup = "required";
|
|
190
|
+
try {
|
|
191
|
+
const selected = modelSelection();
|
|
192
|
+
setup = (await modelsFor(selected, new ProjectCredentialStore()).checkAuth(selected.provider))
|
|
193
|
+
? "ready"
|
|
194
|
+
: "required";
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
/* Unconfigured is a valid inspection state. */
|
|
198
|
+
}
|
|
199
|
+
console.log(JSON.stringify({ agents: config.agents.map((agent) => agent.manifest), setup }, null, 2));
|
|
200
|
+
await stop();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
let current = await createRuntime(config);
|
|
204
|
+
const retained = [current];
|
|
205
|
+
shutdown.push(async () => {
|
|
206
|
+
await Promise.all(retained.map((runtime) => runtime.close()));
|
|
207
|
+
});
|
|
208
|
+
const fetch = async (request) => {
|
|
209
|
+
const path = new URL(request.url).pathname;
|
|
210
|
+
const collection = path.match(/^\/agents\/([^/]+)\/v1\/sessions$/);
|
|
211
|
+
if (request.method === "GET" && collection && retained.length > 1) {
|
|
212
|
+
const response = await current.app.fetch(request);
|
|
213
|
+
if (!response.ok)
|
|
214
|
+
return response;
|
|
215
|
+
const agentId = collection[1];
|
|
216
|
+
const document = (await response.json());
|
|
217
|
+
const summaries = new Map(document.sessions.map((item) => [item.session, item]));
|
|
218
|
+
// A journal may be shared across reloads, but only the owning runtime
|
|
219
|
+
// knows whether a retained session is still running or waiting.
|
|
220
|
+
for (const runtime of retained) {
|
|
221
|
+
if (runtime === current)
|
|
222
|
+
continue;
|
|
223
|
+
const previous = await runtime.app.fetch(request.clone());
|
|
224
|
+
if (!previous.ok)
|
|
225
|
+
continue;
|
|
226
|
+
const history = (await previous.json());
|
|
227
|
+
for (const summary of history.sessions) {
|
|
228
|
+
if (runtime.hasSession(agentId, summary.session) ||
|
|
229
|
+
!summaries.has(summary.session))
|
|
230
|
+
summaries.set(summary.session, summary);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return new Response(JSON.stringify({
|
|
234
|
+
sessions: [...summaries.values()].sort((a, b) => b.startedAt - a.startedAt),
|
|
235
|
+
}), { status: response.status, headers: response.headers });
|
|
236
|
+
}
|
|
237
|
+
const match = path.match(/^\/agents\/([^/]+)\/v1\/(?:sessions|ag-ui\/sessions|media)\/([^/]+)/);
|
|
238
|
+
let agentId = match?.[1];
|
|
239
|
+
let sessionId = match?.[2];
|
|
240
|
+
if (!match && request.method === "POST" && path.endsWith("/v1/ag-ui")) {
|
|
241
|
+
agentId = path.split("/")[2];
|
|
242
|
+
const body = await request
|
|
243
|
+
.clone()
|
|
244
|
+
.json()
|
|
245
|
+
.catch(() => ({}));
|
|
246
|
+
sessionId =
|
|
247
|
+
typeof body.threadId === "string" ? body.threadId : undefined;
|
|
248
|
+
}
|
|
249
|
+
const runtime = agentId && sessionId
|
|
250
|
+
? [...retained]
|
|
251
|
+
.reverse()
|
|
252
|
+
.find((item) => item.hasSession(agentId, sessionId)) ?? current
|
|
253
|
+
: current;
|
|
254
|
+
return runtime.app.fetch(request);
|
|
255
|
+
};
|
|
256
|
+
const hostPort = port(requestedPort ?? process.env.PORT, 4111);
|
|
257
|
+
const binding = bindAddress(flags.get("--host") ?? process.env.HOST, flags.get("--allowed-hosts") ??
|
|
258
|
+
process.env.ALLOWED_HOSTS, hostPort);
|
|
259
|
+
const guarded = async (request) => allowedHost(request.headers.get("host") ?? undefined, binding.hosts)
|
|
260
|
+
? fetch(request)
|
|
261
|
+
: Response.json({
|
|
262
|
+
error: "Host header is not allowed. Set --allowed-hosts or ALLOWED_HOSTS.",
|
|
263
|
+
}, { status: 421 });
|
|
264
|
+
const server = serve({
|
|
265
|
+
fetch: guarded,
|
|
266
|
+
hostname: binding.hostname,
|
|
267
|
+
port: hostPort,
|
|
268
|
+
});
|
|
269
|
+
shutdown.push(() => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))));
|
|
270
|
+
await new Promise((resolve, reject) => {
|
|
271
|
+
server.once("listening", resolve);
|
|
272
|
+
server.once("error", reject);
|
|
273
|
+
});
|
|
274
|
+
const address = `http://${binding.reachable}:${hostPort}`;
|
|
275
|
+
console.log(`Agent runtime on ${address}`);
|
|
276
|
+
if (!binding.loopback)
|
|
277
|
+
console.log(binding.hosts.includes("*")
|
|
278
|
+
? "Serving every Host header; add TLS and access control at the network boundary."
|
|
279
|
+
: `Serving Host headers: ${binding.hosts.join(", ")}`);
|
|
280
|
+
if (command === "dev" && !flags.has("--no-studio")) {
|
|
281
|
+
const dashboard = await studio(address, !flags.has("--no-open"));
|
|
282
|
+
shutdown.push(() => dashboard.close());
|
|
283
|
+
console.log(`Studio on ${dashboard.address}`);
|
|
284
|
+
}
|
|
285
|
+
if (loader) {
|
|
286
|
+
let reload = Promise.resolve();
|
|
287
|
+
loader.vite.watcher.on("all", (event, file) => {
|
|
288
|
+
const path = relative(process.cwd(), file).split(sep).join("/");
|
|
289
|
+
if (stopping ||
|
|
290
|
+
!["add", "change", "unlink"].includes(event) ||
|
|
291
|
+
isAbsolute(path) ||
|
|
292
|
+
path.startsWith("..") ||
|
|
293
|
+
!(path === "nylorun.config.ts" || path.startsWith("agent/")))
|
|
294
|
+
return;
|
|
295
|
+
reload = reload.then(async () => {
|
|
296
|
+
try {
|
|
297
|
+
loader.vite.moduleGraph.invalidateAll();
|
|
298
|
+
const replacement = await createRuntime(await loader.load());
|
|
299
|
+
current = replacement;
|
|
300
|
+
retained.push(replacement);
|
|
301
|
+
console.log(`Reloaded ${path}`);
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
console.error(`Reload failed; existing agents remain active: ${error instanceof Error ? error.message : String(error)}`);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
await stop();
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
void main().catch((error) => {
|
|
316
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
317
|
+
process.exitCode =
|
|
318
|
+
error instanceof ConfigurationCancelled ? error.exitCode : 1;
|
|
319
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RuntimeAgent } from "./contracts.js";
|
|
2
|
+
import type { RuntimePersistence } from "./adapters/journal.js";
|
|
3
|
+
import type { RuntimeMedia } from "./adapters/media.js";
|
|
4
|
+
export interface RuntimeConfig {
|
|
5
|
+
readonly agents: readonly RuntimeAgent[];
|
|
6
|
+
readonly persistence?: RuntimePersistence;
|
|
7
|
+
readonly media?: RuntimeMedia;
|
|
8
|
+
readonly origins?: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function defineRuntime(config: RuntimeConfig): RuntimeConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function defineRuntime(config) {
|
|
2
|
+
if (!config || !Array.isArray(config.agents))
|
|
3
|
+
throw new Error("Runtime config must declare an agents array.");
|
|
4
|
+
for (const agent of config.agents) {
|
|
5
|
+
if (!agent ||
|
|
6
|
+
typeof agent.run !== "function" ||
|
|
7
|
+
typeof agent.id !== "string" ||
|
|
8
|
+
!agent.manifest)
|
|
9
|
+
throw new Error("Every registered agent must satisfy RuntimeAgent.");
|
|
10
|
+
}
|
|
11
|
+
return Object.freeze({
|
|
12
|
+
...config,
|
|
13
|
+
agents: Object.freeze([...config.agents]),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/** Portable values and callable interfaces. No agent engine is required. */
|
|
2
|
+
export type JsonValue = string | number | boolean | null | JsonObject | readonly JsonValue[];
|
|
3
|
+
export type JsonObject = {
|
|
4
|
+
readonly [key: string]: JsonValue;
|
|
5
|
+
};
|
|
6
|
+
export type UserContentPart = {
|
|
7
|
+
readonly type: "text";
|
|
8
|
+
readonly text: string;
|
|
9
|
+
} | {
|
|
10
|
+
readonly type: "media";
|
|
11
|
+
readonly mediaType: string;
|
|
12
|
+
readonly reference: JsonValue;
|
|
13
|
+
};
|
|
14
|
+
export type MessageInput = string | {
|
|
15
|
+
readonly text: string;
|
|
16
|
+
readonly metadata?: JsonObject;
|
|
17
|
+
} | {
|
|
18
|
+
readonly content: readonly UserContentPart[];
|
|
19
|
+
readonly metadata?: JsonObject;
|
|
20
|
+
};
|
|
21
|
+
export type InteractionReply = {
|
|
22
|
+
readonly kind: "approve";
|
|
23
|
+
readonly interactionId: string;
|
|
24
|
+
readonly approved: boolean;
|
|
25
|
+
} | {
|
|
26
|
+
readonly kind: "respond";
|
|
27
|
+
readonly interactionId: string;
|
|
28
|
+
readonly value: JsonValue;
|
|
29
|
+
};
|
|
30
|
+
export type RuntimeInput = MessageInput | InteractionReply;
|
|
31
|
+
export type RuntimeInputEvent = InteractionReply | {
|
|
32
|
+
readonly kind: "user-message" | "interrupt";
|
|
33
|
+
readonly text?: string;
|
|
34
|
+
readonly content?: readonly UserContentPart[];
|
|
35
|
+
};
|
|
36
|
+
/** Lifecycle events are delivered in completion order; other events are diagnostics. */
|
|
37
|
+
export interface RuntimeEvent {
|
|
38
|
+
readonly type: string;
|
|
39
|
+
readonly output?: JsonValue;
|
|
40
|
+
readonly event?: RuntimeInputEvent;
|
|
41
|
+
readonly interaction?: unknown;
|
|
42
|
+
readonly attributes?: unknown;
|
|
43
|
+
}
|
|
44
|
+
export interface RuntimeCompletion {
|
|
45
|
+
readonly status: "completed" | "waiting" | "rejected" | "cancelled" | "stopped";
|
|
46
|
+
readonly events: readonly RuntimeEvent[];
|
|
47
|
+
}
|
|
48
|
+
export interface RuntimeSession {
|
|
49
|
+
readonly id: string;
|
|
50
|
+
input(event: RuntimeInput, options?: {
|
|
51
|
+
readonly signal?: AbortSignal;
|
|
52
|
+
}): {
|
|
53
|
+
readonly completed: Promise<RuntimeCompletion>;
|
|
54
|
+
};
|
|
55
|
+
stream(): AsyncIterable<RuntimeEvent>;
|
|
56
|
+
observe(listener: (event: RuntimeEvent) => void | Promise<void>): () => void;
|
|
57
|
+
stop(reason?: string): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
export interface RuntimeAgent {
|
|
60
|
+
readonly id: string;
|
|
61
|
+
readonly name: string;
|
|
62
|
+
readonly manifest: {
|
|
63
|
+
readonly id: string;
|
|
64
|
+
readonly name: string;
|
|
65
|
+
};
|
|
66
|
+
run(options?: {
|
|
67
|
+
readonly id?: string;
|
|
68
|
+
readonly userId?: string;
|
|
69
|
+
readonly context?: JsonObject;
|
|
70
|
+
}): RuntimeSession;
|
|
71
|
+
close?(): Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
export type PromptContentPart = UserContentPart | {
|
|
74
|
+
readonly type: "tool-call";
|
|
75
|
+
readonly id: string;
|
|
76
|
+
readonly name: string;
|
|
77
|
+
readonly args: JsonObject;
|
|
78
|
+
};
|
|
79
|
+
export type PromptItem = {
|
|
80
|
+
readonly kind: "instructions";
|
|
81
|
+
readonly role: "system";
|
|
82
|
+
readonly content: readonly PromptContentPart[];
|
|
83
|
+
} | {
|
|
84
|
+
readonly kind: "message";
|
|
85
|
+
readonly role: "user" | "assistant";
|
|
86
|
+
readonly content: readonly PromptContentPart[];
|
|
87
|
+
} | {
|
|
88
|
+
readonly kind: "context";
|
|
89
|
+
readonly role: "user";
|
|
90
|
+
readonly content: readonly PromptContentPart[];
|
|
91
|
+
} | {
|
|
92
|
+
readonly kind: "tool-result";
|
|
93
|
+
readonly toolCallId: string;
|
|
94
|
+
readonly toolName: string;
|
|
95
|
+
readonly status: "completed" | "denied" | "failed";
|
|
96
|
+
readonly content: readonly PromptContentPart[];
|
|
97
|
+
};
|
|
98
|
+
export interface RuntimeModelCall {
|
|
99
|
+
readonly sessionId: string;
|
|
100
|
+
readonly prompt: readonly PromptItem[];
|
|
101
|
+
readonly tools: readonly {
|
|
102
|
+
readonly name: string;
|
|
103
|
+
readonly description?: string;
|
|
104
|
+
readonly inputSchema: JsonObject;
|
|
105
|
+
}[];
|
|
106
|
+
readonly outputSchema?: JsonObject;
|
|
107
|
+
readonly model?: {
|
|
108
|
+
readonly id?: string;
|
|
109
|
+
readonly controls?: {
|
|
110
|
+
readonly temperature?: number;
|
|
111
|
+
readonly maxOutputTokens?: number;
|
|
112
|
+
};
|
|
113
|
+
readonly config?: JsonObject;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export interface RuntimeModelCandidate {
|
|
117
|
+
readonly output: readonly ({
|
|
118
|
+
readonly type: "text" | "reasoning";
|
|
119
|
+
readonly text: string;
|
|
120
|
+
} | {
|
|
121
|
+
readonly type: "json";
|
|
122
|
+
readonly value: JsonValue;
|
|
123
|
+
} | {
|
|
124
|
+
readonly type: "tool-call";
|
|
125
|
+
readonly id: string;
|
|
126
|
+
readonly name: string;
|
|
127
|
+
readonly args: JsonObject;
|
|
128
|
+
})[];
|
|
129
|
+
readonly finishReason?: "stop" | "length" | "tool-calls" | "content-filter" | "other";
|
|
130
|
+
readonly usage?: {
|
|
131
|
+
readonly inputTokens?: number;
|
|
132
|
+
readonly outputTokens?: number;
|
|
133
|
+
readonly totalTokens?: number;
|
|
134
|
+
readonly costUsd?: number;
|
|
135
|
+
};
|
|
136
|
+
readonly evidence?: {
|
|
137
|
+
readonly resolvedModel?: string;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export interface RuntimeModelContext {
|
|
141
|
+
readonly signal: AbortSignal;
|
|
142
|
+
reportPreparedCall?(prepared: {
|
|
143
|
+
readonly adapter: string;
|
|
144
|
+
readonly call: JsonValue;
|
|
145
|
+
}): void;
|
|
146
|
+
}
|
|
147
|
+
export type RuntimeModelAdapter = (call: RuntimeModelCall, context: RuntimeModelContext) => Promise<RuntimeModelCandidate>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { defineRuntime } from "./config.js";
|
|
2
|
+
export type { RuntimeConfig } from "./config.js";
|
|
3
|
+
export type * from "./contracts.js";
|
|
4
|
+
export { createRuntime } from "./server/host.js";
|
|
5
|
+
export { piModel } from "./model/pi-model.js";
|
|
6
|
+
export type { PiModelOptions } from "./model/pi-model.js";
|
|
7
|
+
export { localJsonl, memoryHistory, JsonlJournal } from "./adapters/journal.js";
|
|
8
|
+
export type { RuntimePersistence, CanonicalEvent, SessionSummary, } from "./adapters/journal.js";
|
|
9
|
+
export { localMedia, MediaStore, IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes, } from "./adapters/media.js";
|
|
10
|
+
export type { RuntimeMedia, MediaAsset, MediaReference, } from "./adapters/media.js";
|
|
11
|
+
export { projectAsset } from "./assets.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { defineRuntime } from "./config.js";
|
|
2
|
+
export { createRuntime } from "./server/host.js";
|
|
3
|
+
export { piModel } from "./model/pi-model.js";
|
|
4
|
+
export { localJsonl, memoryHistory, JsonlJournal } from "./adapters/journal.js";
|
|
5
|
+
export { localMedia, MediaStore, IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes, } from "./adapters/media.js";
|
|
6
|
+
export { projectAsset } from "./assets.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Credential, CredentialInfo, CredentialStore } from "@earendil-works/pi-ai";
|
|
2
|
+
export declare class ProjectCredentialStore implements CredentialStore {
|
|
3
|
+
#private;
|
|
4
|
+
private readonly file;
|
|
5
|
+
constructor(file?: string);
|
|
6
|
+
read(providerId: string): Promise<Credential | undefined>;
|
|
7
|
+
list(): Promise<readonly CredentialInfo[]>;
|
|
8
|
+
modify(providerId: string, fn: (current: Credential | undefined) => Promise<Credential | undefined>): Promise<Credential | undefined>;
|
|
9
|
+
delete(providerId: string): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
export class ProjectCredentialStore {
|
|
4
|
+
file;
|
|
5
|
+
#chain = Promise.resolve();
|
|
6
|
+
constructor(file = join(process.cwd(), ".env", "auth.json")) {
|
|
7
|
+
this.file = file;
|
|
8
|
+
}
|
|
9
|
+
async read(providerId) {
|
|
10
|
+
return (await this.#all())[providerId];
|
|
11
|
+
}
|
|
12
|
+
async list() {
|
|
13
|
+
return Object.entries(await this.#all()).map(([providerId, credential]) => ({ providerId, type: credential.type }));
|
|
14
|
+
}
|
|
15
|
+
async modify(providerId, fn) {
|
|
16
|
+
let result;
|
|
17
|
+
await this.#serialized(async () => {
|
|
18
|
+
const all = await this.#all();
|
|
19
|
+
const current = all[providerId];
|
|
20
|
+
const next = await fn(current);
|
|
21
|
+
// The pi-ai contract: undefined leaves the entry unchanged.
|
|
22
|
+
result = next ?? current;
|
|
23
|
+
if (next === undefined)
|
|
24
|
+
return;
|
|
25
|
+
all[providerId] = next;
|
|
26
|
+
await this.#write(all);
|
|
27
|
+
});
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
async delete(providerId) {
|
|
31
|
+
await this.#serialized(async () => {
|
|
32
|
+
const all = await this.#all();
|
|
33
|
+
if (!(providerId in all))
|
|
34
|
+
return;
|
|
35
|
+
delete all[providerId];
|
|
36
|
+
await this.#write(all);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async #serialized(operation) {
|
|
40
|
+
const work = this.#chain.then(() => this.#locked(operation));
|
|
41
|
+
this.#chain = work.catch(() => undefined);
|
|
42
|
+
await work;
|
|
43
|
+
}
|
|
44
|
+
async #write(all) {
|
|
45
|
+
await mkdir(dirname(this.file), { recursive: true });
|
|
46
|
+
const temporary = this.file + ".tmp";
|
|
47
|
+
await writeFile(temporary, JSON.stringify(all, null, 2) + "\n", {
|
|
48
|
+
mode: 0o600,
|
|
49
|
+
});
|
|
50
|
+
await rename(temporary, this.file);
|
|
51
|
+
}
|
|
52
|
+
async #all() {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(await readFile(this.file, "utf8"));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error.code === "ENOENT")
|
|
58
|
+
return {};
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async #locked(operation) {
|
|
63
|
+
await mkdir(dirname(this.file), { recursive: true });
|
|
64
|
+
const lock = this.file + ".lock";
|
|
65
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
66
|
+
try {
|
|
67
|
+
await mkdir(lock);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error.code !== "EEXIST")
|
|
72
|
+
throw error;
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
74
|
+
}
|
|
75
|
+
if (attempt === 99)
|
|
76
|
+
throw new Error("Timed out waiting for the credential store.");
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return await operation();
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
await rm(lock, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
export declare class ConfigurationCancelled extends Error {
|
|
3
|
+
readonly signal: "SIGINT" | "SIGTERM";
|
|
4
|
+
readonly exitCode: number;
|
|
5
|
+
constructor(signal: "SIGINT" | "SIGTERM");
|
|
6
|
+
}
|
|
7
|
+
export declare function configureProvider(options?: {
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
root?: string;
|
|
10
|
+
input?: Readable;
|
|
11
|
+
output?: Writable;
|
|
12
|
+
}): Promise<void>;
|