@nylorun/runtime 0.4.0-beta → 0.6.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +29 -47
  3. package/dist/adapters/media.d.ts +2 -18
  4. package/dist/adapters/media.js +2 -52
  5. package/dist/adapters/observe.js +1 -1
  6. package/dist/config.d.ts +17 -10
  7. package/dist/configuration.d.ts +3 -0
  8. package/dist/configuration.js +3 -0
  9. package/dist/contracts.d.ts +5 -166
  10. package/dist/core/main.js +40 -0
  11. package/dist/core/provider.d.ts +12 -0
  12. package/dist/core/provider.js +60 -0
  13. package/dist/core/runtime.d.ts +50 -0
  14. package/dist/core/runtime.js +877 -0
  15. package/dist/core/store.d.ts +16 -0
  16. package/dist/core/store.js +101 -0
  17. package/dist/index.d.ts +6 -11
  18. package/dist/index.js +4 -6
  19. package/dist/media.d.ts +29 -0
  20. package/dist/media.js +53 -0
  21. package/dist/model/defaults.d.ts +17 -0
  22. package/dist/model/defaults.js +21 -0
  23. package/dist/model/http-model.d.ts +12 -0
  24. package/dist/model/http-model.js +299 -0
  25. package/dist/model/pi-model.d.ts +2 -1
  26. package/dist/model/pi-model.js +52 -7
  27. package/dist/node/index.d.ts +5 -0
  28. package/dist/node/index.js +5 -0
  29. package/dist/node/local-sessions.d.ts +5 -0
  30. package/dist/node/local-sessions.js +174 -0
  31. package/dist/redact.d.ts +1 -0
  32. package/dist/redact.js +14 -0
  33. package/dist/server/ag-ui.d.ts +1 -1
  34. package/dist/server/delivery.d.ts +24 -0
  35. package/dist/server/delivery.js +107 -0
  36. package/dist/server/host.d.ts +50 -7
  37. package/dist/server/host.js +304 -307
  38. package/dist/session/api.d.ts +10 -0
  39. package/dist/session/api.js +15 -0
  40. package/dist/session/default.d.ts +5 -0
  41. package/dist/session/default.js +30 -0
  42. package/dist/session/handle.d.ts +27 -0
  43. package/dist/session/handle.js +199 -0
  44. package/dist/session/index.d.ts +2 -0
  45. package/dist/session/index.js +2 -0
  46. package/dist/sessions/host.d.ts +39 -0
  47. package/dist/sessions/host.js +359 -0
  48. package/dist/sessions/store.d.ts +41 -0
  49. package/dist/sessions/store.js +33 -0
  50. package/package.json +23 -12
  51. package/dist/adapters/journal.d.ts +0 -35
  52. package/dist/adapters/journal.js +0 -130
  53. package/dist/cli.d.ts +0 -2
  54. package/dist/cli.js +0 -100
  55. package/dist/dev-entry.js +0 -2
  56. package/dist/dev.d.ts +0 -2
  57. package/dist/dev.js +0 -126
  58. package/dist/environment.d.ts +0 -2
  59. package/dist/environment.js +0 -64
  60. package/dist/launcher.d.ts +0 -1
  61. package/dist/launcher.js +0 -28
  62. package/dist/model/configure.d.ts +0 -12
  63. package/dist/model/configure.js +0 -155
  64. /package/dist/{dev-entry.d.ts → core/main.d.ts} +0 -0
