@niscorp/signal 0.1.0
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/DESIGN.md +139 -0
- package/DOCS.md +502 -0
- package/LICENSE +202 -0
- package/README.md +117 -0
- package/dist/index.cjs +1847 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +591 -0
- package/dist/index.d.ts +591 -0
- package/dist/index.js +1825 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1825 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// src/signal.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/estimate-usage.ts
|
|
6
|
+
var CHARS_PER_TOKEN = 4;
|
|
7
|
+
var PER_MESSAGE = 4;
|
|
8
|
+
var lengthOf = (content) => typeof content === "string" ? content.length : JSON.stringify(content ?? "").length;
|
|
9
|
+
var estimateUsage = (messages, content, toolCalls = []) => {
|
|
10
|
+
let inputTokens = 0;
|
|
11
|
+
for (const message of messages) inputTokens += PER_MESSAGE + Math.ceil(lengthOf(message.content) / CHARS_PER_TOKEN);
|
|
12
|
+
const emitted = content + toolCalls.map((call) => `${call.name}${JSON.stringify(call.args)}`).join("");
|
|
13
|
+
const outputTokens = Math.ceil(emitted.length / CHARS_PER_TOKEN);
|
|
14
|
+
return { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens, reported: false };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/errors.ts
|
|
18
|
+
var ErrorCode = {
|
|
19
|
+
PROVIDER_NOT_FOUND: "E_PROVIDER_NOT_FOUND",
|
|
20
|
+
MISSING_API_KEY: "E_MISSING_API_KEY",
|
|
21
|
+
MISSING_MODEL: "E_MISSING_MODEL",
|
|
22
|
+
MISSING_SDK: "E_MISSING_SDK",
|
|
23
|
+
VALIDATION_FAILED: "E_VALIDATION_FAILED",
|
|
24
|
+
MAX_RETRIES: "E_MAX_RETRIES",
|
|
25
|
+
MAX_ITERATIONS: "E_MAX_ITERATIONS",
|
|
26
|
+
PROVIDER_ERROR: "E_PROVIDER_ERROR",
|
|
27
|
+
TOOL_NOT_FOUND: "E_TOOL_NOT_FOUND",
|
|
28
|
+
TOOL_EXECUTION: "E_TOOL_EXECUTION",
|
|
29
|
+
TOOL_VALIDATION: "E_TOOL_VALIDATION",
|
|
30
|
+
VERB_NOT_SUPPORTED: "E_VERB_NOT_SUPPORTED"
|
|
31
|
+
};
|
|
32
|
+
var SignalError = class extends Error {
|
|
33
|
+
code;
|
|
34
|
+
context;
|
|
35
|
+
constructor(message, code, context) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "SignalError";
|
|
38
|
+
this.code = code;
|
|
39
|
+
this.context = context;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/registry.ts
|
|
44
|
+
var providerRegistry = {
|
|
45
|
+
groq: {
|
|
46
|
+
id: "groq",
|
|
47
|
+
baseUrl: "https://api.groq.com/openai/v1",
|
|
48
|
+
envKey: "GROQ_API_KEY",
|
|
49
|
+
defaultModel: "qwen/qwen3.8-27b",
|
|
50
|
+
endpoint: {
|
|
51
|
+
nativeTools: true,
|
|
52
|
+
nativeJsonMode: true,
|
|
53
|
+
// Groq validates tool args server-side (tool_use_failed 400s).
|
|
54
|
+
validatesToolArgs: true,
|
|
55
|
+
supportsEmbedding: false
|
|
56
|
+
},
|
|
57
|
+
adapter: "openai-compatible",
|
|
58
|
+
// tool_use_failed / json_validate_failed 400s carry the model's
|
|
59
|
+
// attempt in failed_generation — recover and route it.
|
|
60
|
+
wire: ["failed-generation"],
|
|
61
|
+
// Reasoning models stream their trace only when asked to parse it out;
|
|
62
|
+
// `reasoning_format` is rejected on the non-reasoning ones, which is why
|
|
63
|
+
// this is opt-in (see reasoningRequest above).
|
|
64
|
+
reasoningRequest: { reasoning_format: "parsed" }
|
|
65
|
+
},
|
|
66
|
+
openai: {
|
|
67
|
+
id: "openai",
|
|
68
|
+
baseUrl: "https://api.openai.com/v1",
|
|
69
|
+
envKey: "OPENAI_API_KEY",
|
|
70
|
+
defaultModel: "gpt-4o",
|
|
71
|
+
endpoint: {
|
|
72
|
+
nativeTools: true,
|
|
73
|
+
nativeJsonMode: true,
|
|
74
|
+
validatesToolArgs: false,
|
|
75
|
+
supportsEmbedding: true
|
|
76
|
+
},
|
|
77
|
+
adapter: "openai-compatible",
|
|
78
|
+
// 4o-era models occasionally emit JSONL where one value was asked
|
|
79
|
+
// for; the acceptance gate decides, so this can never misfire.
|
|
80
|
+
wire: ["jsonl-lines"]
|
|
81
|
+
},
|
|
82
|
+
openrouter: {
|
|
83
|
+
id: "openrouter",
|
|
84
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
85
|
+
envKey: "OPENROUTER_API_KEY",
|
|
86
|
+
defaultModel: "openai/gpt-4o",
|
|
87
|
+
endpoint: {
|
|
88
|
+
nativeTools: true,
|
|
89
|
+
nativeJsonMode: true,
|
|
90
|
+
validatesToolArgs: false,
|
|
91
|
+
supportsEmbedding: false
|
|
92
|
+
},
|
|
93
|
+
adapter: "openai-compatible",
|
|
94
|
+
// OpenRouter unifies reasoning across the models it routes; `enabled` turns
|
|
95
|
+
// the trace on for those that have one and is ignored by those that don't.
|
|
96
|
+
reasoningRequest: { reasoning: { enabled: true } }
|
|
97
|
+
},
|
|
98
|
+
anthropic: {
|
|
99
|
+
id: "anthropic",
|
|
100
|
+
baseUrl: "https://api.anthropic.com/v1",
|
|
101
|
+
envKey: "ANTHROPIC_API_KEY",
|
|
102
|
+
defaultModel: "claude-sonnet-4-20250514",
|
|
103
|
+
endpoint: {
|
|
104
|
+
nativeTools: true,
|
|
105
|
+
nativeJsonMode: true,
|
|
106
|
+
validatesToolArgs: false,
|
|
107
|
+
supportsEmbedding: false
|
|
108
|
+
},
|
|
109
|
+
adapter: "anthropic"
|
|
110
|
+
},
|
|
111
|
+
google: {
|
|
112
|
+
id: "google",
|
|
113
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
114
|
+
envKey: "GOOGLE_API_KEY",
|
|
115
|
+
defaultModel: "gemini-2.0-flash",
|
|
116
|
+
endpoint: {
|
|
117
|
+
nativeTools: true,
|
|
118
|
+
nativeJsonMode: true,
|
|
119
|
+
validatesToolArgs: false,
|
|
120
|
+
supportsEmbedding: false
|
|
121
|
+
},
|
|
122
|
+
adapter: "google"
|
|
123
|
+
},
|
|
124
|
+
typesafe: {
|
|
125
|
+
id: "typesafe",
|
|
126
|
+
baseUrl: "https://api.typesafe.ai/v1",
|
|
127
|
+
envKey: "TYPESAFE_API_KEY",
|
|
128
|
+
// The moving alias. A caller that records calibration against a model pins
|
|
129
|
+
// a version (`jev-1.13.0`) with `.model()`.
|
|
130
|
+
defaultModel: "jev-latest",
|
|
131
|
+
adapter: "systemone"
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
var modelRegistry = {
|
|
135
|
+
// ── groq ──
|
|
136
|
+
"groq/qwen/qwen3.8-27b": {
|
|
137
|
+
capabilities: { nativeJsonSchema: false, toolsWithStructuredOutput: false, manglesNestedToolArgs: true, multimodal: true },
|
|
138
|
+
// nested: 3 of 20 trials failed
|
|
139
|
+
reasoningEfforts: ["none", "default", "low", "medium", "high"],
|
|
140
|
+
verified: "2026-09-24"
|
|
141
|
+
},
|
|
142
|
+
"groq/openai/gpt-oss-120b": {
|
|
143
|
+
capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: false, manglesNestedToolArgs: true, multimodal: false },
|
|
144
|
+
// nested: 1 of 20 trials failed
|
|
145
|
+
reasoningEfforts: ["low", "medium", "high"],
|
|
146
|
+
verified: "2026-09-24"
|
|
147
|
+
},
|
|
148
|
+
"groq/openai/gpt-oss-20b": {
|
|
149
|
+
capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: false, manglesNestedToolArgs: true, multimodal: false },
|
|
150
|
+
// nested: 6 of 20 trials failed
|
|
151
|
+
reasoningEfforts: ["low", "medium", "high"],
|
|
152
|
+
verified: "2026-09-24"
|
|
153
|
+
},
|
|
154
|
+
// ── openai ──
|
|
155
|
+
"openai/gpt-4o": {
|
|
156
|
+
capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: true, manglesNestedToolArgs: false, multimodal: true },
|
|
157
|
+
// nested: 0 of 20 trials failed
|
|
158
|
+
reasoningEfforts: [],
|
|
159
|
+
verified: "2026-09-24"
|
|
160
|
+
},
|
|
161
|
+
"openai/gpt-4o-mini": {
|
|
162
|
+
capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: true, manglesNestedToolArgs: false, multimodal: true },
|
|
163
|
+
// nested: 0 of 20 trials failed
|
|
164
|
+
reasoningEfforts: [],
|
|
165
|
+
verified: "2026-09-24"
|
|
166
|
+
},
|
|
167
|
+
"openai/gpt-4.1-mini": {
|
|
168
|
+
capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: true, manglesNestedToolArgs: false, multimodal: true },
|
|
169
|
+
// nested: 0 of 20 trials failed
|
|
170
|
+
reasoningEfforts: [],
|
|
171
|
+
verified: "2026-09-24"
|
|
172
|
+
},
|
|
173
|
+
// ── openrouter ──
|
|
174
|
+
"openrouter/qwen/qwen3.8-27b": {
|
|
175
|
+
capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: true, manglesNestedToolArgs: false, multimodal: true },
|
|
176
|
+
// nested: 0 of 20 trials failed
|
|
177
|
+
reasoningEfforts: ["none", "minimal", "low", "medium", "high", "xhigh", "max"],
|
|
178
|
+
verified: "2026-09-24"
|
|
179
|
+
},
|
|
180
|
+
"openrouter/openai/gpt-4o": {
|
|
181
|
+
capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: true, manglesNestedToolArgs: false, multimodal: true },
|
|
182
|
+
// nested: 0 of 20 trials failed
|
|
183
|
+
reasoningEfforts: ["none", "minimal", "low", "medium", "high", "xhigh", "max"],
|
|
184
|
+
verified: "2026-09-24"
|
|
185
|
+
},
|
|
186
|
+
"openrouter/openai/gpt-4o-mini": {
|
|
187
|
+
capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: true, manglesNestedToolArgs: false, multimodal: true },
|
|
188
|
+
// nested: 0 of 20 trials failed
|
|
189
|
+
reasoningEfforts: ["none", "minimal", "low", "medium", "high", "xhigh", "max"],
|
|
190
|
+
verified: "2026-09-24"
|
|
191
|
+
},
|
|
192
|
+
"openrouter/openai/gpt-oss-120b": {
|
|
193
|
+
capabilities: { nativeJsonSchema: false, toolsWithStructuredOutput: true, manglesNestedToolArgs: false, multimodal: false },
|
|
194
|
+
// nested: 0 of 20 trials failed
|
|
195
|
+
reasoningEfforts: ["minimal", "low", "medium", "high", "xhigh", "max"],
|
|
196
|
+
verified: "2026-09-24"
|
|
197
|
+
},
|
|
198
|
+
"openrouter/z-ai/glm-5.2": {
|
|
199
|
+
capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: true, manglesNestedToolArgs: true, multimodal: false },
|
|
200
|
+
// nested: 1 of 20 trials failed
|
|
201
|
+
reasoningEfforts: ["none", "minimal", "low", "medium", "high", "xhigh", "max"],
|
|
202
|
+
verified: "2026-09-24"
|
|
203
|
+
},
|
|
204
|
+
"openrouter/openai/gpt-5.6-luna": {
|
|
205
|
+
capabilities: { nativeJsonSchema: true, toolsWithStructuredOutput: true, manglesNestedToolArgs: false, multimodal: true },
|
|
206
|
+
// nested: 0 of 20 trials failed
|
|
207
|
+
reasoningEfforts: ["none", "minimal", "low", "medium", "high", "xhigh", "max"],
|
|
208
|
+
verified: "2026-09-24"
|
|
209
|
+
},
|
|
210
|
+
"openrouter/openai/gpt-oss-20b": {
|
|
211
|
+
capabilities: { nativeJsonSchema: false, toolsWithStructuredOutput: true, manglesNestedToolArgs: false, multimodal: false },
|
|
212
|
+
// nested: 0 of 20 trials failed
|
|
213
|
+
reasoningEfforts: ["minimal", "low", "medium", "high", "xhigh", "max"],
|
|
214
|
+
verified: "2026-09-24"
|
|
215
|
+
},
|
|
216
|
+
"openrouter/meta-llama/llama-3.3-70b-instruct": {
|
|
217
|
+
capabilities: { nativeJsonSchema: false, toolsWithStructuredOutput: true, manglesNestedToolArgs: true, multimodal: false },
|
|
218
|
+
// nested: 9 of 20 trials failed
|
|
219
|
+
reasoningEfforts: ["none", "minimal", "low", "medium", "high", "xhigh", "max"],
|
|
220
|
+
verified: "2026-09-24"
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
var UNMEASURED_MODEL = {
|
|
224
|
+
nativeJsonSchema: false,
|
|
225
|
+
toolsWithStructuredOutput: false,
|
|
226
|
+
manglesNestedToolArgs: true,
|
|
227
|
+
multimodal: false
|
|
228
|
+
};
|
|
229
|
+
var modelKey = (provider, model) => `${provider}/${model}`;
|
|
230
|
+
var modelEntry = (provider, model) => modelRegistry[modelKey(provider, model)];
|
|
231
|
+
var resolveApiKey = (envKey, explicitKey) => explicitKey ?? (typeof process !== "undefined" ? process.env?.[envKey] : void 0);
|
|
232
|
+
var chatProviderEntry = (id) => {
|
|
233
|
+
const entry = providerRegistry[id];
|
|
234
|
+
return entry === void 0 || entry.adapter === "systemone" ? void 0 : entry;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// src/utils/sdk-loader.ts
|
|
238
|
+
var loadSdk = async (pkg) => {
|
|
239
|
+
try {
|
|
240
|
+
const mod = await import(
|
|
241
|
+
/* @vite-ignore */
|
|
242
|
+
pkg
|
|
243
|
+
);
|
|
244
|
+
return mod.default ?? mod;
|
|
245
|
+
} catch {
|
|
246
|
+
throw new SignalError(
|
|
247
|
+
`Missing dependency: ${pkg}. Install it with: pnpm add ${pkg}`,
|
|
248
|
+
ErrorCode.MISSING_SDK,
|
|
249
|
+
{ package: pkg }
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// src/adapters/openai-compatible.adapter.ts
|
|
255
|
+
var translateContentParts = (parts) => parts.map((part) => {
|
|
256
|
+
if (part.type === "text") return { type: "text", text: part.text };
|
|
257
|
+
if (part.type === "image") {
|
|
258
|
+
const url = part.source.type === "url" ? part.source.url : `data:${part.source.mediaType};base64,${part.source.data}`;
|
|
259
|
+
return { type: "image_url", image_url: { url } };
|
|
260
|
+
}
|
|
261
|
+
return { type: "text", text: "" };
|
|
262
|
+
});
|
|
263
|
+
var translateMessage = (msg) => {
|
|
264
|
+
switch (msg.role) {
|
|
265
|
+
case "system":
|
|
266
|
+
return { role: "system", content: msg.content };
|
|
267
|
+
case "user":
|
|
268
|
+
return {
|
|
269
|
+
role: "user",
|
|
270
|
+
content: typeof msg.content === "string" ? msg.content : translateContentParts(msg.content)
|
|
271
|
+
};
|
|
272
|
+
case "assistant": {
|
|
273
|
+
const out = { role: "assistant", content: msg.content };
|
|
274
|
+
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
275
|
+
out.tool_calls = msg.toolCalls.map((tc) => ({
|
|
276
|
+
id: tc.id,
|
|
277
|
+
type: "function",
|
|
278
|
+
function: { name: tc.name, arguments: tc.args }
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
case "tool":
|
|
284
|
+
return { role: "tool", content: msg.content, tool_call_id: msg.toolCallId };
|
|
285
|
+
default:
|
|
286
|
+
return { role: "user", content: "" };
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
var bindAt = (obj, path) => {
|
|
290
|
+
let parent;
|
|
291
|
+
let current = obj;
|
|
292
|
+
for (const key of path) {
|
|
293
|
+
if (!current || typeof current !== "object") return void 0;
|
|
294
|
+
parent = current;
|
|
295
|
+
current = current[key];
|
|
296
|
+
}
|
|
297
|
+
return typeof current === "function" ? current.bind(parent) : void 0;
|
|
298
|
+
};
|
|
299
|
+
var createSdkInstance = async (config) => {
|
|
300
|
+
const Sdk = await loadSdk("openai");
|
|
301
|
+
const Constructor = typeof Sdk === "function" ? Sdk : Sdk["default"];
|
|
302
|
+
return new Constructor({
|
|
303
|
+
apiKey: config.apiKey,
|
|
304
|
+
baseURL: config.baseUrl,
|
|
305
|
+
dangerouslyAllowBrowser: typeof globalThis.window !== "undefined"
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
var loadSdkFn = async (config, path, missing, strictClient) => {
|
|
309
|
+
if (config.client) {
|
|
310
|
+
const fn2 = bindAt(config.client, path);
|
|
311
|
+
if (fn2) return fn2;
|
|
312
|
+
if (strictClient) {
|
|
313
|
+
throw new SignalError(`Invalid client: expected client.${path.join(".")} to be a function`, ErrorCode.PROVIDER_ERROR);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const fn = bindAt(await createSdkInstance(config), path);
|
|
317
|
+
if (!fn) throw new SignalError(missing.message, missing.code);
|
|
318
|
+
return fn;
|
|
319
|
+
};
|
|
320
|
+
var loadChatCreateFn = (config) => loadSdkFn(config, ["chat", "completions", "create"], { message: "Failed to initialize OpenAI SDK", code: ErrorCode.MISSING_SDK }, true);
|
|
321
|
+
var loadEmbedCreateFn = (config) => loadSdkFn(
|
|
322
|
+
config,
|
|
323
|
+
["embeddings", "create"],
|
|
324
|
+
{ message: "Embedding is not supported by this provider or SDK", code: ErrorCode.PROVIDER_ERROR },
|
|
325
|
+
false
|
|
326
|
+
);
|
|
327
|
+
var createOpenAICompatibleAdapter = async (config) => {
|
|
328
|
+
const chatCreate = await loadChatCreateFn(config);
|
|
329
|
+
const buildParams = (request) => {
|
|
330
|
+
const params = {
|
|
331
|
+
model: request.model,
|
|
332
|
+
messages: request.messages.map(translateMessage)
|
|
333
|
+
};
|
|
334
|
+
if (request.responseFormat) {
|
|
335
|
+
params["response_format"] = request.responseFormat.type === "json_schema" ? { type: "json_schema", json_schema: request.responseFormat.jsonSchema } : { type: "json_object" };
|
|
336
|
+
}
|
|
337
|
+
if (request.tools?.length) params["tools"] = request.tools;
|
|
338
|
+
if (request.toolChoice !== void 0) {
|
|
339
|
+
params["tool_choice"] = typeof request.toolChoice === "string" ? request.toolChoice : { type: "function", function: { name: request.toolChoice.name } };
|
|
340
|
+
}
|
|
341
|
+
if (request.options?.temperature !== void 0) params["temperature"] = request.options.temperature;
|
|
342
|
+
if (request.options?.maxTokens !== void 0) params["max_completion_tokens"] = request.options.maxTokens;
|
|
343
|
+
if (request.options?.topP !== void 0) params["top_p"] = request.options.topP;
|
|
344
|
+
if (request.options?.stopSequences) params["stop"] = request.options.stopSequences;
|
|
345
|
+
if (request.options?.seed !== void 0) params["seed"] = request.options.seed;
|
|
346
|
+
if (request.options?.reasoningEffort !== void 0)
|
|
347
|
+
params["reasoning_effort"] = request.options.reasoningEffort;
|
|
348
|
+
return params;
|
|
349
|
+
};
|
|
350
|
+
const chat = async (request) => {
|
|
351
|
+
const params = buildParams(request);
|
|
352
|
+
let completion;
|
|
353
|
+
try {
|
|
354
|
+
completion = await chatCreate(params);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
throw new SignalError(
|
|
357
|
+
`Provider error: ${error instanceof Error ? error.message : String(error)}`,
|
|
358
|
+
ErrorCode.PROVIDER_ERROR,
|
|
359
|
+
{ raw: error }
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
const choice = completion.choices?.[0];
|
|
363
|
+
return {
|
|
364
|
+
content: choice?.message?.content ?? "",
|
|
365
|
+
toolCalls: choice?.message?.tool_calls?.map((tc) => ({
|
|
366
|
+
id: tc.id,
|
|
367
|
+
name: tc.function.name,
|
|
368
|
+
args: tc.function.arguments
|
|
369
|
+
})),
|
|
370
|
+
usage: {
|
|
371
|
+
inputTokens: completion.usage?.prompt_tokens ?? 0,
|
|
372
|
+
outputTokens: completion.usage?.completion_tokens ?? 0,
|
|
373
|
+
totalTokens: completion.usage?.total_tokens ?? 0
|
|
374
|
+
},
|
|
375
|
+
finishReason: choice?.finish_reason ?? "unknown",
|
|
376
|
+
raw: completion
|
|
377
|
+
};
|
|
378
|
+
};
|
|
379
|
+
async function* chatStream(request, options) {
|
|
380
|
+
const streamParams = {
|
|
381
|
+
...buildParams(request),
|
|
382
|
+
// Ask for the reasoning trace only when the caller wants reasoning — the
|
|
383
|
+
// opt-in that keeps `reasoning_format` off a plain Groq call. Stream-only:
|
|
384
|
+
// the non-stream `chat` path never sees these, so its content is unmoved.
|
|
385
|
+
...request.options?.reasoningEffort !== void 0 && config.reasoningRequest !== void 0 ? config.reasoningRequest : {},
|
|
386
|
+
stream: true,
|
|
387
|
+
stream_options: { include_usage: true }
|
|
388
|
+
};
|
|
389
|
+
let sseStream;
|
|
390
|
+
try {
|
|
391
|
+
sseStream = await chatCreate(streamParams, options);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
throw new SignalError(
|
|
394
|
+
`Provider stream error: ${error instanceof Error ? error.message : String(error)}`,
|
|
395
|
+
ErrorCode.PROVIDER_ERROR,
|
|
396
|
+
{ raw: error }
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
for await (const chunk of sseStream) {
|
|
401
|
+
const choice = chunk.choices?.[0];
|
|
402
|
+
if (choice?.delta?.content) {
|
|
403
|
+
yield { type: "text", text: choice.delta.content };
|
|
404
|
+
}
|
|
405
|
+
const reasoning = choice?.delta?.reasoning ?? choice?.delta?.reasoning_content;
|
|
406
|
+
if (reasoning) {
|
|
407
|
+
yield { type: "reasoning", text: reasoning };
|
|
408
|
+
}
|
|
409
|
+
if (choice?.delta?.tool_calls) {
|
|
410
|
+
for (const tc of choice.delta.tool_calls) {
|
|
411
|
+
yield {
|
|
412
|
+
type: "tool_call",
|
|
413
|
+
index: tc.index,
|
|
414
|
+
id: tc.id,
|
|
415
|
+
name: tc.function?.name,
|
|
416
|
+
argsFragment: tc.function?.arguments
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (chunk.usage) {
|
|
421
|
+
yield {
|
|
422
|
+
type: "usage",
|
|
423
|
+
inputTokens: chunk.usage.prompt_tokens ?? 0,
|
|
424
|
+
outputTokens: chunk.usage.completion_tokens ?? 0,
|
|
425
|
+
totalTokens: chunk.usage.total_tokens ?? 0
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
if (choice?.finish_reason) {
|
|
429
|
+
yield { type: "finish", finishReason: choice.finish_reason };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} catch (error) {
|
|
433
|
+
if (error instanceof SignalError) throw error;
|
|
434
|
+
throw new SignalError(
|
|
435
|
+
`Provider stream error: ${error instanceof Error ? error.message : String(error)}`,
|
|
436
|
+
ErrorCode.PROVIDER_ERROR,
|
|
437
|
+
{ raw: error }
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
let cachedEmbedCreate;
|
|
442
|
+
const embed = async (request) => {
|
|
443
|
+
if (!cachedEmbedCreate) cachedEmbedCreate = await loadEmbedCreateFn(config);
|
|
444
|
+
const params = {
|
|
445
|
+
model: request.model,
|
|
446
|
+
input: request.input
|
|
447
|
+
};
|
|
448
|
+
if (request.dimensions !== void 0) params["dimensions"] = request.dimensions;
|
|
449
|
+
let response;
|
|
450
|
+
try {
|
|
451
|
+
response = await cachedEmbedCreate(params);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
throw new SignalError(
|
|
454
|
+
`Embedding error: ${error instanceof Error ? error.message : String(error)}`,
|
|
455
|
+
ErrorCode.PROVIDER_ERROR,
|
|
456
|
+
{ raw: error }
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
const embeddings = (response.data ?? []).sort((a, b) => a.index - b.index).map((d) => d.embedding);
|
|
460
|
+
return {
|
|
461
|
+
embeddings,
|
|
462
|
+
usage: {
|
|
463
|
+
inputTokens: response.usage?.prompt_tokens ?? 0,
|
|
464
|
+
totalTokens: response.usage?.total_tokens ?? 0
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
};
|
|
468
|
+
return { kind: "chat", id: "openai-compatible", chat, chatStream, embed };
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
// src/adapters/anthropic.adapter.ts
|
|
472
|
+
var createAnthropicAdapter = async (_config) => {
|
|
473
|
+
throw new SignalError(
|
|
474
|
+
"Anthropic adapter is not yet implemented. Use openai-compatible with OpenRouter for Anthropic models.",
|
|
475
|
+
ErrorCode.PROVIDER_ERROR
|
|
476
|
+
);
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// src/adapters/google.adapter.ts
|
|
480
|
+
var createGoogleAdapter = async (_config) => {
|
|
481
|
+
throw new SignalError(
|
|
482
|
+
"Google adapter is not yet implemented. Use openai-compatible with OpenRouter for Google models.",
|
|
483
|
+
ErrorCode.PROVIDER_ERROR
|
|
484
|
+
);
|
|
485
|
+
};
|
|
486
|
+
var InjectedClientSchema = z.object({
|
|
487
|
+
fetch: z.custom((value) => typeof value === "function")
|
|
488
|
+
});
|
|
489
|
+
var BodySchema = z.object({
|
|
490
|
+
model: z.string().optional(),
|
|
491
|
+
answers: z.unknown(),
|
|
492
|
+
usage: z.object({ input_tokens: z.number(), output_tokens: z.number() }).optional()
|
|
493
|
+
});
|
|
494
|
+
var sendOf = (client) => {
|
|
495
|
+
if (client === void 0) return fetch;
|
|
496
|
+
const injected = InjectedClientSchema.safeParse(client);
|
|
497
|
+
if (!injected.success) {
|
|
498
|
+
throw new SignalError("Invalid client: expected client.fetch to be a function", ErrorCode.PROVIDER_ERROR);
|
|
499
|
+
}
|
|
500
|
+
return injected.data.fetch;
|
|
501
|
+
};
|
|
502
|
+
var jsonOrText = (text) => {
|
|
503
|
+
try {
|
|
504
|
+
return JSON.parse(text);
|
|
505
|
+
} catch {
|
|
506
|
+
return text;
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
var providerError = (message, raw) => new SignalError(`Provider error: ${message}`, ErrorCode.PROVIDER_ERROR, { raw });
|
|
510
|
+
var createSystemOneAdapter = async (config) => {
|
|
511
|
+
const send = sendOf(config.client);
|
|
512
|
+
const url = `${config.baseUrl.replace(/\/+$/, "")}/systemone`;
|
|
513
|
+
const decide = async (request, options) => {
|
|
514
|
+
let status;
|
|
515
|
+
let body;
|
|
516
|
+
try {
|
|
517
|
+
const response = await send(url, {
|
|
518
|
+
method: "POST",
|
|
519
|
+
headers: { authorization: `Bearer ${config.apiKey}`, "content-type": "application/json" },
|
|
520
|
+
body: JSON.stringify({ model: request.model, state: request.state, questions: request.questions }),
|
|
521
|
+
...options?.signal !== void 0 && { signal: options.signal }
|
|
522
|
+
});
|
|
523
|
+
status = response.status;
|
|
524
|
+
body = jsonOrText(await response.text());
|
|
525
|
+
} catch (error) {
|
|
526
|
+
throw providerError(error instanceof Error ? error.message : String(error), error);
|
|
527
|
+
}
|
|
528
|
+
if (status < 200 || status >= 300) throw providerError(`HTTP ${status}`, { status, body });
|
|
529
|
+
const parsed = BodySchema.safeParse(body);
|
|
530
|
+
if (!parsed.success) return { answers: void 0, raw: body };
|
|
531
|
+
return {
|
|
532
|
+
...parsed.data.model !== void 0 && { model: parsed.data.model },
|
|
533
|
+
answers: parsed.data.answers,
|
|
534
|
+
...parsed.data.usage !== void 0 && {
|
|
535
|
+
usage: { inputTokens: parsed.data.usage.input_tokens, outputTokens: parsed.data.usage.output_tokens }
|
|
536
|
+
},
|
|
537
|
+
raw: body
|
|
538
|
+
};
|
|
539
|
+
};
|
|
540
|
+
return { kind: "decisions", id: "systemone", decide };
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
// src/transport/protocol.ts
|
|
544
|
+
var RESPOND_DESCRIPTION = "Finish the task. Call this EXACTLY ONCE, ALONE in its own turn, with your final envelope: { response?: string (human-facing text), data (the payload), reasoning?: string (why) }. Calling this ends the run.";
|
|
545
|
+
var EMIT_WITH_TOOLS = (shape, note) => `FINISH PROTOCOL: Use your tools as needed. To finish, your ENTIRE final message must be ONLY the JSON envelope ${shape} \u2014 raw JSON, no prose around it, no code fences.${note} A message without tool calls is treated as your final envelope.`;
|
|
546
|
+
var EMIT_TOOLLESS = (shape, note) => `FINISH PROTOCOL: Your ENTIRE final message must be ONLY the JSON envelope ${shape} \u2014 raw JSON, no prose around it, no code fences.${note}`;
|
|
547
|
+
var NATIVE = (shape, note) => `FINISH PROTOCOL: To finish, reply with no tool calls; your reply is the JSON envelope ${shape}.${note}`;
|
|
548
|
+
var RESPOND = (shape, note) => `FINISH PROTOCOL: To finish, call the \`respond\` tool ALONE in its own turn with the envelope ${shape} as its arguments \u2014 or reply with ONLY that JSON envelope as your entire message.${note}`;
|
|
549
|
+
var envelopeShape = (spec) => {
|
|
550
|
+
if (!spec.hasData) return '{ "reasoning"?: string, "response": string }';
|
|
551
|
+
return spec.responseMode === "required" ? '{ "reasoning"?: string, "response": string, "data": <the payload> }' : '{ "reasoning"?: string, "response"?: string, "data": <the payload> }';
|
|
552
|
+
};
|
|
553
|
+
var finishProtocol = (spec) => {
|
|
554
|
+
const shape = envelopeShape(spec);
|
|
555
|
+
const note = spec.hasData && spec.injectSchemaDoc ? " `data` must match the OUTPUT SCHEMA above." : "";
|
|
556
|
+
switch (spec.transport) {
|
|
557
|
+
case "emit":
|
|
558
|
+
return spec.hasTools ? EMIT_WITH_TOOLS(shape, note) : EMIT_TOOLLESS(shape, note);
|
|
559
|
+
case "native":
|
|
560
|
+
return NATIVE(shape, note);
|
|
561
|
+
case "respond":
|
|
562
|
+
return RESPOND(shape, note);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
var RESPOND_TERMINATION = "You did not finish. Either call the `respond` tool with your final envelope ({ response?, data, reasoning? }), or reply with ONLY that JSON envelope as your entire message \u2014 no surrounding prose.";
|
|
566
|
+
var RESPOND_INVALID = "Your output was invalid: {issues}. Either call `respond`, or reply with ONLY the corrected JSON envelope \u2014 no prose.";
|
|
567
|
+
var CONTENT_TERMINATION = "You did not finish. Reply with ONLY the JSON envelope ({ response?, data, reasoning? }) as your entire message \u2014 no surrounding prose, no code fences.";
|
|
568
|
+
var CONTENT_INVALID = "Your output was invalid: {issues}. Reply with ONLY the corrected JSON envelope as your entire message.";
|
|
569
|
+
var corrections = (transport) => transport === "respond" ? { termination: RESPOND_TERMINATION, invalidOutput: RESPOND_INVALID } : { termination: CONTENT_TERMINATION, invalidOutput: CONTENT_INVALID };
|
|
570
|
+
var bareSchemaPrompt = (jsonSchema) => `OUTPUT SCHEMA: your final reply must be ONLY a JSON value matching this schema \u2014 raw JSON, no prose, no code fences:
|
|
571
|
+
${jsonSchema}`;
|
|
572
|
+
var bareSchemaCorrection = (issues) => `Your output was invalid: ${issues}. Reply with ONLY the corrected JSON value matching the OUTPUT SCHEMA.`;
|
|
573
|
+
var decisionEmulationPrompt = (state) => `Answer every field of the OUTPUT SCHEMA about the STATE below. Each field's description is the question and lists everything you may answer with.
|
|
574
|
+
STATE:
|
|
575
|
+
${state}`;
|
|
576
|
+
var decisionQuestionDescription = (question) => {
|
|
577
|
+
if (question.type === "choice") return `${question.instructions} Options: ${JSON.stringify(question.criteria)}`;
|
|
578
|
+
if (question.type === "score") {
|
|
579
|
+
return `${question.instructions} Answer with the index of the level that fits. Levels, lowest first: ${JSON.stringify(question.criteria)}`;
|
|
580
|
+
}
|
|
581
|
+
return question.criteria === void 0 ? question.instructions : `${question.instructions} Meaning: ${JSON.stringify(question.criteria)}`;
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// src/decide/gate.ts
|
|
585
|
+
var probability = z.number().min(0).max(1);
|
|
586
|
+
var enumOf = (name, options) => {
|
|
587
|
+
const [first, ...rest] = options;
|
|
588
|
+
if (first === void 0) {
|
|
589
|
+
throw new SignalError(`Question "${name}" offers nothing to answer with`, ErrorCode.VALIDATION_FAILED, { question: name });
|
|
590
|
+
}
|
|
591
|
+
return z.enum([first, ...rest]);
|
|
592
|
+
};
|
|
593
|
+
var levelKeysOf = (question) => question.criteria.map((_, level) => String(level));
|
|
594
|
+
var mostProbable = (probabilities) => probabilities.reduce((best, value, level) => value > (probabilities[best] ?? 0) ? level : best, 0);
|
|
595
|
+
var calibratedGate = (name, question) => {
|
|
596
|
+
if (question.type === "choice") {
|
|
597
|
+
const option = enumOf(name, Object.keys(question.criteria));
|
|
598
|
+
return z.object({ choice: option, probabilities: z.record(option, probability), confidence: probability });
|
|
599
|
+
}
|
|
600
|
+
if (question.type === "score") {
|
|
601
|
+
const keys = levelKeysOf(question);
|
|
602
|
+
const level = enumOf(name, keys);
|
|
603
|
+
return z.object({
|
|
604
|
+
score: z.number().min(0).max(keys.length - 1),
|
|
605
|
+
probabilities: z.record(level, probability),
|
|
606
|
+
confidence: probability
|
|
607
|
+
}).transform((answer) => {
|
|
608
|
+
const probabilities = keys.map((key) => answer.probabilities[key] ?? 0);
|
|
609
|
+
return { level: mostProbable(probabilities), score: answer.score, probabilities, confidence: answer.confidence };
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
return z.object({ noul: probability }).transform((answer) => ({ answer: answer.noul >= 0.5, noul: answer.noul }));
|
|
613
|
+
};
|
|
614
|
+
var uncalibratedField = (name, question) => {
|
|
615
|
+
const description = decisionQuestionDescription(question);
|
|
616
|
+
if (question.type === "choice") return enumOf(name, Object.keys(question.criteria)).describe(description);
|
|
617
|
+
if (question.type === "score") return z.number().int().min(0).max(question.criteria.length - 1).describe(description);
|
|
618
|
+
return z.boolean().describe(description);
|
|
619
|
+
};
|
|
620
|
+
var uncalibratedDecision = (question, value) => {
|
|
621
|
+
if (question.type === "choice") return { choice: value };
|
|
622
|
+
if (question.type === "score") return { level: value };
|
|
623
|
+
return { answer: value };
|
|
624
|
+
};
|
|
625
|
+
var assertAsked = (questions) => {
|
|
626
|
+
if (Object.keys(questions).length === 0) {
|
|
627
|
+
throw new SignalError("decide() needs at least one question", ErrorCode.VALIDATION_FAILED);
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
var answersEvery = (questions, decisions) => Object.keys(questions).every((name) => name in decisions);
|
|
631
|
+
var rejected = (issues, raw) => new SignalError(`Decision rejected: ${issues.join("; ")}`, ErrorCode.VALIDATION_FAILED, { issues, raw });
|
|
632
|
+
var issuesOf = (name, error) => error.issues.map((issue) => `${[name, ...issue.path].join(".")}: ${issue.message}`);
|
|
633
|
+
var assertQuestions = (questions) => {
|
|
634
|
+
assertAsked(questions);
|
|
635
|
+
for (const [name, question] of Object.entries(questions)) calibratedGate(name, question);
|
|
636
|
+
};
|
|
637
|
+
var acceptCalibrated = (questions, answers, raw) => {
|
|
638
|
+
const arrived = z.record(z.string(), z.unknown()).safeParse(answers);
|
|
639
|
+
if (!arrived.success) throw rejected(["the provider returned no answers"], raw);
|
|
640
|
+
const decisions = {};
|
|
641
|
+
const issues = [];
|
|
642
|
+
for (const [name, question] of Object.entries(questions)) {
|
|
643
|
+
if (!(name in arrived.data)) {
|
|
644
|
+
issues.push(`${name}: not answered`);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
const gated = calibratedGate(name, question).safeParse(arrived.data[name]);
|
|
648
|
+
if (gated.success) decisions[name] = gated.data;
|
|
649
|
+
else issues.push(...issuesOf(name, gated.error));
|
|
650
|
+
}
|
|
651
|
+
if (issues.length > 0 || !answersEvery(questions, decisions)) throw rejected(issues, raw);
|
|
652
|
+
return decisions;
|
|
653
|
+
};
|
|
654
|
+
var uncalibratedSchemaOf = (questions) => z.strictObject(
|
|
655
|
+
Object.fromEntries(Object.entries(questions).map(([name, question]) => [name, uncalibratedField(name, question)]))
|
|
656
|
+
);
|
|
657
|
+
var acceptUncalibrated = (questions, value, raw) => {
|
|
658
|
+
const gated = uncalibratedSchemaOf(questions).safeParse(value);
|
|
659
|
+
if (!gated.success) throw rejected(issuesOf("decisions", gated.error), raw);
|
|
660
|
+
const decisions = {};
|
|
661
|
+
for (const [name, question] of Object.entries(questions)) decisions[name] = uncalibratedDecision(question, gated.data[name]);
|
|
662
|
+
if (!answersEvery(questions, decisions)) throw rejected(["a question went unanswered"], raw);
|
|
663
|
+
return decisions;
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// src/stream/execute-step-stream.ts
|
|
667
|
+
var assembleToolCall = (map, delta) => {
|
|
668
|
+
let tc = map.get(delta.index);
|
|
669
|
+
if (!tc) {
|
|
670
|
+
tc = { id: delta.id ?? "", name: delta.name ?? "", args: "" };
|
|
671
|
+
map.set(delta.index, tc);
|
|
672
|
+
}
|
|
673
|
+
if (delta.id) tc.id = delta.id;
|
|
674
|
+
if (delta.name) tc.name = delta.name;
|
|
675
|
+
if (delta.argsFragment) tc.args += delta.argsFragment;
|
|
676
|
+
};
|
|
677
|
+
var buildProviderRequest = (model, request) => {
|
|
678
|
+
const providerTools = request.tools && request.tools.length > 0 ? request.tools.map((tool) => ({
|
|
679
|
+
type: "function",
|
|
680
|
+
function: {
|
|
681
|
+
name: tool.name,
|
|
682
|
+
description: tool.description,
|
|
683
|
+
parameters: tool.parameters
|
|
684
|
+
}
|
|
685
|
+
})) : void 0;
|
|
686
|
+
return {
|
|
687
|
+
model,
|
|
688
|
+
messages: request.messages.slice(),
|
|
689
|
+
...providerTools && { tools: providerTools },
|
|
690
|
+
...request.toolChoice !== void 0 && { toolChoice: request.toolChoice },
|
|
691
|
+
...request.responseFormat !== void 0 && { responseFormat: request.responseFormat },
|
|
692
|
+
...request.options !== void 0 && { options: request.options }
|
|
693
|
+
};
|
|
694
|
+
};
|
|
695
|
+
async function* executeStepStream(config) {
|
|
696
|
+
const providerRequest = buildProviderRequest(config.model, config.request);
|
|
697
|
+
const abortSignal = config.streamOptions?.signal;
|
|
698
|
+
let contentBuffer = "";
|
|
699
|
+
let finishReason = "unknown";
|
|
700
|
+
const assembledToolCalls = /* @__PURE__ */ new Map();
|
|
701
|
+
let totalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0, reported: false };
|
|
702
|
+
try {
|
|
703
|
+
for await (const delta of config.adapter.chatStream(providerRequest, abortSignal ? { signal: abortSignal } : void 0)) {
|
|
704
|
+
if (abortSignal?.aborted) return;
|
|
705
|
+
switch (delta.type) {
|
|
706
|
+
case "text":
|
|
707
|
+
contentBuffer += delta.text;
|
|
708
|
+
yield { type: "text", text: delta.text };
|
|
709
|
+
break;
|
|
710
|
+
// Reasoning is passed straight through and deliberately NOT added to
|
|
711
|
+
// contentBuffer: it is the model thinking, not its answer.
|
|
712
|
+
case "reasoning":
|
|
713
|
+
yield { type: "reasoning", text: delta.text };
|
|
714
|
+
break;
|
|
715
|
+
case "tool_call":
|
|
716
|
+
assembleToolCall(assembledToolCalls, delta);
|
|
717
|
+
yield {
|
|
718
|
+
type: "tool_call_delta",
|
|
719
|
+
index: delta.index,
|
|
720
|
+
...delta.id !== void 0 && { id: delta.id },
|
|
721
|
+
...delta.name !== void 0 && { name: delta.name },
|
|
722
|
+
argsText: delta.argsFragment ?? ""
|
|
723
|
+
};
|
|
724
|
+
break;
|
|
725
|
+
case "usage":
|
|
726
|
+
totalUsage = {
|
|
727
|
+
inputTokens: delta.inputTokens,
|
|
728
|
+
outputTokens: delta.outputTokens,
|
|
729
|
+
totalTokens: delta.totalTokens,
|
|
730
|
+
reported: true
|
|
731
|
+
};
|
|
732
|
+
break;
|
|
733
|
+
case "finish":
|
|
734
|
+
finishReason = delta.finishReason;
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
} catch (error) {
|
|
739
|
+
if (abortSignal?.aborted) return;
|
|
740
|
+
throw error;
|
|
741
|
+
}
|
|
742
|
+
const toolCalls = [...assembledToolCalls.values()].map((tc) => {
|
|
743
|
+
let parsed = tc.args;
|
|
744
|
+
if (typeof tc.args === "string" && tc.args.length > 0) {
|
|
745
|
+
try {
|
|
746
|
+
parsed = JSON.parse(tc.args);
|
|
747
|
+
} catch {
|
|
748
|
+
parsed = tc.args;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return { id: tc.id, name: tc.name, args: parsed };
|
|
752
|
+
});
|
|
753
|
+
const result = {
|
|
754
|
+
content: contentBuffer,
|
|
755
|
+
toolCalls,
|
|
756
|
+
usage: totalUsage.reported ? totalUsage : estimateUsage(providerRequest.messages, contentBuffer, toolCalls),
|
|
757
|
+
finishReason,
|
|
758
|
+
raw: null
|
|
759
|
+
};
|
|
760
|
+
yield { type: "done", result };
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// src/output-trust.ts
|
|
764
|
+
var asOutput = (content) => content;
|
|
765
|
+
|
|
766
|
+
// src/run.ts
|
|
767
|
+
var MAX_TOOL_TURNS = 10;
|
|
768
|
+
var ZERO = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
769
|
+
var stringify = (value) => {
|
|
770
|
+
if (typeof value === "string") return value;
|
|
771
|
+
try {
|
|
772
|
+
return JSON.stringify(value) ?? String(value);
|
|
773
|
+
} catch {
|
|
774
|
+
return String(value);
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
var toJsonSchema = (schema) => schema["~standard"].jsonSchema.output({ target: "draft-07" });
|
|
778
|
+
var buildRequest = (spec, caps) => {
|
|
779
|
+
const tools = spec.tools ?? [];
|
|
780
|
+
if (tools.length > 0 && !caps.nativeTools) {
|
|
781
|
+
throw new SignalError(
|
|
782
|
+
"This provider has no native tool calling (capabilities.nativeTools) \u2014 tools are unavailable here.",
|
|
783
|
+
ErrorCode.PROVIDER_ERROR
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
const descriptors = tools.map((tool) => ({
|
|
787
|
+
name: tool.name,
|
|
788
|
+
description: tool.description,
|
|
789
|
+
parameters: toJsonSchema(tool.inputSchema)
|
|
790
|
+
}));
|
|
791
|
+
const grammarViable = spec.schema !== void 0 && caps.nativeJsonSchema && (tools.length === 0 || caps.toolsWithStructuredOutput);
|
|
792
|
+
return {
|
|
793
|
+
...descriptors.length > 0 && { tools: descriptors },
|
|
794
|
+
...spec.schema && { output: { accept: spec.schema } },
|
|
795
|
+
...grammarViable && spec.schema ? {
|
|
796
|
+
responseFormat: {
|
|
797
|
+
type: "json_schema",
|
|
798
|
+
jsonSchema: { name: "output", strict: false, schema: toJsonSchema(spec.schema) }
|
|
799
|
+
}
|
|
800
|
+
} : spec.schema !== void 0 && caps.nativeJsonMode && tools.length === 0 ? { responseFormat: { type: "json_object" } } : {},
|
|
801
|
+
...spec.options && { options: spec.options }
|
|
802
|
+
};
|
|
803
|
+
};
|
|
804
|
+
var buildMeta = (model, usage, start, retries, toolCalls, raws) => ({
|
|
805
|
+
model,
|
|
806
|
+
usage,
|
|
807
|
+
durationMs: Date.now() - start,
|
|
808
|
+
retries,
|
|
809
|
+
toolCalls,
|
|
810
|
+
provider: { raw: raws.length === 1 ? raws[0] : raws, errors: [] }
|
|
811
|
+
});
|
|
812
|
+
async function* runStream(spec, deps) {
|
|
813
|
+
const start = Date.now();
|
|
814
|
+
const requestBase = buildRequest(spec, deps.capabilities);
|
|
815
|
+
const toolMap = new Map((spec.tools ?? []).map((tool) => [tool.name, tool]));
|
|
816
|
+
const messages = [...spec.messages];
|
|
817
|
+
if (spec.schema && requestBase.responseFormat?.type !== "json_schema") {
|
|
818
|
+
messages.push({ role: "system", content: bareSchemaPrompt(JSON.stringify(toJsonSchema(spec.schema))) });
|
|
819
|
+
}
|
|
820
|
+
let usage = ZERO;
|
|
821
|
+
let retries = 0;
|
|
822
|
+
const records = [];
|
|
823
|
+
const raws = [];
|
|
824
|
+
for (let turn = 0; turn < MAX_TOOL_TURNS + spec.retries; turn += 1) {
|
|
825
|
+
let result;
|
|
826
|
+
for await (const event of deps.stepStream({ ...requestBase, messages }, spec.streamOptions)) {
|
|
827
|
+
if (event.type === "text") yield { type: "text", text: event.text };
|
|
828
|
+
if (event.type === "done") result = event.result;
|
|
829
|
+
}
|
|
830
|
+
if (!result) throw new SignalError("stream ended without a done event", ErrorCode.PROVIDER_ERROR);
|
|
831
|
+
usage = {
|
|
832
|
+
inputTokens: usage.inputTokens + result.usage.inputTokens,
|
|
833
|
+
outputTokens: usage.outputTokens + result.usage.outputTokens,
|
|
834
|
+
totalTokens: usage.totalTokens + result.usage.totalTokens
|
|
835
|
+
};
|
|
836
|
+
raws.push(result.raw);
|
|
837
|
+
const calls = result.outcome?.kind === "tool_calls" ? result.outcome.calls : spec.schema ? [] : result.toolCalls;
|
|
838
|
+
if (calls.length > 0) {
|
|
839
|
+
messages.push({
|
|
840
|
+
role: "assistant",
|
|
841
|
+
content: result.content,
|
|
842
|
+
toolCalls: calls.map((call) => ({ id: call.id, name: call.name, args: stringify(call.args) }))
|
|
843
|
+
});
|
|
844
|
+
for (const call of calls) {
|
|
845
|
+
spec.onToolCall?.(call.name, call.args);
|
|
846
|
+
yield { type: "tool_start", name: call.name, args: call.args };
|
|
847
|
+
const tool = toolMap.get(call.name);
|
|
848
|
+
const parsed = tool?.inputSchema.safeParse(call.args);
|
|
849
|
+
const started = Date.now();
|
|
850
|
+
let output2;
|
|
851
|
+
if (!tool) output2 = `error: unknown tool "${call.name}"`;
|
|
852
|
+
else if (parsed && !parsed.success) {
|
|
853
|
+
output2 = `error: input_invalid: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`;
|
|
854
|
+
} else {
|
|
855
|
+
try {
|
|
856
|
+
output2 = await tool.execute(parsed?.data);
|
|
857
|
+
} catch (cause) {
|
|
858
|
+
output2 = `error: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
records.push({ name: call.name, args: call.args, result: output2, durationMs: Date.now() - started });
|
|
862
|
+
yield { type: "tool_end", name: call.name, result: output2 };
|
|
863
|
+
messages.push({ role: "tool", toolCallId: call.id, name: call.name, content: stringify(output2) });
|
|
864
|
+
}
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
if (!spec.schema) {
|
|
868
|
+
const history = [...messages, { role: "assistant", content: result.content }];
|
|
869
|
+
yield {
|
|
870
|
+
type: "done",
|
|
871
|
+
response: asOutput(result.content),
|
|
872
|
+
history,
|
|
873
|
+
meta: buildMeta(deps.model, usage, start, retries, records, raws)
|
|
874
|
+
};
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (result.outcome?.kind === "output") {
|
|
878
|
+
const parsed = spec.schema.safeParse(result.outcome.value);
|
|
879
|
+
if (parsed.success) {
|
|
880
|
+
const history = [...messages, { role: "assistant", content: result.content }];
|
|
881
|
+
yield {
|
|
882
|
+
type: "done",
|
|
883
|
+
response: parsed.data,
|
|
884
|
+
history,
|
|
885
|
+
meta: buildMeta(deps.model, usage, start, retries, records, raws)
|
|
886
|
+
};
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
const evidence = result.outcome?.kind === "failed" ? result.outcome.evidence : "output failed schema validation";
|
|
891
|
+
retries += 1;
|
|
892
|
+
const error = new SignalError(`Response failed schema validation: ${evidence}`, ErrorCode.VALIDATION_FAILED);
|
|
893
|
+
spec.onRetry?.(error, retries);
|
|
894
|
+
if (retries > spec.retries) {
|
|
895
|
+
yield { type: "error", error, recovered: false };
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
yield { type: "retry", reason: evidence, attempt: retries };
|
|
899
|
+
if (result.content.length > 0) messages.push({ role: "assistant", content: result.content });
|
|
900
|
+
messages.push({ role: "system", content: bareSchemaCorrection(evidence) });
|
|
901
|
+
}
|
|
902
|
+
throw new SignalError("Max tool iterations reached", ErrorCode.MAX_ITERATIONS);
|
|
903
|
+
}
|
|
904
|
+
var runComplete = async (spec, deps) => {
|
|
905
|
+
for await (const event of runStream(spec, deps)) {
|
|
906
|
+
if (event.type === "done") return { response: event.response, history: event.history, meta: event.meta };
|
|
907
|
+
if (event.type === "error") throw event.error;
|
|
908
|
+
}
|
|
909
|
+
throw new SignalError("run ended without a result", ErrorCode.PROVIDER_ERROR);
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
// src/wire/repair.ts
|
|
913
|
+
var INVISIBLES = /[]/g;
|
|
914
|
+
var tryParse = (text) => {
|
|
915
|
+
try {
|
|
916
|
+
return { ok: true, value: JSON.parse(text) };
|
|
917
|
+
} catch {
|
|
918
|
+
return { ok: false };
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
var spanCandidates = (text, open, close) => {
|
|
922
|
+
const first = text.indexOf(open);
|
|
923
|
+
if (first < 0) return [];
|
|
924
|
+
const candidates = [];
|
|
925
|
+
for (let last = text.lastIndexOf(close); last > first; last = text.lastIndexOf(close, last - 1)) {
|
|
926
|
+
candidates.push(text.slice(first, last + 1));
|
|
927
|
+
if (candidates.length >= 8) break;
|
|
928
|
+
}
|
|
929
|
+
return candidates;
|
|
930
|
+
};
|
|
931
|
+
var extractJson = (content) => {
|
|
932
|
+
const cleaned = content.replace(INVISIBLES, "").trim();
|
|
933
|
+
if (cleaned.length === 0) return { ok: false };
|
|
934
|
+
const direct = tryParse(cleaned);
|
|
935
|
+
if (direct.ok) return direct;
|
|
936
|
+
const fence = cleaned.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
937
|
+
const body = fence?.[1] !== void 0 ? fence[1].trim() : cleaned;
|
|
938
|
+
if (body !== cleaned) {
|
|
939
|
+
const fenced = tryParse(body);
|
|
940
|
+
if (fenced.ok) return fenced;
|
|
941
|
+
}
|
|
942
|
+
for (const candidate of [...spanCandidates(body, "{", "}"), ...spanCandidates(body, "[", "]")]) {
|
|
943
|
+
const parsed = tryParse(candidate);
|
|
944
|
+
if (parsed.ok) return parsed;
|
|
945
|
+
}
|
|
946
|
+
return { ok: false };
|
|
947
|
+
};
|
|
948
|
+
var repairEscapeDamage = (text) => {
|
|
949
|
+
const candidates = [];
|
|
950
|
+
const noQuoteEscapes = text.replace(/\\'/g, "'");
|
|
951
|
+
if (noQuoteEscapes !== text) candidates.push(noQuoteEscapes);
|
|
952
|
+
const unescaped = text.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
953
|
+
if (unescaped !== text) candidates.push(unescaped);
|
|
954
|
+
return candidates;
|
|
955
|
+
};
|
|
956
|
+
var decodeJsonish = (value) => {
|
|
957
|
+
if (typeof value !== "string") return value;
|
|
958
|
+
const trimmed = value.trim();
|
|
959
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return value;
|
|
960
|
+
const direct = tryParse(trimmed);
|
|
961
|
+
if (direct.ok) return direct.value;
|
|
962
|
+
for (const repaired of repairEscapeDamage(trimmed)) {
|
|
963
|
+
const parsed = tryParse(repaired);
|
|
964
|
+
if (parsed.ok) return parsed.value;
|
|
965
|
+
}
|
|
966
|
+
return value;
|
|
967
|
+
};
|
|
968
|
+
var deepDecodeJsonish = (value) => {
|
|
969
|
+
if (typeof value === "string") {
|
|
970
|
+
const decoded = decodeJsonish(value);
|
|
971
|
+
return decoded === value ? value : deepDecodeJsonish(decoded);
|
|
972
|
+
}
|
|
973
|
+
if (Array.isArray(value)) return value.map(deepDecodeJsonish);
|
|
974
|
+
if (typeof value === "object" && value !== null) {
|
|
975
|
+
const out = {};
|
|
976
|
+
for (const [key, entry] of Object.entries(value)) out[key] = deepDecodeJsonish(entry);
|
|
977
|
+
return out;
|
|
978
|
+
}
|
|
979
|
+
return value;
|
|
980
|
+
};
|
|
981
|
+
var LITERAL_CHARS = /[0-9a-zA-Z.+\-]/;
|
|
982
|
+
var isCompleteLiteral = (buf) => {
|
|
983
|
+
if (buf === "true" || buf === "false" || buf === "null") return true;
|
|
984
|
+
return buf.length > 0 && !Number.isNaN(Number(buf));
|
|
985
|
+
};
|
|
986
|
+
var scan = (text) => {
|
|
987
|
+
const stack = [];
|
|
988
|
+
let inString = false;
|
|
989
|
+
let escape = false;
|
|
990
|
+
let stringIsValue = false;
|
|
991
|
+
let literal = "";
|
|
992
|
+
let boundary;
|
|
993
|
+
let started = false;
|
|
994
|
+
const closers = () => [...stack].reverse().map((frame) => frame.type === "obj" ? "}" : "]").join("");
|
|
995
|
+
const markBoundary = (end) => {
|
|
996
|
+
boundary = { end, closers: closers() };
|
|
997
|
+
};
|
|
998
|
+
const valueDone = (end) => {
|
|
999
|
+
const top = stack[stack.length - 1];
|
|
1000
|
+
if (top) top.expectingValue = false;
|
|
1001
|
+
markBoundary(end);
|
|
1002
|
+
};
|
|
1003
|
+
const endLiteral = (end) => {
|
|
1004
|
+
if (literal.length === 0) return;
|
|
1005
|
+
if (isCompleteLiteral(literal)) valueDone(end);
|
|
1006
|
+
literal = "";
|
|
1007
|
+
};
|
|
1008
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
1009
|
+
const ch = text[i];
|
|
1010
|
+
if (inString) {
|
|
1011
|
+
if (escape) {
|
|
1012
|
+
escape = false;
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
if (ch === "\\") {
|
|
1016
|
+
escape = true;
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
if (ch === '"') {
|
|
1020
|
+
inString = false;
|
|
1021
|
+
if (stringIsValue) valueDone(i + 1);
|
|
1022
|
+
}
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (literal.length > 0 && !LITERAL_CHARS.test(ch)) endLiteral(i);
|
|
1026
|
+
switch (ch) {
|
|
1027
|
+
case "{": {
|
|
1028
|
+
started = true;
|
|
1029
|
+
stack.push({ type: "obj", expectingValue: false });
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
case "[": {
|
|
1033
|
+
started = true;
|
|
1034
|
+
stack.push({ type: "arr", expectingValue: true });
|
|
1035
|
+
break;
|
|
1036
|
+
}
|
|
1037
|
+
case "}":
|
|
1038
|
+
case "]": {
|
|
1039
|
+
if (stack.length === 0) return void 0;
|
|
1040
|
+
stack.pop();
|
|
1041
|
+
valueDone(i + 1);
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
1044
|
+
case '"': {
|
|
1045
|
+
const top = stack[stack.length - 1];
|
|
1046
|
+
inString = true;
|
|
1047
|
+
escape = false;
|
|
1048
|
+
stringIsValue = top === void 0 || top.type === "arr" || top.expectingValue;
|
|
1049
|
+
break;
|
|
1050
|
+
}
|
|
1051
|
+
case ":": {
|
|
1052
|
+
const top = stack[stack.length - 1];
|
|
1053
|
+
if (top?.type === "obj") top.expectingValue = true;
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
case ",":
|
|
1057
|
+
break;
|
|
1058
|
+
default: {
|
|
1059
|
+
if (LITERAL_CHARS.test(ch)) {
|
|
1060
|
+
started = true;
|
|
1061
|
+
literal += ch;
|
|
1062
|
+
}
|
|
1063
|
+
break;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (!inString) endLiteral(text.length);
|
|
1068
|
+
if (!started) return void 0;
|
|
1069
|
+
return { openAtEnd: stack.length + (inString ? 1 : 0), ...boundary && { boundary } };
|
|
1070
|
+
};
|
|
1071
|
+
var closeTruncated = (content) => {
|
|
1072
|
+
const cleaned = content.replace(INVISIBLES, "").trim();
|
|
1073
|
+
if (!cleaned.startsWith("{") && !cleaned.startsWith("[")) return void 0;
|
|
1074
|
+
if (tryParse(cleaned).ok) return void 0;
|
|
1075
|
+
const result = scan(cleaned);
|
|
1076
|
+
if (!result?.boundary || result.openAtEnd === 0) return void 0;
|
|
1077
|
+
const candidate = cleaned.slice(0, result.boundary.end) + result.boundary.closers;
|
|
1078
|
+
return tryParse(candidate).ok ? candidate : void 0;
|
|
1079
|
+
};
|
|
1080
|
+
var isTruncatedJson = (content) => {
|
|
1081
|
+
const cleaned = content.replace(INVISIBLES, "").trim();
|
|
1082
|
+
if (!cleaned.startsWith("{") && !cleaned.startsWith("[")) return false;
|
|
1083
|
+
if (tryParse(cleaned).ok) return false;
|
|
1084
|
+
return (scan(cleaned)?.openAtEnd ?? 0) > 0;
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
// src/wire/strategies/failed-generation.ts
|
|
1088
|
+
var RECOVERABLE_CODES = /* @__PURE__ */ new Set(["tool_use_failed", "json_validate_failed"]);
|
|
1089
|
+
var prop = (obj, key) => obj !== null && typeof obj === "object" ? obj[key] : void 0;
|
|
1090
|
+
var unwrap = (error) => error instanceof SignalError ? error.context?.["raw"] ?? error : error;
|
|
1091
|
+
var errorCode = (error) => {
|
|
1092
|
+
const direct = prop(error, "code");
|
|
1093
|
+
if (typeof direct === "string") return direct;
|
|
1094
|
+
const nested = prop(prop(error, "error"), "code");
|
|
1095
|
+
return typeof nested === "string" ? nested : void 0;
|
|
1096
|
+
};
|
|
1097
|
+
var failedGenerationOf = (error) => {
|
|
1098
|
+
const gen = prop(prop(error, "error"), "failed_generation");
|
|
1099
|
+
return typeof gen === "string" && gen.trim().length > 0 ? gen : void 0;
|
|
1100
|
+
};
|
|
1101
|
+
var asCallShape = (value) => {
|
|
1102
|
+
const name = prop(value, "name");
|
|
1103
|
+
if (typeof name !== "string" || name.length === 0) return void 0;
|
|
1104
|
+
const args = prop(value, "arguments");
|
|
1105
|
+
if (args === void 0) return void 0;
|
|
1106
|
+
if (typeof args === "string") return { name, argsText: args };
|
|
1107
|
+
return { name, args, argsText: JSON.stringify(args) };
|
|
1108
|
+
};
|
|
1109
|
+
var recover = (error) => {
|
|
1110
|
+
const raw = failedGenerationOf(unwrap(error));
|
|
1111
|
+
if (raw === void 0) return { argsText: "", truncated: false };
|
|
1112
|
+
try {
|
|
1113
|
+
const parsed = JSON.parse(raw);
|
|
1114
|
+
const call = asCallShape(parsed);
|
|
1115
|
+
if (call) return { name: call.name, ...call.args !== void 0 && { args: call.args }, argsText: call.argsText, truncated: false };
|
|
1116
|
+
return { args: parsed, argsText: raw, truncated: false };
|
|
1117
|
+
} catch {
|
|
1118
|
+
const truncated = isTruncatedJson(raw);
|
|
1119
|
+
const closed = closeTruncated(raw);
|
|
1120
|
+
if (closed !== void 0) {
|
|
1121
|
+
try {
|
|
1122
|
+
const call = asCallShape(JSON.parse(closed));
|
|
1123
|
+
if (call) return { name: call.name, ...call.args !== void 0 && { args: call.args }, argsText: call.argsText, truncated };
|
|
1124
|
+
} catch {
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return { argsText: raw, truncated };
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
var failedGeneration = {
|
|
1131
|
+
id: "failed-generation",
|
|
1132
|
+
hook: "error",
|
|
1133
|
+
matches: (error) => {
|
|
1134
|
+
const code = errorCode(unwrap(error));
|
|
1135
|
+
return code !== void 0 && RECOVERABLE_CODES.has(code);
|
|
1136
|
+
},
|
|
1137
|
+
recover
|
|
1138
|
+
};
|
|
1139
|
+
|
|
1140
|
+
// src/wire/strategies/jsonl-lines.ts
|
|
1141
|
+
var jsonlLines = {
|
|
1142
|
+
id: "jsonl-lines",
|
|
1143
|
+
hook: "response",
|
|
1144
|
+
candidates: (content) => {
|
|
1145
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("{") || line.startsWith("["));
|
|
1146
|
+
return lines.length > 1 ? lines.reverse() : [];
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
// src/wire/strategies/index.ts
|
|
1151
|
+
var REGISTRY = {
|
|
1152
|
+
[failedGeneration.id]: failedGeneration,
|
|
1153
|
+
[jsonlLines.id]: jsonlLines
|
|
1154
|
+
};
|
|
1155
|
+
var resolveWireStrategies = (ids) => ids.map((id) => {
|
|
1156
|
+
const strategy = REGISTRY[id];
|
|
1157
|
+
if (!strategy) throw new Error(`Unknown wire strategy "${id}" \u2014 known: ${Object.keys(REGISTRY).join(", ")}`);
|
|
1158
|
+
return strategy;
|
|
1159
|
+
});
|
|
1160
|
+
var errorStrategies = (strategies) => strategies.filter((strategy) => strategy.hook === "error");
|
|
1161
|
+
var responseStrategies = (strategies) => strategies.filter((strategy) => strategy.hook === "response");
|
|
1162
|
+
var recoverRejection = (strategies, error) => {
|
|
1163
|
+
for (const strategy of errorStrategies(strategies)) {
|
|
1164
|
+
if (!strategy.matches(error)) continue;
|
|
1165
|
+
const rejection = strategy.recover(error);
|
|
1166
|
+
if (rejection) return { strategy: strategy.id, rejection };
|
|
1167
|
+
}
|
|
1168
|
+
return void 0;
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
// src/wire/router.ts
|
|
1172
|
+
var toolTurn = (calls, notes) => ({
|
|
1173
|
+
outcome: { kind: "tool_calls", calls },
|
|
1174
|
+
wire: notes.length > 0 ? { notes } : {}
|
|
1175
|
+
});
|
|
1176
|
+
var output = (value, rung, note) => ({
|
|
1177
|
+
outcome: { kind: "output", value },
|
|
1178
|
+
wire: note === void 0 ? { rung } : { rung, notes: [note] }
|
|
1179
|
+
});
|
|
1180
|
+
var failed = (evidence, flags = {}) => ({
|
|
1181
|
+
outcome: {
|
|
1182
|
+
kind: "failed",
|
|
1183
|
+
evidence,
|
|
1184
|
+
...flags.truncated === true && { truncated: true }
|
|
1185
|
+
},
|
|
1186
|
+
wire: {}
|
|
1187
|
+
});
|
|
1188
|
+
var tryParse2 = (text) => {
|
|
1189
|
+
try {
|
|
1190
|
+
return { ok: true, value: JSON.parse(text) };
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
return { ok: false, why: error instanceof Error ? error.message : String(error) };
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
var whyNotJson = (text) => {
|
|
1196
|
+
const direct = tryParse2(text.trim());
|
|
1197
|
+
return direct.ok ? void 0 : direct.why;
|
|
1198
|
+
};
|
|
1199
|
+
function* ladder(text, strategies) {
|
|
1200
|
+
const trimmed = text.trim();
|
|
1201
|
+
if (trimmed.length === 0) return;
|
|
1202
|
+
const direct = tryParse2(trimmed);
|
|
1203
|
+
if (direct.ok) yield { value: direct.value, rung: "parse" };
|
|
1204
|
+
const extracted = extractJson(trimmed);
|
|
1205
|
+
if (extracted.ok) yield { value: extracted.value, rung: "extract" };
|
|
1206
|
+
for (const repaired of repairEscapeDamage(trimmed)) {
|
|
1207
|
+
const parsed = tryParse2(repaired);
|
|
1208
|
+
if (parsed.ok) yield { value: parsed.value, rung: "repair-escapes" };
|
|
1209
|
+
}
|
|
1210
|
+
for (const strategy of strategies) {
|
|
1211
|
+
for (const candidateText of strategy.candidates(trimmed)) {
|
|
1212
|
+
const parsed = tryParse2(candidateText);
|
|
1213
|
+
if (parsed.ok) yield { value: parsed.value, rung: strategy.id };
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
const closed = closeTruncated(trimmed);
|
|
1217
|
+
if (closed !== void 0) {
|
|
1218
|
+
const parsed = tryParse2(closed);
|
|
1219
|
+
if (parsed.ok) yield { value: parsed.value, rung: "close-truncated" };
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
var firstAccepted = (candidates, accept) => {
|
|
1223
|
+
let nearMiss;
|
|
1224
|
+
for (const candidate of candidates) {
|
|
1225
|
+
const direct = accept.safeParse(candidate.value);
|
|
1226
|
+
if (direct.success) return { winner: candidate };
|
|
1227
|
+
const deep = deepDecodeJsonish(candidate.value);
|
|
1228
|
+
if (deep !== candidate.value && accept.safeParse(deep).success) {
|
|
1229
|
+
return { winner: { value: deep, rung: `${candidate.rung}+deep-decode` } };
|
|
1230
|
+
}
|
|
1231
|
+
nearMiss ??= direct.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
1232
|
+
}
|
|
1233
|
+
return { ...nearMiss !== void 0 && { nearMiss } };
|
|
1234
|
+
};
|
|
1235
|
+
var decodedCall = (call) => ({ ...call, args: decodeJsonish(call.args) });
|
|
1236
|
+
var finishAttempt = (call, request) => {
|
|
1237
|
+
if (!request.accept) return void 0;
|
|
1238
|
+
const source = typeof call.args === "string" ? ladder(call.args, request.responseStrategies) : [{ value: call.args, rung: "call-args" }];
|
|
1239
|
+
const { winner, nearMiss } = firstAccepted(source, request.accept);
|
|
1240
|
+
if (winner) {
|
|
1241
|
+
const note = call.name === request.outputTool ? void 0 : `output arrived as a call to undeclared tool "${call.name}"`;
|
|
1242
|
+
return output(winner.value, winner.rung, note);
|
|
1243
|
+
}
|
|
1244
|
+
if (nearMiss !== void 0) {
|
|
1245
|
+
return failed(`the call to "${call.name}" carried an invalid output: ${nearMiss}`);
|
|
1246
|
+
}
|
|
1247
|
+
return void 0;
|
|
1248
|
+
};
|
|
1249
|
+
var contentEvidence = (content, nearMiss) => {
|
|
1250
|
+
if (nearMiss !== void 0) return nearMiss;
|
|
1251
|
+
const snippet = content.trim().slice(0, 160);
|
|
1252
|
+
if (snippet.length === 0) return "the turn produced no output";
|
|
1253
|
+
const why = whyNotJson(content);
|
|
1254
|
+
return why === void 0 ? `output is not valid JSON \u2014 it began: ${snippet}` : `output is not valid JSON: ${why}. It began: ${snippet}`;
|
|
1255
|
+
};
|
|
1256
|
+
var contentTurn = (request) => {
|
|
1257
|
+
if (!request.accept) return failed("no acceptance schema for a content turn");
|
|
1258
|
+
const { winner, nearMiss } = firstAccepted(ladder(request.content, request.responseStrategies), request.accept);
|
|
1259
|
+
if (winner) return output(winner.value, winner.rung);
|
|
1260
|
+
return failed(contentEvidence(request.content, nearMiss), {
|
|
1261
|
+
truncated: isTruncatedJson(request.content)
|
|
1262
|
+
});
|
|
1263
|
+
};
|
|
1264
|
+
var routeResponse = (request) => {
|
|
1265
|
+
const isExit = (call) => call.name === request.outputTool;
|
|
1266
|
+
const domainCalls = request.toolCalls.filter((call) => request.declared.has(call.name) && !isExit(call));
|
|
1267
|
+
const attempts = request.toolCalls.filter((call) => !request.declared.has(call.name) || isExit(call));
|
|
1268
|
+
if (domainCalls.length > 0) {
|
|
1269
|
+
const dropped = attempts.map((call) => `dropped call to "${call.name}" in a mixed turn \u2014 finish attempts must come alone`);
|
|
1270
|
+
return toolTurn(domainCalls.map(decodedCall), dropped);
|
|
1271
|
+
}
|
|
1272
|
+
for (const call of attempts) {
|
|
1273
|
+
const routed = finishAttempt(call, request);
|
|
1274
|
+
if (routed) return routed;
|
|
1275
|
+
}
|
|
1276
|
+
if (attempts.length > 0) {
|
|
1277
|
+
return failed(`called unknown tool(s) ${attempts.map((call) => `"${call.name}"`).join(", ")}`);
|
|
1278
|
+
}
|
|
1279
|
+
return contentTurn(request);
|
|
1280
|
+
};
|
|
1281
|
+
var recoveredCallCounter = 0;
|
|
1282
|
+
var routeRejection = (rejection, request) => {
|
|
1283
|
+
const name = rejection.name;
|
|
1284
|
+
if (name !== void 0 && request.declared.has(name) && name !== request.outputTool) {
|
|
1285
|
+
recoveredCallCounter += 1;
|
|
1286
|
+
const args = rejection.args ?? decodeJsonish(rejection.argsText);
|
|
1287
|
+
return toolTurn([{ id: `recovered_${recoveredCallCounter}`, name, args }], []);
|
|
1288
|
+
}
|
|
1289
|
+
const call = { name: name ?? "", args: rejection.args ?? rejection.argsText };
|
|
1290
|
+
const routed = finishAttempt(call, { ...request});
|
|
1291
|
+
if (routed) {
|
|
1292
|
+
if (routed.outcome.kind === "failed" && rejection.truncated) {
|
|
1293
|
+
return { ...routed, outcome: { ...routed.outcome, truncated: true } };
|
|
1294
|
+
}
|
|
1295
|
+
return routed;
|
|
1296
|
+
}
|
|
1297
|
+
return failed(
|
|
1298
|
+
rejection.argsText.length > 0 ? `provider rejected the attempt${name !== void 0 ? ` (call to unknown tool "${name}")` : ""} and nothing salvageable remained` : "provider rejected the attempt; nothing was recovered",
|
|
1299
|
+
{ truncated: rejection.truncated }
|
|
1300
|
+
);
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
// src/signal.ts
|
|
1304
|
+
var resolveProvider = (config) => {
|
|
1305
|
+
if (typeof config.provider === "string") {
|
|
1306
|
+
const entry = providerRegistry[config.provider];
|
|
1307
|
+
if (!entry) throw new SignalError(`Unknown provider: ${config.provider}`, ErrorCode.PROVIDER_NOT_FOUND);
|
|
1308
|
+
const apiKey2 = resolveApiKey(entry.envKey, config.apiKey);
|
|
1309
|
+
if (!apiKey2) throw new SignalError(`Missing API key for ${config.provider}. Set ${entry.envKey} or pass apiKey.`, ErrorCode.MISSING_API_KEY);
|
|
1310
|
+
return {
|
|
1311
|
+
model: config.model ?? entry.defaultModel,
|
|
1312
|
+
apiKey: apiKey2,
|
|
1313
|
+
baseUrl: entry.baseUrl,
|
|
1314
|
+
adapterType: entry.adapter,
|
|
1315
|
+
...entry.adapter !== "systemone" && entry.reasoningRequest !== void 0 && { reasoningRequest: entry.reasoningRequest }
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
const custom = config.provider;
|
|
1319
|
+
const apiKey = custom.apiKey ?? config.apiKey;
|
|
1320
|
+
if (!apiKey) throw new SignalError("Missing API key for custom provider", ErrorCode.MISSING_API_KEY);
|
|
1321
|
+
const model = config.model ?? custom.model;
|
|
1322
|
+
if (!model) throw new SignalError("Missing model for custom provider", ErrorCode.MISSING_MODEL);
|
|
1323
|
+
return { model, apiKey, baseUrl: custom.baseUrl, adapterType: custom.adapter ?? "openai-compatible" };
|
|
1324
|
+
};
|
|
1325
|
+
var FALLBACK_CAPABILITIES = {
|
|
1326
|
+
nativeTools: false,
|
|
1327
|
+
nativeJsonSchema: false,
|
|
1328
|
+
nativeJsonMode: false,
|
|
1329
|
+
toolsWithStructuredOutput: false,
|
|
1330
|
+
validatesToolArgs: false,
|
|
1331
|
+
manglesNestedToolArgs: false,
|
|
1332
|
+
multimodal: false,
|
|
1333
|
+
supportsEmbedding: false
|
|
1334
|
+
};
|
|
1335
|
+
var adapterTypeOf = (config) => typeof config.provider === "string" ? providerRegistry[config.provider]?.adapter : config.provider.adapter ?? "openai-compatible";
|
|
1336
|
+
var kindOf = (adapterType) => adapterType === "systemone" ? "decisions" : "chat";
|
|
1337
|
+
var modelRowOf = (config) => {
|
|
1338
|
+
if (typeof config.provider !== "string") return void 0;
|
|
1339
|
+
const entry = providerRegistry[config.provider];
|
|
1340
|
+
if (entry === void 0) return void 0;
|
|
1341
|
+
const model = config.model ?? entry.defaultModel;
|
|
1342
|
+
return { provider: config.provider, model, row: modelEntry(config.provider, model) };
|
|
1343
|
+
};
|
|
1344
|
+
var resolveCapabilities = (config) => {
|
|
1345
|
+
if (typeof config.provider === "string") {
|
|
1346
|
+
const entry = providerRegistry[config.provider];
|
|
1347
|
+
if (entry === void 0 || entry.adapter === "systemone") return FALLBACK_CAPABILITIES;
|
|
1348
|
+
return { ...entry.endpoint, ...modelRowOf(config)?.row?.capabilities ?? UNMEASURED_MODEL };
|
|
1349
|
+
}
|
|
1350
|
+
const declared = config.provider.adapter === "systemone" ? {} : config.provider.capabilities;
|
|
1351
|
+
return { ...FALLBACK_CAPABILITIES, ...declared };
|
|
1352
|
+
};
|
|
1353
|
+
var assertEffort = (config, effort) => {
|
|
1354
|
+
if (effort === void 0) return;
|
|
1355
|
+
const resolved = modelRowOf(config);
|
|
1356
|
+
const row = resolved?.row;
|
|
1357
|
+
if (resolved === void 0 || row === void 0 || row.reasoningEfforts.includes(effort)) return;
|
|
1358
|
+
const accepted = row.reasoningEfforts.length === 0 ? "nothing \u2014 it takes no reasoning effort" : row.reasoningEfforts.join(", ");
|
|
1359
|
+
throw new SignalError(
|
|
1360
|
+
`reasoningEffort "${effort}" is not accepted by ${resolved.provider} ${resolved.model} (accepts: ${accepted}; measured ${row.verified})`,
|
|
1361
|
+
ErrorCode.VALIDATION_FAILED
|
|
1362
|
+
);
|
|
1363
|
+
};
|
|
1364
|
+
var wireIdsOf = (config) => {
|
|
1365
|
+
const source = typeof config.provider === "string" ? providerRegistry[config.provider] : config.provider;
|
|
1366
|
+
return source === void 0 || source.adapter === "systemone" ? [] : source.wire ?? [];
|
|
1367
|
+
};
|
|
1368
|
+
var createAdapter = async (resolved, client) => {
|
|
1369
|
+
switch (resolved.adapterType) {
|
|
1370
|
+
case "openai-compatible":
|
|
1371
|
+
return createOpenAICompatibleAdapter({ apiKey: resolved.apiKey, baseUrl: resolved.baseUrl, client, ...resolved.reasoningRequest !== void 0 && { reasoningRequest: resolved.reasoningRequest } });
|
|
1372
|
+
case "anthropic":
|
|
1373
|
+
return createAnthropicAdapter({ apiKey: resolved.apiKey});
|
|
1374
|
+
case "google":
|
|
1375
|
+
return createGoogleAdapter({ apiKey: resolved.apiKey});
|
|
1376
|
+
case "systemone":
|
|
1377
|
+
return createSystemOneAdapter({ apiKey: resolved.apiKey, baseUrl: resolved.baseUrl, client });
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
var buildMessages = (config, input) => {
|
|
1381
|
+
const messages = [];
|
|
1382
|
+
if (config.systemPrompt) messages.push({ role: "system", content: config.systemPrompt });
|
|
1383
|
+
if (config.history) messages.push(...config.history);
|
|
1384
|
+
messages.push({ role: "user", content: input });
|
|
1385
|
+
return messages;
|
|
1386
|
+
};
|
|
1387
|
+
var recoveredStepResult = (recovered, routed, raw, sent) => ({
|
|
1388
|
+
content: "",
|
|
1389
|
+
toolCalls: routed.outcome.kind === "tool_calls" ? routed.outcome.calls : [],
|
|
1390
|
+
// A rejected generation costs tokens — the provider processed the request and
|
|
1391
|
+
// billed for it — and the 400 body carries none. The prompt is known exactly
|
|
1392
|
+
// and the model's attempt is in the rejection, so this is countable rather
|
|
1393
|
+
// than lost. Marked unreported, because it is a reckoning.
|
|
1394
|
+
usage: estimateUsage(sent, recovered.rejection.argsText, routed.outcome.kind === "tool_calls" ? routed.outcome.calls : []),
|
|
1395
|
+
finishReason: "error_recovered",
|
|
1396
|
+
raw,
|
|
1397
|
+
outcome: routed.outcome,
|
|
1398
|
+
wire: {
|
|
1399
|
+
...routed.wire,
|
|
1400
|
+
recovered: {
|
|
1401
|
+
strategy: recovered.strategy,
|
|
1402
|
+
...recovered.rejection.name !== void 0 && { name: recovered.rejection.name },
|
|
1403
|
+
truncated: recovered.rejection.truncated
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
var createSignalFromConfig = (config) => {
|
|
1408
|
+
let cachedAdapter;
|
|
1409
|
+
let cachedAdapterKey;
|
|
1410
|
+
const wireStrategies = resolveWireStrategies(wireIdsOf(config));
|
|
1411
|
+
assertEffort(config, config.options?.reasoningEffort);
|
|
1412
|
+
const getAdapter = async () => {
|
|
1413
|
+
const resolved = resolveProvider(config);
|
|
1414
|
+
const key = `${resolved.adapterType}:${resolved.baseUrl}:${resolved.apiKey}`;
|
|
1415
|
+
if (cachedAdapter && cachedAdapterKey === key) return cachedAdapter;
|
|
1416
|
+
cachedAdapter = await createAdapter(resolved, config.client);
|
|
1417
|
+
cachedAdapterKey = key;
|
|
1418
|
+
return cachedAdapter;
|
|
1419
|
+
};
|
|
1420
|
+
const getChatAdapter = async (verb) => {
|
|
1421
|
+
const adapter = await getAdapter();
|
|
1422
|
+
if (adapter.kind === "chat") return adapter;
|
|
1423
|
+
throw new SignalError(
|
|
1424
|
+
`${verb}() is not available on a decision provider \u2014 it answers decide() and generates no text`,
|
|
1425
|
+
ErrorCode.VERB_NOT_SUPPORTED,
|
|
1426
|
+
{ verb, adapter: adapter.id }
|
|
1427
|
+
);
|
|
1428
|
+
};
|
|
1429
|
+
const fork = (override) => createSignalFromConfig({ ...config, ...override });
|
|
1430
|
+
const step = async (request) => {
|
|
1431
|
+
const adapter = await getChatAdapter("step");
|
|
1432
|
+
const resolved = resolveProvider(config);
|
|
1433
|
+
const messages = request.messages.slice();
|
|
1434
|
+
const providerTools = request.tools && request.tools.length > 0 ? request.tools.map((tool) => ({
|
|
1435
|
+
type: "function",
|
|
1436
|
+
function: {
|
|
1437
|
+
name: tool.name,
|
|
1438
|
+
description: tool.description,
|
|
1439
|
+
parameters: tool.parameters
|
|
1440
|
+
}
|
|
1441
|
+
})) : void 0;
|
|
1442
|
+
const providerRequest = {
|
|
1443
|
+
model: resolved.model,
|
|
1444
|
+
messages,
|
|
1445
|
+
...providerTools && { tools: providerTools },
|
|
1446
|
+
...request.toolChoice !== void 0 && { toolChoice: request.toolChoice },
|
|
1447
|
+
...request.responseFormat !== void 0 && { responseFormat: request.responseFormat },
|
|
1448
|
+
// The client's own options are the floor; a request may override them
|
|
1449
|
+
// field by field. `complete`/`stream` already run on config.options —
|
|
1450
|
+
// step used to drop them, so a configured client behaved differently
|
|
1451
|
+
// depending on which door the caller came through.
|
|
1452
|
+
...(config.options !== void 0 || request.options !== void 0) && {
|
|
1453
|
+
options: { ...config.options, ...request.options }
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
assertEffort(config, providerRequest.options?.reasoningEffort);
|
|
1457
|
+
const declared = new Set((request.tools ?? []).map((tool) => tool.name));
|
|
1458
|
+
let response;
|
|
1459
|
+
try {
|
|
1460
|
+
response = await adapter.chat(providerRequest);
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
const recovered = recoverRejection(wireStrategies, error);
|
|
1463
|
+
if (!recovered) throw error;
|
|
1464
|
+
const routed2 = routeRejection(recovered.rejection, {
|
|
1465
|
+
declared,
|
|
1466
|
+
...request.output && { accept: request.output.accept },
|
|
1467
|
+
...request.output?.outputTool !== void 0 && { outputTool: request.output.outputTool },
|
|
1468
|
+
responseStrategies: responseStrategies(wireStrategies)
|
|
1469
|
+
});
|
|
1470
|
+
return recoveredStepResult(recovered, routed2, error, providerRequest.messages);
|
|
1471
|
+
}
|
|
1472
|
+
const toolCalls = (response.toolCalls ?? []).map((call) => {
|
|
1473
|
+
let parsed = call.args;
|
|
1474
|
+
if (typeof call.args === "string") {
|
|
1475
|
+
try {
|
|
1476
|
+
parsed = JSON.parse(call.args);
|
|
1477
|
+
} catch {
|
|
1478
|
+
parsed = call.args;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
return { id: call.id, name: call.name, args: parsed };
|
|
1482
|
+
});
|
|
1483
|
+
const result = {
|
|
1484
|
+
content: response.content,
|
|
1485
|
+
toolCalls,
|
|
1486
|
+
// The non-streaming body always carries usage, so a zero here is a real
|
|
1487
|
+
// zero rather than a missing frame.
|
|
1488
|
+
usage: { ...response.usage, reported: true },
|
|
1489
|
+
finishReason: response.finishReason,
|
|
1490
|
+
raw: response.raw
|
|
1491
|
+
};
|
|
1492
|
+
if (!request.output) return result;
|
|
1493
|
+
const routed = routeResponse({
|
|
1494
|
+
content: result.content,
|
|
1495
|
+
toolCalls: result.toolCalls,
|
|
1496
|
+
declared,
|
|
1497
|
+
accept: request.output.accept,
|
|
1498
|
+
...request.output.outputTool !== void 0 && { outputTool: request.output.outputTool },
|
|
1499
|
+
responseStrategies: responseStrategies(wireStrategies)
|
|
1500
|
+
});
|
|
1501
|
+
return { ...result, outcome: routed.outcome, wire: routed.wire };
|
|
1502
|
+
};
|
|
1503
|
+
const stepStream = (request, streamOptions) => {
|
|
1504
|
+
const run = async function* () {
|
|
1505
|
+
const adapter = await getChatAdapter("stepStream");
|
|
1506
|
+
const resolved = resolveProvider(config);
|
|
1507
|
+
const declared = new Set((request.tools ?? []).map((tool) => tool.name));
|
|
1508
|
+
const merged = config.options !== void 0 || request.options !== void 0 ? { ...request, options: { ...config.options, ...request.options } } : request;
|
|
1509
|
+
assertEffort(config, merged.options?.reasoningEffort);
|
|
1510
|
+
try {
|
|
1511
|
+
for await (const event of executeStepStream({
|
|
1512
|
+
adapter,
|
|
1513
|
+
model: resolved.model,
|
|
1514
|
+
request: merged,
|
|
1515
|
+
...streamOptions && { streamOptions }
|
|
1516
|
+
})) {
|
|
1517
|
+
if (event.type === "done" && request.output) {
|
|
1518
|
+
const routed = routeResponse({
|
|
1519
|
+
content: event.result.content,
|
|
1520
|
+
toolCalls: event.result.toolCalls,
|
|
1521
|
+
declared,
|
|
1522
|
+
accept: request.output.accept,
|
|
1523
|
+
...request.output.outputTool !== void 0 && { outputTool: request.output.outputTool },
|
|
1524
|
+
responseStrategies: responseStrategies(wireStrategies)
|
|
1525
|
+
});
|
|
1526
|
+
yield { type: "done", result: { ...event.result, outcome: routed.outcome, wire: routed.wire } };
|
|
1527
|
+
continue;
|
|
1528
|
+
}
|
|
1529
|
+
yield event;
|
|
1530
|
+
}
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
const recovered = recoverRejection(wireStrategies, error);
|
|
1533
|
+
if (!recovered) throw error;
|
|
1534
|
+
const routed = routeRejection(recovered.rejection, {
|
|
1535
|
+
declared,
|
|
1536
|
+
...request.output && { accept: request.output.accept },
|
|
1537
|
+
...request.output?.outputTool !== void 0 && { outputTool: request.output.outputTool },
|
|
1538
|
+
responseStrategies: responseStrategies(wireStrategies)
|
|
1539
|
+
});
|
|
1540
|
+
yield { type: "done", result: recoveredStepResult(recovered, routed, error, request.messages) };
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
return run();
|
|
1544
|
+
};
|
|
1545
|
+
const decideCalibrated = async (adapter, model, request, started) => {
|
|
1546
|
+
const response = await adapter.decide(
|
|
1547
|
+
{ model, state: request.state, questions: request.questions },
|
|
1548
|
+
request.options?.signal !== void 0 ? { signal: request.options.signal } : void 0
|
|
1549
|
+
);
|
|
1550
|
+
const decisions = acceptCalibrated(request.questions, response.answers, response.raw);
|
|
1551
|
+
const inputTokens = response.usage?.inputTokens ?? 0;
|
|
1552
|
+
const outputTokens = response.usage?.outputTokens ?? 0;
|
|
1553
|
+
return {
|
|
1554
|
+
calibrated: true,
|
|
1555
|
+
decisions,
|
|
1556
|
+
meta: {
|
|
1557
|
+
model: response.model ?? model,
|
|
1558
|
+
usage: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens, reported: response.usage !== void 0 },
|
|
1559
|
+
durationMs: Date.now() - started,
|
|
1560
|
+
provider: { raw: response.raw }
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
};
|
|
1564
|
+
const decideEmulated = async (model, request, started) => {
|
|
1565
|
+
const state = typeof request.state === "string" ? request.state : JSON.stringify(request.state);
|
|
1566
|
+
const result = await runComplete(
|
|
1567
|
+
{
|
|
1568
|
+
messages: [{ role: "user", content: decisionEmulationPrompt(state) }],
|
|
1569
|
+
schema: uncalibratedSchemaOf(request.questions),
|
|
1570
|
+
tools: void 0,
|
|
1571
|
+
retries: config.retries ?? 2,
|
|
1572
|
+
options: config.options,
|
|
1573
|
+
...request.options?.signal !== void 0 && { streamOptions: { signal: request.options.signal } }
|
|
1574
|
+
},
|
|
1575
|
+
{ stepStream, model, capabilities: resolveCapabilities(config) }
|
|
1576
|
+
);
|
|
1577
|
+
return {
|
|
1578
|
+
calibrated: false,
|
|
1579
|
+
decisions: acceptUncalibrated(request.questions, result.response, result.meta.provider.raw),
|
|
1580
|
+
meta: {
|
|
1581
|
+
model: result.meta.model,
|
|
1582
|
+
// complete() sums what providers reported with what it had to estimate
|
|
1583
|
+
// and does not say which, so this total cannot be vouched for as measured.
|
|
1584
|
+
usage: { ...result.meta.usage, reported: false },
|
|
1585
|
+
durationMs: Date.now() - started,
|
|
1586
|
+
provider: { raw: result.meta.provider.raw }
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
};
|
|
1590
|
+
return {
|
|
1591
|
+
// Builder methods
|
|
1592
|
+
apiKey: (key) => fork({ apiKey: key }),
|
|
1593
|
+
model: (m) => fork({ model: m }),
|
|
1594
|
+
systemPrompt: (prompt) => fork({ systemPrompt: prompt }),
|
|
1595
|
+
history: (msgs) => fork({ history: msgs }),
|
|
1596
|
+
schema: (s) => fork({ schema: s }),
|
|
1597
|
+
tools: (t) => fork({ tools: t }),
|
|
1598
|
+
retries: (count) => fork({ retries: count }),
|
|
1599
|
+
options: (opts) => fork({ options: { ...config.options, ...opts } }),
|
|
1600
|
+
onRetry: (handler) => fork({ onRetry: handler }),
|
|
1601
|
+
onToolCall: (handler) => fork({ onToolCall: handler }),
|
|
1602
|
+
describe: () => {
|
|
1603
|
+
if (typeof config.provider === "string") {
|
|
1604
|
+
const entry = providerRegistry[config.provider];
|
|
1605
|
+
return {
|
|
1606
|
+
provider: config.provider,
|
|
1607
|
+
model: config.model ?? entry?.defaultModel,
|
|
1608
|
+
kind: kindOf(adapterTypeOf(config)),
|
|
1609
|
+
capabilities: resolveCapabilities(config),
|
|
1610
|
+
modelKnown: modelRowOf(config)?.row !== void 0
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
return {
|
|
1614
|
+
provider: config.provider.baseUrl,
|
|
1615
|
+
model: config.model ?? config.provider.model,
|
|
1616
|
+
kind: kindOf(adapterTypeOf(config)),
|
|
1617
|
+
capabilities: resolveCapabilities(config),
|
|
1618
|
+
modelKnown: false
|
|
1619
|
+
};
|
|
1620
|
+
},
|
|
1621
|
+
// Execution — thin wrappers over the step core (src/complete.ts).
|
|
1622
|
+
complete: async (input) => runComplete(
|
|
1623
|
+
{
|
|
1624
|
+
messages: buildMessages(config, input),
|
|
1625
|
+
schema: config.schema,
|
|
1626
|
+
tools: config.tools,
|
|
1627
|
+
retries: config.retries ?? 2,
|
|
1628
|
+
options: config.options,
|
|
1629
|
+
onRetry: config.onRetry,
|
|
1630
|
+
onToolCall: config.onToolCall
|
|
1631
|
+
},
|
|
1632
|
+
{ stepStream, model: resolveProvider(config).model, capabilities: resolveCapabilities(config) }
|
|
1633
|
+
),
|
|
1634
|
+
stream: (input, streamOptions) => runStream(
|
|
1635
|
+
{
|
|
1636
|
+
messages: buildMessages(config, input),
|
|
1637
|
+
schema: config.schema,
|
|
1638
|
+
tools: config.tools,
|
|
1639
|
+
retries: config.retries ?? 2,
|
|
1640
|
+
options: config.options,
|
|
1641
|
+
streamOptions,
|
|
1642
|
+
onRetry: config.onRetry,
|
|
1643
|
+
onToolCall: config.onToolCall
|
|
1644
|
+
},
|
|
1645
|
+
{ stepStream, model: resolveProvider(config).model, capabilities: resolveCapabilities(config) }
|
|
1646
|
+
),
|
|
1647
|
+
// ─── Low-level primitives — the execution core itself ────
|
|
1648
|
+
step,
|
|
1649
|
+
stepStream,
|
|
1650
|
+
// ─── Embedding ────────────────────────────────────────────
|
|
1651
|
+
embed: (async (input, options) => {
|
|
1652
|
+
const adapter = await getChatAdapter("embed");
|
|
1653
|
+
if (!adapter.embed) {
|
|
1654
|
+
throw new SignalError("This provider does not support embedding", ErrorCode.PROVIDER_ERROR);
|
|
1655
|
+
}
|
|
1656
|
+
const resolved = resolveProvider(config);
|
|
1657
|
+
const response = await adapter.embed({
|
|
1658
|
+
model: resolved.model,
|
|
1659
|
+
input,
|
|
1660
|
+
dimensions: options?.dimensions
|
|
1661
|
+
});
|
|
1662
|
+
options?.onUsage?.(response.usage, resolved.model);
|
|
1663
|
+
if (typeof input === "string") {
|
|
1664
|
+
const vector = response.embeddings[0];
|
|
1665
|
+
if (!vector) throw new SignalError("No embedding returned", ErrorCode.PROVIDER_ERROR);
|
|
1666
|
+
return vector;
|
|
1667
|
+
}
|
|
1668
|
+
return response.embeddings;
|
|
1669
|
+
}),
|
|
1670
|
+
// ─── Decisions ────────────────────────────────────────────
|
|
1671
|
+
decide: async (request) => {
|
|
1672
|
+
assertQuestions(request.questions);
|
|
1673
|
+
const started = Date.now();
|
|
1674
|
+
const adapter = await getAdapter();
|
|
1675
|
+
const model = resolveProvider(config).model;
|
|
1676
|
+
return adapter.kind === "decisions" ? decideCalibrated(adapter, model, request, started) : decideEmulated(model, request, started);
|
|
1677
|
+
},
|
|
1678
|
+
// ─── Low-level: token counting (heuristic) ───────────────
|
|
1679
|
+
count: async (input) => {
|
|
1680
|
+
const CHARS_PER_TOKEN2 = 4;
|
|
1681
|
+
if (typeof input === "string") {
|
|
1682
|
+
return Math.ceil(input.length / CHARS_PER_TOKEN2);
|
|
1683
|
+
}
|
|
1684
|
+
let total = 0;
|
|
1685
|
+
for (const msg of input) {
|
|
1686
|
+
total += 4;
|
|
1687
|
+
if (typeof msg.content === "string") {
|
|
1688
|
+
total += Math.ceil(msg.content.length / CHARS_PER_TOKEN2);
|
|
1689
|
+
continue;
|
|
1690
|
+
}
|
|
1691
|
+
for (const part of msg.content) {
|
|
1692
|
+
if (part.type === "text") {
|
|
1693
|
+
total += Math.ceil(part.text.length / CHARS_PER_TOKEN2);
|
|
1694
|
+
} else {
|
|
1695
|
+
total += 256;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
return total;
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
};
|
|
1703
|
+
var createSignal = (provider, options) => createSignalFromConfig({ provider, ...options });
|
|
1704
|
+
|
|
1705
|
+
// src/tools/define-tool.ts
|
|
1706
|
+
var defineTool = (config) => ({
|
|
1707
|
+
name: config.name,
|
|
1708
|
+
description: config.description,
|
|
1709
|
+
inputSchema: config.input,
|
|
1710
|
+
execute: config.execute
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1713
|
+
// src/transport/resolve.ts
|
|
1714
|
+
var RESPOND_TOOL_NAME = "respond";
|
|
1715
|
+
var FULL_PARAMS_MAX_CHARS = 8192;
|
|
1716
|
+
var analyzeWireSchema = (schema) => {
|
|
1717
|
+
try {
|
|
1718
|
+
const serialized = schema["~standard"].jsonSchema.output({ target: "draft-07" });
|
|
1719
|
+
const text = JSON.stringify(serialized);
|
|
1720
|
+
return { serialized, hasRef: text.includes('"$ref"'), size: text.length };
|
|
1721
|
+
} catch {
|
|
1722
|
+
return { serialized: null, hasRef: false, size: Number.POSITIVE_INFINITY };
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
var toJsonSchemaRecord = (schema) => {
|
|
1726
|
+
const convert = schema["~standard"].jsonSchema.output;
|
|
1727
|
+
try {
|
|
1728
|
+
return convert({ target: "draft-07" });
|
|
1729
|
+
} catch {
|
|
1730
|
+
return convert({ target: "draft-07", libraryOptions: { unrepresentable: "any" } });
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
var respondParams = (spec, caps, analysis, fullIsViable) => {
|
|
1734
|
+
if (caps.validatesToolArgs) {
|
|
1735
|
+
return {
|
|
1736
|
+
detail: "permissive",
|
|
1737
|
+
parameters: {
|
|
1738
|
+
type: "object",
|
|
1739
|
+
properties: spec.hasData ? { response: {}, data: {}, reasoning: {} } : { response: {}, reasoning: {} }
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
if (!spec.hasData || fullIsViable) {
|
|
1744
|
+
return { detail: "full", parameters: analysis.serialized ?? toJsonSchemaRecord(spec.wire) };
|
|
1745
|
+
}
|
|
1746
|
+
return { detail: "loose", parameters: toJsonSchemaRecord(spec.looseWire) };
|
|
1747
|
+
};
|
|
1748
|
+
var protocolSpec = (transport, spec, injectSchemaDoc) => ({
|
|
1749
|
+
transport,
|
|
1750
|
+
responseMode: spec.responseMode,
|
|
1751
|
+
hasData: spec.hasData,
|
|
1752
|
+
hasTools: spec.hasTools,
|
|
1753
|
+
injectSchemaDoc
|
|
1754
|
+
});
|
|
1755
|
+
var resolveTransport = (spec, caps) => {
|
|
1756
|
+
const analysis = analyzeWireSchema(spec.wire);
|
|
1757
|
+
const fullIsViable = analysis.serialized !== null && !analysis.hasRef && analysis.size <= FULL_PARAMS_MAX_CHARS;
|
|
1758
|
+
const nativeIsViable = caps.nativeJsonSchema && fullIsViable && (!spec.hasTools || caps.toolsWithStructuredOutput);
|
|
1759
|
+
const choice = spec.choice ?? "auto";
|
|
1760
|
+
if (choice === "native" && !nativeIsViable) {
|
|
1761
|
+
throw new SignalError(
|
|
1762
|
+
spec.hasTools && !caps.toolsWithStructuredOutput ? 'transport "native" needs a provider that combines tools with response_format (capabilities.toolsWithStructuredOutput)' : 'transport "native" needs capabilities.nativeJsonSchema and a wire schema that serializes small and non-recursive',
|
|
1763
|
+
ErrorCode.VALIDATION_FAILED
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
const respondEnforces = !caps.validatesToolArgs && !caps.manglesNestedToolArgs && (!spec.hasData || fullIsViable);
|
|
1767
|
+
let transport;
|
|
1768
|
+
if (choice !== "auto") transport = choice;
|
|
1769
|
+
else if (nativeIsViable && spec.hasData) transport = "native";
|
|
1770
|
+
else if (respondEnforces || spec.forceTool) transport = "respond";
|
|
1771
|
+
else transport = "emit";
|
|
1772
|
+
if (transport === "emit" && spec.forceTool) {
|
|
1773
|
+
throw new SignalError(
|
|
1774
|
+
"output.forceTool cannot combine with the emit transport \u2014 the final turn must be a content-only message",
|
|
1775
|
+
ErrorCode.VALIDATION_FAILED
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
if (transport === "native") {
|
|
1779
|
+
const serialized = analysis.serialized ?? toJsonSchemaRecord(spec.wire);
|
|
1780
|
+
const injectSchemaDoc2 = false;
|
|
1781
|
+
return {
|
|
1782
|
+
transport,
|
|
1783
|
+
responseMode: spec.responseMode,
|
|
1784
|
+
responseFormat: {
|
|
1785
|
+
type: "json_schema",
|
|
1786
|
+
// strict:false — strict mode rejects open records and several
|
|
1787
|
+
// keywords; non-strict still constrains and never hard-fails.
|
|
1788
|
+
jsonSchema: { name: "envelope", strict: false, schema: serialized }
|
|
1789
|
+
},
|
|
1790
|
+
injectSchemaDoc: injectSchemaDoc2,
|
|
1791
|
+
finishProtocol: finishProtocol(protocolSpec(transport, spec, injectSchemaDoc2)),
|
|
1792
|
+
corrections: corrections(transport)
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
if (transport === "emit") {
|
|
1796
|
+
const injectSchemaDoc2 = spec.hasData;
|
|
1797
|
+
return {
|
|
1798
|
+
transport,
|
|
1799
|
+
responseMode: spec.responseMode,
|
|
1800
|
+
injectSchemaDoc: injectSchemaDoc2,
|
|
1801
|
+
finishProtocol: finishProtocol(protocolSpec(transport, spec, injectSchemaDoc2)),
|
|
1802
|
+
corrections: corrections(transport)
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
const { detail: respondDetail, parameters } = respondParams(spec, caps, analysis, fullIsViable);
|
|
1806
|
+
const injectSchemaDoc = spec.hasData && respondDetail !== "full";
|
|
1807
|
+
return {
|
|
1808
|
+
transport,
|
|
1809
|
+
responseMode: spec.responseMode,
|
|
1810
|
+
respondDetail,
|
|
1811
|
+
respondDescriptor: { name: RESPOND_TOOL_NAME, description: RESPOND_DESCRIPTION, parameters },
|
|
1812
|
+
outputToolName: RESPOND_TOOL_NAME,
|
|
1813
|
+
...spec.forceTool && { toolChoice: "required" },
|
|
1814
|
+
injectSchemaDoc,
|
|
1815
|
+
finishProtocol: finishProtocol(protocolSpec(transport, spec, injectSchemaDoc)),
|
|
1816
|
+
corrections: corrections(transport)
|
|
1817
|
+
};
|
|
1818
|
+
};
|
|
1819
|
+
|
|
1820
|
+
// src/types.ts
|
|
1821
|
+
var REASONING_EFFORTS = ["none", "default", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
1822
|
+
|
|
1823
|
+
export { ErrorCode, REASONING_EFFORTS, RESPOND_TOOL_NAME, SignalError, UNMEASURED_MODEL, chatProviderEntry, closeTruncated, createSignal, decodeJsonish, deepDecodeJsonish, defineTool, extractJson, isTruncatedJson, modelEntry, modelKey, modelRegistry, providerRegistry, repairEscapeDamage, resolveTransport, routeRejection, routeResponse };
|
|
1824
|
+
//# sourceMappingURL=index.js.map
|
|
1825
|
+
//# sourceMappingURL=index.js.map
|