@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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { mkdir, writeFile, rename, rm } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { ProjectCredentialStore } from "./auth-store.js";
|
|
6
|
+
import { modelsFor } from "./models.js";
|
|
7
|
+
export class ConfigurationCancelled extends Error {
|
|
8
|
+
signal;
|
|
9
|
+
exitCode;
|
|
10
|
+
constructor(signal) {
|
|
11
|
+
super(`Provider configuration cancelled (${signal}).`);
|
|
12
|
+
this.signal = signal;
|
|
13
|
+
this.exitCode = signal === "SIGINT" ? 130 : 143;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// Internal options also allow isolated prompt tests without changing process globals.
|
|
17
|
+
export async function configureProvider(options = {}) {
|
|
18
|
+
const root = options.root ?? process.cwd();
|
|
19
|
+
const output = options.output ?? process.stdout;
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const signal = controller.signal;
|
|
22
|
+
const forwardAbort = () => controller.abort(options.signal.reason);
|
|
23
|
+
options.signal?.throwIfAborted();
|
|
24
|
+
const store = new ProjectCredentialStore(join(root, ".env", "auth.json"));
|
|
25
|
+
const models = modelsFor({ provider: "", model: "" }, store);
|
|
26
|
+
const providers = models.getProviders();
|
|
27
|
+
const prompt = createInterface({
|
|
28
|
+
input: options.input ?? process.stdin,
|
|
29
|
+
output,
|
|
30
|
+
});
|
|
31
|
+
const onInt = () => controller.abort(new ConfigurationCancelled("SIGINT"));
|
|
32
|
+
const onClose = () => controller.abort(new Error("Configuration input closed before setup completed."));
|
|
33
|
+
const closeOnAbort = () => prompt.close();
|
|
34
|
+
prompt.on("SIGINT", onInt);
|
|
35
|
+
prompt.on("close", onClose);
|
|
36
|
+
signal.addEventListener("abort", closeOnAbort, { once: true });
|
|
37
|
+
options.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
38
|
+
if (options.signal?.aborted)
|
|
39
|
+
forwardAbort();
|
|
40
|
+
async function question(message) {
|
|
41
|
+
signal.throwIfAborted();
|
|
42
|
+
return prompt.question(message, { signal });
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
signal.throwIfAborted();
|
|
46
|
+
output.write("0. Custom OpenAI-compatible provider\n");
|
|
47
|
+
providers.forEach((provider, index) => output.write(`${index + 1}. ${provider.name} (${provider.id})\n`));
|
|
48
|
+
const choice = Number(await question("Choose a provider: "));
|
|
49
|
+
if (choice === 0) {
|
|
50
|
+
const baseUrl = (await question("OpenAI-compatible base URL: "))
|
|
51
|
+
.trim()
|
|
52
|
+
.replace(/\/$/, "");
|
|
53
|
+
const model = (await question("Model id: ")).trim();
|
|
54
|
+
if (!baseUrl || !model)
|
|
55
|
+
throw new Error("A base URL and model id are required.");
|
|
56
|
+
const selection = {
|
|
57
|
+
provider: "custom",
|
|
58
|
+
model,
|
|
59
|
+
custom: { baseUrl },
|
|
60
|
+
};
|
|
61
|
+
const customModels = modelsFor(selection, store);
|
|
62
|
+
await customModels.login("custom", "api_key", interaction());
|
|
63
|
+
await save(selection);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const chosen = providers[choice - 1];
|
|
67
|
+
if (!chosen)
|
|
68
|
+
throw new Error("Choose a listed provider.");
|
|
69
|
+
const available = models.getModels(chosen.id);
|
|
70
|
+
available.forEach((model, index) => output.write(`${index + 1}. ${model.name} (${model.id})\n`));
|
|
71
|
+
const model = available[Number(await question("Choose a model: ")) - 1];
|
|
72
|
+
if (!model)
|
|
73
|
+
throw new Error("Choose a listed model.");
|
|
74
|
+
if (!(await models.checkAuth(chosen.id, { signal }))) {
|
|
75
|
+
await models.login(chosen.id, chosen.auth.oauth ? "oauth" : "api_key", interaction());
|
|
76
|
+
}
|
|
77
|
+
await save({ provider: chosen.id, model: model.id });
|
|
78
|
+
}
|
|
79
|
+
signal.throwIfAborted();
|
|
80
|
+
output.write("Provider configuration saved.\n");
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
throw signal.aborted ? signal.reason : error;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
options.signal?.removeEventListener("abort", forwardAbort);
|
|
87
|
+
signal.removeEventListener("abort", closeOnAbort);
|
|
88
|
+
prompt.removeListener("SIGINT", onInt);
|
|
89
|
+
prompt.removeListener("close", onClose);
|
|
90
|
+
prompt.close();
|
|
91
|
+
}
|
|
92
|
+
function interaction() {
|
|
93
|
+
return {
|
|
94
|
+
signal,
|
|
95
|
+
prompt: async (item) => question(item.message + ": "),
|
|
96
|
+
notify: (event) => output.write((event.url ?? event.verificationUri ?? event.message) + "\n"),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function save(selection) {
|
|
100
|
+
signal.throwIfAborted();
|
|
101
|
+
const directory = join(root, "config");
|
|
102
|
+
const temporary = join(directory, `.model-${randomUUID()}.json`);
|
|
103
|
+
try {
|
|
104
|
+
await mkdir(directory, { recursive: true });
|
|
105
|
+
await writeFile(temporary, JSON.stringify(selection, null, 2) + "\n", {
|
|
106
|
+
signal,
|
|
107
|
+
});
|
|
108
|
+
signal.throwIfAborted();
|
|
109
|
+
await rename(temporary, join(directory, "model.json"));
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await rm(temporary, { force: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ProjectCredentialStore } from "./auth-store.js";
|
|
2
|
+
export type Selection = Readonly<{
|
|
3
|
+
provider: string;
|
|
4
|
+
model: string;
|
|
5
|
+
custom?: Readonly<{
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
}>;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function modelsFor(selection: Selection, credentials: ProjectCredentialStore): import("@earendil-works/pi-ai").MutableModels;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createProvider, envApiKeyAuth, } from "@earendil-works/pi-ai";
|
|
2
|
+
import { stream, streamSimple, } from "@earendil-works/pi-ai/api/openai-completions";
|
|
3
|
+
import { builtinModels } from "@earendil-works/pi-ai/providers/all";
|
|
4
|
+
export function modelsFor(selection, credentials) {
|
|
5
|
+
const models = builtinModels({ credentials });
|
|
6
|
+
if (!selection.custom)
|
|
7
|
+
return models;
|
|
8
|
+
const model = {
|
|
9
|
+
id: selection.model,
|
|
10
|
+
name: selection.model,
|
|
11
|
+
api: "openai-completions",
|
|
12
|
+
provider: "custom",
|
|
13
|
+
baseUrl: selection.custom.baseUrl,
|
|
14
|
+
reasoning: false,
|
|
15
|
+
input: ["text", "image"],
|
|
16
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
17
|
+
contextWindow: 128000,
|
|
18
|
+
maxTokens: 16384,
|
|
19
|
+
};
|
|
20
|
+
models.setProvider(createProvider({
|
|
21
|
+
id: "custom",
|
|
22
|
+
name: "Custom OpenAI-compatible",
|
|
23
|
+
baseUrl: selection.custom.baseUrl,
|
|
24
|
+
auth: {
|
|
25
|
+
apiKey: envApiKeyAuth("Custom API key", ["NYLO_CUSTOM_API_KEY"]),
|
|
26
|
+
},
|
|
27
|
+
models: [model],
|
|
28
|
+
api: { stream, streamSimple },
|
|
29
|
+
}));
|
|
30
|
+
return models;
|
|
31
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RuntimeModelAdapter } from "../contracts.js";
|
|
2
|
+
import type { RuntimeMedia } from "../adapters/media.js";
|
|
3
|
+
import { type Selection } from "./models.js";
|
|
4
|
+
export interface PiModelOptions {
|
|
5
|
+
readonly root?: string;
|
|
6
|
+
readonly selection?: Selection;
|
|
7
|
+
readonly media?: Pick<RuntimeMedia, "dataUrl">;
|
|
8
|
+
}
|
|
9
|
+
/** A plain portable callable. Provider credentials are read only when it is invoked. */
|
|
10
|
+
export declare function piModel(options?: PiModelOptions): RuntimeModelAdapter;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { scrub } from "../adapters/journal.js";
|
|
3
|
+
import { ProjectCredentialStore } from "./auth-store.js";
|
|
4
|
+
import { modelsFor } from "./models.js";
|
|
5
|
+
import { modelSelection, projectSecrets } from "./settings.js";
|
|
6
|
+
const emptyUsage = () => ({
|
|
7
|
+
input: 0,
|
|
8
|
+
output: 0,
|
|
9
|
+
totalTokens: 0,
|
|
10
|
+
cacheRead: 0,
|
|
11
|
+
cacheWrite: 0,
|
|
12
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
13
|
+
});
|
|
14
|
+
/** A plain portable callable. Provider credentials are read only when it is invoked. */
|
|
15
|
+
export function piModel(options = {}) {
|
|
16
|
+
return async (call, context) => {
|
|
17
|
+
context.signal.throwIfAborted();
|
|
18
|
+
const root = options.root ?? process.cwd();
|
|
19
|
+
const selection = options.selection ?? modelSelection(root);
|
|
20
|
+
const registry = modelsFor(selection, new ProjectCredentialStore(join(root, ".env", "auth.json")));
|
|
21
|
+
const selected = registry.getModel(selection.provider, selection.model);
|
|
22
|
+
if (!selected)
|
|
23
|
+
throw new Error("Unknown model. Run nylorun configure.");
|
|
24
|
+
const messages = [];
|
|
25
|
+
const instructions = [];
|
|
26
|
+
const content = async (parts) => {
|
|
27
|
+
const result = [];
|
|
28
|
+
for (const part of parts) {
|
|
29
|
+
if (part.type === "text")
|
|
30
|
+
result.push({ type: "text", text: part.text });
|
|
31
|
+
else if (part.type === "media") {
|
|
32
|
+
if (!selected.input.includes("image"))
|
|
33
|
+
throw new Error("The configured model does not accept images.");
|
|
34
|
+
const ref = part.reference;
|
|
35
|
+
if (!ref ||
|
|
36
|
+
typeof ref !== "object" ||
|
|
37
|
+
Array.isArray(ref) ||
|
|
38
|
+
!("agentId" in ref) ||
|
|
39
|
+
!("assetId" in ref) ||
|
|
40
|
+
typeof ref.agentId !== "string" ||
|
|
41
|
+
typeof ref.assetId !== "string")
|
|
42
|
+
throw new Error("Expected a local media reference.");
|
|
43
|
+
const asset = await options.media?.dataUrl({ agentId: ref.agentId, assetId: ref.assetId }, call.sessionId);
|
|
44
|
+
if (!asset)
|
|
45
|
+
throw new Error("Media is unavailable; pass the shared media adapter to piModel({ media }).");
|
|
46
|
+
const comma = asset.url.indexOf(",");
|
|
47
|
+
result.push({
|
|
48
|
+
type: "image",
|
|
49
|
+
data: asset.url.slice(comma + 1),
|
|
50
|
+
mimeType: asset.asset.mediaType,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
};
|
|
56
|
+
for (const item of call.prompt) {
|
|
57
|
+
if (item.kind === "instructions") {
|
|
58
|
+
instructions.push(...item.content.flatMap((part) => part.type === "text" ? [part.text] : []));
|
|
59
|
+
}
|
|
60
|
+
else if (item.kind === "tool-result") {
|
|
61
|
+
messages.push({
|
|
62
|
+
role: "toolResult",
|
|
63
|
+
toolCallId: item.toolCallId,
|
|
64
|
+
toolName: item.toolName,
|
|
65
|
+
isError: item.status !== "completed",
|
|
66
|
+
timestamp: Date.now(),
|
|
67
|
+
content: await content(item.content),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else if (item.role === "assistant") {
|
|
71
|
+
messages.push({
|
|
72
|
+
role: "assistant",
|
|
73
|
+
api: selected.api,
|
|
74
|
+
provider: selected.provider,
|
|
75
|
+
model: selected.id,
|
|
76
|
+
timestamp: Date.now(),
|
|
77
|
+
usage: emptyUsage(),
|
|
78
|
+
stopReason: item.content.some((p) => p.type === "tool-call")
|
|
79
|
+
? "toolUse"
|
|
80
|
+
: "stop",
|
|
81
|
+
content: item.content.flatMap((part) => part.type === "tool-call"
|
|
82
|
+
? [
|
|
83
|
+
{
|
|
84
|
+
type: "toolCall",
|
|
85
|
+
id: part.id,
|
|
86
|
+
name: part.name,
|
|
87
|
+
arguments: { ...part.args },
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
: part.type === "text"
|
|
91
|
+
? [{ type: "text", text: part.text }]
|
|
92
|
+
: []),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
else
|
|
96
|
+
messages.push({
|
|
97
|
+
role: "user",
|
|
98
|
+
content: await content(item.content),
|
|
99
|
+
timestamp: Date.now(),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const tools = call.tools.map((tool) => ({
|
|
103
|
+
name: tool.name,
|
|
104
|
+
description: tool.description ?? "",
|
|
105
|
+
parameters: { ...tool.inputSchema },
|
|
106
|
+
}));
|
|
107
|
+
const request = {
|
|
108
|
+
systemPrompt: instructions.join("\n"),
|
|
109
|
+
messages,
|
|
110
|
+
tools,
|
|
111
|
+
};
|
|
112
|
+
const secrets = projectSecrets(root);
|
|
113
|
+
// Publish only portable references, never the materialized provider image bytes.
|
|
114
|
+
context.reportPreparedCall?.({
|
|
115
|
+
adapter: "runtime.pi-ai",
|
|
116
|
+
call: scrub(call, secrets),
|
|
117
|
+
});
|
|
118
|
+
try {
|
|
119
|
+
const response = await registry.complete(selected, request, {
|
|
120
|
+
signal: context.signal,
|
|
121
|
+
temperature: call.model?.controls?.temperature,
|
|
122
|
+
maxTokens: call.model?.controls?.maxOutputTokens,
|
|
123
|
+
...(call.model?.config
|
|
124
|
+
? { samplingParams: { ...call.model.config } }
|
|
125
|
+
: {}),
|
|
126
|
+
});
|
|
127
|
+
context.signal.throwIfAborted();
|
|
128
|
+
if (response.stopReason === "error" ||
|
|
129
|
+
response.stopReason === "aborted" ||
|
|
130
|
+
response.stopReason === "deferred")
|
|
131
|
+
throw new Error(response.errorMessage ?? `Provider stopped: ${response.stopReason}`);
|
|
132
|
+
const output = [];
|
|
133
|
+
for (const part of response.content) {
|
|
134
|
+
if (part.type === "text")
|
|
135
|
+
output.push({ type: "text", text: part.text });
|
|
136
|
+
else if (part.type === "thinking")
|
|
137
|
+
output.push({ type: "reasoning", text: part.thinking });
|
|
138
|
+
else if (part.type === "toolCall")
|
|
139
|
+
output.push({
|
|
140
|
+
type: "tool-call",
|
|
141
|
+
id: part.id,
|
|
142
|
+
name: part.name,
|
|
143
|
+
args: part.arguments,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
output,
|
|
148
|
+
finishReason: response.stopReason === "toolUse"
|
|
149
|
+
? "tool-calls"
|
|
150
|
+
: response.stopReason === "length"
|
|
151
|
+
? "length"
|
|
152
|
+
: "stop",
|
|
153
|
+
usage: {
|
|
154
|
+
inputTokens: response.usage.input,
|
|
155
|
+
outputTokens: response.usage.output,
|
|
156
|
+
totalTokens: response.usage.totalTokens,
|
|
157
|
+
costUsd: response.usage.cost.total,
|
|
158
|
+
},
|
|
159
|
+
evidence: { resolvedModel: response.responseModel ?? selected.id },
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
throw new Error(String(scrub(error instanceof Error ? error.message : String(error), projectSecrets(root))));
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export function modelSelection(root = process.cwd()) {
|
|
4
|
+
try {
|
|
5
|
+
const value = JSON.parse(readFileSync(join(root, "config", "model.json"), "utf8"));
|
|
6
|
+
if (typeof value.provider === "string" &&
|
|
7
|
+
value.provider &&
|
|
8
|
+
typeof value.model === "string" &&
|
|
9
|
+
value.model &&
|
|
10
|
+
(value.custom === undefined || typeof value.custom.baseUrl === "string"))
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
/* Report one actionable setup error without file or credential contents. */
|
|
15
|
+
}
|
|
16
|
+
throw new Error("Run nylorun configure to connect a model provider.");
|
|
17
|
+
}
|
|
18
|
+
export function projectSecrets(root = process.cwd()) {
|
|
19
|
+
const values = Object.entries(process.env)
|
|
20
|
+
.filter(([key]) => /key|token|secret|password|credential/i.test(key))
|
|
21
|
+
.flatMap(([, value]) => (value ? [value] : []));
|
|
22
|
+
try {
|
|
23
|
+
const collect = (value) => {
|
|
24
|
+
if (typeof value === "string")
|
|
25
|
+
values.push(value);
|
|
26
|
+
else if (value && typeof value === "object")
|
|
27
|
+
Object.values(value).forEach(collect);
|
|
28
|
+
};
|
|
29
|
+
collect(JSON.parse(readFileSync(join(root, ".env", "auth.json"), "utf8")));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* The vault may not exist before setup. */
|
|
33
|
+
}
|
|
34
|
+
return values;
|
|
35
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CanonicalEvent } from "../adapters/journal.js";
|
|
2
|
+
/** Minimal truthful AG-UI projection from the same canonical events Studio displays. */
|
|
3
|
+
export declare function agUiEvents(events: readonly CanonicalEvent[], threadId: string, runId: string): readonly Record<string, unknown>[];
|
|
4
|
+
/**
|
|
5
|
+
* Preserve headers installed by the Hono host (notably loopback CORS) when a
|
|
6
|
+
* route returns a raw streaming Response instead of `context.json()`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function sse(events: readonly Record<string, unknown>[], inherited?: HeadersInit): Response;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Minimal truthful AG-UI projection from the same canonical events Studio displays. */
|
|
2
|
+
export function agUiEvents(events, threadId, runId) {
|
|
3
|
+
const output = [
|
|
4
|
+
{ type: "RUN_STARTED", threadId, runId },
|
|
5
|
+
];
|
|
6
|
+
for (const event of events) {
|
|
7
|
+
if (event.type === "tool.sealed") {
|
|
8
|
+
const calls = Array.isArray(event.payload.attributes
|
|
9
|
+
?.executable)
|
|
10
|
+
? event.payload.attributes
|
|
11
|
+
.executable
|
|
12
|
+
: [];
|
|
13
|
+
for (const call of calls) {
|
|
14
|
+
const id = typeof call.callId === "string" ? call.callId : undefined;
|
|
15
|
+
const name = typeof call.toolName === "string" ? call.toolName : undefined;
|
|
16
|
+
if (!id || !name)
|
|
17
|
+
continue;
|
|
18
|
+
output.push({ type: "TOOL_CALL_START", toolCallId: id, toolCallName: name }, {
|
|
19
|
+
type: "TOOL_CALL_ARGS",
|
|
20
|
+
toolCallId: id,
|
|
21
|
+
delta: JSON.stringify(call.args ?? {}),
|
|
22
|
+
}, { type: "TOOL_CALL_END", toolCallId: id });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else if (event.type === "tool.completed" &&
|
|
26
|
+
typeof event.payload.callId === "string") {
|
|
27
|
+
output.push({
|
|
28
|
+
type: "TOOL_CALL_RESULT",
|
|
29
|
+
messageId: `nylorun-${threadId}-${event.seq}`,
|
|
30
|
+
toolCallId: event.payload.callId,
|
|
31
|
+
content: JSON.stringify(event.payload.attributes ?? {
|
|
32
|
+
outcome: event.payload.outcome ?? "completed",
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
else if (event.type === "final" && event.payload.output !== undefined) {
|
|
37
|
+
const messageId = `nylorun-${threadId}-${event.seq}`;
|
|
38
|
+
const text = typeof event.payload.output === "string"
|
|
39
|
+
? event.payload.output
|
|
40
|
+
: JSON.stringify(event.payload.output);
|
|
41
|
+
output.push({ type: "TEXT_MESSAGE_START", messageId, role: "assistant" }, { type: "TEXT_MESSAGE_CONTENT", messageId, delta: text }, { type: "TEXT_MESSAGE_END", messageId });
|
|
42
|
+
}
|
|
43
|
+
else if (event.type === "error") {
|
|
44
|
+
output.push({
|
|
45
|
+
type: "RUN_ERROR",
|
|
46
|
+
message: typeof event.payload.message === "string"
|
|
47
|
+
? event.payload.message
|
|
48
|
+
: "Agent run failed.",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
output.push({ type: "RUN_FINISHED", threadId, runId });
|
|
53
|
+
return Object.freeze(output);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Preserve headers installed by the Hono host (notably loopback CORS) when a
|
|
57
|
+
* route returns a raw streaming Response instead of `context.json()`.
|
|
58
|
+
*/
|
|
59
|
+
export function sse(events, inherited) {
|
|
60
|
+
const headers = new Headers(inherited);
|
|
61
|
+
headers.set("content-type", "text/event-stream; charset=utf-8");
|
|
62
|
+
headers.set("cache-control", "no-cache");
|
|
63
|
+
headers.set("connection", "keep-alive");
|
|
64
|
+
return new Response(events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join(""), {
|
|
65
|
+
headers,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RuntimeEvent } from "../contracts.js";
|
|
2
|
+
/** Optional diagnostic enrichment; engines need not emit model diagnostics. */
|
|
3
|
+
export declare function observedPayload(event: RuntimeEvent): Record<string, unknown>;
|
|
4
|
+
export declare function modelRequestDigests(value: unknown): {
|
|
5
|
+
prompt: string;
|
|
6
|
+
tools: string;
|
|
7
|
+
model: string;
|
|
8
|
+
output: string;
|
|
9
|
+
configuration: string;
|
|
10
|
+
context: string;
|
|
11
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/** Optional diagnostic enrichment; engines need not emit model diagnostics. */
|
|
3
|
+
export function observedPayload(event) {
|
|
4
|
+
const attributes = record(event.attributes);
|
|
5
|
+
if (event.type !== "model.requested" || !record(attributes?.call))
|
|
6
|
+
return { ...event };
|
|
7
|
+
return { ...event, digests: modelRequestDigests(attributes) };
|
|
8
|
+
}
|
|
9
|
+
export function modelRequestDigests(value) {
|
|
10
|
+
const attributes = record(value) ?? {};
|
|
11
|
+
const call = record(attributes.call) ?? {};
|
|
12
|
+
const configuration = record(attributes.configuration) ?? {};
|
|
13
|
+
return {
|
|
14
|
+
prompt: digest(call.prompt),
|
|
15
|
+
tools: digest({ call: call.tools, contracts: configuration.toolContracts }),
|
|
16
|
+
model: digest(call.model ?? null),
|
|
17
|
+
output: digest(call.outputSchema ?? null),
|
|
18
|
+
configuration: digest(attributes.configuration),
|
|
19
|
+
context: digest(attributes.context),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function record(value) {
|
|
23
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
24
|
+
? value
|
|
25
|
+
: undefined;
|
|
26
|
+
}
|
|
27
|
+
function digest(value) {
|
|
28
|
+
return createHash("sha256").update(canonical(value)).digest("hex");
|
|
29
|
+
}
|
|
30
|
+
function canonical(value) {
|
|
31
|
+
if (Array.isArray(value))
|
|
32
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
33
|
+
if (value && typeof value === "object")
|
|
34
|
+
return `{${Object.entries(value)
|
|
35
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
36
|
+
.map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`)
|
|
37
|
+
.join(",")}}`;
|
|
38
|
+
return JSON.stringify(value) ?? "null";
|
|
39
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { RuntimeConfig } from "../config.js";
|
|
3
|
+
export declare function createRuntime(options: RuntimeConfig): Promise<Readonly<{
|
|
4
|
+
app: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
|
|
5
|
+
hasSession: (agentId: string, sessionId: string) => boolean;
|
|
6
|
+
close: () => Promise<void>;
|
|
7
|
+
}>>;
|
|
8
|
+
/** Host headers a loopback bind answers to, on the port actually in use. */
|
|
9
|
+
export declare function loopbackHosts(port: number): readonly string[];
|
|
10
|
+
/**
|
|
11
|
+
* Decide whether a request's Host header is served. Entries are exact
|
|
12
|
+
* `host:port` values or bare host names that accept any port; `*` accepts all.
|
|
13
|
+
*/
|
|
14
|
+
export declare function allowedHost(header: string | undefined, allowed: readonly string[]): boolean;
|