@@ -0,0 +1,16 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import type { LiveEvent } from "@nylorun/core/contracts";
3
+ export declare function canonical(value: unknown): string;
4
+ export declare class Store {
5
+ readonly db: DatabaseSync;
6
+ constructor(path: string);
7
+ tx<T>(fn: () => T): T;
8
+ get<T = any>(table: string, id: string): T | undefined;
9
+ all<T = any>(table: string): T[];
10
+ put(table: string, id: string, body: unknown): void;
11
+ event(sessionId: string, turnId: string | null, type: string, payload: unknown): LiveEvent;
12
+ history(sessionId: string, cursor?: string): {
13
+ items: LiveEvent[];
14
+ cursor: string | null;
15
+ };
16
+ }
@@ -0,0 +1,101 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import { randomUUID } from "node:crypto";
3
+ export function canonical(value) {
4
+ if (Array.isArray(value))
5
+ return "[" + value.map(canonical).join(",") + "]";
6
+ if (value && typeof value === "object")
7
+ return ("{" +
8
+ Object.keys(value)
9
+ .sort()
10
+ .filter((k) => value[k] !== undefined)
11
+ .map((k) => JSON.stringify(k) + ":" + canonical(value[k]))
12
+ .join(",") +
13
+ "}");
14
+ return JSON.stringify(value) ?? "null";
15
+ }
16
+ export class Store {
17
+ db;
18
+ constructor(path) {
19
+ this.db = new DatabaseSync(path);
20
+ this.db
21
+ .exec(`PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000;
22
+ CREATE TABLE IF NOT EXISTS definitions(id TEXT PRIMARY KEY, body TEXT NOT NULL);
23
+ CREATE TABLE IF NOT EXISTS sessions(id TEXT PRIMARY KEY, body TEXT NOT NULL);
24
+ CREATE TABLE IF NOT EXISTS commands(id TEXT PRIMARY KEY, body TEXT NOT NULL);
25
+ CREATE TABLE IF NOT EXISTS checkpoints(id TEXT PRIMARY KEY, body TEXT NOT NULL);
26
+ CREATE TABLE IF NOT EXISTS effects(id TEXT PRIMARY KEY, body TEXT NOT NULL);
27
+ CREATE TABLE IF NOT EXISTS actions(id TEXT PRIMARY KEY, body TEXT NOT NULL);
28
+ CREATE TABLE IF NOT EXISTS events(sequence INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, body TEXT NOT NULL);
29
+ CREATE INDEX IF NOT EXISTS events_session ON events(session_id, sequence);`);
30
+ }
31
+ tx(fn) {
32
+ this.db.exec("BEGIN IMMEDIATE");
33
+ try {
34
+ const result = fn();
35
+ this.db.exec("COMMIT");
36
+ return result;
37
+ }
38
+ catch (e) {
39
+ this.db.exec("ROLLBACK");
40
+ throw e;
41
+ }
42
+ }
43
+ get(table, id) {
44
+ const row = this.db.prepare(`SELECT body FROM ${table} WHERE id=?`).get(id);
45
+ return row ? JSON.parse(String(row.body)) : undefined;
46
+ }
47
+ all(table) {
48
+ return this.db
49
+ .prepare(`SELECT body FROM ${table}`)
50
+ .all()
51
+ .map((r) => JSON.parse(String(r.body)));
52
+ }
53
+ put(table, id, body) {
54
+ this.db
55
+ .prepare(`INSERT INTO ${table}(id,body) VALUES(?,?) ON CONFLICT(id) DO UPDATE SET body=excluded.body`)
56
+ .run(id, JSON.stringify(body));
57
+ }
58
+ event(sessionId, turnId, type, payload) {
59
+ const base = {
60
+ eventId: randomUUID(),
61
+ sessionId,
62
+ turnId,
63
+ createdAt: new Date().toISOString(),
64
+ type,
65
+ payload,
66
+ };
67
+ const result = this.db
68
+ .prepare("INSERT INTO events(session_id,body) VALUES(?,?)")
69
+ .run(sessionId, "{}");
70
+ const event = {
71
+ ...base,
72
+ cursor: Buffer.from(`${sessionId}:${result.lastInsertRowid}`).toString("base64url"),
73
+ };
74
+ this.db
75
+ .prepare("UPDATE events SET body=? WHERE sequence=?")
76
+ .run(JSON.stringify(event), result.lastInsertRowid);
77
+ return event;
78
+ }
79
+ history(sessionId, cursor) {
80
+ let sequence = 0;
81
+ if (cursor) {
82
+ const decoded = Buffer.from(cursor, "base64url").toString();
83
+ const prefix = `${sessionId}:`;
84
+ if (!decoded.startsWith(prefix) ||
85
+ !/^\d+$/.test(decoded.slice(prefix.length)))
86
+ throw new Error("Invalid cursor");
87
+ sequence = Number(decoded.slice(prefix.length));
88
+ }
89
+ const items = this.db
90
+ .prepare("SELECT body FROM events WHERE session_id=? AND sequence>? ORDER BY sequence")
91
+ .all(sessionId, sequence)
92
+ .map((r) => JSON.parse(String(r.body)));
93
+ const last = this.db
94
+ .prepare("SELECT body FROM events WHERE session_id=? ORDER BY sequence DESC LIMIT 1")
95
+ .get(sessionId);
96
+ return {
97
+ items,
98
+ cursor: last ? JSON.parse(String(last.body)).cursor : null,
99
+ };
100
+ }
101
+ }
package/dist/index.d.ts CHANGED
@@ -1,11 +1,6 @@
1
- export type { RuntimeConfig } from "./config.js";
2
- export type * from "./contracts.js";
3
- export { Runtime, serveAgents, type AgentRouterOptions, type RuntimeActor, type ServeAgentsOptions, } from "./server/host.js";
4
- export { localJsonl, memoryHistory, JsonlJournal } from "./adapters/journal.js";
5
- export type { RuntimeDurability, CanonicalEvent, SessionSummary, } from "./adapters/journal.js";
6
- export { jsonlObserver } from "./adapters/observe.js";
7
- export { localMedia, MediaStore, IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes, } from "./adapters/media.js";
8
- export type { RuntimeMedia, MediaAsset, MediaReference, } from "./adapters/media.js";
9
- export { piModel } from "./model/pi-model.js";
10
- export type { PiModelOptions } from "./model/pi-model.js";
11
- export { projectAsset } from "./assets.js";
1
+ export { CoreRuntime, createRuntime, startRuntime, type RuntimeOptions } from "./core/runtime.js";
2
+ export { scriptedModel, gatewayModel, type ModelProvider } from "./core/provider.js";
3
+ export { httpModel, type HttpModelOptions, type ModelEnvironment } from "./model/http-model.js";
4
+ export type { RuntimeAgent } from "./contracts.js";
5
+ export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes } from "./media.js";
6
+ export type { RuntimeModelAdapter } from "./contracts.js";
package/dist/index.js CHANGED
@@ -1,6 +1,4 @@
1
- export { Runtime, serveAgents, } from "./server/host.js";
2
- export { localJsonl, memoryHistory, JsonlJournal } from "./adapters/journal.js";
3
- export { jsonlObserver } from "./adapters/observe.js";
4
- export { localMedia, MediaStore, IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes, } from "./adapters/media.js";
5
- export { piModel } from "./model/pi-model.js";
6
- export { projectAsset } from "./assets.js";
1
+ export { CoreRuntime, createRuntime, startRuntime } from "./core/runtime.js";
2
+ export { scriptedModel, gatewayModel } from "./core/provider.js";
3
+ export { httpModel } from "./model/http-model.js";
4
+ export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes } from "./media.js";
@@ -0,0 +1,29 @@
1
+ export declare const IMAGE_MEDIA_TYPES: readonly string[];
2
+ export declare const MAX_IMAGE_BYTES: number;
3
+ export interface MediaAsset {
4
+ readonly id: string;
5
+ readonly mediaType: string;
6
+ readonly bytes: number;
7
+ readonly kind: "input" | "generated";
8
+ }
9
+ /** Opaque reference retained by Harness and resolved by the configured media adapter. */
10
+ export interface MediaReference {
11
+ readonly agentId: string;
12
+ readonly assetId: string;
13
+ }
14
+ export declare function decodeImageBase64(mediaType: string, base64: string): Uint8Array;
15
+ /** Verify the asserted media type and lightweight file signature for a supported image. */
16
+ export declare function validateImageBytes(mediaType: string, bytes: Uint8Array): void;
17
+ export interface RuntimeMedia {
18
+ saveInput(agentId: string, sessionId: string, mediaType: string, base64: string): Promise<MediaAsset>;
19
+ saveGenerated(agentId: string, sessionId: string, mediaType: string, bytes: Uint8Array): Promise<MediaAsset>;
20
+ dataUrl(reference: MediaReference, sessionId: string): Promise<{
21
+ readonly asset: MediaAsset;
22
+ readonly url: string;
23
+ } | undefined>;
24
+ latestInput(agentId: string, sessionId: string): Promise<MediaAsset | undefined>;
25
+ read(agentId: string, sessionId: string, assetId: string): Promise<{
26
+ asset: MediaAsset;
27
+ bytes: Uint8Array;
28
+ } | undefined>;
29
+ }
package/dist/media.js ADDED
@@ -0,0 +1,53 @@
1
+ export const IMAGE_MEDIA_TYPES = Object.freeze([
2
+ "image/jpeg",
3
+ "image/png",
4
+ "image/webp",
5
+ ]);
6
+ export const MAX_IMAGE_BYTES = 8 * 1024 * 1024;
7
+ export function decodeImageBase64(mediaType, base64) {
8
+ if (!IMAGE_MEDIA_TYPES.includes(mediaType))
9
+ throw new Error("Only JPEG, PNG, and WebP images are supported.");
10
+ if (typeof base64 !== "string" || base64 === "")
11
+ throw new Error("Image data is required.");
12
+ if (base64.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(base64))
13
+ throw new Error("Image data must be canonical base64.");
14
+ const decoded = atob(base64);
15
+ const bytes = Uint8Array.from(decoded, (character) => character.charCodeAt(0));
16
+ if (btoa(decoded) !== base64)
17
+ throw new Error("Image data must be canonical base64.");
18
+ validateImageBytes(mediaType, bytes);
19
+ return bytes;
20
+ }
21
+ /** Verify the asserted media type and lightweight file signature for a supported image. */
22
+ export function validateImageBytes(mediaType, bytes) {
23
+ if (!IMAGE_MEDIA_TYPES.includes(mediaType))
24
+ throw new Error("Only JPEG, PNG, and WebP images are supported.");
25
+ if (bytes.byteLength === 0 || bytes.byteLength > MAX_IMAGE_BYTES)
26
+ throw new Error("Images must be no larger than 8 MiB.");
27
+ const signature = mediaType === "image/jpeg"
28
+ ? bytes.byteLength >= 3 &&
29
+ bytes[0] === 0xff &&
30
+ bytes[1] === 0xd8 &&
31
+ bytes[2] === 0xff
32
+ : mediaType === "image/png"
33
+ ? bytes.byteLength >= 8 &&
34
+ bytes[0] === 0x89 &&
35
+ bytes[1] === 0x50 &&
36
+ bytes[2] === 0x4e &&
37
+ bytes[3] === 0x47 &&
38
+ bytes[4] === 0x0d &&
39
+ bytes[5] === 0x0a &&
40
+ bytes[6] === 0x1a &&
41
+ bytes[7] === 0x0a
42
+ : bytes.byteLength >= 12 &&
43
+ bytes[0] === 0x52 &&
44
+ bytes[1] === 0x49 &&
45
+ bytes[2] === 0x46 &&
46
+ bytes[3] === 0x46 &&
47
+ bytes[8] === 0x57 &&
48
+ bytes[9] === 0x45 &&
49
+ bytes[10] === 0x42 &&
50
+ bytes[11] === 0x50;
51
+ if (!signature)
52
+ throw new Error(`Image bytes do not match ${mediaType}.`);
53
+ }
@@ -0,0 +1,17 @@
1
+ import type { ModelAdapter } from "@nylorun/core/define";
2
+ import { type ModelEnvironment } from "./http-model.js";
3
+ export interface ModelPreview {
4
+ readonly invocationId: string;
5
+ readonly text: string;
6
+ }
7
+ export interface ModelFactoryOptions {
8
+ readonly environment?: ModelEnvironment;
9
+ readonly media?: import("../media.js").RuntimeMedia;
10
+ readonly onPreview?: (preview: ModelPreview) => void;
11
+ }
12
+ /** Installed only by the Node launcher before application imports. */
13
+ export declare function installNodeModelFactory(factory: (options: ModelFactoryOptions) => ModelAdapter): void;
14
+ export declare function defaultModel(options: ModelFactoryOptions): ModelAdapter;
15
+ export declare function processEnvironment(): ModelEnvironment;
16
+ export declare function installRuntimeLifecycle(register: (close: () => Promise<void>) => void): void;
17
+ export declare function registerRuntimeLifecycle(close: () => Promise<void>): void;
@@ -0,0 +1,21 @@
1
+ import { httpModel } from "./http-model.js";
2
+ let nodeFactory;
3
+ /** Installed only by the Node launcher before application imports. */
4
+ export function installNodeModelFactory(factory) {
5
+ nodeFactory = factory;
6
+ }
7
+ export function defaultModel(options) {
8
+ return options.environment === undefined && nodeFactory
9
+ ? nodeFactory(options)
10
+ : httpModel(options);
11
+ }
12
+ export function processEnvironment() {
13
+ return typeof process === "undefined" ? {} : process.env;
14
+ }
15
+ let runtimeLifecycle;
16
+ export function installRuntimeLifecycle(register) {
17
+ runtimeLifecycle = register;
18
+ }
19
+ export function registerRuntimeLifecycle(close) {
20
+ runtimeLifecycle?.(close);
21
+ }
@@ -0,0 +1,12 @@
1
+ import type { ModelAdapter } from "@nylorun/core/define";
2
+ import type { ModelFactoryOptions } from "./defaults.js";
3
+ export type ModelEnvironment = Readonly<Record<string, string | undefined>>;
4
+ export interface HttpModelOptions extends ModelFactoryOptions {
5
+ readonly provider?: "openai" | "custom" | "anthropic";
6
+ readonly model?: string;
7
+ readonly apiKey?: string;
8
+ readonly baseUrl?: string;
9
+ readonly fetch?: typeof fetch;
10
+ }
11
+ /** Fetch-only adapter for OpenAI-compatible and Anthropic HTTP endpoints. */
12
+ export declare function httpModel(options?: HttpModelOptions): ModelAdapter;
@@ -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
+ }
@@ -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
- /** A plain portable callable. Provider credentials are read only when it is invoked. */
10
+ /** Node model adapter. Local provider configuration is read only when invoked. */
10
11
  export declare function piModel(options?: PiModelOptions): RuntimeModelAdapter;