@telnyx/agent-harness 0.1.0-beta.0 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -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 +11 -1
- package/dist/harness.js +208 -35
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/ports.d.ts +14 -0
- package/dist/speculative.d.ts +36 -0
- package/dist/speculative.js +272 -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
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
/** In-memory ownership and commitment; never opens a storage transaction. */
|
|
3
|
+
export class HarnessSpeculationConflictError extends Error {
|
|
4
|
+
code = "HARNESS_SPECULATION_CONFLICT";
|
|
5
|
+
constructor() { super("Speculative turn already has a different terminal decision"); this.name = "HarnessSpeculationConflictError"; }
|
|
6
|
+
}
|
|
7
|
+
export class HarnessSpeculationUnsupportedError extends Error {
|
|
8
|
+
code = "HARNESS_SPECULATION_UNSUPPORTED";
|
|
9
|
+
constructor(detail) { super(`Speculation does not support ${detail}`); this.name = "HarnessSpeculationUnsupportedError"; }
|
|
10
|
+
}
|
|
11
|
+
function deferred() {
|
|
12
|
+
let resolve;
|
|
13
|
+
let reject;
|
|
14
|
+
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
|
|
15
|
+
void promise.catch(() => { });
|
|
16
|
+
return { promise, resolve, reject };
|
|
17
|
+
}
|
|
18
|
+
const contexts = new AsyncLocalStorage();
|
|
19
|
+
const owners = new WeakMap();
|
|
20
|
+
function drain(owner) {
|
|
21
|
+
if (owner.draining)
|
|
22
|
+
return;
|
|
23
|
+
owner.draining = true;
|
|
24
|
+
try {
|
|
25
|
+
while (!owner.exclusive) {
|
|
26
|
+
const next = owner.pending.values().next().value;
|
|
27
|
+
if (!next || (next.exclusive && owner.active > 0))
|
|
28
|
+
return;
|
|
29
|
+
owner.pending.delete(next);
|
|
30
|
+
next.start();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
owner.draining = false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** FIFO exclusive reservations; ordinary callers still run concurrently. */
|
|
38
|
+
export function serializeTurn(activation, work, exclusive = true, signal, onQueuedCancel) {
|
|
39
|
+
let owner = owners.get(activation);
|
|
40
|
+
if (!owner) {
|
|
41
|
+
owner = { active: 0, exclusive: false, draining: false, pending: new Set() };
|
|
42
|
+
owners.set(activation, owner);
|
|
43
|
+
}
|
|
44
|
+
const completion = deferred();
|
|
45
|
+
const inherited = contexts.getStore() ?? [];
|
|
46
|
+
if (inherited.some((context) => context.activation === activation && context.active
|
|
47
|
+
&& (exclusive || context.exclusive || [...owner.pending].some((entry) => entry.exclusive)))) {
|
|
48
|
+
completion.reject(new HarnessSpeculationUnsupportedError("recursive turns across an exclusive reservation"));
|
|
49
|
+
return completion.promise;
|
|
50
|
+
}
|
|
51
|
+
const context = { activation, exclusive, active: false };
|
|
52
|
+
const reservation = {
|
|
53
|
+
exclusive,
|
|
54
|
+
start() {
|
|
55
|
+
signal?.removeEventListener("abort", cancel);
|
|
56
|
+
context.active = true;
|
|
57
|
+
owner.active++;
|
|
58
|
+
owner.exclusive = exclusive;
|
|
59
|
+
const release = () => {
|
|
60
|
+
context.active = false;
|
|
61
|
+
owner.active--;
|
|
62
|
+
if (exclusive)
|
|
63
|
+
owner.exclusive = false;
|
|
64
|
+
drain(owner);
|
|
65
|
+
};
|
|
66
|
+
try {
|
|
67
|
+
const result = contexts.run([...inherited, context], work);
|
|
68
|
+
void Promise.resolve(result).then((value) => { release(); completion.resolve(value); }, (error) => { release(); completion.reject(error); });
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
release();
|
|
72
|
+
completion.reject(error);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const cancel = () => {
|
|
77
|
+
if (!owner.pending.delete(reservation))
|
|
78
|
+
return;
|
|
79
|
+
signal?.removeEventListener("abort", cancel);
|
|
80
|
+
// Deletion is the pending/start winner. Never run execution or acquire
|
|
81
|
+
// ownership for cancellation, nor inherit the abort dispatcher's context.
|
|
82
|
+
try {
|
|
83
|
+
if (onQueuedCancel) {
|
|
84
|
+
void contexts.run(inherited, onQueuedCancel).then(completion.resolve, completion.reject);
|
|
85
|
+
}
|
|
86
|
+
else
|
|
87
|
+
completion.reject(signal?.reason);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
completion.reject(error);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
drain(owner);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
owner.pending.add(reservation);
|
|
97
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
98
|
+
if (signal?.aborted)
|
|
99
|
+
cancel();
|
|
100
|
+
// Register before invoking work, including synchronous reentry.
|
|
101
|
+
else
|
|
102
|
+
drain(owner);
|
|
103
|
+
return completion.promise;
|
|
104
|
+
}
|
|
105
|
+
// The AI SDK also retains stream content, even when our consumer drains it.
|
|
106
|
+
// Bound total text, not just the currently unread queue. A separate chunk cap
|
|
107
|
+
// bounds object overhead for small provider deltas.
|
|
108
|
+
const MAX_TEXT_BYTES = 1_048_576;
|
|
109
|
+
const MAX_TEXT_CHUNKS = 16_384;
|
|
110
|
+
export function speculativeTurn(activation, signal, cancellation, execute) {
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const confirmation = deferred();
|
|
113
|
+
const stopped = deferred();
|
|
114
|
+
let decision = "held";
|
|
115
|
+
let confirmedInput;
|
|
116
|
+
let failure;
|
|
117
|
+
let failed = false;
|
|
118
|
+
let ended = false;
|
|
119
|
+
let executionClosed = false;
|
|
120
|
+
let taken = false;
|
|
121
|
+
let textBytes = 0;
|
|
122
|
+
let textChunks = 0;
|
|
123
|
+
let streamClosed = false;
|
|
124
|
+
const chunks = [];
|
|
125
|
+
const writes = new Set();
|
|
126
|
+
let changed = deferred();
|
|
127
|
+
const wake = () => { changed.resolve(); changed = deferred(); };
|
|
128
|
+
const stopWith = (error) => {
|
|
129
|
+
if (ended || controller.signal.aborted)
|
|
130
|
+
return;
|
|
131
|
+
if (decision === "held")
|
|
132
|
+
decision = "discarded";
|
|
133
|
+
failed = true;
|
|
134
|
+
failure = error;
|
|
135
|
+
chunks.length = 0;
|
|
136
|
+
confirmation.reject(error);
|
|
137
|
+
stopped.reject(error);
|
|
138
|
+
controller.abort(error);
|
|
139
|
+
wake();
|
|
140
|
+
};
|
|
141
|
+
const stop = () => stopWith(cancellation());
|
|
142
|
+
if (signal?.aborted)
|
|
143
|
+
stop();
|
|
144
|
+
else
|
|
145
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
146
|
+
const assertLive = () => {
|
|
147
|
+
if (controller.signal.aborted)
|
|
148
|
+
throw failure;
|
|
149
|
+
if (executionClosed)
|
|
150
|
+
throw new Error("Speculative execution already finished");
|
|
151
|
+
};
|
|
152
|
+
const control = {
|
|
153
|
+
signal: controller.signal,
|
|
154
|
+
assertLive,
|
|
155
|
+
fail: stopWith,
|
|
156
|
+
write(work) {
|
|
157
|
+
assertLive();
|
|
158
|
+
if (decision !== "committed")
|
|
159
|
+
throw new Error("Speculative writes require commitment");
|
|
160
|
+
const completion = deferred();
|
|
161
|
+
writes.add(completion.promise);
|
|
162
|
+
void completion.promise.then(() => { writes.delete(completion.promise); }, () => { writes.delete(completion.promise); });
|
|
163
|
+
try {
|
|
164
|
+
void work().then(completion.resolve, completion.reject);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
completion.reject(error);
|
|
168
|
+
}
|
|
169
|
+
return completion.promise;
|
|
170
|
+
},
|
|
171
|
+
confirmed: async () => { const input = await confirmation.promise; assertLive(); return input; },
|
|
172
|
+
text(delta) {
|
|
173
|
+
assertLive();
|
|
174
|
+
textBytes += Buffer.byteLength(delta, "utf8");
|
|
175
|
+
textChunks++;
|
|
176
|
+
if (textBytes > MAX_TEXT_BYTES || textChunks > MAX_TEXT_CHUNKS) {
|
|
177
|
+
stopWith(new RangeError("Speculative text limit exceeded"));
|
|
178
|
+
throw failure;
|
|
179
|
+
}
|
|
180
|
+
if (!delta)
|
|
181
|
+
return;
|
|
182
|
+
if (!streamClosed) {
|
|
183
|
+
chunks.push(delta);
|
|
184
|
+
wake();
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
const owned = serializeTurn(activation, async () => {
|
|
189
|
+
assertLive();
|
|
190
|
+
try {
|
|
191
|
+
return await Promise.race([execute(control).catch((error) => {
|
|
192
|
+
stopWith(error);
|
|
193
|
+
throw error;
|
|
194
|
+
}), stopped.promise]);
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
executionClosed = true;
|
|
198
|
+
// Issued host effects cannot be undone. Cancellation settles the public
|
|
199
|
+
// result promptly but cannot let a later turn overtake those effects.
|
|
200
|
+
// Recheck: an admitted callback can issue another tracked effect while
|
|
201
|
+
// an earlier batch is settling. The abort fence rejects new admissions.
|
|
202
|
+
while (writes.size)
|
|
203
|
+
await Promise.allSettled([...writes]);
|
|
204
|
+
}
|
|
205
|
+
}, true, controller.signal);
|
|
206
|
+
// Cancellation removes queued reservations and settles active callers
|
|
207
|
+
// promptly, independently of the ownership drain for issued effects.
|
|
208
|
+
const result = Promise.race([owned, stopped.promise]);
|
|
209
|
+
void result.then(() => { ended = true; signal?.removeEventListener("abort", stop); wake(); }, (error) => {
|
|
210
|
+
// A failed speculative model can never later publish its user input.
|
|
211
|
+
stopWith(error);
|
|
212
|
+
ended = true;
|
|
213
|
+
signal?.removeEventListener("abort", stop);
|
|
214
|
+
wake();
|
|
215
|
+
});
|
|
216
|
+
return Object.freeze({
|
|
217
|
+
result,
|
|
218
|
+
textStream: {
|
|
219
|
+
[Symbol.asyncIterator]() {
|
|
220
|
+
if (taken)
|
|
221
|
+
throw new Error("Speculative text stream already has a consumer");
|
|
222
|
+
taken = true;
|
|
223
|
+
const iterator = (async function* () {
|
|
224
|
+
while (true) {
|
|
225
|
+
if (failed)
|
|
226
|
+
throw failure;
|
|
227
|
+
if (streamClosed)
|
|
228
|
+
return;
|
|
229
|
+
const chunk = chunks.shift();
|
|
230
|
+
if (chunk !== undefined)
|
|
231
|
+
yield chunk;
|
|
232
|
+
else if (ended)
|
|
233
|
+
return;
|
|
234
|
+
else
|
|
235
|
+
await changed.promise;
|
|
236
|
+
}
|
|
237
|
+
})();
|
|
238
|
+
return {
|
|
239
|
+
next: () => iterator.next(),
|
|
240
|
+
return: async () => {
|
|
241
|
+
streamClosed = true;
|
|
242
|
+
chunks.length = 0;
|
|
243
|
+
if (decision === "held")
|
|
244
|
+
stop();
|
|
245
|
+
wake();
|
|
246
|
+
return iterator.return();
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
commit(input) {
|
|
252
|
+
if (decision === "discarded" || (decision === "committed" && input !== confirmedInput)) {
|
|
253
|
+
const conflict = Promise.reject(new HarnessSpeculationConflictError());
|
|
254
|
+
void conflict.catch(() => { });
|
|
255
|
+
return conflict;
|
|
256
|
+
}
|
|
257
|
+
if (decision === "held") {
|
|
258
|
+
decision = "committed";
|
|
259
|
+
confirmedInput = input;
|
|
260
|
+
confirmation.resolve(input);
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
263
|
+
},
|
|
264
|
+
discard() {
|
|
265
|
+
if (decision === "committed")
|
|
266
|
+
return false;
|
|
267
|
+
stop();
|
|
268
|
+
return true;
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=speculative.js.map
|
|
@@ -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
|
},
|