@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 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.1` 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.
@@ -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
@@ -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,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
- return speculation && confirmedForModel !== undefined ? { messages: confirmedMessages(stepMessages) } : undefined;
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
@@ -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 Error("skill name is invalid");
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 Error("unknown skill");
319
- boundedText(row.content, maxOutputBytes, "skill content");
320
- return boundedResult(Object.freeze({ name, path, origin: "activated-skill", content: row.content }), maxOutputBytes);
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.1",
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.15.2",
40
+ "@telnyx/edge-runtime": "^0.16.1",
41
41
  "ai": "6.0.266",
42
42
  "zod": "4.3.6"
43
43
  },