@telnyx/agent-harness 0.1.0-beta.0 → 0.1.0-beta.2

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 CHANGED
@@ -7,10 +7,11 @@ execution. The Node-only test adapter is intentionally isolated to a subpath.
7
7
  ## Beta and release gate
8
8
 
9
9
  This source change does not publish a package. The planned initial release is
10
- `0.1.0-beta.0` under the `beta` tag. The package remains `UNLICENSED`; an owner
10
+ `0.1.0-beta.2` under the `beta` tag. The package remains `UNLICENSED`; an owner
11
11
  must approve npm organization/package ownership, distribution terms, and trusted
12
12
  publisher bootstrap before the main-only publication workflow is dispatched. A
13
- registry-installed `@telnyx/edge-runtime@0.15.2` is required: the runtime
13
+ registry-installed `@telnyx/edge-runtime@^0.16.0` is required — one copy,
14
+ shared with whatever extends `Agent`, or the host key will not match: the runtime
14
15
  adapter uses its `./internal` `getAgentHarnessHost` and
15
16
  `getAgentHarnessAuthorizationProvider` seams. Source validation is not evidence
16
17
  that the beta is published or live; see `docs/agent-harness/beta-release.md` for
@@ -19,7 +20,7 @@ fresh preflight, publication, and rollback gates.
19
20
  ## Installation
20
21
 
21
22
  ```bash
22
- npm install @telnyx/agent-harness@beta @telnyx/edge-runtime@^0.15.2
23
+ npm install @telnyx/agent-harness@beta @telnyx/edge-runtime@^0.16.0
23
24
  ```
24
25
 
25
26
  The package requires Node 22.13 or newer.
