@telnyx/agent-harness 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # Telnyx Agent Harness
2
+
3
+ `@telnyx/agent-harness` is a beta package for composing bounded AI SDK turns with
4
+ portable host ports in a Node-hosted actor runtime. It does not support browser
5
+ execution. The Node-only test adapter is intentionally isolated to a subpath.
6
+
7
+ ## Beta and release gate
8
+
9
+ This source change does not publish a package. The planned initial release is
10
+ `0.1.0-beta.0` under the `beta` tag. The package remains `UNLICENSED`; an owner
11
+ must approve npm organization/package ownership, distribution terms, and trusted
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
14
+ adapter uses its `./internal` `getAgentHarnessHost` and
15
+ `getAgentHarnessAuthorizationProvider` seams. Source validation is not evidence
16
+ that the beta is published or live; see `docs/agent-harness/beta-release.md` for
17
+ fresh preflight, publication, and rollback gates.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @telnyx/agent-harness@beta @telnyx/edge-runtime@^0.15.2
23
+ ```
24
+
25
+ The package requires Node 22.13 or newer.
26
+
27
+ ## Exports
28
+
29
+ ```ts
30
+ import { createHarness } from "@telnyx/agent-harness";
31
+ import { createNodeAgentHarnessPorts } from "@telnyx/agent-harness/node";
32
+ import {
33
+ AGENT_HARNESS_PORTS_VERSION,
34
+ defineAgentHarnessPortsContract,
35
+ } from "@telnyx/agent-harness/contract";
36
+ ```
37
+
38
+ - `@telnyx/agent-harness` exports portable ports, `createHarness`, and the
39
+ trusted runtime adapter. It does not export the Node adapter.
40
+ - `@telnyx/agent-harness/node` exports `createNodeAgentHarnessPorts()` for
41
+ deterministic Node tests. Its in-memory state is available across
42
+ `reopen()` only within the current process; it is not a production durability
43
+ host.
44
+ - `@telnyx/agent-harness/contract` exports the executable Vitest host-adapter
45
+ contract and `AGENT_HARNESS_PORTS_VERSION` (`2`). Root-only consumers do not
46
+ install Vitest. Contract consumers must add Vitest to their own test
47
+ environment.
48
+
49
+ ## Bounded turn example
50
+
51
+ ```ts
52
+ import { createHarness } from "@telnyx/agent-harness";
53
+
54
+ const harness = createHarness({
55
+ ports,
56
+ model,
57
+ limits: { steps: 4 },
58
+ tools: {},
59
+ });
60
+
61
+ const result = await harness.turn("Summarize this input");
62
+ console.log(result.text);
63
+ ```
64
+
65
+ A harness captures its ports, model, instructions, and tool map at construction.
66
+ Each completed model step is committed before callbacks run. This beta does not
67
+ claim exactly-once external effects or crash recovery; production durability,
68
+ concurrency ownership, and provider configuration remain responsibilities of
69
+ the selected host.
70
+
71
+ ## Scheduling dispatch and recovery lifecycle
72
+
73
+ `createHarnessWithScheduling` constructs a durable harness with bounded
74
+ scheduling tools (`schedule`, `list_schedules`, `cancel_schedule`) and a unified
75
+ `dispatch` + `recover` surface that handles both scheduler and harness tasks.
76
+
77
+ ### Construction
78
+
79
+ ```ts
80
+ import { createHarnessWithScheduling } from "@telnyx/agent-harness";
81
+
82
+ const { harness, scheduling } = createHarnessWithScheduling({
83
+ ports,
84
+ model,
85
+ limits: { steps: 8 },
86
+ tools: {},
87
+ });
88
+
89
+ // harness extends Harness: accept, get, cancel, recover, run, dispatch, turn,
90
+ // approvals, list, status — all the durable run-ledger surfaces.
91
+ // scheduling exposes: schedule, listSchedules, cancelSchedule, dispatch, recover.
92
+ ```
93
+
94
+ The returned `harness.dispatch` and `harness.recover` are **unified** entry
95
+ points that route both scheduler and harness tasks through the same durable
96
+ ownership checks. Call them from the host task handler.
97
+
98
+ ### Task delivery (dispatch)
99
+
100
+ The host receives tasks from the durable task queue and must route them through
101
+ `harness.dispatch(task)`. Dispatch validates the private harness envelope and
102
+ routes:
103
+
104
+ 1. Scheduler tasks (`__telnyx_agent_harness_schedule`) → scheduling admission
105
+ (creates a run occurrence for the scheduled prompt).
106
+ 2. Harness run tasks (`__telnyx_agent_harness_run`) → `harness.run(runId)`.
107
+ 3. Approval resume tasks (`__telnyx_agent_harness_approval_resume`) →
108
+ `harness.run(runId, batch)` with a deterministic batch fence.
109
+
110
+ Required ordering: the host must call `harness.dispatch(task)` for every task it
111
+ receives. Dispatch is idempotent for already-completed or already-admitted runs.
112
+
113
+ ### Recovery after restart (recover)
114
+
115
+ On actor restart, the host must call `harness.recover()` **before** processing
116
+ new tasks. Recovery:
117
+
118
+ 1. Marks uncertain interrupted runs (effect started but not committed) as
119
+ `outcome_unknown` — at-least-once delivery, never exactly-once.
120
+ 2. Re-enqueues accepted and `resume_pending` runs for re-execution.
121
+ 3. Expires approvals whose `expiresAt` has passed.
122
+ 4. Reconciles schedule occurrences with committed run admissions (repairs the
123
+ occurrence → run correlation for runs that committed before the occurrence
124
+ was updated).
125
+ 5. Re-arms active interval schedules and cancels stale scheduled tasks.
126
+
127
+ ```ts
128
+ // On actor startup:
129
+ await harness.recover();
130
+ // Then process queued tasks:
131
+ for (const task of await ports.tasks.list()) {
132
+ await harness.dispatch(task);
133
+ }
134
+ ```
135
+
136
+ ### Fresh actor
137
+
138
+ A fresh actor with no prior state starts empty. `accept(input, { key })`
139
+ admits a run with an idempotency key. The first call queues a task; a second
140
+ call with the same key returns the existing run (duplicate: true) without
141
+ re-queuing.
142
+
143
+ ### Restart behavior
144
+
145
+ After a process restart, `recover()` re-enqueues all non-terminal runs. A run
146
+ that was `running` when the process stopped is reset to `accepted` and
147
+ re-queued. A run that was `awaiting_approval` or `resume_pending` is
148
+ re-evaluated: if its approvals are still valid, it is re-queued for resumption;
149
+ if an approval expired, the run fails with `HARNESS_APPROVAL_EXPIRED`.
150
+
151
+ ### Duplicate delivery
152
+
153
+ Duplicate task delivery is safe: `dispatch` checks the durable run status
154
+ before claiming. An already-completed or already-canceled run is returned
155
+ without re-execution. A run that is already `running` (claimed by the current
156
+ activation) is returned without re-claiming.
157
+
158
+ ### Failed run behavior
159
+
160
+ A run that fails with a safe failure code (`HARNESS_PROVIDER_ERROR`,
161
+ `HARNESS_STEP_LIMIT`, `HARNESS_CANCELLED`, etc.) is marked `failed` with that
162
+ code. A run whose failure left an effect in an uncertain state (effect started
163
+ but not committed) is marked `outcome_unknown`. The host can retry or inspect
164
+ these runs; recovery does not automatically retry `outcome_unknown` runs.
165
+
166
+ ### Idempotency and occurrence bookkeeping
167
+
168
+ Schedule occurrences are tracked per `(schedule_id, scheduled_for)` pair. An
169
+ occurrence starts as `pending`, transitions to `admitted` when a run is
170
+ accepted, and records the run's journal sequence after completion. This
171
+ makes the occurrence → run → journal relationship recoverable without
172
+ inferring from actor-global history.
173
+
174
+ ## Validation
175
+
176
+ Package development and release preparation are validated with the repository's
177
+ lint, test, build, clean-consumer, and registry-SDK preflight gates. The
178
+ clean-consumer gate installs the packed package outside the monorepo and checks
179
+ the root, `./node`, and `./contract` exports without private workspace masking.
@@ -0,0 +1,140 @@
1
+ import type { JSONValue } from "ai";
2
+ import type { AgentHarnessAuthorizationIdentity, AgentHarnessPorts } from "./ports.js";
3
+ export declare const HARNESS_APPROVAL_STATUSES: readonly ["pending", "granted", "denied", "expired", "consumed"];
4
+ export type HarnessApprovalStatus = (typeof HARNESS_APPROVAL_STATUSES)[number];
5
+ export declare class HarnessAuthorizationError extends Error {
6
+ readonly code: "identity_unavailable" | "unauthenticated" | "forbidden" | "policy_unavailable";
7
+ constructor(code: "identity_unavailable" | "unauthenticated" | "forbidden" | "policy_unavailable");
8
+ }
9
+ export declare class HarnessApprovalError extends Error {
10
+ readonly code: "expired" | "replayed" | "conflicting" | "consumed" | "missing";
11
+ constructor(code: "expired" | "replayed" | "conflicting" | "consumed" | "missing");
12
+ }
13
+ export interface HarnessApprovalRequest {
14
+ /** Pinned AI SDK approval id when the request originated from a native approval part. */
15
+ readonly id?: string;
16
+ readonly runId: string;
17
+ readonly toolName: string;
18
+ readonly toolCallId: string;
19
+ readonly arguments: unknown;
20
+ readonly requiredAction: string;
21
+ readonly expiresAt: number;
22
+ }
23
+ export interface HarnessApproval {
24
+ readonly id: string;
25
+ readonly runId: string;
26
+ readonly toolName: string;
27
+ readonly toolCallId: string;
28
+ readonly arguments: JSONValue;
29
+ readonly requiredAction: string;
30
+ readonly authorization: AgentHarnessAuthorizationIdentity;
31
+ readonly status: HarnessApprovalStatus;
32
+ readonly expiresAt: number;
33
+ readonly createdAt: number;
34
+ readonly updatedAt: number;
35
+ }
36
+ export interface HarnessApprovalAudit {
37
+ readonly event: string;
38
+ /** Fixed operation class; excludes approval inputs and credentials. */
39
+ readonly action: string;
40
+ /** Fixed outcome class; excludes provider and policy response bodies. */
41
+ readonly outcome: string;
42
+ readonly at: number;
43
+ }
44
+ export interface HarnessApprovalLedger {
45
+ inspect(id: string): Promise<HarnessApproval>;
46
+ grant(id: string): Promise<HarnessApproval>;
47
+ deny(id: string): Promise<HarnessApproval>;
48
+ }
49
+ interface HarnessApprovalExecutionLedger extends HarnessApprovalLedger {
50
+ /** Private execution seam; never exposed through createHarness().approvals. */
51
+ request(request: HarnessApprovalRequest): Promise<Readonly<{
52
+ id: string;
53
+ status: "pending";
54
+ }>>;
55
+ /** Private audit seam; host callers receive no raw journal/audit surface. */
56
+ audit(id: string): Promise<readonly HarnessApprovalAudit[]>;
57
+ /** Private deterministic recovery seam; never exposed to host callers. */
58
+ inspectUnchecked(id: string): Promise<HarnessApproval | undefined>;
59
+ /** Private harness seam: reauthorizes one granted effect without consuming it. */
60
+ reauthorizeEffect(runId: string, toolCallId: string | undefined, toolName: string, input: unknown): Promise<Readonly<{
61
+ id: string;
62
+ input: JSONValue;
63
+ }> | undefined>;
64
+ /** Private synchronous seam: consumes a reauthorized grant inside the effect-start transaction. */
65
+ consumeAuthorizedEffect(runId: string, approvalId: string | undefined): void;
66
+ /** Runs the approval record and private continuation callback in one SQL transaction. */
67
+ requestWithPause(request: HarnessApprovalRequest, pause: () => void): Promise<Readonly<{
68
+ id: string;
69
+ status: "pending";
70
+ }>>;
71
+ /** Persists a validated step-wide approval batch and one continuation transition atomically. */
72
+ requestBatchWithPause(requests: readonly HarnessApprovalRequest[], pause: (created: readonly Readonly<{
73
+ id: string;
74
+ status: "pending";
75
+ }>[]) => void): Promise<readonly Readonly<{
76
+ id: string;
77
+ status: "pending";
78
+ }>[]>;
79
+ /** Records a grant and resumes only after the whole batch has durable granted responses. */
80
+ grantWithResume(id: string, decide: (approval: HarnessApproval) => void, resume: (approval: HarnessApproval) => void): Promise<HarnessApproval>;
81
+ /** Runs the denied decision and private terminalization callback in one SQL transaction. */
82
+ denyWithCancel(id: string, cancel: (approval: HarnessApproval) => void): Promise<HarnessApproval>;
83
+ }
84
+ export declare function createHarnessApprovalLedger(ports: AgentHarnessPorts): HarnessApprovalLedger;
85
+ export declare function createHarnessApprovalExecutionLedger(ports: AgentHarnessPorts): HarnessApprovalExecutionLedger;
86
+ export interface HarnessApprovalMessageRequest {
87
+ readonly runId: string;
88
+ readonly approvalId: string;
89
+ readonly toolCallId: string;
90
+ readonly toolName: string;
91
+ readonly journalSeq: number;
92
+ /** Durable message sequence immediately preceding this private envelope. */
93
+ readonly afterMessageSeq: number;
94
+ /** Durable approval batch identifier. Legacy callers default to epoch zero. */
95
+ readonly approvalEpoch?: number;
96
+ readonly signature?: string;
97
+ /** Private canonical execution input; never projected into AgentMessage storage. */
98
+ readonly canonicalToolCall: Readonly<{
99
+ toolCallId: string;
100
+ toolName: string;
101
+ input: unknown;
102
+ }>;
103
+ }
104
+ export interface HarnessApprovalMessageDecision {
105
+ readonly approvalId: string;
106
+ readonly approved: boolean;
107
+ readonly reason?: string;
108
+ }
109
+ interface HarnessApprovalMessageJournal {
110
+ /** Allocates a private per-run envelope sequence; it is unrelated to AgentMessage sequence. */
111
+ nextRunSequence(runId: string): number;
112
+ request(request: HarnessApprovalMessageRequest): Promise<void>;
113
+ decide(decision: HarnessApprovalMessageDecision): Promise<void>;
114
+ restore(runId: string, toolCallId: string, journalSeq: number): Promise<readonly unknown[]>;
115
+ /** Restores only completed private envelopes for one durable continuation. */
116
+ restoreRun(runId: string): Promise<readonly Readonly<{
117
+ afterMessageSeq: number;
118
+ journalSeq: number;
119
+ approvalId: string;
120
+ toolCall: CanonicalToolCall;
121
+ messages: readonly unknown[];
122
+ }>[]>;
123
+ }
124
+ interface HarnessApprovalMessageJournalExecution extends HarnessApprovalMessageJournal {
125
+ /** Allocates a durable per-run approval batch identifier. */
126
+ nextRunApprovalEpoch(runId: string): number;
127
+ /** Private continuation seam; runs inside the enclosing approval transaction. */
128
+ requestInTransaction(request: HarnessApprovalMessageRequest): void;
129
+ /** Private continuation seam; runs inside the enclosing approval transaction. */
130
+ decideInTransaction(decision: HarnessApprovalMessageDecision): void;
131
+ }
132
+ type CanonicalToolCall = Readonly<{
133
+ toolCallId: string;
134
+ toolName: string;
135
+ input: JSONValue;
136
+ }>;
137
+ /** Private SQL codec for the pinned AI SDK approval-message subset only. */
138
+ export declare function createHarnessApprovalMessageJournal(ports: AgentHarnessPorts): HarnessApprovalMessageJournalExecution;
139
+ export {};
140
+ //# sourceMappingURL=approvals.d.ts.map