@telnyx/agent-harness 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -0
- package/dist/approvals.d.ts +140 -0
- package/dist/approvals.js +699 -0
- package/dist/channel.d.ts +178 -0
- package/dist/channel.js +184 -0
- package/dist/contract.d.ts +50 -0
- package/dist/contract.js +278 -0
- package/dist/durable.d.ts +81 -0
- package/dist/durable.js +650 -0
- package/dist/harness.d.ts +199 -0
- package/dist/harness.js +1223 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +19 -0
- package/dist/lifecycle.d.ts +9 -0
- package/dist/lifecycle.js +25 -0
- package/dist/node-adapter.d.ts +71 -0
- package/dist/node-adapter.js +736 -0
- package/dist/ports.d.ts +99 -0
- package/dist/ports.js +3 -0
- package/dist/runtime-adapter.d.ts +15 -0
- package/dist/runtime-adapter.js +70 -0
- package/dist/runtime-config.d.ts +38 -0
- package/dist/runtime-config.js +104 -0
- package/dist/scheduling.d.ts +46 -0
- package/dist/scheduling.js +425 -0
- package/dist/steps.d.ts +52 -0
- package/dist/steps.js +310 -0
- package/dist/tool-context.d.ts +8 -0
- package/dist/tool-context.js +9 -0
- package/dist/workspace.d.ts +151 -0
- package/dist/workspace.js +404 -0
- package/package.json +58 -0
package/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
|