@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.
- package/CHANGELOG.md +55 -0
- package/README.md +29 -47
- 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/configuration.d.ts +3 -0
- package/dist/configuration.js +3 -0
- package/dist/contracts.d.ts +5 -166
- package/dist/core/main.js +40 -0
- package/dist/core/provider.d.ts +12 -0
- package/dist/core/provider.js +60 -0
- package/dist/core/runtime.d.ts +50 -0
- package/dist/core/runtime.js +877 -0
- package/dist/core/store.d.ts +16 -0
- package/dist/core/store.js +101 -0
- package/dist/index.d.ts +6 -11
- package/dist/index.js +4 -6
- 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 +174 -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 +50 -7
- package/dist/server/host.js +304 -307
- package/dist/session/api.d.ts +10 -0
- package/dist/session/api.js +15 -0
- package/dist/session/default.d.ts +5 -0
- package/dist/session/default.js +30 -0
- package/dist/session/handle.d.ts +27 -0
- package/dist/session/handle.js +199 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/sessions/host.d.ts +39 -0
- package/dist/sessions/host.js +359 -0
- package/dist/sessions/store.d.ts +41 -0
- package/dist/sessions/store.js +33 -0
- package/package.json +23 -12
- package/dist/adapters/journal.d.ts +0 -35
- package/dist/adapters/journal.js +0 -130
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -100
- package/dist/dev-entry.js +0 -2
- package/dist/dev.d.ts +0 -2
- package/dist/dev.js +0 -126
- package/dist/environment.d.ts +0 -2
- package/dist/environment.js +0 -64
- package/dist/launcher.d.ts +0 -1
- package/dist/launcher.js +0 -28
- package/dist/model/configure.d.ts +0 -12
- package/dist/model/configure.js +0 -155
- /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
|
|
2
|
-
export type
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export
|
|
6
|
-
export {
|
|
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 {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
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";
|
package/dist/media.d.ts
ADDED
|
@@ -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
|
+
}
|
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;
|