@@ -61,6 +62,56 @@ const harness = createHarness({
61
62
  const result = await harness.turn("Summarize this input");
62
63
  console.log(result.text);
63
64
  ```
65
+ ## Speculative turns (begin / commit / discard)
66
+
67
+ `harness.begin(input, { signal })` starts the model once its activation is available and returns a
68
+ speculative turn handle. Nothing durable is written until `commit(confirmed)`
69
+ is called — no user row, no assistant row, no tool-result row, no SQL run row.
70
+ The handle exposes a `textStream` (async iterable of text deltas), a `result`
71
+ promise, `commit(confirmedText)`, and `discard()`.
72
+
73
+ ```ts
74
+ const turn = harness.begin("guess", { signal });
75
+ // confirmedTranscript is the recognizer's Promise<string | undefined>.
76
+ // Confirmation must run concurrently: stream completion can wait for commit.
77
+ const commitment = confirmedTranscript.then((text) =>
78
+ text === undefined ? turn.discard() : turn.commit(text));
79
+ void commitment.catch(() => {}); // observe even if the consumer exits early
80
+ try {
81
+ for await (const delta of turn.textStream) prepareProvisionalText(delta);
82
+ await commitment;
83
+ await turn.result;
84
+ } finally {
85
+ turn.discard(); // no-op if commitment already won
86
+ }
87
+ ```
88
+
89
+ `commit` publishes the confirmed text (not the guess) as the user row, then
90
+ allows completed assistant/tool steps to publish. In-flight model work continues
91
+ without restarting; all tools are deferred until commitment. Provisional text
92
+ can be synthesized early, but audio must be held until confirmation. `discard`
93
+ before commitment aborts the model, fences late callbacks, and leaves no durable trace; the next
94
+ turn's `seq` follows the previous confirmed turn's with no hole. All tools —
95
+ including read-only tools — wait until commit; there is no tool opt-in
96
+ framework. Durable step contexts, pre-model lifecycle hooks, turn/model-step
97
+ middleware, and structured output fail closed on `begin`; use `turn` for those.
98
+ A static output or any nonempty construction-captured `outputs` catalog makes
99
+ `begin` refuse synchronously, even with no runtime output currently selected.
100
+ The default voice hook waits for confirmation and falls back to `turn` on that
101
+ refusal; absent or empty captured catalogs still permit speculation.
102
+ Unread text is cleared when its single consumer returns. Total text is limited
103
+ to 1,048,576 UTF-8 bytes / 16,384 deltas, failing with `RangeError` on excess.
104
+ Always commit/discard/abort held work. Cancellation cannot undo issued host
105
+ effects; they retain activation ownership until they settle. Recursive turns
106
+ across that exclusive reservation are rejected rather than deadlocking.
107
+ An error after commit may leave partial history, and repeated commit does not retry.
108
+ Speculation reserves exclusive ownership in FIFO order: an already-active
109
+ ordinary turn on the same activation can delay the voice model start even after
110
+ confirmation. All earlier ordinary turns must finish first; later turns remain
111
+ blocked until exclusive ownership is released. Ordinary turns run concurrently
112
+ only when no exclusive reservation intervenes. Other activations are unaffected.
113
+ See the [transactional voice contract](https://github.com/team-telnyx/edge-compute-statefulactor/blob/compute-953/transactional-voice-turn/docs/agent-harness/transactional-voice-turn.md)
114
+ for the full design.
64
115
 
65
116
  A harness captures its ports, model, instructions, and tool map at construction.
66
117
  Each completed model step is committed before callbacks run. This beta does not
@@ -68,6 +119,16 @@ claim exactly-once external effects or crash recovery; production durability,
68
119
  concurrency ownership, and provider configuration remain responsibilities of
69
120
  the selected host.
70
121
 
122
+ ## Bounded host diagnostics
123
+
124
+ See [bounded host diagnostics](../../docs/agent-harness/diagnostics.md) for
125
+ filtered waiting-run discovery, known-run `get`/`status`, known-approval-ID
126
+ inspection, and active schedule listing. These are trusted in-process host
127
+ capabilities, not authenticated remote endpoints. The guide explains inspection's
128
+ authorization/audit/expiry boundaries and the missing pending-approval discovery,
129
+ run pagination, host step inventory, and schedule-occurrence APIs. Run discovery
130
+ alone does not supply an approval ID.
131
+
71
132
  ## Scheduling dispatch and recovery lifecycle
72
133
 
73
134
  `createHarnessWithScheduling` constructs a durable harness with bounded
@@ -171,6 +232,47 @@ accepted, and records the run's journal sequence after completion. This
171
232
  makes the occurrence → run → journal relationship recoverable without
172
233
  inferring from actor-global history.
173
234
 
235
+ ## Virtual workspace skill activation
236
+
237
+ `createVirtualWorkspace(ports)` stores inert skill documents at
238
+ `skills/<name>/SKILL.md`. Use the exact `name` returned by
239
+ `workspace.discoverSkills()` when calling `workspace.activateSkill(name)` or
240
+ the `activate_skill({ name })` tool from `createVirtualWorkspaceTools(workspace)`.
241
+ Names are case-sensitive: there is no trimming, aliasing, fuzzy matching, or
242
+ conversion from a path or document title. For example, the registered name for
243
+ `skills/Guide-v1/SKILL.md` is `Guide-v1`, not `guide-v1`, `Guide`, or the file
244
+ path. Discovery can return a bounded prefix with `truncated: true`; absence
245
+ from a truncated catalog does not prove the document does not exist.
246
+
247
+ Activation returns `{ name, path, origin: "activated-skill", content }` as prompt
248
+ data; it does not execute the document. Both the raw UTF-8 content and the full
249
+ JSON result including metadata must fit `maxOutputBytes`. Content is not
250
+ silently truncated during activation.
251
+
252
+ Activation validation/lookup/bound rejections use the root-exported
253
+ `HarnessSkillActivationError`, with a fixed `code` and fixed message:
254
+
255
+ | Code | Meaning |
256
+ | --- | --- |
257
+ | `HARNESS_SKILL_INVALID_NAME` | Empty, non-string, slash, backslash, or NUL-containing name. |
258
+ | `HARNESS_SKILL_NOT_FOUND` | No document exists at the exact registered path. |
259
+ | `HARNESS_SKILL_CONTENT_BOUND` | Raw UTF-8 content exceeds `maxOutputBytes`. |
260
+ | `HARNESS_SKILL_OUTPUT_BOUND` | Full serialized activation result exceeds `maxOutputBytes`. |
261
+
262
+ These errors do not include the requested name, arguments, prompt, content, or
263
+ raw cause. Input-schema rejection before tool execution and unrelated storage
264
+ errors are not converted into activation errors. Direct callers can inspect
265
+ `error instanceof HarnessSkillActivationError` and `error.code`; existing tool
266
+ middleware can catch the same local error from `await next()` and rethrow it.
267
+ Do not log arbitrary errors or tool arguments as part of this diagnostic path.
268
+
269
+ These are **local diagnostics, not persisted run causes**. A durable tool that
270
+ throws after its effect-start fence still produces
271
+ `outcome_unknown / HARNESS_OUTCOME_UNKNOWN`. The typed error does not make that
272
+ run safe to retry: duplicate dispatch, `run`, and recovery do not re-execute it.
273
+ This does not recover a historical exception or establish why an earlier run
274
+ failed; investigate already-retained evidence without blindly retrying effects.
275
+
174
276
  ## Validation
175
277
 
176
278
  Package development and release preparation are validated with the repository's
@@ -41,7 +41,28 @@ export interface HarnessApprovalAudit {
41
41
  readonly outcome: string;
42
42
  readonly at: number;
43
43
  }
44
+ export interface HarnessPendingApprovalSummary {
45
+ readonly id: string;
46
+ readonly runId: string;
47
+ readonly toolName: string;
48
+ readonly status: "pending";
49
+ readonly expiresAt: number;
50
+ readonly createdAt: number;
51
+ readonly updatedAt: number;
52
+ }
53
+ export interface HarnessPendingApprovalListOptions {
54
+ /** Safe integer from 1 through 100; defaults to 50. */
55
+ readonly limit?: number;
56
+ /** Opaque actor/organization/caller/run-bound keyset continuation. */
57
+ readonly cursor?: string;
58
+ }
59
+ export interface HarnessPendingApprovalPage {
60
+ readonly items: readonly HarnessPendingApprovalSummary[];
61
+ readonly nextCursor?: string;
62
+ }
44
63
  export interface HarnessApprovalLedger {
64
+ /** Enumerate minimal unexpired pending summaries with explicit host collection authority. */
65
+ listPending(runId: string, options?: HarnessPendingApprovalListOptions): Promise<HarnessPendingApprovalPage>;
45
66
  inspect(id: string): Promise<HarnessApproval>;
46
67
  grant(id: string): Promise<HarnessApproval>;
47
68
  deny(id: string): Promise<HarnessApproval>;
package/dist/approvals.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ensureHarnessDurabilitySchema } from "./durable.js";
1
+ import { ensureHarnessDurabilitySchema, HARNESS_DURABILITY_SCHEMA_VERSION } from "./durable.js";
2
2
  export const HARNESS_APPROVAL_STATUSES = ["pending", "granted", "denied", "expired", "consumed"];
3
3
  export class HarnessAuthorizationError extends Error {
4
4
  code;
@@ -208,9 +208,27 @@ async function authorize(ports, action, row, persistedSubject = false) {
208
208
  throw new HarnessAuthorizationError("forbidden");
209
209
  throw new HarnessAuthorizationError("policy_unavailable");
210
210
  }
211
+ function collectionIdentity(ports) {
212
+ let identity;
213
+ try {
214
+ identity = ports.authorization?.identity();
215
+ }
216
+ catch {
217
+ throw new HarnessAuthorizationError("identity_unavailable");
218
+ }
219
+ if (identity === undefined)
220
+ throw new HarnessAuthorizationError("identity_unavailable");
221
+ if (identity === null || typeof identity.serviceAccountId !== "string" || typeof identity.organizationId !== "string"
222
+ || !identity.serviceAccountId.trim() || !identity.organizationId.trim()
223
+ || Buffer.byteLength(identity.serviceAccountId, "utf8") > MAX_ID_BYTES
224
+ || Buffer.byteLength(identity.organizationId, "utf8") > MAX_ID_BYTES)
225
+ throw new HarnessAuthorizationError("unauthenticated");
226
+ return Object.freeze({ serviceAccountId: identity.serviceAccountId, organizationId: identity.organizationId });
227
+ }
211
228
  export function createHarnessApprovalLedger(ports) {
212
229
  const execution = createHarnessApprovalExecutionLedger(ports);
213
230
  return Object.freeze({
231
+ listPending: execution.listPending,
214
232
  inspect: execution.inspect,
215
233
  grant: execution.grant,
216
234
  deny: execution.deny,
@@ -277,6 +295,80 @@ export function createHarnessApprovalExecutionLedger(ports) {
277
295
  return new HarnessApprovalError(status === requested ? "replayed" : "conflicting");
278
296
  };
279
297
  return Object.freeze({
298
+ async listPending(runId, options = {}) {
299
+ if (typeof runId !== "string")
300
+ throw new Error("run id must be a string");
301
+ nonblank(runId, "run id");
302
+ if (options === null || typeof options !== "object" || Array.isArray(options)
303
+ || Object.keys(options).some((key) => key !== "limit" && key !== "cursor"))
304
+ throw new Error("invalid pending approval options");
305
+ const limit = options.limit === undefined ? 50 : options.limit;
306
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
307
+ throw new Error("pending approval limit must be an integer from 1 through 100");
308
+ const identity = collectionIdentity(ports);
309
+ const scope = Buffer.from(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([ports.identity.id, identity.organizationId, identity.serviceAccountId, runId])))).toString("hex");
310
+ let afterAt = -1;
311
+ let afterId = "";
312
+ if (options.cursor !== undefined) {
313
+ try {
314
+ if (typeof options.cursor !== "string" || options.cursor.length > 8192 || !/^[A-Za-z0-9_-]+$/.test(options.cursor))
315
+ throw new Error();
316
+ const decoded = Buffer.from(options.cursor, "base64url");
317
+ if (decoded.toString("base64url") !== options.cursor)
318
+ throw new Error();
319
+ const value = JSON.parse(decoded.toString("utf8"));
320
+ if (!Array.isArray(value) || value.length !== 4 || value[0] !== 1 || value[1] !== scope
321
+ || !Number.isSafeInteger(value[2]) || value[2] < 0 || typeof value[3] !== "string")
322
+ throw new Error();
323
+ nonblank(value[3], "cursor id");
324
+ afterAt = value[2];
325
+ afterId = value[3];
326
+ }
327
+ catch {
328
+ throw new Error("invalid pending approval cursor");
329
+ }
330
+ }
331
+ let decision;
332
+ try {
333
+ decision = await ports.authorization?.authorizePendingApprovalSummaries?.({
334
+ identity, actorId: ports.identity.id, action: "agent_harness.approvals.list_pending",
335
+ resource: `agent_harness/runs/${encodeURIComponent(runId)}/pending_approval_summaries`, runId,
336
+ });
337
+ }
338
+ catch {
339
+ throw new HarnessAuthorizationError("policy_unavailable");
340
+ }
341
+ if (decision?.kind === "deny")
342
+ throw new HarnessAuthorizationError("forbidden");
343
+ if (decision?.kind !== "allow")
344
+ throw new HarnessAuthorizationError("policy_unavailable");
345
+ const live = collectionIdentity(ports);
346
+ if (live.organizationId !== identity.organizationId || live.serviceAccountId !== identity.serviceAccountId) {
347
+ throw new HarnessAuthorizationError("unauthenticated");
348
+ }
349
+ // Discovery is SELECT-only: never initialize/migrate a database on a read.
350
+ const schemaExists = ports.sql.exec("SELECT name FROM sqlite_master WHERE type = 'table' AND name = '__telnyx_agent_harness_schema'").toArray().length > 0;
351
+ if (!schemaExists)
352
+ return Object.freeze({ items: Object.freeze([]) });
353
+ const versions = ports.sql.exec("SELECT version FROM __telnyx_agent_harness_schema LIMIT 2").toArray();
354
+ if (versions.length !== 1 || versions[0]?.version !== HARNESS_DURABILITY_SCHEMA_VERSION)
355
+ throw new Error("pending approval schema is unsupported");
356
+ const rows = ports.sql.exec("SELECT id,run_id,tool_name,status,expires_at,created_at,updated_at FROM __telnyx_agent_harness_approvals WHERE run_id = ? AND organization_id = ? AND status = 'pending' AND expires_at > ? AND (created_at > ? OR (created_at = ? AND id > ?)) ORDER BY created_at,id LIMIT ?", runId, identity.organizationId, ports.clock.now(), afterAt, afterAt, afterId, limit + 1).toArray();
357
+ for (const row of rows) {
358
+ if ([row.id, row.run_id, row.tool_name].some((value) => typeof value !== "string" || !value.trim() || Buffer.byteLength(value, "utf8") > MAX_ID_BYTES)
359
+ || [row.created_at, row.updated_at, row.expires_at].some((value) => !Number.isSafeInteger(value) || value < 0)) {
360
+ throw new Error("invalid pending approval summary");
361
+ }
362
+ }
363
+ const selected = rows.slice(0, limit);
364
+ const last = selected.at(-1);
365
+ return Object.freeze({ items: Object.freeze(selected.map((row) => Object.freeze({
366
+ id: row.id, runId: row.run_id, toolName: row.tool_name, status: "pending",
367
+ expiresAt: row.expires_at, createdAt: row.created_at, updatedAt: row.updated_at,
368
+ }))), ...(rows.length > limit && last !== undefined ? {
369
+ nextCursor: Buffer.from(JSON.stringify([1, scope, last.created_at, last.id])).toString("base64url"),
370
+ } : {}) });
371
+ },
280
372
  async request(request) {
281
373
  return requestApproval(request);
282
374
  },
@@ -0,0 +1,33 @@
1
+ import { type HarnessRunStatus } from "./durable.js";
2
+ import type { AgentHarnessPorts } from "./ports.js";
3
+ export interface HarnessRunInspectionOptions {
4
+ /** Step-page size, default 25, maximum 50. */
5
+ readonly limit?: number;
6
+ /** Opaque actor/run-scoped continuation; not authority or an atomic snapshot. */
7
+ readonly cursor?: string;
8
+ }
9
+ /** Bounded persisted metadata only; identifiers can still be sensitive. */
10
+ export interface HarnessRunInspection {
11
+ readonly run: Readonly<{
12
+ id: string;
13
+ status: HarnessRunStatus;
14
+ failureCode?: string;
15
+ }>;
16
+ readonly journalSeq?: number;
17
+ readonly scheduleOccurrences: readonly Readonly<{
18
+ scheduleId: string;
19
+ scheduledFor: number;
20
+ journalSeq?: number;
21
+ }>[];
22
+ readonly steps: readonly Readonly<{
23
+ name: string;
24
+ version: string;
25
+ phase: "prepared" | "effect_started" | "completed";
26
+ journalSeq: number;
27
+ outputFailure?: "tool_output_serialization_failure";
28
+ }>[];
29
+ readonly nextCursor?: string;
30
+ }
31
+ /** Private implementation of the trusted host read; never initializes storage. */
32
+ export declare function inspectHarnessRun(ports: AgentHarnessPorts, runId: string, options?: HarnessRunInspectionOptions): Promise<HarnessRunInspection | undefined>;
33
+ //# sourceMappingURL=diagnostics.d.ts.map
@@ -0,0 +1,107 @@
1
+ import { createHash } from "node:crypto";
2
+ import { HARNESS_DURABILITY_SCHEMA_VERSION, HARNESS_RUN_STATUSES } from "./durable.js";
3
+ const failures = new Set(["HARNESS_CANCELLED", "HARNESS_OBSERVER_ERROR", "HARNESS_PROVIDER_ERROR", "HARNESS_STEP_LIMIT", "HARNESS_TOOL_OUTPUT_ERROR", "HARNESS_APPROVAL_INVALID", "HARNESS_APPROVAL_EXPIRED", "HARNESS_AUTHORIZATION_REVOKED", "HARNESS_RUN_FAILED", "HARNESS_OUTCOME_UNKNOWN"]);
4
+ function text(value) {
5
+ if (typeof value !== "string" || !value.trim() || Buffer.byteLength(value, "utf8") > 512)
6
+ throw new Error("Invalid run inspection data");
7
+ }
8
+ function sequence(value) {
9
+ if (!Number.isSafeInteger(value) || value < 0)
10
+ throw new Error("Invalid run inspection data");
11
+ }
12
+ /** Private implementation of the trusted host read; never initializes storage. */
13
+ export async function inspectHarnessRun(ports, runId, options = {}) {
14
+ let limit;
15
+ let after = 0;
16
+ let afterName = "";
17
+ let scope;
18
+ try {
19
+ text(runId);
20
+ text(ports.identity.id);
21
+ scope = createHash("sha256").update(JSON.stringify(["run-inspection-v1", ports.identity.id, runId])).digest("hex");
22
+ if (options === null || typeof options !== "object" || Object.getPrototypeOf(options) !== Object.prototype)
23
+ throw new Error();
24
+ for (const key of Reflect.ownKeys(options)) {
25
+ if (key !== "limit" && key !== "cursor")
26
+ throw new Error();
27
+ if (!("value" in Object.getOwnPropertyDescriptor(options, key)))
28
+ throw new Error();
29
+ }
30
+ limit = options.limit === undefined ? 25 : options.limit;
31
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 50)
32
+ throw new Error();
33
+ if (options.cursor !== undefined) {
34
+ if (typeof options.cursor !== "string" || options.cursor.length > 8192 || !/^[A-Za-z0-9_-]+$/.test(options.cursor))
35
+ throw new Error();
36
+ const decoded = Buffer.from(options.cursor, "base64url");
37
+ if (decoded.toString("base64url") !== options.cursor)
38
+ throw new Error();
39
+ const tuple = JSON.parse(decoded.toString("utf8"));
40
+ if (!Array.isArray(tuple) || tuple.length !== 4 || tuple[0] !== 1 || tuple[1] !== scope)
41
+ throw new Error();
42
+ sequence(tuple[2]);
43
+ text(tuple[3]);
44
+ after = tuple[2];
45
+ afterName = tuple[3];
46
+ if (Buffer.from(JSON.stringify(tuple)).toString("base64url") !== options.cursor)
47
+ throw new Error();
48
+ }
49
+ }
50
+ catch {
51
+ throw new Error("Invalid run inspection options");
52
+ }
53
+ try {
54
+ const present = ports.sql.exec("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '__telnyx_agent_harness_schema'").toArray();
55
+ if (present.length === 0)
56
+ return undefined;
57
+ const versions = ports.sql.exec("SELECT version FROM __telnyx_agent_harness_schema LIMIT 2").toArray();
58
+ if (versions.length !== 1 || versions[0]?.version !== HARNESS_DURABILITY_SCHEMA_VERSION)
59
+ throw new Error();
60
+ const row = ports.sql.exec("SELECT id,status,failure_code FROM __telnyx_agent_harness_runs WHERE id = ?", runId).toArray()[0];
61
+ if (row === undefined)
62
+ return undefined;
63
+ text(row.id);
64
+ if (!HARNESS_RUN_STATUSES.includes(row.status) || (row.failure_code !== null && !failures.has(row.failure_code)))
65
+ throw new Error();
66
+ const rows = ports.sql.exec("SELECT name,version,status,journal_seq,output_failure FROM __telnyx_agent_harness_steps WHERE run_id = ? AND (journal_seq > ? OR (journal_seq = ? AND name > ?)) ORDER BY journal_seq,name LIMIT ?", runId, after, after, afterName, limit + 1).toArray();
67
+ const steps = rows.slice(0, limit).map(step => {
68
+ text(step.name);
69
+ text(step.version);
70
+ sequence(step.journal_seq);
71
+ if (!["prepared", "effect_started", "completed"].includes(step.status) || (step.output_failure !== null && step.output_failure !== "tool_output_serialization_failure"))
72
+ throw new Error();
73
+ return Object.freeze({ name: step.name, version: step.version, phase: step.status, journalSeq: step.journal_seq, ...(step.output_failure === null ? {} : { outputFailure: "tool_output_serialization_failure" }) });
74
+ });
75
+ const last = steps.at(-1);
76
+ const journal = ports.sql.exec("SELECT journal_seq FROM __telnyx_agent_harness_run_journal WHERE run_id = ?", runId).toArray()[0];
77
+ if (journal !== undefined)
78
+ sequence(journal.journal_seq);
79
+ const hasOccurrences = ports.sql.exec("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '__telnyx_agent_harness_schedule_occurrences'").toArray().length > 0;
80
+ const occurrences = hasOccurrences ? ports.sql.exec("SELECT schedule_id,scheduled_for,journal_seq FROM __telnyx_agent_harness_schedule_occurrences WHERE run_id = ? ORDER BY scheduled_for,schedule_id LIMIT 33", runId).toArray() : [];
81
+ if (occurrences.length > 32)
82
+ throw new Error();
83
+ const scheduleOccurrences = Object.freeze(occurrences.map(occurrence => {
84
+ text(occurrence.schedule_id);
85
+ sequence(occurrence.scheduled_for);
86
+ if (occurrence.journal_seq !== null)
87
+ sequence(occurrence.journal_seq);
88
+ return Object.freeze({ scheduleId: occurrence.schedule_id, scheduledFor: occurrence.scheduled_for, ...(occurrence.journal_seq === null ? {} : { journalSeq: occurrence.journal_seq }) });
89
+ }));
90
+ const result = Object.freeze({
91
+ run: Object.freeze({ id: row.id, status: row.status, ...(row.failure_code === null ? {} : { failureCode: row.failure_code }) }),
92
+ ...(journal === undefined ? {} : { journalSeq: journal.journal_seq }),
93
+ scheduleOccurrences,
94
+ steps: Object.freeze(steps),
95
+ ...(rows.length <= limit || last === undefined ? {} : {
96
+ nextCursor: Buffer.from(JSON.stringify([1, scope, last.journalSeq, last.name])).toString("base64url"),
97
+ }),
98
+ });
99
+ if (Buffer.byteLength(JSON.stringify(result), "utf8") > 262144)
100
+ throw new Error();
101
+ return result;
102
+ }
103
+ catch {
104
+ throw new Error("Run inspection unavailable: unsupported schema or invalid stored data");
105
+ }
106
+ }
107
+ //# sourceMappingURL=diagnostics.js.map
package/dist/durable.d.ts CHANGED
@@ -74,6 +74,7 @@ export interface HarnessRunLedgerOptions {
74
74
  /** Approval-capable harnesses bind admission to a host-attested subject. */
75
75
  readonly requireApprovalIdentity?: boolean;
76
76
  }
77
+ export declare const HARNESS_DURABILITY_SCHEMA_VERSION = 8;
77
78
  export declare function ensureHarnessDurabilitySchema(ports: AgentHarnessPorts): void;
78
79
  export declare function createHarnessRunLedger(ports: AgentHarnessPorts, execute: (input: string, signal: AbortSignal, steps: HarnessStepContext) => Promise<unknown>, options?: HarnessRunLedgerOptions): HarnessRunLedger;
79
80
  export declare function createHarnessRunExecutionLedger(ports: AgentHarnessPorts, execute: (input: string, signal: AbortSignal, steps: HarnessStepContext) => Promise<unknown>, options?: HarnessRunLedgerOptions): HarnessRunExecutionLedger;
package/dist/durable.js CHANGED
@@ -39,7 +39,8 @@ function scheduleRollbackStep(error) {
39
39
  const APPROVAL_SCHEMA_VERSION = 6;
40
40
  // v7 records the run-owned sequence for the admitted input. v8 adds an
41
41
  // out-of-band durable tool-output failure marker.
42
- const SCHEMA_VERSION = 8;
42
+ export const HARNESS_DURABILITY_SCHEMA_VERSION = 8;
43
+ const SCHEMA_VERSION = HARNESS_DURABILITY_SCHEMA_VERSION;
43
44
  const MAX_INPUT_BYTES = 64 * 1024;
44
45
  const MAX_KEY_BYTES = 512;
45
46
  const TASK_METHOD = "__telnyx_agent_harness_run";
package/dist/harness.d.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  import { type FinishReason, type GenerateTextOnFinishCallback, type GenerateTextOnStartCallback, type GenerateTextOnStepFinishCallback, type GenerateTextOnStepStartCallback, type GenerateTextOnToolCallFinishCallback, type GenerateTextOnToolCallStartCallback, type LanguageModel, type ToolSet } from "ai";
2
+ import { voiceTraceContext, type VoiceTraceScope, type VoiceTraceContext } from "@telnyx/edge-runtime/internal";
2
3
  import type { AgentHarnessPorts } from "./ports.js";
4
+ import { type HarnessRunInspection, type HarnessRunInspectionOptions } from "./diagnostics.js";
3
5
  import { type HarnessApprovalLedger } from "./approvals.js";
4
6
  import { type HarnessCheckpoint, type HarnessRunLedger } from "./durable.js";
5
7
  import { type HarnessMiddleware } from "./lifecycle.js";
6
8
  import { type HarnessRuntimeConfig } from "./runtime-config.js";
7
9
  import type { HarnessStepContext } from "./steps.js";
10
+ import { type HarnessSpeculativeTurn } from "./speculative.js";
11
+ export { HarnessSpeculationConflictError, HarnessSpeculationUnsupportedError, type HarnessSpeculativeTurn, } from "./speculative.js";
8
12
  /** Structural subset of an AI SDK output definition used for parsed-result inference. */
9
13
  interface HarnessOutputDefinition<Parsed = unknown> {
10
14
  parseCompleteOutput(options: {
@@ -66,6 +70,8 @@ export interface HarnessSpec<State extends Record<string, unknown>, TOOLS extend
66
70
  ports: AgentHarnessPorts<State>;
67
71
  /** AI SDK v6 language model. */
68
72
  model: LanguageModel;
73
+ /** Construction-only, in-memory metadata diagnostics. Never selected from runtime state. */
74
+ diagnostics?: VoiceTraceScope;
69
75
  /** Host-owned models selectable only by a named runtime configuration key. */
70
76
  models?: Readonly<Record<string, LanguageModel>>;
71
77
  /** The one explicit, flat tool set available to the model. */
@@ -167,6 +173,7 @@ export interface HarnessStep {
167
173
  export interface HarnessTurnOptions<TOOLS extends ToolSet = ToolSet, OutputDefinition extends HarnessOutputDefinition | undefined = undefined> {
168
174
  /** Caller-owned cancellation signal passed to AI SDK without transport coupling. */
169
175
  signal?: AbortSignal;
176
+ [voiceTraceContext]?: VoiceTraceContext;
170
177
  /** Durable context supplied only by the harness run ledger. */
171
178
  steps?: HarnessStepContext;
172
179
  /** Runs after the completed step has been durably journaled. */
@@ -189,11 +196,14 @@ export interface HarnessTurnOptions<TOOLS extends ToolSet = ToolSet, OutputDefin
189
196
  }
190
197
  /** Portable harness constructed by {@link createHarness}. */
191
198
  export interface Harness<TOOLS extends ToolSet = ToolSet, OutputDefinition extends HarnessOutputDefinition | undefined = undefined> extends HarnessRunLedger {
199
+ /** SELECT-only persisted phases for a known run; trusted host capability, not caller authorization. */
200
+ inspectRun(runId: string, options?: HarnessRunInspectionOptions): Promise<HarnessRunInspection | undefined>;
201
+ /** Start a provisional streaming direct turn, with no writes or tools until commitment. */
202
+ begin(input: string, options?: HarnessTurnOptions<TOOLS, OutputDefinition>): HarnessSpeculativeTurn<HarnessTurnResult<HarnessParsedOutput<OutputDefinition>>>;
192
203
  turn(input: string, options?: HarnessTurnOptions<TOOLS, OutputDefinition>): Promise<HarnessTurnResult<HarnessParsedOutput<OutputDefinition>>>;
193
204
  /** Durable, authorization-gated approval inspection and decisions. */
194
205
  readonly approvals: HarnessApprovalLedger;
195
206
  }
196
207
  /** Construct a portable AI SDK v6 turn engine over an explicit ports object. */
197
208
  export declare function createHarness<State extends Record<string, unknown>, TOOLS extends ToolSet, OutputDefinition extends HarnessOutputDefinition | undefined = undefined>(spec: HarnessSpec<State, TOOLS, OutputDefinition>): Harness<TOOLS, OutputDefinition>;
198
- export {};
199
209
  //# sourceMappingURL=harness.d.ts.map