@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.
- package/README.md +179 -0
- package/dist/approvals.d.ts +140 -0
- package/dist/approvals.js +699 -0
- package/dist/channel.d.ts +178 -0
- package/dist/channel.js +184 -0
- package/dist/contract.d.ts +50 -0
- package/dist/contract.js +278 -0
- package/dist/durable.d.ts +81 -0
- package/dist/durable.js +650 -0
- package/dist/harness.d.ts +199 -0
- package/dist/harness.js +1223 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +19 -0
- package/dist/lifecycle.d.ts +9 -0
- package/dist/lifecycle.js +25 -0
- package/dist/node-adapter.d.ts +71 -0
- package/dist/node-adapter.js +736 -0
- package/dist/ports.d.ts +99 -0
- package/dist/ports.js +3 -0
- package/dist/runtime-adapter.d.ts +15 -0
- package/dist/runtime-adapter.js +70 -0
- package/dist/runtime-config.d.ts +38 -0
- package/dist/runtime-config.js +104 -0
- package/dist/scheduling.d.ts +46 -0
- package/dist/scheduling.js +425 -0
- package/dist/steps.d.ts +52 -0
- package/dist/steps.js +310 -0
- package/dist/tool-context.d.ts +8 -0
- package/dist/tool-context.js +9 -0
- package/dist/workspace.d.ts +151 -0
- package/dist/workspace.js +404 -0
- package/package.json +58 -0
package/dist/harness.js
ADDED
|
@@ -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
|