@telnyx/agent-harness 0.1.0-beta.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.
@@ -0,0 +1,1223 @@
1
+ import { GatewayError } from "@ai-sdk/gateway";
2
+ import { AISDKError, APICallError, RetryError, asSchema, generateText, jsonSchema, stepCountIs, wrapLanguageModel, } from "ai";
3
+ import { z } from "zod/v4";
4
+ import { isToolOutputSerializationFailure } from "./steps.js";
5
+ import { HarnessApprovalError, createHarnessApprovalExecutionLedger, createHarnessApprovalMessageJournal, } from "./approvals.js";
6
+ import { createHarnessRunExecutionLedger, HarnessApprovalPause, } from "./durable.js";
7
+ import { runMiddleware } from "./lifecycle.js";
8
+ import { withHarnessToolContext } from "./tool-context.js";
9
+ import { resolveHarnessRuntimeConfig, } from "./runtime-config.js";
10
+ const maxStepsSchema = z.number().int().positive();
11
+ const TOOL_OUTPUT_PREFIX = "\u001e@telnyx/agent-harness:tool-output:v1:";
12
+ const DURABLE_TOOL_STEP_VERSION = "tool-execution-v1";
13
+ /** Raised after the final allowed model step still requests executable tools. */
14
+ export class HarnessStepLimitError extends Error {
15
+ maxSteps;
16
+ completedSteps;
17
+ lastToolCalls;
18
+ code = "HARNESS_STEP_LIMIT";
19
+ constructor(maxSteps, completedSteps, lastToolCalls) {
20
+ super(`Harness exhausted its ${maxSteps}-step limit after ${completedSteps} completed steps`);
21
+ this.maxSteps = maxSteps;
22
+ this.completedSteps = completedSteps;
23
+ this.lastToolCalls = lastToolCalls;
24
+ this.name = "HarnessStepLimitError";
25
+ }
26
+ }
27
+ /** Stable provider-boundary error containing only allowlisted diagnostics. */
28
+ export class HarnessProviderError extends Error {
29
+ diagnostics;
30
+ code = "HARNESS_PROVIDER_ERROR";
31
+ constructor(diagnostics) {
32
+ super("Model provider request failed");
33
+ this.diagnostics = diagnostics;
34
+ this.name = "HarnessProviderError";
35
+ }
36
+ }
37
+ /** Local tool failure used when a successful execution returns an unsafe output. */
38
+ export class HarnessToolOutputError extends Error {
39
+ toolName;
40
+ code = "HARNESS_TOOL_OUTPUT_ERROR";
41
+ constructor(toolName) {
42
+ super(`Harness tool ${toolName} returned output that is not JSON-safe`);
43
+ this.toolName = toolName;
44
+ this.name = "HarnessToolOutputError";
45
+ }
46
+ }
47
+ /** Stable cancellation classification for an aborted caller-owned turn signal. */
48
+ export class HarnessCancellationError extends Error {
49
+ code = "HARNESS_CANCELLED";
50
+ constructor() {
51
+ super("Harness turn was cancelled");
52
+ this.name = "HarnessCancellationError";
53
+ }
54
+ }
55
+ /** Stable aggregate retaining a primary turn failure when its observer also fails. */
56
+ export class HarnessObserverError extends Error {
57
+ primary;
58
+ observer;
59
+ phase;
60
+ code = "HARNESS_OBSERVER_ERROR";
61
+ constructor(primary, observer, phase) {
62
+ super(`Harness ${phase} observer failed after the primary turn failure`);
63
+ this.primary = primary;
64
+ this.observer = observer;
65
+ this.phase = phase;
66
+ this.name = "HarnessObserverError";
67
+ }
68
+ }
69
+ function historyMessages(stored) {
70
+ return stored.map((message) => {
71
+ if (message.role === "assistant") {
72
+ const content = [];
73
+ if (message.content.length > 0 || message.toolCalls === undefined) {
74
+ content.push({ type: "text", text: message.content });
75
+ }
76
+ content.push(...(message.toolCalls ?? []).map((call) => ({
77
+ type: "tool-call",
78
+ toolCallId: call.id,
79
+ toolName: call.name,
80
+ input: call.args,
81
+ })));
82
+ return { role: "assistant", content };
83
+ }
84
+ if (message.role === "tool") {
85
+ if (message.name === undefined || message.toolCallId === undefined) {
86
+ throw new Error("Persisted tool messages require name and toolCallId");
87
+ }
88
+ return {
89
+ role: "tool",
90
+ content: [
91
+ {
92
+ type: "tool-result",
93
+ toolCallId: message.toolCallId,
94
+ toolName: message.name,
95
+ output: decodeToolOutput(message.content),
96
+ },
97
+ ],
98
+ };
99
+ }
100
+ return { role: message.role, content: message.content };
101
+ });
102
+ }
103
+ function errorMessage(error) {
104
+ if (error instanceof Error)
105
+ return error.message;
106
+ if (typeof error === "string")
107
+ return error;
108
+ try {
109
+ return JSON.stringify(error) ?? String(error);
110
+ }
111
+ catch {
112
+ return "Tool execution failed";
113
+ }
114
+ }
115
+ function encodeToolOutput(output) {
116
+ return `${TOOL_OUTPUT_PREFIX}${JSON.stringify(output)}`;
117
+ }
118
+ const TOOL_OUTPUT_MAX_DEPTH = 64;
119
+ const TOOL_OUTPUT_MAX_NODES = 10_000;
120
+ function cloneToolOutput(toolName, value) {
121
+ const ancestors = new Set();
122
+ let nodes = 0;
123
+ const clone = (candidate, depth) => {
124
+ nodes += 1;
125
+ if (nodes > TOOL_OUTPUT_MAX_NODES || depth > TOOL_OUTPUT_MAX_DEPTH) {
126
+ throw new HarnessToolOutputError(toolName);
127
+ }
128
+ if (candidate === null || typeof candidate === "string" || typeof candidate === "boolean") {
129
+ return candidate;
130
+ }
131
+ if (typeof candidate === "number") {
132
+ if (!Number.isFinite(candidate))
133
+ throw new HarnessToolOutputError(toolName);
134
+ return candidate;
135
+ }
136
+ if (typeof candidate !== "object" || ancestors.has(candidate)) {
137
+ throw new HarnessToolOutputError(toolName);
138
+ }
139
+ let prototype;
140
+ try {
141
+ prototype = Object.getPrototypeOf(candidate);
142
+ }
143
+ catch {
144
+ throw new HarnessToolOutputError(toolName);
145
+ }
146
+ if (!Array.isArray(candidate) && prototype !== Object.prototype && prototype !== null) {
147
+ throw new HarnessToolOutputError(toolName);
148
+ }
149
+ ancestors.add(candidate);
150
+ try {
151
+ if (Array.isArray(candidate)) {
152
+ const result = [];
153
+ for (let index = 0; index < candidate.length; index += 1) {
154
+ result.push(clone(safeProperty(candidate, index), depth + 1));
155
+ }
156
+ return result;
157
+ }
158
+ const result = {};
159
+ let keys;
160
+ try {
161
+ keys = Object.keys(candidate);
162
+ }
163
+ catch {
164
+ throw new HarnessToolOutputError(toolName);
165
+ }
166
+ for (const key of keys) {
167
+ Object.defineProperty(result, key, {
168
+ configurable: true,
169
+ enumerable: true,
170
+ value: clone(safeProperty(candidate, key), depth + 1),
171
+ writable: true,
172
+ });
173
+ }
174
+ return result;
175
+ }
176
+ finally {
177
+ ancestors.delete(candidate);
178
+ }
179
+ };
180
+ try {
181
+ return clone(value, 0);
182
+ }
183
+ catch (error) {
184
+ if (error instanceof HarnessToolOutputError)
185
+ throw error;
186
+ throw new HarnessToolOutputError(toolName);
187
+ }
188
+ }
189
+ function toJsonValue(value) {
190
+ const encoded = JSON.stringify(value ?? null);
191
+ if (encoded === undefined)
192
+ throw new Error("Tool output is not JSON-safe");
193
+ return JSON.parse(encoded);
194
+ }
195
+ function decodeToolOutputEnvelope(content) {
196
+ if (!content.startsWith(TOOL_OUTPUT_PREFIX)) {
197
+ return { output: { type: "text", value: content } };
198
+ }
199
+ try {
200
+ const parsed = JSON.parse(content.slice(TOOL_OUTPUT_PREFIX.length));
201
+ const record = objectRecord(parsed);
202
+ const type = record?.type;
203
+ const value = record?.value;
204
+ if ((type === "text" || type === "error-text") && typeof value === "string") {
205
+ return { output: { type, value }, ...(typeof record?.journalMarker === "string" ? { journalMarker: record.journalMarker } : {}) };
206
+ }
207
+ if (type === "json" || type === "error-json") {
208
+ return { output: { type, value: value }, ...(typeof record?.journalMarker === "string" ? { journalMarker: record.journalMarker } : {}) };
209
+ }
210
+ }
211
+ catch {
212
+ // Fall through to the stable corruption error below.
213
+ }
214
+ throw new Error("Persisted harness tool output is invalid");
215
+ }
216
+ function decodeToolOutput(content) {
217
+ return decodeToolOutputEnvelope(content).output;
218
+ }
219
+ function journalForStep(step, journalMarkers = new Map(), calls = step.toolCalls) {
220
+ const messages = [
221
+ {
222
+ role: "assistant",
223
+ content: step.text,
224
+ ...(calls.length > 0
225
+ ? {
226
+ toolCalls: calls.map((call) => ({
227
+ id: call.toolCallId,
228
+ name: call.toolName,
229
+ args: call.input,
230
+ })),
231
+ }
232
+ : {}),
233
+ },
234
+ ];
235
+ const outputByCallId = new Map(step.content
236
+ .filter((part) => part.type === "tool-result" || part.type === "tool-error")
237
+ .map((part) => [part.toolCallId, part]));
238
+ for (const call of calls) {
239
+ const output = outputByCallId.get(call.toolCallId);
240
+ if (output === undefined) {
241
+ throw new Error(`Completed tool call ${call.toolCallId} has no result or error`);
242
+ }
243
+ const stored = output.type === "tool-error"
244
+ ? { type: "error-text", value: errorMessage(output.error) }
245
+ : typeof output.output === "string"
246
+ ? { type: "text", value: output.output }
247
+ : { type: "json", value: toJsonValue(output.output) };
248
+ messages.push({
249
+ role: "tool",
250
+ content: encodeToolOutput({ ...stored, ...(journalMarkers.get(call.toolCallId) === undefined ? {} : { journalMarker: journalMarkers.get(call.toolCallId) }) }),
251
+ name: call.toolName,
252
+ toolCallId: call.toolCallId,
253
+ });
254
+ }
255
+ return messages;
256
+ }
257
+ function journalForApprovedToolCalls(calls, journalMarkers) {
258
+ if (calls.length === 0)
259
+ return [];
260
+ return [
261
+ { role: "assistant", content: "", toolCalls: calls.map((call) => ({ id: call.toolCallId, name: call.toolName, args: call.input })) },
262
+ ...calls.map((call) => ({
263
+ role: "tool",
264
+ content: encodeToolOutput({ ...call.output, ...(journalMarkers.get(call.toolCallId) === undefined ? {} : { journalMarker: journalMarkers.get(call.toolCallId) }) }),
265
+ name: call.toolName,
266
+ toolCallId: call.toolCallId,
267
+ })),
268
+ ];
269
+ }
270
+ function observedStep(step, journalThrough) {
271
+ const outputByCallId = new Map(step.content
272
+ .filter((part) => part.type === "tool-result" || part.type === "tool-error")
273
+ .map((part) => [part.toolCallId, part]));
274
+ return Object.freeze({
275
+ number: step.stepNumber + 1,
276
+ text: step.text,
277
+ finishReason: step.finishReason,
278
+ journalThrough,
279
+ toolCalls: Object.freeze(step.toolCalls.map((call) => Object.freeze({
280
+ id: call.toolCallId,
281
+ name: call.toolName,
282
+ input: freezeJsonValue(cloneToolOutput(call.toolName, call.input)),
283
+ }))),
284
+ toolResults: Object.freeze(step.toolCalls.flatMap((call) => {
285
+ const result = outputByCallId.get(call.toolCallId);
286
+ if (result === undefined)
287
+ return [];
288
+ return [
289
+ Object.freeze({
290
+ id: call.toolCallId,
291
+ name: call.toolName,
292
+ output: freezeJsonValue(result.type === "tool-error"
293
+ ? errorMessage(result.error)
294
+ : typeof result.output === "string"
295
+ ? result.output
296
+ : toJsonValue(result.output)),
297
+ isError: result.type === "tool-error",
298
+ }),
299
+ ];
300
+ })),
301
+ });
302
+ }
303
+ function objectRecord(value) {
304
+ return typeof value === "object" && value !== null
305
+ ? value
306
+ : undefined;
307
+ }
308
+ const DIAGNOSTIC_MAX_DEPTH = 4;
309
+ const DIAGNOSTIC_MAX_NODES = 24;
310
+ const DIAGNOSTIC_MAX_ARRAY_ITEMS = 8;
311
+ const DIAGNOSTIC_IDENTIFIER_MAX_LENGTH = 64;
312
+ const GATEWAY_CAUSE_NAMES = new Set([
313
+ "GatewayError",
314
+ "GatewayAuthenticationError",
315
+ "GatewayFailedDependencyError",
316
+ "GatewayForbiddenError",
317
+ "GatewayInternalServerError",
318
+ "GatewayInvalidRequestError",
319
+ "GatewayModelNotFoundError",
320
+ "GatewayRateLimitError",
321
+ "GatewayResponseError",
322
+ "GatewayTimeoutError",
323
+ ]);
324
+ const AI_SDK_GATEWAY_AUTH_PRODUCTION_MESSAGE = "Unauthenticated. Configure AI_GATEWAY_API_KEY or use a provider module. Learn more: https://ai-sdk.dev/unauthenticated-ai-gateway";
325
+ const AI_SDK_GATEWAY_AUTH_DEVELOPMENT_MESSAGE = "\u001b[1m\u001b[31mUnauthenticated request to AI Gateway.\u001b[0m\n\n" +
326
+ "To authenticate, set the \u001b[33mAI_GATEWAY_API_KEY\u001b[0m environment variable with your API key.\n\n" +
327
+ "Alternatively, you can use a provider module instead of the AI Gateway.\n\n" +
328
+ "Learn more: \u001b[34mhttps://ai-sdk.dev/unauthenticated-ai-gateway\u001b[0m\n\n";
329
+ const SENSITIVE_IDENTIFIER = /(?:authorization|bearer|credential|password|secret|token|api[_.-]?key)/i;
330
+ const SAFE_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;
331
+ function safeProperty(record, property) {
332
+ try {
333
+ return record[property];
334
+ }
335
+ catch {
336
+ return undefined;
337
+ }
338
+ }
339
+ function isExactWrappedGatewayAuthenticationError(error) {
340
+ const record = objectRecord(error);
341
+ if (record === undefined)
342
+ return false;
343
+ const name = safeProperty(record, "name");
344
+ const message = safeProperty(record, "message");
345
+ if (name === "GatewayAuthenticationError") {
346
+ return message === AI_SDK_GATEWAY_AUTH_DEVELOPMENT_MESSAGE;
347
+ }
348
+ try {
349
+ return (name === "GatewayError" &&
350
+ message === AI_SDK_GATEWAY_AUTH_PRODUCTION_MESSAGE &&
351
+ AISDKError.isInstance(error));
352
+ }
353
+ catch {
354
+ return false;
355
+ }
356
+ }
357
+ function knownCauseName(error) {
358
+ try {
359
+ if (APICallError.isInstance(error))
360
+ return "AI_APICallError";
361
+ }
362
+ catch {
363
+ // Hostile proxies are not trusted provider errors.
364
+ }
365
+ try {
366
+ if (RetryError.isInstance(error))
367
+ return "AI_RetryError";
368
+ }
369
+ catch {
370
+ // Hostile proxies are not trusted provider errors.
371
+ }
372
+ try {
373
+ if (GatewayError.isInstance(error)) {
374
+ const name = safeProperty(error, "name");
375
+ return typeof name === "string" && GATEWAY_CAUSE_NAMES.has(name)
376
+ ? name
377
+ : "GatewayError";
378
+ }
379
+ }
380
+ catch {
381
+ // Hostile proxies are not trusted Gateway errors.
382
+ }
383
+ return isExactWrappedGatewayAuthenticationError(error)
384
+ ? "GatewayAuthenticationError"
385
+ : undefined;
386
+ }
387
+ function validStatusCode(value) {
388
+ return (typeof value === "number" &&
389
+ Number.isInteger(value) &&
390
+ value >= 100 &&
391
+ value <= 599);
392
+ }
393
+ function diagnosticSource(error) {
394
+ const queue = [{ value: error, depth: 0 }];
395
+ const visited = new Set();
396
+ let nodes = 0;
397
+ let fallbackCauseName = knownCauseName(error) ?? "UnknownError";
398
+ let fallback = { causeName: fallbackCauseName };
399
+ const enqueueArray = (value, depth) => {
400
+ if (!Array.isArray(value) || depth > DIAGNOSTIC_MAX_DEPTH)
401
+ return;
402
+ const count = Math.min(value.length, DIAGNOSTIC_MAX_ARRAY_ITEMS);
403
+ for (let index = 0; index < count; index += 1) {
404
+ queue.push({ value: safeProperty(value, index), depth });
405
+ }
406
+ };
407
+ while (queue.length > 0 && nodes < DIAGNOSTIC_MAX_NODES) {
408
+ const item = queue.shift();
409
+ if (item === undefined || item.depth > DIAGNOSTIC_MAX_DEPTH)
410
+ continue;
411
+ const candidate = item.value;
412
+ if (visited.has(candidate))
413
+ continue;
414
+ visited.add(candidate);
415
+ nodes += 1;
416
+ if (Array.isArray(candidate)) {
417
+ enqueueArray(candidate, item.depth + 1);
418
+ continue;
419
+ }
420
+ const record = objectRecord(candidate);
421
+ if (record === undefined)
422
+ continue;
423
+ const causeName = knownCauseName(candidate);
424
+ const statusCode = safeProperty(record, "statusCode");
425
+ const retryable = safeProperty(record, "isRetryable") ?? safeProperty(record, "retryable");
426
+ if (causeName !== undefined) {
427
+ fallbackCauseName = causeName;
428
+ fallback = {
429
+ causeName,
430
+ ...(validStatusCode(statusCode) ? { statusCode } : {}),
431
+ ...(typeof retryable === "boolean" ? { retryable } : {}),
432
+ };
433
+ if (validStatusCode(statusCode))
434
+ return fallback;
435
+ }
436
+ if (item.depth >= DIAGNOSTIC_MAX_DEPTH)
437
+ continue;
438
+ queue.push({ value: safeProperty(record, "lastError"), depth: item.depth + 1 });
439
+ queue.push({ value: safeProperty(record, "cause"), depth: item.depth + 1 });
440
+ enqueueArray(safeProperty(record, "errors"), item.depth + 1);
441
+ }
442
+ return fallback.causeName === fallbackCauseName
443
+ ? fallback
444
+ : { causeName: fallbackCauseName };
445
+ }
446
+ function safeIdentifier(value, fallback) {
447
+ if (typeof value !== "string" ||
448
+ value.length === 0 ||
449
+ value.length > DIAGNOSTIC_IDENTIFIER_MAX_LENGTH ||
450
+ !SAFE_IDENTIFIER.test(value) ||
451
+ value.includes("://") ||
452
+ SENSITIVE_IDENTIFIER.test(value)) {
453
+ return fallback;
454
+ }
455
+ return value;
456
+ }
457
+ function isProviderFailure(error) {
458
+ try {
459
+ if (APICallError.isInstance(error) || RetryError.isInstance(error))
460
+ return true;
461
+ }
462
+ catch {
463
+ return false;
464
+ }
465
+ return knownCauseName(error) !== undefined;
466
+ }
467
+ function providerDiagnostics(model, error) {
468
+ const source = diagnosticSource(error);
469
+ const provider = typeof model === "string"
470
+ ? "gateway"
471
+ : safeIdentifier(safeProperty(model, "provider"), "unknown-provider");
472
+ const modelId = typeof model === "string"
473
+ ? safeIdentifier(model, "unknown-model")
474
+ : safeIdentifier(safeProperty(model, "modelId"), "unknown-model");
475
+ return Object.freeze({
476
+ provider,
477
+ modelId,
478
+ causeName: source.causeName,
479
+ ...(source.statusCode !== undefined ? { statusCode: source.statusCode } : {}),
480
+ ...(source.retryable !== undefined ? { retryable: source.retryable } : {}),
481
+ });
482
+ }
483
+ function freezeJsonValue(value) {
484
+ if (typeof value !== "object" || value === null || Object.isFrozen(value))
485
+ return value;
486
+ for (const child of Object.values(value)) {
487
+ if (child !== undefined)
488
+ freezeJsonValue(child);
489
+ }
490
+ return Object.freeze(value);
491
+ }
492
+ function cloneFrozenToolData(toolName, field, value) {
493
+ try {
494
+ return freezeJsonValue(cloneToolOutput(toolName, value));
495
+ }
496
+ catch {
497
+ throw new TypeError(`Harness tool ${toolName} ${field} must be JSON-safe`);
498
+ }
499
+ }
500
+ function promiseLike(value) {
501
+ if ((typeof value !== "object" || value === null) && typeof value !== "function") {
502
+ return false;
503
+ }
504
+ return typeof value.then === "function";
505
+ }
506
+ function snapshotSchema(toolName, field, flexibleSchema) {
507
+ const source = asSchema(flexibleSchema);
508
+ const validate = source.validate;
509
+ let materialized;
510
+ try {
511
+ const captured = source.jsonSchema;
512
+ materialized = promiseLike(captured)
513
+ ? Promise.resolve(captured).then((value) => ({
514
+ ok: true,
515
+ value: cloneFrozenToolData(toolName, `${field} JSON Schema`, value),
516
+ }), (error) => ({ ok: false, error }))
517
+ : {
518
+ ok: true,
519
+ value: cloneFrozenToolData(toolName, `${field} JSON Schema`, captured),
520
+ };
521
+ }
522
+ catch (error) {
523
+ materialized = { ok: false, error };
524
+ }
525
+ const resolveSchema = async () => {
526
+ const result = await materialized;
527
+ if (!result.ok)
528
+ throw result.error;
529
+ return result.value;
530
+ };
531
+ const schema = Object.freeze(jsonSchema(resolveSchema, {
532
+ ...(validate === undefined
533
+ ? {}
534
+ : { validate: (value) => validate.call(source, value) }),
535
+ }));
536
+ return Object.freeze({
537
+ schema,
538
+ async preflight() {
539
+ await schema.jsonSchema;
540
+ },
541
+ });
542
+ }
543
+ function snapshotTools(tools) {
544
+ const schemas = new Map();
545
+ const entries = Object.entries(tools).map(([name, tool]) => {
546
+ const candidate = objectRecord(tool);
547
+ if (candidate === undefined) {
548
+ throw new TypeError(`Harness tool ${name} must be an object`);
549
+ }
550
+ const inputSchema = snapshotSchema(name, "inputSchema", candidate.inputSchema);
551
+ const toolSchemas = [inputSchema];
552
+ const outputSchema = candidate.outputSchema === undefined
553
+ ? undefined
554
+ : snapshotSchema(name, "outputSchema", candidate.outputSchema);
555
+ if (outputSchema !== undefined)
556
+ toolSchemas.push(outputSchema);
557
+ schemas.set(name, Object.freeze(toolSchemas));
558
+ const execute = candidate.execute;
559
+ const capturedTool = Object.freeze({
560
+ ...(candidate.type === "function" ? { type: "function" } : {}),
561
+ ...(candidate.description !== undefined ? { description: candidate.description } : {}),
562
+ ...(candidate.title !== undefined ? { title: candidate.title } : {}),
563
+ ...(candidate.providerOptions !== undefined
564
+ ? {
565
+ providerOptions: cloneFrozenToolData(name, "providerOptions", candidate.providerOptions),
566
+ }
567
+ : {}),
568
+ ...(candidate.metadata !== undefined
569
+ ? { metadata: cloneFrozenToolData(name, "metadata", candidate.metadata) }
570
+ : {}),
571
+ ...(candidate.needsApproval === true ? { needsApproval: true } : {}),
572
+ inputSchema: inputSchema.schema,
573
+ ...(candidate.inputExamples !== undefined
574
+ ? {
575
+ inputExamples: cloneFrozenToolData(name, "inputExamples", candidate.inputExamples),
576
+ }
577
+ : {}),
578
+ ...(candidate.strict !== undefined ? { strict: candidate.strict } : {}),
579
+ ...(outputSchema !== undefined ? { outputSchema: outputSchema.schema } : {}),
580
+ async execute(...args) {
581
+ return await execute.apply(this, args);
582
+ },
583
+ });
584
+ return [name, capturedTool];
585
+ });
586
+ const capturedTools = Object.freeze(Object.fromEntries(entries));
587
+ return Object.freeze({
588
+ tools: capturedTools,
589
+ preflight(names) {
590
+ return Promise.all(names.flatMap((name) => schemas.get(name) ?? []).map((schema) => schema.preflight()))
591
+ .then(() => undefined);
592
+ },
593
+ });
594
+ }
595
+ const ACTION_MAX_BYTES = 512;
596
+ const ACTION_PATTERN = /^[A-Za-z0-9]+(?:\.[A-Za-z0-9]+)*$/;
597
+ const APPROVAL_TTL_MIN_MS = 300_000;
598
+ const APPROVAL_TTL_MAX_MS = 86_400_000;
599
+ function validateTools(tools, authorization) {
600
+ const names = Object.keys(tools);
601
+ const requiresApproval = names.some((name) => objectRecord(tools[name])?.needsApproval === true);
602
+ const mapped = authorization?.toolActions;
603
+ const approvalTtlMs = authorization?.approvalTtlMs ?? Number.NaN;
604
+ if (requiresApproval && mapped === undefined) {
605
+ throw new TypeError("Harness approval-capable tools require a private tool action map");
606
+ }
607
+ if (requiresApproval && (!Number.isSafeInteger(approvalTtlMs)
608
+ || approvalTtlMs < APPROVAL_TTL_MIN_MS
609
+ || approvalTtlMs > APPROVAL_TTL_MAX_MS)) {
610
+ throw new TypeError("Harness approval TTL must be a host-owned safe integer between 300000 and 86400000 ms");
611
+ }
612
+ if (mapped !== undefined) {
613
+ const mappedNames = Object.keys(mapped);
614
+ if (mappedNames.length !== names.length || mappedNames.some((name) => !Object.hasOwn(tools, name))) {
615
+ throw new TypeError("Harness tool action map must cover the exact supplied tool set");
616
+ }
617
+ for (const name of names) {
618
+ const action = mapped[name];
619
+ if (typeof action !== "string" || !action.trim() || Buffer.byteLength(action, "utf8") > ACTION_MAX_BYTES || !ACTION_PATTERN.test(action)) {
620
+ throw new TypeError(`Harness tool ${name} action is invalid`);
621
+ }
622
+ }
623
+ }
624
+ for (const [name, tool] of Object.entries(tools)) {
625
+ const candidate = objectRecord(tool);
626
+ if (candidate === undefined) {
627
+ throw new TypeError(`Harness tool ${name} must be an object`);
628
+ }
629
+ if (candidate.type !== undefined && candidate.type !== "function") {
630
+ throw new TypeError(`Harness tool ${name} must be an ordinary function tool`);
631
+ }
632
+ if (typeof candidate.execute !== "function") {
633
+ throw new TypeError(`Harness tool ${name} must define execute`);
634
+ }
635
+ if (candidate.needsApproval !== undefined && candidate.needsApproval !== true) {
636
+ throw new TypeError(`Harness tool ${name} needsApproval must be true when supplied`);
637
+ }
638
+ if (candidate.supportsDeferredResults !== undefined) {
639
+ throw new TypeError(`Harness tool ${name} cannot use deferred results`);
640
+ }
641
+ for (const extension of [
642
+ "onInputStart",
643
+ "onInputDelta",
644
+ "onInputAvailable",
645
+ "toModelOutput",
646
+ ]) {
647
+ if (candidate[extension] !== undefined) {
648
+ throw new TypeError(`Harness tool ${name} cannot use ${extension}`);
649
+ }
650
+ }
651
+ }
652
+ }
653
+ /** Construct a portable AI SDK v6 turn engine over an explicit ports object. */
654
+ export function createHarness(spec) {
655
+ const messagePort = spec.ports.messages;
656
+ const appendMessage = messagePort.append.bind(messagePort);
657
+ const appendMessages = messagePort.appendMany.bind(messagePort);
658
+ const allMessages = messagePort.all.bind(messagePort);
659
+ const getState = spec.ports.state.get.bind(spec.ports.state);
660
+ const model = spec.model;
661
+ const instructions = spec.instructions;
662
+ validateTools(spec.tools, spec.authorization);
663
+ // Authorization is a construction-time host capability. Copy its validated
664
+ // values before any asynchronous turn can observe mutable host input.
665
+ const authorization = spec.authorization === undefined
666
+ ? undefined
667
+ : Object.freeze({
668
+ toolActions: Object.freeze({ ...spec.authorization.toolActions }),
669
+ ...(spec.authorization.approvalTtlMs === undefined ? {} : { approvalTtlMs: spec.authorization.approvalTtlMs }),
670
+ });
671
+ const requiresApprovalIdentity = Object.values(spec.tools).some((tool) => objectRecord(tool)?.needsApproval === true);
672
+ const toolSnapshot = snapshotTools(spec.tools);
673
+ const tools = toolSnapshot.tools;
674
+ const models = Object.freeze({ ...(spec.models ?? {}) });
675
+ const outputs = Object.freeze({ ...(spec.outputs ?? {}) });
676
+ const maxSteps = maxStepsSchema.parse(spec.limits.steps);
677
+ const approvals = createHarnessApprovalExecutionLedger(spec.ports);
678
+ const approvalMessages = createHarnessApprovalMessageJournal(spec.ports);
679
+ const staticRuntimeConfig = Object.freeze({
680
+ model,
681
+ instructions,
682
+ tools,
683
+ limits: Object.freeze({ steps: maxSteps }),
684
+ models,
685
+ outputs,
686
+ ...(spec.output === undefined ? {} : { output: spec.output }),
687
+ ...(spec.activeTools === undefined ? {} : { activeTools: Object.freeze([...spec.activeTools]) }),
688
+ });
689
+ const lifecycleMiddleware = Object.freeze({
690
+ turn: Object.freeze([...(spec.middleware?.turn ?? [])]),
691
+ modelStep: Object.freeze([...(spec.middleware?.modelStep ?? [])]),
692
+ tool: Object.freeze([...(spec.middleware?.tool ?? [])]),
693
+ });
694
+ if (typeof model === "string" && lifecycleMiddleware.modelStep.length > 0) {
695
+ throw new TypeError("Harness model-step middleware requires an executable LanguageModel");
696
+ }
697
+ let lastValidRuntimeConfig = resolveHarnessRuntimeConfig(staticRuntimeConfig, undefined).config;
698
+ let runtimeConfigReadSequence = 0;
699
+ let lastPublishedRuntimeConfigReadSequence = 0;
700
+ const turn = async (input, options = {}) => {
701
+ if (options.signal?.aborted) {
702
+ const cancellation = new HarnessCancellationError();
703
+ try {
704
+ await options.onCancel?.(cancellation);
705
+ }
706
+ catch (observer) {
707
+ throw new HarnessObserverError(cancellation, observer, "cancel");
708
+ }
709
+ throw cancellation;
710
+ }
711
+ try {
712
+ const runtimeConfigReadSequenceForTurn = ++runtimeConfigReadSequence;
713
+ const state = await getState();
714
+ const resolution = resolveHarnessRuntimeConfig(staticRuntimeConfig, state.__telnyx_agent_harness, lastValidRuntimeConfig);
715
+ const runtimeConfig = resolution.config;
716
+ if (typeof runtimeConfig.model === "string" && lifecycleMiddleware.modelStep.length > 0) {
717
+ throw new TypeError("Harness model-step middleware requires an executable LanguageModel");
718
+ }
719
+ if (options.steps === undefined && Object.values(runtimeConfig.tools ?? {}).some((tool) => objectRecord(tool)?.needsApproval === true)) {
720
+ throw new TypeError("Harness approval-capable tools require durable runs");
721
+ }
722
+ if (resolution.valid && runtimeConfigReadSequenceForTurn > lastPublishedRuntimeConfigReadSequence) {
723
+ lastValidRuntimeConfig = resolution.config;
724
+ lastPublishedRuntimeConfigReadSequence = runtimeConfigReadSequenceForTurn;
725
+ }
726
+ const turnContext = Object.freeze({
727
+ phase: "turn",
728
+ input,
729
+ config: runtimeConfig,
730
+ signal: options.signal,
731
+ });
732
+ return await runMiddleware(turnContext, lifecycleMiddleware.turn, async () => {
733
+ await toolSnapshot.preflight(Object.keys(runtimeConfig.tools ?? {}));
734
+ const priorMessages = await allMessages();
735
+ const inputSteps = options.steps;
736
+ const inputJournal = inputSteps === undefined
737
+ ? undefined
738
+ : spec.ports.sql.transactionSync(() => {
739
+ const ownedRun = spec.ports.sql.exec("SELECT id FROM __telnyx_agent_harness_runs WHERE id = ?", inputSteps.runId).toArray()[0];
740
+ // Direct internal turn tests can project historical approval envelopes
741
+ // without owning a durable run. Only ledger-owned runs use this fence.
742
+ if (ownedRun === undefined)
743
+ return undefined;
744
+ const existing = spec.ports.sql.exec("SELECT journal_seq FROM __telnyx_agent_harness_run_inputs WHERE run_id = ?", inputSteps.runId).toArray()[0];
745
+ if (existing !== undefined)
746
+ return existing;
747
+ const legacy = spec.ports.sql.exec("SELECT run_id FROM __telnyx_agent_harness_legacy_run_inputs WHERE run_id = ?", inputSteps.runId).toArray()[0];
748
+ if (legacy !== undefined)
749
+ return { legacy: true };
750
+ const journalSeq = (priorMessages.at(-1)?.seq ?? 0) + 1;
751
+ spec.ports.sql.exec("INSERT INTO __telnyx_agent_harness_run_inputs(run_id,journal_seq) VALUES (?,?)", inputSteps.runId, journalSeq);
752
+ return { journal_seq: journalSeq };
753
+ });
754
+ const journaledInput = inputJournal === undefined
755
+ ? undefined
756
+ : "legacy" in inputJournal
757
+ ? priorMessages.at(-1)?.role === "user" && priorMessages.at(-1)?.content === input
758
+ ? priorMessages.at(-1)
759
+ : undefined
760
+ : priorMessages.find((message) => message.seq === inputJournal.journal_seq);
761
+ if (inputJournal !== undefined && "legacy" in inputJournal && journaledInput === undefined) {
762
+ throw new Error("legacy durable input journal cannot be safely recovered");
763
+ }
764
+ if (inputJournal !== undefined && "legacy" in inputJournal && journaledInput !== undefined) {
765
+ spec.ports.sql.transactionSync(() => {
766
+ spec.ports.sql.exec("INSERT INTO __telnyx_agent_harness_run_inputs(run_id,journal_seq) VALUES (?,?)", inputSteps.runId, journaledInput.seq);
767
+ spec.ports.sql.exec("DELETE FROM __telnyx_agent_harness_legacy_run_inputs WHERE run_id = ?", inputSteps.runId);
768
+ });
769
+ }
770
+ if (journaledInput !== undefined && (journaledInput.role !== "user" || journaledInput.content !== input)) {
771
+ throw new Error("durable input journal does not match admitted input");
772
+ }
773
+ // A run-owned reserved sequence distinguishes a retry before append from
774
+ // a retry after append. Actor-global history is never a replay fence.
775
+ const replaysJournaledInput = journaledInput !== undefined;
776
+ let journalThrough = replaysJournaledInput
777
+ ? priorMessages.at(-1).seq
778
+ : await appendMessage({ role: "user", content: input });
779
+ const storedMessages = !replaysJournaledInput
780
+ ? await allMessages()
781
+ : priorMessages;
782
+ const restoredApprovals = options.steps === undefined
783
+ ? []
784
+ : await approvalMessages.restoreRun(options.steps.runId);
785
+ const messages = [];
786
+ let restoredApprovalIndex = 0;
787
+ const projectedRestoredToolCallIds = new Set();
788
+ const restoredApprovalCalls = new Map(restoredApprovals.map(({ toolCall }) => [toolCall.toolCallId, toolCall]));
789
+ const completedApprovedToolCalls = new Map();
790
+ const replayedCompletedApprovalToolCallIds = new Set();
791
+ for (const stored of storedMessages) {
792
+ messages.push(...historyMessages([stored]));
793
+ const restoredAtBoundary = [];
794
+ while (restoredApprovals[restoredApprovalIndex]?.afterMessageSeq === stored.seq) {
795
+ restoredAtBoundary.push(...restoredApprovals[restoredApprovalIndex++].messages);
796
+ }
797
+ if (restoredAtBoundary.length > 0) {
798
+ const assistantParts = [];
799
+ const toolParts = [];
800
+ for (const message of restoredAtBoundary) {
801
+ if (message.role === "assistant") {
802
+ if (typeof message.content === "string")
803
+ throw new Error("approval message chronology is invalid");
804
+ assistantParts.push(...message.content);
805
+ }
806
+ if (message.role === "tool")
807
+ toolParts.push(...message.content);
808
+ }
809
+ if (assistantParts.length === 0 || toolParts.length === 0)
810
+ throw new Error("approval message chronology is invalid");
811
+ messages.push({ role: "assistant", content: assistantParts }, { role: "tool", content: toolParts });
812
+ }
813
+ }
814
+ if (restoredApprovalIndex !== restoredApprovals.length)
815
+ throw new Error("approval message chronology is invalid");
816
+ const journaledToolCallMarkers = new Set(storedMessages.flatMap((message) => message.role === "tool" ? [decodeToolOutputEnvelope(message.content).journalMarker].filter((marker) => marker !== undefined) : []));
817
+ const projectedToolCallIds = new Set(storedMessages.flatMap((message) => message.role === "tool" && message.toolCallId !== undefined ? [message.toolCallId] : []));
818
+ options.steps?.reconcileToolCallJournal(journaledToolCallMarkers);
819
+ // Provider call IDs are correlation values. Durable logical positions are
820
+ // owned by the run-scoped step context so unrelated runs cannot move them.
821
+ const durableToolCallCounts = new Map();
822
+ let boundaryFailed = false;
823
+ let boundaryError;
824
+ let capReached = false;
825
+ let modelStepNumber = 0;
826
+ const limitReached = stepCountIs(runtimeConfig.limits.steps);
827
+ const assertBoundary = () => {
828
+ if (boundaryFailed)
829
+ throw boundaryError;
830
+ };
831
+ const modelForTurn = lifecycleMiddleware.modelStep.length === 0 || typeof runtimeConfig.model === "string"
832
+ ? runtimeConfig.model
833
+ : wrapLanguageModel({
834
+ model: runtimeConfig.model,
835
+ middleware: {
836
+ wrapGenerate: async ({ doGenerate }) => (await runMiddleware(Object.freeze({
837
+ phase: "modelStep",
838
+ config: runtimeConfig,
839
+ stepNumber: ++modelStepNumber,
840
+ signal: options.signal,
841
+ }), lifecycleMiddleware.modelStep, async () => await doGenerate())),
842
+ },
843
+ });
844
+ const toolsForTurn = Object.freeze(Object.fromEntries(Object.entries(runtimeConfig.tools ?? {}).map(([toolName, candidate]) => {
845
+ const execute = candidate.execute;
846
+ return [
847
+ toolName,
848
+ Object.freeze({
849
+ ...candidate,
850
+ async execute(...args) {
851
+ const toolCallOptions = args[1];
852
+ const toolCallId = typeof toolCallOptions?.toolCallId === "string"
853
+ ? toolCallOptions.toolCallId
854
+ : undefined;
855
+ const durableToolCallOrdinal = options.steps === undefined
856
+ ? (durableToolCallCounts.get(toolName) ?? 0)
857
+ : options.steps.nextToolCallOrdinal(toolName, toolCallId);
858
+ if (options.steps === undefined) {
859
+ durableToolCallCounts.set(toolName, durableToolCallOrdinal + 1);
860
+ }
861
+ let coreStarted = false;
862
+ let coreCompleted = false;
863
+ let replayingCompletedApproval = false;
864
+ try {
865
+ const output = await runMiddleware(Object.freeze({
866
+ phase: "tool",
867
+ config: runtimeConfig,
868
+ toolName,
869
+ toolCallId,
870
+ signal: options.signal,
871
+ }), lifecycleMiddleware.tool, async () => {
872
+ const stepName = `tool:${toolName}:${durableToolCallOrdinal}`;
873
+ const stepVersion = spec.durability?.toolStepVersion ?? DURABLE_TOOL_STEP_VERSION;
874
+ const restoringCompletedApproval = options.steps !== undefined
875
+ && toolCallId !== undefined
876
+ && restoredApprovalCalls.has(toolCallId)
877
+ && !projectedToolCallIds.has(toolCallId)
878
+ && options.steps.isCompleted(stepName, { input: args[0], version: stepVersion });
879
+ replayingCompletedApproval = restoringCompletedApproval;
880
+ const authorizedEffect = options.steps === undefined || restoringCompletedApproval
881
+ ? undefined
882
+ : await approvals.reauthorizeEffect(options.steps.runId, toolCallId, toolName, args[0]);
883
+ const approvalId = authorizedEffect?.id;
884
+ const effectArgs = authorizedEffect === undefined
885
+ ? args
886
+ : [authorizedEffect.input, ...args.slice(1)];
887
+ coreStarted = true;
888
+ const result = options.steps === undefined
889
+ ? cloneToolOutput(toolName, await execute.apply(this, args))
890
+ : await options.steps.step(stepName, async () => await withHarnessToolContext(Object.freeze({ runId: options.steps.runId, toolName, ordinal: durableToolCallOrdinal }), async () => await execute.apply(this, effectArgs)), {
891
+ external: true,
892
+ beforeEffect: () => approvals.consumeAuthorizedEffect(options.steps.runId, approvalId),
893
+ input: args[0],
894
+ version: stepVersion,
895
+ });
896
+ coreCompleted = true;
897
+ return result;
898
+ });
899
+ const restoredCall = toolCallId === undefined ? undefined : restoredApprovalCalls.get(toolCallId);
900
+ if (restoredCall !== undefined) {
901
+ const approvedOutput = typeof output === "string"
902
+ ? { type: "text", value: output }
903
+ : { type: "json", value: toJsonValue(output) };
904
+ completedApprovedToolCalls.set(toolCallId, Object.freeze({
905
+ ...restoredCall,
906
+ output: approvedOutput,
907
+ }));
908
+ if (replayingCompletedApproval)
909
+ replayedCompletedApprovalToolCallIds.add(toolCallId);
910
+ }
911
+ return cloneToolOutput(toolName, output);
912
+ }
913
+ catch (error) {
914
+ // AI SDK represents ordinary tool execution failures as durable tool-error
915
+ // parts. A middleware short-circuit failure has no core execution to encode,
916
+ // so preserve it as the turn's authoritative boundary error instead.
917
+ const durableToolOutputSerializationFailure = isToolOutputSerializationFailure(error);
918
+ if ((options.steps !== undefined || !coreStarted || coreCompleted)
919
+ && !boundaryFailed) {
920
+ boundaryFailed = true;
921
+ boundaryError = durableToolOutputSerializationFailure ? new HarnessToolOutputError(toolName) : error;
922
+ }
923
+ throw error;
924
+ }
925
+ },
926
+ }),
927
+ ];
928
+ })));
929
+ const durableSteps = options.steps;
930
+ const onToolCallStart = durableSteps === undefined
931
+ ? options.callbacks?.onToolCallStart
932
+ : async (event) => {
933
+ // AI SDK executes the per-call callback synchronously in model order
934
+ // before any awaited callback can permit a later call to execute.
935
+ // Reserve here rather than in wrapped execute(), whose start order is
936
+ // concurrent and therefore not durable identity.
937
+ durableSteps.nextToolCallOrdinal(event.toolCall.toolName, event.toolCall.toolCallId);
938
+ await options.callbacks?.onToolCallStart?.(event);
939
+ };
940
+ const generation = generateText({
941
+ model: modelForTurn,
942
+ tools: toolsForTurn,
943
+ ...(runtimeConfig.output === undefined ? {} : { output: runtimeConfig.output }),
944
+ abortSignal: options.signal,
945
+ system: runtimeConfig.instructions,
946
+ messages,
947
+ experimental_onStart: options.callbacks?.onStart,
948
+ experimental_onStepStart: options.callbacks?.onStepStart,
949
+ experimental_onToolCallStart: onToolCallStart,
950
+ experimental_onToolCallFinish: options.callbacks?.onToolCallFinish,
951
+ prepareStep() {
952
+ assertBoundary();
953
+ return undefined;
954
+ },
955
+ async stopWhen(context) {
956
+ assertBoundary();
957
+ const reached = await limitReached(context);
958
+ if (reached)
959
+ capReached = true;
960
+ return reached;
961
+ },
962
+ async onStepFinish(step) {
963
+ try {
964
+ assertBoundary();
965
+ const approvalRequests = step.content.filter((part) => part.type === "tool-approval-request");
966
+ const pendingRequests = options.steps === undefined ? [] : await Promise.all(approvalRequests.map(async (request) => replayedCompletedApprovalToolCallIds.has(request.toolCall.toolCallId)
967
+ || (await approvals.inspectUnchecked(request.approvalId))?.status === "granted"
968
+ ? undefined
969
+ : request));
970
+ if (pendingRequests.some((request) => request !== undefined) && options.steps !== undefined) {
971
+ const requests = pendingRequests.filter((request) => request !== undefined).map((request) => {
972
+ const call = step.toolCalls.find((candidate) => candidate.toolCallId === request.toolCall.toolCallId);
973
+ const action = call === undefined ? undefined : authorization?.toolActions[call.toolName];
974
+ const ttl = authorization?.approvalTtlMs;
975
+ if (call === undefined || action === undefined || ttl === undefined)
976
+ throw new Error("approval request linkage is invalid");
977
+ return { request, call, action, ttl };
978
+ });
979
+ if (requests.length !== approvalRequests.length)
980
+ throw new Error("approval request batch mixes granted and pending requests");
981
+ const pendingToolCallIds = new Set(requests.map(({ call }) => call.toolCallId));
982
+ const restoredCalls = [...completedApprovedToolCalls.values()]
983
+ .filter((call) => !projectedRestoredToolCallIds.has(call.toolCallId));
984
+ const restoredToolCallIds = new Set(restoredCalls.map((call) => call.toolCallId));
985
+ const completedCalls = [
986
+ ...step.toolCalls.filter((call) => !pendingToolCallIds.has(call.toolCallId) && !restoredToolCallIds.has(call.toolCallId)),
987
+ ...restoredCalls,
988
+ ];
989
+ if (completedCalls.length > 0) {
990
+ const journalMarkers = options.steps.associateToolCallJournal(completedCalls.map((call) => ({
991
+ toolName: call.toolName,
992
+ toolCallId: call.toolCallId,
993
+ })));
994
+ journalThrough = await appendMessages([
995
+ ...journalForApprovedToolCalls(restoredCalls, journalMarkers),
996
+ ...journalForStep(step, journalMarkers, step.toolCalls.filter((call) => !pendingToolCallIds.has(call.toolCallId) && !restoredToolCallIds.has(call.toolCallId))),
997
+ ]);
998
+ for (const marker of journalMarkers.values())
999
+ journaledToolCallMarkers.add(marker);
1000
+ for (const call of completedCalls)
1001
+ projectedToolCallIds.add(call.toolCallId);
1002
+ for (const call of restoredCalls)
1003
+ projectedRestoredToolCallIds.add(call.toolCallId);
1004
+ }
1005
+ await approvals.requestBatchWithPause(requests.map(({ request, call, action, ttl }) => ({
1006
+ id: request.approvalId, runId: options.steps.runId, toolName: call.toolName, toolCallId: call.toolCallId,
1007
+ arguments: call.input, requiredAction: action, expiresAt: spec.ports.clock.now() + ttl,
1008
+ })), () => {
1009
+ const firstJournalSeq = approvalMessages.nextRunSequence(options.steps.runId);
1010
+ const approvalEpoch = approvalMessages.nextRunApprovalEpoch(options.steps.runId);
1011
+ for (const [index, { request, call }] of requests.entries())
1012
+ approvalMessages.requestInTransaction({
1013
+ runId: options.steps.runId, approvalId: request.approvalId, toolCallId: call.toolCallId, toolName: call.toolName,
1014
+ journalSeq: firstJournalSeq + index, afterMessageSeq: journalThrough, approvalEpoch,
1015
+ canonicalToolCall: { toolCallId: call.toolCallId, toolName: call.toolName, input: call.input },
1016
+ ...(request.signature === undefined ? {} : { signature: request.signature }),
1017
+ });
1018
+ ledger.pauseForApproval(options.steps.runId);
1019
+ });
1020
+ throw new HarnessApprovalPause();
1021
+ }
1022
+ const restoredCalls = [...completedApprovedToolCalls.values()]
1023
+ .filter((call) => !projectedRestoredToolCallIds.has(call.toolCallId));
1024
+ const restoredToolCallIds = new Set(restoredCalls.map((call) => call.toolCallId));
1025
+ const completedCalls = [
1026
+ ...step.toolCalls.filter((call) => !restoredToolCallIds.has(call.toolCallId)),
1027
+ ...restoredCalls,
1028
+ ];
1029
+ const journalMarkers = completedCalls.length === 0
1030
+ ? new Map()
1031
+ : options.steps?.associateToolCallJournal(completedCalls.map((call) => ({
1032
+ toolName: call.toolName,
1033
+ toolCallId: call.toolCallId,
1034
+ }))) ?? new Map();
1035
+ journalThrough = await appendMessages([
1036
+ ...journalForApprovedToolCalls(restoredCalls, journalMarkers),
1037
+ ...journalForStep(step, journalMarkers, step.toolCalls.filter((call) => !restoredToolCallIds.has(call.toolCallId))),
1038
+ ]);
1039
+ for (const marker of journalMarkers.values())
1040
+ journaledToolCallMarkers.add(marker);
1041
+ for (const call of completedCalls)
1042
+ projectedToolCallIds.add(call.toolCallId);
1043
+ for (const call of restoredCalls)
1044
+ projectedRestoredToolCallIds.add(call.toolCallId);
1045
+ await options.onStepCommitted?.(observedStep(step, journalThrough));
1046
+ await options.callbacks?.onStepFinish?.(step);
1047
+ }
1048
+ catch (error) {
1049
+ if (!boundaryFailed) {
1050
+ boundaryFailed = true;
1051
+ boundaryError = error;
1052
+ }
1053
+ throw error;
1054
+ }
1055
+ },
1056
+ onFinish: options.callbacks?.onFinish,
1057
+ });
1058
+ let result;
1059
+ try {
1060
+ result = await generation;
1061
+ }
1062
+ catch (error) {
1063
+ if (boundaryFailed)
1064
+ throw boundaryError;
1065
+ if (isProviderFailure(error)) {
1066
+ throw new HarnessProviderError(providerDiagnostics(runtimeConfig.model, error));
1067
+ }
1068
+ throw error;
1069
+ }
1070
+ assertBoundary();
1071
+ if (capReached) {
1072
+ const lastStep = result.steps.at(-1);
1073
+ throw new HarnessStepLimitError(runtimeConfig.limits.steps, result.steps.length, (lastStep?.toolCalls ?? []).map((call) => ({
1074
+ id: call.toolCallId,
1075
+ name: call.toolName,
1076
+ })));
1077
+ }
1078
+ const output = Object.freeze({
1079
+ text: result.text,
1080
+ stepCount: result.steps.length,
1081
+ finishReason: result.finishReason,
1082
+ journalThrough,
1083
+ usage: Object.freeze({
1084
+ inputTokens: result.totalUsage.inputTokens,
1085
+ outputTokens: result.totalUsage.outputTokens,
1086
+ totalTokens: result.totalUsage.totalTokens,
1087
+ }),
1088
+ ...(runtimeConfig.output === undefined
1089
+ ? {}
1090
+ : { output: result.output }),
1091
+ });
1092
+ await options.onOutput?.(output);
1093
+ return output;
1094
+ });
1095
+ }
1096
+ catch (error) {
1097
+ const cancellation = error instanceof HarnessCancellationError
1098
+ || (options.signal?.aborted === true
1099
+ && error instanceof Error
1100
+ && error.name === "AbortError");
1101
+ if (cancellation) {
1102
+ const classified = error instanceof HarnessCancellationError
1103
+ ? error
1104
+ : new HarnessCancellationError();
1105
+ try {
1106
+ await options.onCancel?.(classified);
1107
+ }
1108
+ catch (observer) {
1109
+ throw new HarnessObserverError(classified, observer, "cancel");
1110
+ }
1111
+ throw classified;
1112
+ }
1113
+ try {
1114
+ await options.onError?.(error);
1115
+ }
1116
+ catch (observer) {
1117
+ throw new HarnessObserverError(error, observer, "error");
1118
+ }
1119
+ throw error;
1120
+ }
1121
+ };
1122
+ const ledger = createHarnessRunExecutionLedger(spec.ports, async (input, signal, steps) => {
1123
+ // Journal correlation enriches scheduling recovery, but it must not turn
1124
+ // an otherwise executable durable run into a failed one when the journal
1125
+ // port is temporarily unavailable.
1126
+ let journalBefore;
1127
+ try {
1128
+ journalBefore = await spec.ports.messages.count();
1129
+ }
1130
+ catch { /* correlation is repairable */ }
1131
+ const persistJournal = (journalSeq) => {
1132
+ spec.ports.sql.transactionSync(() => spec.ports.sql.exec("INSERT INTO __telnyx_agent_harness_run_journal(run_id,journal_seq) VALUES (?,?) ON CONFLICT(run_id) DO UPDATE SET journal_seq = excluded.journal_seq", steps.runId, journalSeq));
1133
+ };
1134
+ // A cancellation can terminalize this claimed run before the model turn
1135
+ // reaches a later journal boundary. Persist the known pre-turn boundary
1136
+ // first, so restart correlation never depends on a post-cancel port read.
1137
+ if (journalBefore !== undefined)
1138
+ persistJournal(journalBefore);
1139
+ try {
1140
+ const result = await turn(input, {
1141
+ signal,
1142
+ steps,
1143
+ ...(spec.durability?.onToolCallStart === undefined
1144
+ ? {}
1145
+ : { callbacks: { onToolCallStart: spec.durability.onToolCallStart } }),
1146
+ });
1147
+ // This is written within the durable turn before ledger.run() marks the
1148
+ // run completed. A scheduler occurrence can recover its exact journal
1149
+ // linkage by run id even if its post-dispatch wrapper never resumes.
1150
+ persistJournal(result.journalThrough);
1151
+ return result;
1152
+ }
1153
+ catch (error) {
1154
+ // A terminal turn can fail after durably appending the admitted input or
1155
+ // a tool boundary. The serialized actor journal advanced only for this
1156
+ // run, so persist that exact boundary for recovery; failures before any
1157
+ // append intentionally have no journal correlation to invent.
1158
+ const hasScheduledJournalMarker = spec.ports.sql.exec("SELECT run_id FROM __telnyx_agent_harness_run_journal WHERE run_id = ?", steps.runId).toArray().length > 0;
1159
+ if (journalBefore !== undefined || hasScheduledJournalMarker) {
1160
+ try {
1161
+ const journalAfter = await spec.ports.messages.count();
1162
+ if (journalBefore === undefined || journalAfter > journalBefore)
1163
+ persistJournal(journalAfter);
1164
+ }
1165
+ catch { /* correlation is repairable */ }
1166
+ }
1167
+ throw error;
1168
+ }
1169
+ }, { checkpoints: spec.durability?.checkpoints, requireApprovalIdentity: requiresApprovalIdentity });
1170
+ const approvalService = Object.freeze({
1171
+ inspect: approvals.inspect,
1172
+ async grant(id) {
1173
+ let approval;
1174
+ try {
1175
+ approval = await approvals.grantWithResume(id, (granted) => {
1176
+ approvalMessages.decideInTransaction({ approvalId: id, approved: true });
1177
+ }, (granted) => ledger.resumeForApproval(granted.runId));
1178
+ }
1179
+ catch (error) {
1180
+ if (error instanceof HarnessApprovalError && error.code === "expired") {
1181
+ const expired = await approvals.inspectUnchecked(id);
1182
+ if (expired?.status === "expired")
1183
+ await ledger.expireForApproval(expired.runId);
1184
+ }
1185
+ throw error;
1186
+ }
1187
+ await ledger.resume(approval.runId);
1188
+ return approval;
1189
+ },
1190
+ async deny(id) {
1191
+ let approval;
1192
+ try {
1193
+ approval = await approvals.denyWithCancel(id, (denied) => {
1194
+ approvalMessages.decideInTransaction({ approvalId: id, approved: false, reason: "denied" });
1195
+ ledger.resumeForApproval(denied.runId);
1196
+ });
1197
+ }
1198
+ catch (error) {
1199
+ if (error instanceof HarnessApprovalError && error.code === "expired") {
1200
+ const expired = await approvals.inspectUnchecked(id);
1201
+ if (expired?.status === "expired")
1202
+ await ledger.expireForApproval(expired.runId);
1203
+ }
1204
+ throw error;
1205
+ }
1206
+ await ledger.resume(approval.runId);
1207
+ return approval;
1208
+ },
1209
+ });
1210
+ return Object.freeze({
1211
+ turn,
1212
+ accept: ledger.accept,
1213
+ get: ledger.get,
1214
+ status: ledger.status,
1215
+ list: ledger.list,
1216
+ cancel: ledger.cancel,
1217
+ recover: ledger.recover,
1218
+ run: ledger.run,
1219
+ dispatch: ledger.dispatch,
1220
+ approvals: approvalService,
1221
+ });
1222
+ }
1223
+ //# sourceMappingURL=harness.js.map