@telnyx/agent-harness 0.1.0-beta.1 → 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 +55 -3
- package/dist/approvals.d.ts +21 -0
- package/dist/approvals.js +93 -1
- package/dist/diagnostics.d.ts +33 -0
- package/dist/diagnostics.js +107 -0
- package/dist/durable.d.ts +1 -0
- package/dist/durable.js +2 -1
- package/dist/harness.d.ts +7 -0
- package/dist/harness.js +27 -5
- package/dist/index.d.ts +2 -1
- package/dist/ports.d.ts +14 -0
- package/dist/voice-trace.d.ts +6 -0
- package/dist/voice-trace.js +160 -0
- package/dist/workspace.d.ts +13 -0
- package/dist/workspace.js +25 -5
- package/package.json +2 -2
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.
|
|
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
|
|
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.
|
|
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.
|
|
@@ -118,6 +119,16 @@ claim exactly-once external effects or crash recovery; production durability,
|
|
|
118
119
|
concurrency ownership, and provider configuration remain responsibilities of
|
|
119
120
|
the selected host.
|
|
120
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
|
+
|
|
121
132
|
## Scheduling dispatch and recovery lifecycle
|
|
122
133
|
|
|
123
134
|
`createHarnessWithScheduling` constructs a durable harness with bounded
|
|
@@ -221,6 +232,47 @@ accepted, and records the run's journal sequence after completion. This
|
|
|
221
232
|
makes the occurrence → run → journal relationship recoverable without
|
|
222
233
|
inferring from actor-global history.
|
|
223
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
|
+
|
|
224
276
|
## Validation
|
|
225
277
|
|
|
226
278
|
Package development and release preparation are validated with the repository's
|
package/dist/approvals.d.ts
CHANGED
|
@@ -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
|
|
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,5 +1,7 @@
|
|
|
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";
|
|
@@ -68,6 +70,8 @@ export interface HarnessSpec<State extends Record<string, unknown>, TOOLS extend
|
|
|
68
70
|
ports: AgentHarnessPorts<State>;
|
|
69
71
|
/** AI SDK v6 language model. */
|
|
70
72
|
model: LanguageModel;
|
|
73
|
+
/** Construction-only, in-memory metadata diagnostics. Never selected from runtime state. */
|
|
74
|
+
diagnostics?: VoiceTraceScope;
|
|
71
75
|
/** Host-owned models selectable only by a named runtime configuration key. */
|
|
72
76
|
models?: Readonly<Record<string, LanguageModel>>;
|
|
73
77
|
/** The one explicit, flat tool set available to the model. */
|
|
@@ -169,6 +173,7 @@ export interface HarnessStep {
|
|
|
169
173
|
export interface HarnessTurnOptions<TOOLS extends ToolSet = ToolSet, OutputDefinition extends HarnessOutputDefinition | undefined = undefined> {
|
|
170
174
|
/** Caller-owned cancellation signal passed to AI SDK without transport coupling. */
|
|
171
175
|
signal?: AbortSignal;
|
|
176
|
+
[voiceTraceContext]?: VoiceTraceContext;
|
|
172
177
|
/** Durable context supplied only by the harness run ledger. */
|
|
173
178
|
steps?: HarnessStepContext;
|
|
174
179
|
/** Runs after the completed step has been durably journaled. */
|
|
@@ -191,6 +196,8 @@ export interface HarnessTurnOptions<TOOLS extends ToolSet = ToolSet, OutputDefin
|
|
|
191
196
|
}
|
|
192
197
|
/** Portable harness constructed by {@link createHarness}. */
|
|
193
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>;
|
|
194
201
|
/** Start a provisional streaming direct turn, with no writes or tools until commitment. */
|
|
195
202
|
begin(input: string, options?: HarnessTurnOptions<TOOLS, OutputDefinition>): HarnessSpeculativeTurn<HarnessTurnResult<HarnessParsedOutput<OutputDefinition>>>;
|
|
196
203
|
turn(input: string, options?: HarnessTurnOptions<TOOLS, OutputDefinition>): Promise<HarnessTurnResult<HarnessParsedOutput<OutputDefinition>>>;
|
package/dist/harness.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { GatewayError } from "@ai-sdk/gateway";
|
|
2
2
|
import { AISDKError, APICallError, RetryError, asSchema, generateText, streamText, jsonSchema, stepCountIs, wrapLanguageModel, } from "ai";
|
|
3
3
|
import { z } from "zod/v4";
|
|
4
|
+
import { traceActive, traceBind, voiceTraceContext, traceAttempt, traceStage } from "@telnyx/edge-runtime/internal";
|
|
5
|
+
import { traceModel, traceToolExecution } from "./voice-trace.js";
|
|
4
6
|
import { isToolOutputSerializationFailure } from "./steps.js";
|
|
7
|
+
import { inspectHarnessRun } from "./diagnostics.js";
|
|
5
8
|
import { HarnessApprovalError, createHarnessApprovalExecutionLedger, createHarnessApprovalMessageJournal, } from "./approvals.js";
|
|
6
9
|
import { createHarnessRunExecutionLedger, HarnessApprovalPause, } from "./durable.js";
|
|
7
10
|
import { runMiddleware } from "./lifecycle.js";
|
|
@@ -95,6 +98,10 @@ function captureSpeculativeOptions(options) {
|
|
|
95
98
|
const captured = Object.create(null);
|
|
96
99
|
for (const key of Reflect.ownKeys(options)) {
|
|
97
100
|
const descriptor = Object.getOwnPropertyDescriptor(options, key);
|
|
101
|
+
if (key === voiceTraceContext && descriptor.enumerable && "value" in descriptor) {
|
|
102
|
+
Object.defineProperty(captured, key, descriptor);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
98
105
|
if (typeof key !== "string" || !descriptor.enumerable || !("value" in descriptor)
|
|
99
106
|
|| (!supported.has(key) && !unsupported.has(key))
|
|
100
107
|
|| (unsupported.has(key) && descriptor.value !== undefined)) {
|
|
@@ -696,6 +703,7 @@ function validateTools(tools, authorization) {
|
|
|
696
703
|
/** Construct a portable AI SDK v6 turn engine over an explicit ports object. */
|
|
697
704
|
export function createHarness(spec) {
|
|
698
705
|
const activation = spec.ports.activation;
|
|
706
|
+
const diagnostics = traceBind(spec.diagnostics, activation);
|
|
699
707
|
const messagePort = spec.ports.messages;
|
|
700
708
|
const appendMessagePort = messagePort.append.bind(messagePort);
|
|
701
709
|
const appendMessagesPort = messagePort.appendMany.bind(messagePort);
|
|
@@ -742,6 +750,7 @@ export function createHarness(spec) {
|
|
|
742
750
|
let runtimeConfigReadSequence = 0;
|
|
743
751
|
let lastPublishedRuntimeConfigReadSequence = 0;
|
|
744
752
|
const executeTurn = async (input, options = {}, speculation) => {
|
|
753
|
+
const trace = options[voiceTraceContext]?.scope === diagnostics ? options[voiceTraceContext] : traceAttempt(diagnostics, 0, "confirmed");
|
|
745
754
|
const committedEffect = (work) => speculation ? speculation.write(work) : work();
|
|
746
755
|
const appendMessage = (message) => speculation
|
|
747
756
|
? speculation.write(() => appendMessagePort(message)) : appendMessagePort(message);
|
|
@@ -987,8 +996,8 @@ export function createHarness(spec) {
|
|
|
987
996
|
assertBoundary();
|
|
988
997
|
coreStarted = true;
|
|
989
998
|
const result = options.steps === undefined
|
|
990
|
-
? cloneToolOutput(toolName, await committedEffect(async () => await execute.apply(this, effectArgs)))
|
|
991
|
-
: await options.steps.step(stepName, async () => await withHarnessToolContext(Object.freeze({ runId: options.steps.runId, toolName, ordinal: durableToolCallOrdinal }), async () => await execute.apply(this, effectArgs)), {
|
|
999
|
+
? cloneToolOutput(toolName, await committedEffect(async () => { traceToolExecution(trace, effectArgs[1]?.experimental_context); return await execute.apply(this, effectArgs); }))
|
|
1000
|
+
: await options.steps.step(stepName, async () => await withHarnessToolContext(Object.freeze({ runId: options.steps.runId, toolName, ordinal: durableToolCallOrdinal }), async () => { traceToolExecution(trace, effectArgs[1]?.experimental_context); return await execute.apply(this, effectArgs); }), {
|
|
992
1001
|
external: true,
|
|
993
1002
|
beforeEffect: () => approvals.consumeAuthorizedEffect(options.steps.runId, approvalId),
|
|
994
1003
|
input: args[0],
|
|
@@ -1039,7 +1048,7 @@ export function createHarness(spec) {
|
|
|
1039
1048
|
await options.callbacks?.onToolCallStart?.(event);
|
|
1040
1049
|
};
|
|
1041
1050
|
const generationOptions = {
|
|
1042
|
-
model: modelForTurn,
|
|
1051
|
+
model: traceModel(modelForTurn, trace),
|
|
1043
1052
|
tools: toolsForTurn,
|
|
1044
1053
|
...(runtimeConfig.output === undefined ? {} : { output: runtimeConfig.output }),
|
|
1045
1054
|
abortSignal: options.signal,
|
|
@@ -1051,7 +1060,13 @@ export function createHarness(spec) {
|
|
|
1051
1060
|
experimental_onToolCallFinish: options.callbacks?.onToolCallFinish,
|
|
1052
1061
|
prepareStep({ messages: stepMessages }) {
|
|
1053
1062
|
assertBoundary();
|
|
1054
|
-
|
|
1063
|
+
const confirmed = speculation && confirmedForModel !== undefined ? { messages: confirmedMessages(stepMessages) } : undefined;
|
|
1064
|
+
if (!traceActive(trace))
|
|
1065
|
+
return confirmed;
|
|
1066
|
+
// An empty per-step handoff carries no diagnostic metadata into results.
|
|
1067
|
+
// The source model and executor privately share its object identity.
|
|
1068
|
+
const handoff = Object.freeze({});
|
|
1069
|
+
return { ...confirmed, model: traceModel(modelForTurn, trace, handoff), experimental_context: handoff };
|
|
1055
1070
|
},
|
|
1056
1071
|
async stopWhen(context) {
|
|
1057
1072
|
assertBoundary();
|
|
@@ -1177,8 +1192,10 @@ export function createHarness(spec) {
|
|
|
1177
1192
|
onChunk: ({ chunk }) => {
|
|
1178
1193
|
assertBoundary();
|
|
1179
1194
|
const part = objectRecord(chunk);
|
|
1180
|
-
if (part?.type === "text-delta" && typeof part.text === "string")
|
|
1195
|
+
if (part?.type === "text-delta" && typeof part.text === "string") {
|
|
1196
|
+
traceStage(trace, "text_forwarded");
|
|
1181
1197
|
speculation.text(part.text);
|
|
1198
|
+
}
|
|
1182
1199
|
},
|
|
1183
1200
|
});
|
|
1184
1201
|
await streamed.consumeStream({ onError: onStreamError });
|
|
@@ -1213,6 +1230,8 @@ export function createHarness(spec) {
|
|
|
1213
1230
|
name: call.toolName,
|
|
1214
1231
|
})));
|
|
1215
1232
|
}
|
|
1233
|
+
if (!speculation)
|
|
1234
|
+
traceStage(trace, "text_forwarded");
|
|
1216
1235
|
const output = Object.freeze({
|
|
1217
1236
|
text: result.text,
|
|
1218
1237
|
stepCount: result.steps.length,
|
|
@@ -1267,6 +1286,7 @@ export function createHarness(spec) {
|
|
|
1267
1286
|
signal: control.signal,
|
|
1268
1287
|
onStepCommitted: capturedOptions.onStepCommitted,
|
|
1269
1288
|
onOutput: capturedOptions.onOutput,
|
|
1289
|
+
[voiceTraceContext]: capturedOptions[voiceTraceContext],
|
|
1270
1290
|
})), control));
|
|
1271
1291
|
};
|
|
1272
1292
|
const ledger = createHarnessRunExecutionLedger(spec.ports, async (input, signal, steps) => {
|
|
@@ -1318,6 +1338,7 @@ export function createHarness(spec) {
|
|
|
1318
1338
|
}
|
|
1319
1339
|
}, { checkpoints: spec.durability?.checkpoints, requireApprovalIdentity: requiresApprovalIdentity });
|
|
1320
1340
|
const approvalService = Object.freeze({
|
|
1341
|
+
listPending: approvals.listPending,
|
|
1321
1342
|
inspect: approvals.inspect,
|
|
1322
1343
|
async grant(id) {
|
|
1323
1344
|
let approval;
|
|
@@ -1362,6 +1383,7 @@ export function createHarness(spec) {
|
|
|
1362
1383
|
begin,
|
|
1363
1384
|
accept: ledger.accept,
|
|
1364
1385
|
get: ledger.get,
|
|
1386
|
+
inspectRun: (runId, options) => inspectHarnessRun(spec.ports, runId, options),
|
|
1365
1387
|
status: ledger.status,
|
|
1366
1388
|
list: ledger.list,
|
|
1367
1389
|
cancel: ledger.cancel,
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,9 @@ export * from "./runtime-adapter.js";
|
|
|
4
4
|
export * from "./harness.js";
|
|
5
5
|
export { HarnessSpeculationConflictError, HarnessSpeculationUnsupportedError, type HarnessSpeculativeTurn, } from "./speculative.js";
|
|
6
6
|
export { HARNESS_RUN_STATUSES, HarnessApprovalPause, HarnessInjectedCrash, createHarnessRunLedger, ensureHarnessDurabilitySchema, type HarnessCheckpoint, type HarnessRun, type HarnessRunAcceptance, type HarnessRunLedger, type HarnessRunLedgerOptions, type HarnessRunListOptions, type HarnessRunStatus, } from "./durable.js";
|
|
7
|
-
export { HARNESS_APPROVAL_STATUSES, HarnessApprovalError, HarnessAuthorizationError, type HarnessApproval, type HarnessApprovalLedger, type HarnessApprovalStatus, } from "./approvals.js";
|
|
7
|
+
export { HARNESS_APPROVAL_STATUSES, HarnessApprovalError, HarnessAuthorizationError, type HarnessApproval, type HarnessApprovalLedger, type HarnessApprovalStatus, type HarnessPendingApprovalSummary, type HarnessPendingApprovalPage, type HarnessPendingApprovalListOptions, } from "./approvals.js";
|
|
8
8
|
export * from "./steps.js";
|
|
9
|
+
export type { HarnessRunInspection, HarnessRunInspectionOptions } from "./diagnostics.js";
|
|
9
10
|
export * from "./lifecycle.js";
|
|
10
11
|
export * from "./channel.js";
|
|
11
12
|
export * from "./scheduling.js";
|
package/dist/ports.d.ts
CHANGED
|
@@ -17,6 +17,20 @@ export interface AgentHarnessAuthorizationIdentity {
|
|
|
17
17
|
/** Versioned private authorization capability required for approval decisions. */
|
|
18
18
|
export interface HarnessAuthorizationPort {
|
|
19
19
|
identity(): AgentHarnessAuthorizationIdentity | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Optional explicit authority to enumerate minimal pending summaries, including
|
|
22
|
+
* pagination lookahead, for this actor/organization/run. An individual read
|
|
23
|
+
* allow is not collection authority. Hosts must opt in and enforce run access.
|
|
24
|
+
*/
|
|
25
|
+
authorizePendingApprovalSummaries?(request: Readonly<{
|
|
26
|
+
identity: AgentHarnessAuthorizationIdentity;
|
|
27
|
+
actorId: string;
|
|
28
|
+
action: "agent_harness.approvals.list_pending";
|
|
29
|
+
resource: string;
|
|
30
|
+
runId: string;
|
|
31
|
+
}>): Promise<Readonly<{
|
|
32
|
+
kind: "allow" | "deny" | "unavailable";
|
|
33
|
+
}>>;
|
|
20
34
|
authorize(request: Readonly<{
|
|
21
35
|
identity: AgentHarnessAuthorizationIdentity;
|
|
22
36
|
action: string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { LanguageModel } from "ai";
|
|
2
|
+
import { type VoiceTraceContext } from "@telnyx/edge-runtime/internal";
|
|
3
|
+
export declare function traceToolExecution(context: VoiceTraceContext | undefined, handoff: unknown): void;
|
|
4
|
+
/** Observes the selected host object; never resolves or replaces a model. */
|
|
5
|
+
export declare function traceModel(model: LanguageModel, context: VoiceTraceContext | undefined, handoff?: object): LanguageModel;
|
|
6
|
+
//# sourceMappingURL=voice-trace.d.ts.map
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { traceExecution, traceGenerated, traceActive, traceGap, traceRequest, traceRequestEnd, traceProviderFields, traceNormalized, } from "@telnyx/edge-runtime/internal";
|
|
2
|
+
/** Data-only projection: never execute provider-owned diagnostic accessors. */
|
|
3
|
+
function ownData(value, key) {
|
|
4
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
5
|
+
if (descriptor && !("value" in descriptor))
|
|
6
|
+
throw new Error();
|
|
7
|
+
return descriptor?.value;
|
|
8
|
+
}
|
|
9
|
+
function replaceStream(result, stream, fault) {
|
|
10
|
+
try {
|
|
11
|
+
const descriptors = Object.getOwnPropertyDescriptors(result);
|
|
12
|
+
descriptors.stream = { ...descriptors.stream, value: stream };
|
|
13
|
+
delete descriptors.stream.get;
|
|
14
|
+
delete descriptors.stream.set;
|
|
15
|
+
return Object.create(Object.getPrototypeOf(result), descriptors);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
fault();
|
|
19
|
+
// Never return the original stream: its raw chunks were requested by us.
|
|
20
|
+
return new Proxy({}, { get(_target, key) { return key === "stream" ? stream : Reflect.get(result, key, result); } });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const stepRequests = new WeakMap();
|
|
24
|
+
export function traceToolExecution(context, handoff) {
|
|
25
|
+
const step = handoff && typeof handoff === "object" ? stepRequests.get(handoff) : undefined;
|
|
26
|
+
traceExecution(context, step && !step.ambiguous ? step.request : undefined);
|
|
27
|
+
}
|
|
28
|
+
function requestStarted(handoff, request) {
|
|
29
|
+
if (!handoff)
|
|
30
|
+
return;
|
|
31
|
+
const previous = stepRequests.get(handoff);
|
|
32
|
+
stepRequests.set(handoff, { request, active: (previous?.active ?? 0) + 1, ambiguous: previous?.ambiguous === true || (previous?.active ?? 0) > 0 });
|
|
33
|
+
}
|
|
34
|
+
function requestEnded(handoff) {
|
|
35
|
+
const step = handoff && stepRequests.get(handoff);
|
|
36
|
+
if (step)
|
|
37
|
+
step.active--;
|
|
38
|
+
}
|
|
39
|
+
/** Observes the selected host object; never resolves or replaces a model. */
|
|
40
|
+
export function traceModel(model, context, handoff) {
|
|
41
|
+
if (!traceActive(context))
|
|
42
|
+
return model;
|
|
43
|
+
if (typeof model === "string" || model.specificationVersion !== "v3") {
|
|
44
|
+
traceGap(context, "unsupported");
|
|
45
|
+
return model;
|
|
46
|
+
}
|
|
47
|
+
// A separate extensible facade avoids fixed-own-method Proxy invariants.
|
|
48
|
+
return new Proxy({}, {
|
|
49
|
+
get(_facade, key) {
|
|
50
|
+
const target = model;
|
|
51
|
+
if (key === "doGenerate")
|
|
52
|
+
return async (params) => {
|
|
53
|
+
if (!traceActive(context))
|
|
54
|
+
return target.doGenerate.call(target, params);
|
|
55
|
+
const request = traceRequest(context, "generate");
|
|
56
|
+
requestStarted(handoff, request);
|
|
57
|
+
try {
|
|
58
|
+
const result = await target.doGenerate.call(target, params);
|
|
59
|
+
traceGenerated(context, request, result);
|
|
60
|
+
traceRequestEnd(context, request, "generate");
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
traceRequestEnd(context, request, "generate", true);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
requestEnded(handoff);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
if (key === "doStream")
|
|
72
|
+
return async (params) => {
|
|
73
|
+
if (!traceActive(context))
|
|
74
|
+
return target.doStream.call(target, params);
|
|
75
|
+
// Preserve pre-existing caller raw semantics without interpreting ownership.
|
|
76
|
+
if (params.includeRawChunks === true) {
|
|
77
|
+
traceGap(context, "unsupported");
|
|
78
|
+
return target.doStream.call(target, params);
|
|
79
|
+
}
|
|
80
|
+
const request = traceRequest(context, "stream");
|
|
81
|
+
requestStarted(handoff, request);
|
|
82
|
+
try {
|
|
83
|
+
const result = await target.doStream.call(target, { ...params, includeRawChunks: true });
|
|
84
|
+
let rawSeen = false;
|
|
85
|
+
const reader = result.stream.getReader();
|
|
86
|
+
let ended = false;
|
|
87
|
+
const end = (reason) => {
|
|
88
|
+
if (ended)
|
|
89
|
+
return;
|
|
90
|
+
ended = true;
|
|
91
|
+
requestEnded(handoff);
|
|
92
|
+
if (reason)
|
|
93
|
+
traceGap(context, reason);
|
|
94
|
+
else if (!rawSeen)
|
|
95
|
+
traceGap(context, "missing_raw");
|
|
96
|
+
traceRequestEnd(context, request, "stream", reason !== undefined);
|
|
97
|
+
};
|
|
98
|
+
const stream = new ReadableStream({
|
|
99
|
+
async pull(controller) {
|
|
100
|
+
try {
|
|
101
|
+
// Raw-only reads satisfy this same consumer demand; never drain ahead.
|
|
102
|
+
while (!ended) {
|
|
103
|
+
const next = await reader.read();
|
|
104
|
+
if (ended)
|
|
105
|
+
return;
|
|
106
|
+
if (next.done) {
|
|
107
|
+
end();
|
|
108
|
+
reader.releaseLock();
|
|
109
|
+
controller.close();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const part = next.value;
|
|
113
|
+
let type;
|
|
114
|
+
try {
|
|
115
|
+
type = ownData(part, "type");
|
|
116
|
+
if (type === "raw") {
|
|
117
|
+
rawSeen = true;
|
|
118
|
+
traceProviderFields(context, request, ownData(part, "rawValue"), "delta");
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
traceGap(context, "projection_failure");
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
traceNormalized(context, request, type);
|
|
127
|
+
controller.enqueue(part);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
end("source_failure");
|
|
133
|
+
reader.releaseLock();
|
|
134
|
+
controller.error(error);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
async cancel(reason) {
|
|
138
|
+
end("cancelled");
|
|
139
|
+
try {
|
|
140
|
+
await reader.cancel(reason);
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
reader.releaseLock();
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
}, { highWaterMark: 0 });
|
|
147
|
+
return replaceStream(result, stream, () => traceGap(context, "projection_failure"));
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
requestEnded(handoff);
|
|
151
|
+
traceGap(context, "source_failure");
|
|
152
|
+
traceRequestEnd(context, request, "stream", true);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
return Reflect.get(target, key, target);
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=voice-trace.js.map
|
package/dist/workspace.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { AgentHarnessPorts } from "./ports.js";
|
|
2
2
|
declare const TOOL_WORKSPACE_CAPABILITY: unique symbol;
|
|
3
|
+
declare const SKILL_ACTIVATION_MESSAGES: Readonly<{
|
|
4
|
+
HARNESS_SKILL_INVALID_NAME: "skill name is invalid";
|
|
5
|
+
HARNESS_SKILL_NOT_FOUND: "unknown skill";
|
|
6
|
+
HARNESS_SKILL_CONTENT_BOUND: "skill content exceeds configured bound";
|
|
7
|
+
HARNESS_SKILL_OUTPUT_BOUND: "workspace output exceeds configured bound";
|
|
8
|
+
}>;
|
|
9
|
+
export type HarnessSkillActivationErrorCode = keyof typeof SKILL_ACTIVATION_MESSAGES;
|
|
10
|
+
/** A fixed-code activation rejection with no caller input or skill content. */
|
|
11
|
+
export declare class HarnessSkillActivationError extends Error {
|
|
12
|
+
readonly code: HarnessSkillActivationErrorCode;
|
|
13
|
+
constructor(code: HarnessSkillActivationErrorCode);
|
|
14
|
+
}
|
|
3
15
|
export interface VirtualWorkspaceLimits {
|
|
4
16
|
readonly maxFileBytes?: number;
|
|
5
17
|
readonly maxWorkspaceBytes?: number;
|
|
@@ -74,6 +86,7 @@ export interface VirtualWorkspace {
|
|
|
74
86
|
limit?: number;
|
|
75
87
|
}>): Promise<readonly WorkspaceJournalEntry[]>;
|
|
76
88
|
discoverSkills(): Promise<WorkspaceSkillCatalog>;
|
|
89
|
+
/** Load inert prompt data using an exact, case-sensitive name from discoverSkills(). */
|
|
77
90
|
activateSkill(name: string): Promise<ActivatedWorkspaceSkill>;
|
|
78
91
|
assemblePrompt(base: string, skills: readonly string[]): Promise<Readonly<{
|
|
79
92
|
text: string;
|
package/dist/workspace.js
CHANGED
|
@@ -9,6 +9,21 @@ const MAX_PATH_BYTES = 128;
|
|
|
9
9
|
const SKILL_PATH = /^skills\/([^/]+)\/SKILL\.md$/;
|
|
10
10
|
const TOOL_MUTATION_RESULT = Symbol("tool-mutation-result");
|
|
11
11
|
const TOOL_WORKSPACE_CAPABILITY = Symbol("tool-workspace-capability");
|
|
12
|
+
const SKILL_ACTIVATION_MESSAGES = Object.freeze({
|
|
13
|
+
HARNESS_SKILL_INVALID_NAME: "skill name is invalid",
|
|
14
|
+
HARNESS_SKILL_NOT_FOUND: "unknown skill",
|
|
15
|
+
HARNESS_SKILL_CONTENT_BOUND: "skill content exceeds configured bound",
|
|
16
|
+
HARNESS_SKILL_OUTPUT_BOUND: "workspace output exceeds configured bound",
|
|
17
|
+
});
|
|
18
|
+
/** A fixed-code activation rejection with no caller input or skill content. */
|
|
19
|
+
export class HarnessSkillActivationError extends Error {
|
|
20
|
+
code;
|
|
21
|
+
constructor(code) {
|
|
22
|
+
super(SKILL_ACTIVATION_MESSAGES[code]);
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.name = "HarnessSkillActivationError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
12
27
|
const workspaceOutputBounds = new WeakMap();
|
|
13
28
|
function one(rows) {
|
|
14
29
|
return rows[0];
|
|
@@ -311,13 +326,18 @@ export function createVirtualWorkspace(ports, limits = {}) {
|
|
|
311
326
|
},
|
|
312
327
|
async activateSkill(name) {
|
|
313
328
|
if (typeof name !== "string" || !name || name.includes("/") || name.includes("\\") || name.includes("\0"))
|
|
314
|
-
throw new
|
|
329
|
+
throw new HarnessSkillActivationError("HARNESS_SKILL_INVALID_NAME");
|
|
315
330
|
const path = `skills/${name}/SKILL.md`;
|
|
316
331
|
const row = get(path);
|
|
317
332
|
if (row === undefined)
|
|
318
|
-
throw new
|
|
319
|
-
|
|
320
|
-
|
|
333
|
+
throw new HarnessSkillActivationError("HARNESS_SKILL_NOT_FOUND");
|
|
334
|
+
requireText(row.content, "skill content");
|
|
335
|
+
if (Buffer.byteLength(row.content, "utf8") > maxOutputBytes)
|
|
336
|
+
throw new HarnessSkillActivationError("HARNESS_SKILL_CONTENT_BOUND");
|
|
337
|
+
const result = Object.freeze({ name, path, origin: "activated-skill", content: row.content });
|
|
338
|
+
if (Buffer.byteLength(JSON.stringify(result), "utf8") > maxOutputBytes)
|
|
339
|
+
throw new HarnessSkillActivationError("HARNESS_SKILL_OUTPUT_BOUND");
|
|
340
|
+
return result;
|
|
321
341
|
},
|
|
322
342
|
async assemblePrompt(base, names) {
|
|
323
343
|
requireText(base, "application prompt");
|
|
@@ -395,7 +415,7 @@ export function createVirtualWorkspaceTools(workspace) {
|
|
|
395
415
|
execute: async ({ needle }) => boundedToolResult(workspace, Object.freeze({ origin: "workspace", ...(await workspace.grep(needle)) })),
|
|
396
416
|
}),
|
|
397
417
|
activate_skill: tool({
|
|
398
|
-
description: "Explicitly load one inert SKILL.md document as prompt data with its provenance.",
|
|
418
|
+
description: "Explicitly load one inert SKILL.md document as prompt data with its provenance. Use the exact registered skill name, not a path or display title.",
|
|
399
419
|
inputSchema: z.object({ name: z.string().min(1) }),
|
|
400
420
|
execute: async ({ name }) => boundedToolResult(workspace, await workspace.activateSkill(name)),
|
|
401
421
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telnyx/agent-harness",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
4
|
"description": "Telnyx Agent Harness beta: portable ports, bounded turns, and Node test-host adapter.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@ai-sdk/gateway": "3.0.181",
|
|
39
39
|
"@sqlite.org/sqlite-wasm": "^3.53.0-build1",
|
|
40
|
-
"@telnyx/edge-runtime": "^0.
|
|
40
|
+
"@telnyx/edge-runtime": "^0.16.1",
|
|
41
41
|
"ai": "6.0.266",
|
|
42
42
|
"zod": "4.3.6"
|
|
43
43
|
},
|