@anureo/sdk 0.2.0 → 0.3.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 +162 -65
- package/package.json +29 -24
- package/src/adapter/identity.d.ts +41 -0
- package/src/adapter/identity.js +71 -0
- package/src/client-options.d.ts +71 -0
- package/src/client-options.js +0 -0
- package/src/client.d.ts +106 -0
- package/src/client.js +254 -0
- package/src/diagnostics/errors.d.ts +15 -0
- package/src/diagnostics/errors.js +59 -0
- package/src/diagnostics/events.d.ts +46 -0
- package/src/diagnostics/events.js +47 -0
- package/src/diagnostics.d.ts +20 -0
- package/src/diagnostics.js +56 -0
- package/src/errors.d.ts +48 -0
- package/src/errors.js +63 -0
- package/src/index.d.ts +27 -0
- package/src/index.js +23 -0
- package/src/lifecycle/approval.d.ts +16 -0
- package/src/lifecycle/approval.js +18 -0
- package/src/lifecycle/deferred.d.ts +10 -0
- package/src/lifecycle/deferred.js +12 -0
- package/src/lifecycle/errors.d.ts +18 -0
- package/src/lifecycle/errors.js +45 -0
- package/src/lifecycle/events.d.ts +35 -0
- package/src/lifecycle/events.js +141 -0
- package/src/lifecycle/execution-observation.d.ts +33 -0
- package/src/lifecycle/execution-observation.js +0 -0
- package/src/lifecycle/immutable.d.ts +12 -0
- package/src/lifecycle/immutable.js +28 -0
- package/src/lifecycle/input.d.ts +9 -0
- package/src/lifecycle/input.js +19 -0
- package/src/lifecycle/interactions.d.ts +129 -0
- package/src/lifecycle/interactions.js +282 -0
- package/src/lifecycle/limits.d.ts +7 -0
- package/src/lifecycle/limits.js +13 -0
- package/src/lifecycle/result.d.ts +77 -0
- package/src/lifecycle/result.js +213 -0
- package/src/lifecycle/value.d.ts +4 -0
- package/src/lifecycle/value.js +5 -0
- package/src/ports.d.ts +224 -0
- package/src/ports.js +0 -0
- package/src/privacy/file-change-limits.d.ts +20 -0
- package/src/privacy/file-change-limits.js +124 -0
- package/src/privacy/secrets.d.ts +205 -0
- package/src/privacy/secrets.js +564 -0
- package/src/process.d.ts +92 -0
- package/src/process.js +331 -0
- package/src/queue.d.ts +143 -0
- package/src/queue.js +266 -0
- package/src/request.d.ts +28 -0
- package/src/request.js +81 -0
- package/src/requests/guard.d.ts +15 -0
- package/src/requests/guard.js +93 -0
- package/src/session.d.ts +459 -0
- package/src/session.js +1135 -0
- package/src/structured-output/adapter.d.ts +8 -0
- package/src/structured-output/adapter.js +54 -0
- package/src/structured-output/bridge.d.ts +81 -0
- package/src/structured-output/bridge.js +274 -0
- package/src/structured-output/errors.d.ts +17 -0
- package/src/structured-output/errors.js +31 -0
- package/src/structured-output/limits.d.ts +19 -0
- package/src/structured-output/limits.js +0 -0
- package/src/structured-output/schema.d.ts +20 -0
- package/src/structured-output/schema.js +122 -0
- package/src/structured-output/session.d.ts +329 -0
- package/src/structured-output/session.js +846 -0
- package/src/structured-output/stdio.mjs +83 -0
- package/src/structured-output/tool.d.ts +38 -0
- package/src/structured-output/tool.js +34 -0
- package/src/turn.d.ts +75 -0
- package/src/turn.js +95 -0
- package/src/types.d.ts +62 -0
- package/src/types.js +0 -0
- package/dist/diagnostics.d.ts +0 -15
- package/dist/diagnostics.js +0 -34
- package/dist/errors.d.ts +0 -15
- package/dist/file-change-limits.d.ts +0 -5
- package/dist/identity.d.ts +0 -13
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -1474
- package/dist/interactions.d.ts +0 -20
- package/dist/ports.d.ts +0 -66
- package/dist/process.d.ts +0 -34
- package/dist/process.js +0 -191
- package/dist/queue.d.ts +0 -36
- package/dist/queue.js +0 -166
- package/dist/runtime.d.ts +0 -85
- package/dist/secrets.d.ts +0 -28
package/dist/index.js
DELETED
|
@@ -1,1474 +0,0 @@
|
|
|
1
|
-
// packages/core/src/runtime.ts
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
-
assertEvent,
|
|
5
|
-
assertRequest,
|
|
6
|
-
assertResult,
|
|
7
|
-
negotiateVersion,
|
|
8
|
-
supportsCapability,
|
|
9
|
-
supportsExtension,
|
|
10
|
-
validateJsonSchema
|
|
11
|
-
} from "@anureo/protocol";
|
|
12
|
-
|
|
13
|
-
// packages/core/src/errors.ts
|
|
14
|
-
class HarnessSdkError extends Error {
|
|
15
|
-
code;
|
|
16
|
-
retryable;
|
|
17
|
-
outcomeUnknown;
|
|
18
|
-
details;
|
|
19
|
-
name = "HarnessSdkError";
|
|
20
|
-
constructor(code, message = code, retryable = false, outcomeUnknown = false, details) {
|
|
21
|
-
super(message);
|
|
22
|
-
this.code = code;
|
|
23
|
-
this.retryable = retryable;
|
|
24
|
-
this.outcomeUnknown = outcomeUnknown;
|
|
25
|
-
this.details = details;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
function sdkError(cause, fallback = "HARNESS_ERROR", outcomeUnknown = false) {
|
|
29
|
-
if (cause instanceof HarnessSdkError)
|
|
30
|
-
return cause;
|
|
31
|
-
if (cause && typeof cause === "object" && "code" in cause && typeof cause.code === "string" && /^[A-Z][A-Z0-9_]{0,80}$/.test(cause.code)) {
|
|
32
|
-
return new HarnessSdkError(cause.code, cause.code, "retryable" in cause && cause.retryable === true, "outcomeUnknown" in cause ? cause.outcomeUnknown === true : outcomeUnknown);
|
|
33
|
-
}
|
|
34
|
-
return new HarnessSdkError(fallback, fallback, false, outcomeUnknown);
|
|
35
|
-
}
|
|
36
|
-
function waitForRequest(operation, options = {}, defaultTimeoutMs = 30000) {
|
|
37
|
-
if (options.signal?.aborted)
|
|
38
|
-
return Promise.reject(new HarnessSdkError("REQUEST_ABORTED", "Request aborted before dispatch"));
|
|
39
|
-
const timeoutMs = options.timeoutMs ?? defaultTimeoutMs;
|
|
40
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
41
|
-
return Promise.reject(new HarnessSdkError("INVALID_PARAMS", "timeoutMs must be positive"));
|
|
42
|
-
return new Promise((resolve, reject) => {
|
|
43
|
-
let settled = false;
|
|
44
|
-
const finish = (callback) => {
|
|
45
|
-
if (settled)
|
|
46
|
-
return;
|
|
47
|
-
settled = true;
|
|
48
|
-
clearTimeout(timer);
|
|
49
|
-
options.signal?.removeEventListener("abort", abort);
|
|
50
|
-
callback();
|
|
51
|
-
};
|
|
52
|
-
const abort = () => finish(() => reject(new HarnessSdkError("REQUEST_ABORTED", "Request outcome is unknown after dispatch", false, true)));
|
|
53
|
-
const timer = setTimeout(() => finish(() => reject(new HarnessSdkError("REQUEST_TIMEOUT", "Request outcome is unknown after dispatch", false, true))), timeoutMs);
|
|
54
|
-
options.signal?.addEventListener("abort", abort, { once: true });
|
|
55
|
-
try {
|
|
56
|
-
operation().then((value) => finish(() => resolve(value)), (cause) => finish(() => reject(cause)));
|
|
57
|
-
} catch (cause) {
|
|
58
|
-
finish(() => reject(cause));
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// packages/core/src/interactions.ts
|
|
64
|
-
import { createHash } from "node:crypto";
|
|
65
|
-
import { assertInteractionResponse } from "@anureo/protocol";
|
|
66
|
-
class InteractionRegistry {
|
|
67
|
-
publish;
|
|
68
|
-
limits;
|
|
69
|
-
beforeResponse;
|
|
70
|
-
records = new Map;
|
|
71
|
-
constructor(publish, limits, beforeResponse) {
|
|
72
|
-
this.publish = publish;
|
|
73
|
-
this.limits = limits;
|
|
74
|
-
this.beforeResponse = beforeResponse;
|
|
75
|
-
}
|
|
76
|
-
get pendingCount() {
|
|
77
|
-
return [...this.records.values()].filter((record) => record.state !== "resolved").length;
|
|
78
|
-
}
|
|
79
|
-
pending() {
|
|
80
|
-
return [...this.records.values()].filter((record) => record.state === "pending").map((record) => structuredClone(record.request));
|
|
81
|
-
}
|
|
82
|
-
begin(request, responder) {
|
|
83
|
-
if (this.records.has(request.interactionId))
|
|
84
|
-
throw new HarnessSdkError("INTERACTION_ALREADY_EXISTS");
|
|
85
|
-
if (this.pendingCount >= this.limits.maxPending)
|
|
86
|
-
throw new HarnessSdkError("BACKPRESSURE_EXCEEDED", "Pending interaction limit exceeded");
|
|
87
|
-
const ttl = request.expiresAt ? Math.min(this.limits.timeoutMs, new Date(request.expiresAt).getTime() - Date.now()) : this.limits.timeoutMs;
|
|
88
|
-
if (!Number.isFinite(ttl) || ttl <= 0)
|
|
89
|
-
throw new HarnessSdkError("INTERACTION_EXPIRED");
|
|
90
|
-
if (this.records.size >= this.limits.maxPending + 1024) {
|
|
91
|
-
for (const [id, record] of this.records)
|
|
92
|
-
if (record.state === "resolved") {
|
|
93
|
-
this.records.delete(id);
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
const result = new Promise((resolve, reject) => {
|
|
98
|
-
const timer = setTimeout(() => {
|
|
99
|
-
this.cancel(request.interactionId, "timeout", "expired").catch(() => {});
|
|
100
|
-
}, ttl);
|
|
101
|
-
timer.unref?.();
|
|
102
|
-
this.records.set(request.interactionId, { request: structuredClone(request), responder, state: "pending", timer, resolve, reject });
|
|
103
|
-
});
|
|
104
|
-
result.catch(() => {});
|
|
105
|
-
return result;
|
|
106
|
-
}
|
|
107
|
-
async respond(id, requestId, response) {
|
|
108
|
-
const record = this.records.get(id);
|
|
109
|
-
if (!record)
|
|
110
|
-
throw new HarnessSdkError("INTERACTION_EXPIRED");
|
|
111
|
-
assertInteractionResponse(record.request, response);
|
|
112
|
-
this.beforeResponse?.(record.request, response);
|
|
113
|
-
const fingerprint = createHash("sha256").update(JSON.stringify(response)).digest("hex");
|
|
114
|
-
if (record.responseRequestId === requestId && record.responseFingerprint === fingerprint && record.responseOperation)
|
|
115
|
-
return record.responseOperation;
|
|
116
|
-
if (record.state !== "pending")
|
|
117
|
-
throw new HarnessSdkError("INTERACTION_ALREADY_RESOLVED");
|
|
118
|
-
record.state = "responding";
|
|
119
|
-
record.responseRequestId = requestId;
|
|
120
|
-
record.responseFingerprint = fingerprint;
|
|
121
|
-
clearTimeout(record.timer);
|
|
122
|
-
record.responseOperation = (async () => {
|
|
123
|
-
try {
|
|
124
|
-
await waitForRequest(() => record.responder(response), { timeoutMs: this.limits.responseTimeoutMs ?? 30000 });
|
|
125
|
-
if (record.state === "resolved")
|
|
126
|
-
throw new HarnessSdkError("RESULT_UNKNOWN", "Interaction ended while response was in flight", false, true);
|
|
127
|
-
const result = { interactionId: id, outcome: "responded" };
|
|
128
|
-
record.state = "resolved";
|
|
129
|
-
await this.publish(result);
|
|
130
|
-
record.resolve(response);
|
|
131
|
-
return result;
|
|
132
|
-
} catch (cause) {
|
|
133
|
-
const error = sdkError(cause, "RESULT_UNKNOWN", true);
|
|
134
|
-
if (record.state !== "resolved") {
|
|
135
|
-
record.state = "resolved";
|
|
136
|
-
await this.publish({ interactionId: id, outcome: "unknown", reason: error.code });
|
|
137
|
-
}
|
|
138
|
-
record.reject(error);
|
|
139
|
-
throw error;
|
|
140
|
-
}
|
|
141
|
-
})();
|
|
142
|
-
return record.responseOperation;
|
|
143
|
-
}
|
|
144
|
-
async cancel(id, reason = "cancelled", outcome = "cancelled") {
|
|
145
|
-
const record = this.records.get(id);
|
|
146
|
-
if (!record)
|
|
147
|
-
throw new HarnessSdkError("INTERACTION_EXPIRED");
|
|
148
|
-
if (record.state === "resolved")
|
|
149
|
-
throw new HarnessSdkError("INTERACTION_ALREADY_RESOLVED");
|
|
150
|
-
if (record.state === "responding") {
|
|
151
|
-
if (reason === "cancelled")
|
|
152
|
-
throw new HarnessSdkError("INTERACTION_ALREADY_RESOLVED");
|
|
153
|
-
record.state = "resolved";
|
|
154
|
-
clearTimeout(record.timer);
|
|
155
|
-
const result = { interactionId: id, outcome: "unknown", reason };
|
|
156
|
-
try {
|
|
157
|
-
await this.publish(result);
|
|
158
|
-
} finally {
|
|
159
|
-
record.reject(new HarnessSdkError("RESULT_UNKNOWN", "Interaction ended while response was in flight", false, true));
|
|
160
|
-
}
|
|
161
|
-
return result;
|
|
162
|
-
}
|
|
163
|
-
record.state = "responding";
|
|
164
|
-
clearTimeout(record.timer);
|
|
165
|
-
try {
|
|
166
|
-
await waitForRequest(() => record.responder(null, reason), { timeoutMs: this.limits.responseTimeoutMs ?? 30000 });
|
|
167
|
-
} catch {
|
|
168
|
-
outcome = "unknown";
|
|
169
|
-
}
|
|
170
|
-
record.state = "resolved";
|
|
171
|
-
const result = { interactionId: id, outcome, reason };
|
|
172
|
-
try {
|
|
173
|
-
await this.publish(result);
|
|
174
|
-
} finally {
|
|
175
|
-
record.reject(new HarnessSdkError(outcome === "unknown" ? "RESULT_UNKNOWN" : "INTERACTION_CANCELLED", "Interaction is no longer actionable", false, outcome === "unknown"));
|
|
176
|
-
}
|
|
177
|
-
return result;
|
|
178
|
-
}
|
|
179
|
-
async cancelAll(reason, turnId) {
|
|
180
|
-
const ids = [...this.records].filter(([, record]) => record.state !== "resolved" && (turnId === undefined || record.request.turnId === turnId)).map(([id]) => id);
|
|
181
|
-
await Promise.allSettled(ids.map((id) => this.cancel(id, reason, reason === "connection_lost" ? "unknown" : "cancelled")));
|
|
182
|
-
}
|
|
183
|
-
async resolveExternally(result) {
|
|
184
|
-
const record = this.records.get(result.interactionId);
|
|
185
|
-
if (!record || record.state === "resolved")
|
|
186
|
-
return;
|
|
187
|
-
record.state = "resolved";
|
|
188
|
-
clearTimeout(record.timer);
|
|
189
|
-
try {
|
|
190
|
-
await this.publish(result);
|
|
191
|
-
} finally {
|
|
192
|
-
record.reject(new HarnessSdkError(result.outcome === "unknown" ? "RESULT_UNKNOWN" : "INTERACTION_ALREADY_RESOLVED", "Interaction was resolved by the native peer", false, result.outcome === "unknown"));
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// packages/core/src/queue.ts
|
|
198
|
-
class BoundedEventQueue {
|
|
199
|
-
sizeOf;
|
|
200
|
-
entries = [];
|
|
201
|
-
bytes = 0;
|
|
202
|
-
reader;
|
|
203
|
-
waiting;
|
|
204
|
-
controls = [];
|
|
205
|
-
ended = false;
|
|
206
|
-
failure;
|
|
207
|
-
writers = new Set;
|
|
208
|
-
limits;
|
|
209
|
-
constructor(limits = {}, sizeOf = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength) {
|
|
210
|
-
this.sizeOf = sizeOf;
|
|
211
|
-
this.limits = { maxEvents: limits.maxEvents ?? 2048, maxBytes: limits.maxBytes ?? 16 * 1024 * 1024, maxFrameBytes: limits.maxFrameBytes ?? 8 * 1024 * 1024, maxWaitingWriters: limits.maxWaitingWriters ?? 32, maxControlEvents: limits.maxControlEvents ?? 33 };
|
|
212
|
-
if (Object.values(this.limits).some((n) => !Number.isSafeInteger(n) || n <= 0))
|
|
213
|
-
throw new HarnessSdkError("INVALID_PARAMS", "Queue limits must be positive integers");
|
|
214
|
-
}
|
|
215
|
-
get bufferedEvents() {
|
|
216
|
-
return this.entries.length + this.controls.length;
|
|
217
|
-
}
|
|
218
|
-
get bufferedBytes() {
|
|
219
|
-
return this.bytes;
|
|
220
|
-
}
|
|
221
|
-
wakeWriters() {
|
|
222
|
-
for (const wake of this.writers)
|
|
223
|
-
wake();
|
|
224
|
-
this.writers.clear();
|
|
225
|
-
}
|
|
226
|
-
assertOpen() {
|
|
227
|
-
if (this.ended)
|
|
228
|
-
throw this.failure ?? new HarnessSdkError("STREAM_CLOSED");
|
|
229
|
-
}
|
|
230
|
-
checkedSize(value) {
|
|
231
|
-
const bytes = this.sizeOf(value);
|
|
232
|
-
if (bytes > this.limits.maxFrameBytes || bytes > this.limits.maxBytes)
|
|
233
|
-
throw new HarnessSdkError("FRAME_TOO_LARGE");
|
|
234
|
-
return bytes;
|
|
235
|
-
}
|
|
236
|
-
assertWritable(value) {
|
|
237
|
-
this.assertOpen();
|
|
238
|
-
const bytes = this.checkedSize(value);
|
|
239
|
-
if (!this.waiting && (this.entries.length >= this.limits.maxEvents || this.bytes + bytes > this.limits.maxBytes))
|
|
240
|
-
throw new HarnessSdkError("BACKPRESSURE_EXCEEDED");
|
|
241
|
-
}
|
|
242
|
-
push(value) {
|
|
243
|
-
this.assertWritable(value);
|
|
244
|
-
const bytes = this.checkedSize(value);
|
|
245
|
-
if (this.waiting) {
|
|
246
|
-
const waiting = this.waiting;
|
|
247
|
-
this.waiting = undefined;
|
|
248
|
-
waiting.resolve({ done: false, value });
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
this.entries.push({ value, bytes });
|
|
252
|
-
this.bytes += bytes;
|
|
253
|
-
}
|
|
254
|
-
async write(value, accepted) {
|
|
255
|
-
const bytes = this.checkedSize(value);
|
|
256
|
-
while (!this.waiting && (this.entries.length >= this.limits.maxEvents || this.bytes + bytes > this.limits.maxBytes)) {
|
|
257
|
-
this.assertOpen();
|
|
258
|
-
if (this.writers.size >= this.limits.maxWaitingWriters)
|
|
259
|
-
throw new HarnessSdkError("BACKPRESSURE_EXCEEDED");
|
|
260
|
-
await new Promise((resolve) => this.writers.add(resolve));
|
|
261
|
-
}
|
|
262
|
-
this.push(value);
|
|
263
|
-
accepted?.();
|
|
264
|
-
}
|
|
265
|
-
pushControl(value) {
|
|
266
|
-
this.assertOpen();
|
|
267
|
-
this.checkedSize(value);
|
|
268
|
-
if (this.waiting) {
|
|
269
|
-
const waiter = this.waiting;
|
|
270
|
-
this.waiting = undefined;
|
|
271
|
-
waiter.resolve({ done: false, value });
|
|
272
|
-
} else {
|
|
273
|
-
if (this.controls.length >= this.limits.maxControlEvents)
|
|
274
|
-
throw new HarnessSdkError("BACKPRESSURE_EXCEEDED", "Control event limit exceeded");
|
|
275
|
-
this.controls.push(value);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
endWithControl(value) {
|
|
279
|
-
if (this.ended)
|
|
280
|
-
return;
|
|
281
|
-
this.pushControl(value);
|
|
282
|
-
this.end();
|
|
283
|
-
}
|
|
284
|
-
end(cause) {
|
|
285
|
-
if (this.ended)
|
|
286
|
-
return;
|
|
287
|
-
this.ended = true;
|
|
288
|
-
this.failure = cause;
|
|
289
|
-
this.wakeWriters();
|
|
290
|
-
if (this.waiting) {
|
|
291
|
-
const waiter = this.waiting;
|
|
292
|
-
this.waiting = undefined;
|
|
293
|
-
cause ? waiter.reject(cause) : waiter.resolve({ done: true, value: undefined });
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
[Symbol.asyncIterator]() {
|
|
297
|
-
if (this.reader)
|
|
298
|
-
throw new HarnessSdkError("EVENT_STREAM_ALREADY_OWNED");
|
|
299
|
-
const reader = Symbol("reader");
|
|
300
|
-
this.reader = reader;
|
|
301
|
-
let detached = false;
|
|
302
|
-
const detach = () => {
|
|
303
|
-
detached = true;
|
|
304
|
-
if (this.reader === reader)
|
|
305
|
-
this.reader = undefined;
|
|
306
|
-
if (this.waiting?.reader === reader) {
|
|
307
|
-
const waiter = this.waiting;
|
|
308
|
-
this.waiting = undefined;
|
|
309
|
-
waiter.resolve({ done: true, value: undefined });
|
|
310
|
-
}
|
|
311
|
-
return { done: true, value: undefined };
|
|
312
|
-
};
|
|
313
|
-
return {
|
|
314
|
-
next: () => {
|
|
315
|
-
if (detached)
|
|
316
|
-
return Promise.resolve({ done: true, value: undefined });
|
|
317
|
-
const entry = this.entries.shift();
|
|
318
|
-
if (entry) {
|
|
319
|
-
this.bytes -= entry.bytes;
|
|
320
|
-
this.wakeWriters();
|
|
321
|
-
return Promise.resolve({ done: false, value: entry.value });
|
|
322
|
-
}
|
|
323
|
-
if (this.controls.length)
|
|
324
|
-
return Promise.resolve({ done: false, value: this.controls.shift() });
|
|
325
|
-
if (this.ended) {
|
|
326
|
-
detach();
|
|
327
|
-
return this.failure ? Promise.reject(this.failure) : Promise.resolve({ done: true, value: undefined });
|
|
328
|
-
}
|
|
329
|
-
if (this.waiting)
|
|
330
|
-
return Promise.reject(new HarnessSdkError("CONCURRENT_EVENT_READ"));
|
|
331
|
-
return new Promise((resolve, reject) => {
|
|
332
|
-
this.waiting = { reader, resolve, reject };
|
|
333
|
-
this.wakeWriters();
|
|
334
|
-
});
|
|
335
|
-
},
|
|
336
|
-
return: async () => detach(),
|
|
337
|
-
throw: async (cause) => {
|
|
338
|
-
detach();
|
|
339
|
-
throw cause;
|
|
340
|
-
}
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// packages/core/src/file-change-limits.ts
|
|
346
|
-
var FILE_CHANGE_MATERIAL_LIMIT = 50;
|
|
347
|
-
var FILE_CHANGE_TEXT_LIMIT = 256 * 1024;
|
|
348
|
-
function boundFileChangeItem(item) {
|
|
349
|
-
if (item.type !== "toolCall" || item.fileChanges === undefined)
|
|
350
|
-
return item;
|
|
351
|
-
let bytes = FILE_CHANGE_TEXT_LIMIT, materials = 0, truncated = false;
|
|
352
|
-
const encoder = new TextEncoder;
|
|
353
|
-
const take = (value) => {
|
|
354
|
-
const encoded = encoder.encode(value);
|
|
355
|
-
if (encoded.byteLength <= bytes) {
|
|
356
|
-
bytes -= encoded.byteLength;
|
|
357
|
-
return { value, complete: true };
|
|
358
|
-
}
|
|
359
|
-
let prefix = new TextDecoder().decode(encoded.subarray(0, bytes));
|
|
360
|
-
const end = prefix.lastIndexOf(`
|
|
361
|
-
`);
|
|
362
|
-
prefix = end < 0 ? "" : prefix.slice(0, end + 1);
|
|
363
|
-
bytes -= encoder.encode(prefix).byteLength;
|
|
364
|
-
truncated = true;
|
|
365
|
-
return { value: prefix, complete: false };
|
|
366
|
-
};
|
|
367
|
-
const fileChanges = item.fileChanges.map((change) => {
|
|
368
|
-
let partial = false;
|
|
369
|
-
const next = [];
|
|
370
|
-
for (const material of change.materials) {
|
|
371
|
-
if (material.format === "unavailable") {
|
|
372
|
-
next.push(material);
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
if (++materials > FILE_CHANGE_MATERIAL_LIMIT) {
|
|
376
|
-
partial = truncated = true;
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
if (material.format === "unified") {
|
|
380
|
-
const patch = take(material.patch);
|
|
381
|
-
partial ||= !patch.complete;
|
|
382
|
-
next.push(patch.value ? { ...material, patch: patch.value } : { format: "unavailable", reason: "tooLarge" });
|
|
383
|
-
} else {
|
|
384
|
-
const oldText = material.oldText === null ? null : take(material.oldText);
|
|
385
|
-
const newText = take(material.newText);
|
|
386
|
-
if (oldText !== null && !oldText.complete || !newText.complete) {
|
|
387
|
-
partial = true;
|
|
388
|
-
next.push({ format: "unavailable", reason: "tooLarge" });
|
|
389
|
-
} else
|
|
390
|
-
next.push({ ...material, oldText: oldText === null ? null : oldText.value, newText: newText.value });
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
if (!next.length && change.materials.length)
|
|
394
|
-
next.push({ format: "unavailable", reason: "tooLarge" });
|
|
395
|
-
return { ...change, materials: next, ...partial ? { completeness: "partial", ...change.stats ? { stats: { ...change.stats, accuracy: "partial" } } : {} } : {} };
|
|
396
|
-
});
|
|
397
|
-
const mirror = (value) => {
|
|
398
|
-
if (typeof value === "string")
|
|
399
|
-
return take(value).value;
|
|
400
|
-
if (Array.isArray(value))
|
|
401
|
-
return value.map(mirror);
|
|
402
|
-
if (value && typeof value === "object")
|
|
403
|
-
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, mirror(child)]));
|
|
404
|
-
return value;
|
|
405
|
-
};
|
|
406
|
-
const result = { ...item, fileChanges, ...item.input !== undefined ? { input: mirror(item.input) } : {}, ...item.output !== undefined ? { output: mirror(item.output) } : {}, ...item.raw !== undefined ? { raw: mirror(item.raw) } : {}, ...item.extensions ? { extensions: mirror(item.extensions) } : {} };
|
|
407
|
-
if (truncated)
|
|
408
|
-
result.extensions = { ...result.extensions, "x-anureo/fileChangesTruncated": true };
|
|
409
|
-
return result;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// packages/core/src/secrets.ts
|
|
413
|
-
import { assertDefinition } from "@anureo/protocol";
|
|
414
|
-
class SecretRedactor {
|
|
415
|
-
onRemember;
|
|
416
|
-
values = new Set;
|
|
417
|
-
streams = new Map;
|
|
418
|
-
secretBytes = 0;
|
|
419
|
-
constructor(onRemember) {
|
|
420
|
-
this.onRemember = onRemember;
|
|
421
|
-
}
|
|
422
|
-
remember(value) {
|
|
423
|
-
if (value === undefined || value === null)
|
|
424
|
-
return;
|
|
425
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
426
|
-
const text = String(value);
|
|
427
|
-
if (text && !this.values.has(text)) {
|
|
428
|
-
const bytes = new TextEncoder().encode(text).byteLength;
|
|
429
|
-
if (this.values.size >= 128 || this.secretBytes + bytes > 64 * 1024)
|
|
430
|
-
throw new HarnessSdkError("BACKPRESSURE_EXCEEDED", "Marked secret limit exceeded");
|
|
431
|
-
this.onRemember?.(text);
|
|
432
|
-
this.values.add(text);
|
|
433
|
-
this.secretBytes += bytes;
|
|
434
|
-
}
|
|
435
|
-
} else if (Array.isArray(value))
|
|
436
|
-
value.forEach((entry) => this.remember(entry));
|
|
437
|
-
else
|
|
438
|
-
for (const entry of Object.values(value))
|
|
439
|
-
this.remember(entry);
|
|
440
|
-
}
|
|
441
|
-
rememberResponse(request, response) {
|
|
442
|
-
if (request.kind === "userInput" && response.kind === "userInput") {
|
|
443
|
-
for (const question of request.questions)
|
|
444
|
-
if (question.secret)
|
|
445
|
-
this.remember(response.answers[question.id]);
|
|
446
|
-
}
|
|
447
|
-
if (request.kind === "elicitation" && response.kind === "elicitation" && response.content && request.schema !== undefined)
|
|
448
|
-
this.fromSchema(request.schema, response.content, request.schema);
|
|
449
|
-
if (request.kind === "dynamicTool" && response.kind === "dynamicTool" && request.schema !== undefined)
|
|
450
|
-
this.fromSchema(request.schema, response, request.schema);
|
|
451
|
-
}
|
|
452
|
-
fromSchema(schema, value, root, depth = 0) {
|
|
453
|
-
if (depth > 32 || typeof schema !== "object" || !schema || value === undefined)
|
|
454
|
-
return;
|
|
455
|
-
if (schema.writeOnly === true || schema.secret === true) {
|
|
456
|
-
this.remember(value);
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
if (typeof schema.$ref === "string" && schema.$ref.startsWith("#/") && typeof root === "object") {
|
|
460
|
-
let target = root;
|
|
461
|
-
for (const token of schema.$ref.slice(2).split("/")) {
|
|
462
|
-
const key = token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
463
|
-
target = target && typeof target === "object" && !Array.isArray(target) ? target[key] : undefined;
|
|
464
|
-
}
|
|
465
|
-
if (typeof target === "boolean" || target && typeof target === "object" && !Array.isArray(target))
|
|
466
|
-
this.fromSchema(target, value, root, depth + 1);
|
|
467
|
-
}
|
|
468
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
469
|
-
const properties = schema.properties;
|
|
470
|
-
if (properties && typeof properties === "object" && !Array.isArray(properties)) {
|
|
471
|
-
for (const [key, child] of Object.entries(properties))
|
|
472
|
-
if (typeof child === "boolean" || child && typeof child === "object" && !Array.isArray(child))
|
|
473
|
-
this.fromSchema(child, value[key], root, depth + 1);
|
|
474
|
-
}
|
|
475
|
-
if (schema.additionalProperties && typeof schema.additionalProperties === "object" && !Array.isArray(schema.additionalProperties)) {
|
|
476
|
-
for (const [key, child] of Object.entries(value))
|
|
477
|
-
if (!properties || typeof properties !== "object" || !Object.hasOwn(properties, key))
|
|
478
|
-
this.fromSchema(schema.additionalProperties, child, root, depth + 1);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
if (Array.isArray(value) && schema.items && (typeof schema.items === "boolean" || typeof schema.items === "object" && !Array.isArray(schema.items)))
|
|
482
|
-
for (const child of value)
|
|
483
|
-
this.fromSchema(schema.items, child, root, depth + 1);
|
|
484
|
-
for (const key of ["allOf", "anyOf", "oneOf"])
|
|
485
|
-
if (Array.isArray(schema[key])) {
|
|
486
|
-
for (const branch of schema[key])
|
|
487
|
-
if (typeof branch === "boolean" || branch && typeof branch === "object" && !Array.isArray(branch))
|
|
488
|
-
this.fromSchema(branch, value, root, depth + 1);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
scan(value, final) {
|
|
492
|
-
const values = [...this.values].sort((a, b) => b.length - a.length);
|
|
493
|
-
const output = [];
|
|
494
|
-
for (let offset = 0;offset < value.length; ) {
|
|
495
|
-
const candidates = values.filter((secret) => secret[0] === value[offset]);
|
|
496
|
-
const secret = candidates.find((entry) => value.startsWith(entry, offset));
|
|
497
|
-
if (secret) {
|
|
498
|
-
output.push("[redacted]");
|
|
499
|
-
offset += secret.length;
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
const remaining = value.slice(offset);
|
|
503
|
-
if (!final && candidates.some((entry) => entry.startsWith(remaining)))
|
|
504
|
-
return { text: output.join(""), pending: remaining };
|
|
505
|
-
output.push(value[offset]);
|
|
506
|
-
offset++;
|
|
507
|
-
}
|
|
508
|
-
return { text: output.join(""), pending: "" };
|
|
509
|
-
}
|
|
510
|
-
text(value) {
|
|
511
|
-
return this.values.size ? this.scan(value, true).text : value;
|
|
512
|
-
}
|
|
513
|
-
delta(stream, value) {
|
|
514
|
-
if (!this.values.size)
|
|
515
|
-
return value;
|
|
516
|
-
const result = this.scan((this.streams.get(stream) ?? "") + value, false);
|
|
517
|
-
if (result.pending)
|
|
518
|
-
this.streams.set(stream, result.pending);
|
|
519
|
-
else
|
|
520
|
-
this.streams.delete(stream);
|
|
521
|
-
return result.text;
|
|
522
|
-
}
|
|
523
|
-
json(value) {
|
|
524
|
-
if (typeof value === "string")
|
|
525
|
-
return this.text(value);
|
|
526
|
-
if (typeof value === "number" || typeof value === "boolean")
|
|
527
|
-
return this.values.has(String(value)) ? "[redacted]" : value;
|
|
528
|
-
if (Array.isArray(value))
|
|
529
|
-
return value.map((entry) => this.json(entry));
|
|
530
|
-
if (value && typeof value === "object")
|
|
531
|
-
return Object.fromEntries(Object.entries(value).map(([key, child]) => [this.text(key), this.json(child)]));
|
|
532
|
-
return value;
|
|
533
|
-
}
|
|
534
|
-
schema(schema) {
|
|
535
|
-
if (typeof schema === "boolean")
|
|
536
|
-
return schema;
|
|
537
|
-
const output = { ...schema };
|
|
538
|
-
for (const key of ["title", "description", "default", "examples"])
|
|
539
|
-
if (output[key] !== undefined)
|
|
540
|
-
output[key] = this.json(output[key]);
|
|
541
|
-
for (const key of ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"]) {
|
|
542
|
-
const map = output[key];
|
|
543
|
-
if (map && typeof map === "object" && !Array.isArray(map))
|
|
544
|
-
output[key] = Object.fromEntries(Object.entries(map).map(([name, child]) => [name, typeof child === "boolean" || child && typeof child === "object" && !Array.isArray(child) ? this.schema(child) : child]));
|
|
545
|
-
}
|
|
546
|
-
for (const key of ["additionalProperties", "unevaluatedProperties", "propertyNames", "items", "contains", "additionalItems", "unevaluatedItems", "not", "if", "then", "else", "contentSchema"]) {
|
|
547
|
-
const child = output[key];
|
|
548
|
-
if (typeof child === "boolean" || child && typeof child === "object" && !Array.isArray(child))
|
|
549
|
-
output[key] = this.schema(child);
|
|
550
|
-
}
|
|
551
|
-
for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) {
|
|
552
|
-
const children = output[key];
|
|
553
|
-
if (Array.isArray(children))
|
|
554
|
-
output[key] = children.map((child) => typeof child === "boolean" || child && typeof child === "object" && !Array.isArray(child) ? this.schema(child) : child);
|
|
555
|
-
}
|
|
556
|
-
return output;
|
|
557
|
-
}
|
|
558
|
-
capabilities(value) {
|
|
559
|
-
if (!value.config)
|
|
560
|
-
return value;
|
|
561
|
-
return { ...value, config: { ...value.config, options: value.config.options.map((option) => ({ ...option, ...option.title !== undefined ? { title: this.text(option.title) } : {}, ...option.value !== undefined ? { value: this.json(option.value) } : {}, schema: this.schema(option.schema) })) } };
|
|
562
|
-
}
|
|
563
|
-
goal(value) {
|
|
564
|
-
return { ...value, goal: value.goal === null ? null : this.json(value.goal) };
|
|
565
|
-
}
|
|
566
|
-
state(value) {
|
|
567
|
-
return { ...value, ...value.metadata ? { metadata: this.json(value.metadata) } : {}, config: Object.fromEntries(Object.entries(value.config).map(([id, entry]) => [id, this.json(entry)])), capabilities: this.capabilities(value.capabilities), ...value.goal ? { goal: this.goal(value.goal) } : {} };
|
|
568
|
-
}
|
|
569
|
-
result(method, value) {
|
|
570
|
-
if (method === "session/config/set")
|
|
571
|
-
return this.state(value);
|
|
572
|
-
if (method === "goal/set" || method === "goal/clear")
|
|
573
|
-
return this.goal(value);
|
|
574
|
-
return value;
|
|
575
|
-
}
|
|
576
|
-
input(block) {
|
|
577
|
-
if (block.type === "text")
|
|
578
|
-
return { ...block, text: this.text(block.text) };
|
|
579
|
-
if (block.type === "file")
|
|
580
|
-
return { ...block, uri: this.text(block.uri), ...block.name ? { name: this.text(block.name) } : {} };
|
|
581
|
-
return { ...block, ...block.url ? { url: this.text(block.url) } : {}, ...block.data ? { data: this.text(block.data) } : {} };
|
|
582
|
-
}
|
|
583
|
-
item(item) {
|
|
584
|
-
if (item.extensions)
|
|
585
|
-
item = { ...item, extensions: this.json(item.extensions) };
|
|
586
|
-
switch (item.type) {
|
|
587
|
-
case "agentMessage":
|
|
588
|
-
return { ...item, text: this.text(item.text) };
|
|
589
|
-
case "userMessage":
|
|
590
|
-
return { ...item, content: item.content.map((block) => this.input(block)) };
|
|
591
|
-
case "reasoning":
|
|
592
|
-
return { ...item, ...item.text ? { text: this.text(item.text) } : {}, ...item.summary ? { summary: this.text(item.summary) } : {} };
|
|
593
|
-
case "toolCall":
|
|
594
|
-
return boundFileChangeItem({ ...item, ...item.image ? { image: { ...item.image, ...item.image.path ? { path: this.text(item.image.path) } : {}, ...item.image.revisedPrompt ? { revisedPrompt: this.text(item.image.revisedPrompt) } : {} } } : {}, ...item.fileChanges !== undefined ? { fileChanges: item.fileChanges.map((change) => ({ ...change, path: this.text(change.path), ...change.oldPath !== undefined ? { oldPath: this.text(change.oldPath) } : {}, materials: change.materials.map((material) => material.format === "unified" ? { ...material, patch: this.text(material.patch) } : material.format === "text" ? { ...material, oldText: material.oldText === null ? null : this.text(material.oldText), newText: this.text(material.newText) } : material) })) } : {}, title: this.text(item.title), ...item.input !== undefined ? { input: this.json(item.input) } : {}, ...item.output !== undefined ? { output: this.json(item.output) } : {}, ...item.raw !== undefined ? { raw: this.json(item.raw) } : {}, ...item.locations ? { locations: item.locations.map((location) => ({ ...location, path: this.text(location.path) })) } : {} });
|
|
595
|
-
case "plan":
|
|
596
|
-
return { ...item, steps: item.steps.map((step) => ({ ...step, text: this.text(step.text) })) };
|
|
597
|
-
case "subagent":
|
|
598
|
-
return { ...item, ...item.name ? { name: this.text(item.name) } : {}, ...item.task ? { task: this.text(item.task) } : {} };
|
|
599
|
-
case "review":
|
|
600
|
-
return { ...item, ...item.text ? { text: this.text(item.text) } : {} };
|
|
601
|
-
case "compaction":
|
|
602
|
-
return { ...item, ...item.summary ? { summary: this.text(item.summary) } : {} };
|
|
603
|
-
case "hook":
|
|
604
|
-
return { ...item, name: this.text(item.name), ...item.input !== undefined ? { input: this.json(item.input) } : {}, ...item.output !== undefined ? { output: this.json(item.output) } : {} };
|
|
605
|
-
case "notice":
|
|
606
|
-
return { ...item, text: this.text(item.text) };
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
interaction(request) {
|
|
610
|
-
const common = { ...request, ...request.title ? { title: this.text(request.title) } : {}, ...request.privatePayload ? { privatePayload: this.json(request.privatePayload) } : {} };
|
|
611
|
-
switch (common.kind) {
|
|
612
|
-
case "approval":
|
|
613
|
-
return { ...common, ...common.input !== undefined ? { input: this.json(common.input) } : {}, options: common.options.map((option) => ({ ...option, label: this.text(option.label), ...option.description ? { description: this.text(option.description) } : {} })) };
|
|
614
|
-
case "userInput":
|
|
615
|
-
return { ...common, questions: common.questions.map((question) => ({ ...question, prompt: this.text(question.prompt), ...question.options ? { options: question.options.map((option) => ({ ...option, label: this.text(option.label) })) } : {} })) };
|
|
616
|
-
case "elicitation":
|
|
617
|
-
return { ...common, ...common.message ? { message: this.text(common.message) } : {} };
|
|
618
|
-
case "dynamicTool":
|
|
619
|
-
return { ...common, arguments: this.json(common.arguments) };
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
deltaValue(kind, value) {
|
|
623
|
-
if (kind === "patch" && value && typeof value === "object" && !Array.isArray(value) && (("type" in value) || ("fileChanges" in value))) {
|
|
624
|
-
assertDefinition("Item", value);
|
|
625
|
-
return this.item(value);
|
|
626
|
-
}
|
|
627
|
-
return this.json(value);
|
|
628
|
-
}
|
|
629
|
-
event(event) {
|
|
630
|
-
switch (event.type) {
|
|
631
|
-
case "item/started":
|
|
632
|
-
case "item/completed": {
|
|
633
|
-
if (event.type === "item/completed")
|
|
634
|
-
this.streams.delete(event.payload.item.id);
|
|
635
|
-
return { ...event, payload: { item: this.item(event.payload.item) } };
|
|
636
|
-
}
|
|
637
|
-
case "item/delta":
|
|
638
|
-
return { ...event, payload: { ...event.payload, ...event.payload.text !== undefined ? { text: this.delta(event.payload.itemId, event.payload.text) } : {}, ...event.payload.value !== undefined ? { value: this.deltaValue(event.payload.kind, event.payload.value) } : {} } };
|
|
639
|
-
case "notice":
|
|
640
|
-
return { ...event, payload: { ...event.payload, message: this.text(event.payload.message), ...event.payload.raw !== undefined ? { raw: this.json(event.payload.raw) } : {} } };
|
|
641
|
-
case "extension":
|
|
642
|
-
return { ...event, payload: { ...event.payload, data: this.json(event.payload.data) } };
|
|
643
|
-
case "session/metadata":
|
|
644
|
-
return { ...event, payload: this.json(event.payload) };
|
|
645
|
-
case "session/state":
|
|
646
|
-
return { ...event, payload: this.state(event.payload) };
|
|
647
|
-
case "interaction/requested":
|
|
648
|
-
return { ...event, payload: this.interaction(event.payload) };
|
|
649
|
-
case "turn/completed":
|
|
650
|
-
return { ...event, payload: { ...event.payload, ...event.payload.error ? { error: { ...event.payload.error, message: this.text(event.payload.error.message), ...event.payload.error.details ? { details: this.json(event.payload.error.details) } : {} } } : {} } };
|
|
651
|
-
default:
|
|
652
|
-
return event;
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
history(page) {
|
|
656
|
-
return { ...page, items: page.items.map((entry) => ({ ...entry, item: this.item(entry.item) })) };
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// packages/core/src/runtime.ts
|
|
661
|
-
function immutable(value) {
|
|
662
|
-
const copy = structuredClone(value);
|
|
663
|
-
const freeze = (entry) => {
|
|
664
|
-
if (entry && typeof entry === "object") {
|
|
665
|
-
for (const child of Object.values(entry))
|
|
666
|
-
freeze(child);
|
|
667
|
-
Object.freeze(entry);
|
|
668
|
-
}
|
|
669
|
-
};
|
|
670
|
-
freeze(copy);
|
|
671
|
-
return copy;
|
|
672
|
-
}
|
|
673
|
-
function wireError(cause) {
|
|
674
|
-
const error = sdkError(cause, "PROTOCOL_ERROR", true);
|
|
675
|
-
return { code: error.code, message: error.code, retryable: error.retryable, outcomeUnknown: error.outcomeUnknown };
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
class ManagedHarnessSession {
|
|
679
|
-
options;
|
|
680
|
-
clientCapabilities;
|
|
681
|
-
_binding;
|
|
682
|
-
_capabilities;
|
|
683
|
-
queue;
|
|
684
|
-
interactions;
|
|
685
|
-
streamId;
|
|
686
|
-
seq = 0;
|
|
687
|
-
activeTurnId;
|
|
688
|
-
started = new Set;
|
|
689
|
-
ended = new Set;
|
|
690
|
-
deliveredStarted = new Set;
|
|
691
|
-
deliveredEnded = new Set;
|
|
692
|
-
items = new Map;
|
|
693
|
-
backend;
|
|
694
|
-
closed = false;
|
|
695
|
-
recovering = false;
|
|
696
|
-
controlBusy = false;
|
|
697
|
-
maintenancePending = false;
|
|
698
|
-
emitter = Promise.resolve();
|
|
699
|
-
pendingEmissions = 0;
|
|
700
|
-
disposePromise;
|
|
701
|
-
failurePromise;
|
|
702
|
-
failureCause;
|
|
703
|
-
emergencyPublication = false;
|
|
704
|
-
backendDisposal;
|
|
705
|
-
requests = new Map;
|
|
706
|
-
state;
|
|
707
|
-
createId;
|
|
708
|
-
secrets;
|
|
709
|
-
constructor(binding, capabilities, options = {}, initial, clientCapabilities) {
|
|
710
|
-
this.options = options;
|
|
711
|
-
this.clientCapabilities = clientCapabilities;
|
|
712
|
-
this.createId = options.createId ?? randomUUID;
|
|
713
|
-
this._binding = immutable(binding);
|
|
714
|
-
this._capabilities = immutable(capabilities);
|
|
715
|
-
this.secrets = new SecretRedactor((value) => this.options.onMarkedSecret?.(this.binding, value));
|
|
716
|
-
this.streamId = this.createId();
|
|
717
|
-
this.queue = new BoundedEventQueue({ ...options.queue, maxControlEvents: (options.maxPendingInteractions ?? 32) + 1 });
|
|
718
|
-
this.state = { binding: structuredClone(binding), capabilities: structuredClone(capabilities), config: structuredClone(initial?.config ?? {}), ...initial?.model ? { model: initial.model } : {}, status: "opening", capabilityRevision: 1 };
|
|
719
|
-
this.interactions = new InteractionRegistry(async (payload) => {
|
|
720
|
-
const event = { type: "interaction/resolved", payload };
|
|
721
|
-
if (this.emergencyPublication) {
|
|
722
|
-
this.queue.pushControl(this.makeEvent(event, {}));
|
|
723
|
-
this.seq++;
|
|
724
|
-
} else
|
|
725
|
-
await this.emit(event);
|
|
726
|
-
}, { maxPending: options.maxPendingInteractions ?? 32, timeoutMs: options.interactionTimeoutMs ?? 600000, responseTimeoutMs: options.requestTimeoutMs }, (request, response) => this.secrets.rememberResponse(request, response));
|
|
727
|
-
}
|
|
728
|
-
get binding() {
|
|
729
|
-
return this._binding;
|
|
730
|
-
}
|
|
731
|
-
get capabilities() {
|
|
732
|
-
return this._capabilities;
|
|
733
|
-
}
|
|
734
|
-
get isClosed() {
|
|
735
|
-
return this.closed;
|
|
736
|
-
}
|
|
737
|
-
get snapshot() {
|
|
738
|
-
return this.secrets.state(structuredClone(this.state));
|
|
739
|
-
}
|
|
740
|
-
events() {
|
|
741
|
-
return this.queue;
|
|
742
|
-
}
|
|
743
|
-
reserveFork(binding) {
|
|
744
|
-
this.requireBackend();
|
|
745
|
-
if (binding.adapterId !== this.binding.adapterId || binding.runtimeConfigId !== this.binding.runtimeConfigId)
|
|
746
|
-
throw new HarnessSdkError("SESSION_BINDING_MISMATCH");
|
|
747
|
-
if (this.activeTurnId || this.controlBusy || this.maintenancePending)
|
|
748
|
-
throw new HarnessSdkError("SESSION_BUSY");
|
|
749
|
-
this.controlBusy = true;
|
|
750
|
-
return () => {
|
|
751
|
-
this.controlBusy = false;
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
async attach(backend) {
|
|
755
|
-
if (this.backend)
|
|
756
|
-
throw new HarnessSdkError("PROTOCOL_ERROR", "Backend already attached");
|
|
757
|
-
for (const key of ["sessionId", "adapterId", "runtimeConfigId"])
|
|
758
|
-
if (backend.binding[key] !== this.binding[key])
|
|
759
|
-
throw new HarnessSdkError("SESSION_BINDING_MISMATCH");
|
|
760
|
-
if (this.closed) {
|
|
761
|
-
await backend.dispose();
|
|
762
|
-
throw this.failureCause ?? new HarnessSdkError("STREAM_CLOSED", "Session closed before backend attachment", false, true);
|
|
763
|
-
}
|
|
764
|
-
this.backend = backend;
|
|
765
|
-
this._binding = immutable(backend.binding);
|
|
766
|
-
this._capabilities = immutable(backend.capabilities);
|
|
767
|
-
this.state = { ...this.state, binding: structuredClone(backend.binding), capabilities: structuredClone(backend.capabilities), status: this.activeTurnId ? "running" : "ready" };
|
|
768
|
-
await this.emit({ type: "session/state", payload: this.snapshot });
|
|
769
|
-
}
|
|
770
|
-
makeEvent(payload, scope) {
|
|
771
|
-
payload = this.secrets.event(payload);
|
|
772
|
-
const turnId = scope.turnId ?? (payload.type === "turn/started" || payload.type === "turn/completed" ? payload.payload.turnId : undefined);
|
|
773
|
-
const itemId = scope.itemId ?? (payload.type === "item/delta" ? payload.payload.itemId : payload.type === "item/started" || payload.type === "item/completed" ? payload.payload.item.id : undefined);
|
|
774
|
-
const event = { sessionId: this.binding.sessionId, streamId: this.streamId, seq: this.seq + 1, timestamp: (this.options.now ?? (() => new Date().toISOString()))(), ...scope, ...turnId ? { turnId } : {}, ...itemId ? { itemId } : {}, ...payload };
|
|
775
|
-
assertEvent(event);
|
|
776
|
-
return event;
|
|
777
|
-
}
|
|
778
|
-
prepare(payload, scope) {
|
|
779
|
-
if (payload.type === "turn/started") {
|
|
780
|
-
const id = payload.payload.turnId;
|
|
781
|
-
if (this.ended.has(id) || this.started.has(id))
|
|
782
|
-
return false;
|
|
783
|
-
if (this.activeTurnId && this.activeTurnId !== id)
|
|
784
|
-
throw new HarnessSdkError("PROTOCOL_ERROR", "Concurrent native turns");
|
|
785
|
-
this.activeTurnId = id;
|
|
786
|
-
this.started.add(id);
|
|
787
|
-
this.state.status = "running";
|
|
788
|
-
this.state.activeTurnId = id;
|
|
789
|
-
} else if (payload.type === "turn/completed") {
|
|
790
|
-
const id = payload.payload.turnId;
|
|
791
|
-
if (this.ended.has(id))
|
|
792
|
-
return false;
|
|
793
|
-
if (!this.started.has(id))
|
|
794
|
-
throw new HarnessSdkError("PROTOCOL_ERROR", "Turn completed without started");
|
|
795
|
-
this.ended.add(id);
|
|
796
|
-
if (this.activeTurnId === id) {
|
|
797
|
-
this.activeTurnId = undefined;
|
|
798
|
-
delete this.state.activeTurnId;
|
|
799
|
-
this.state.status = this.recovering ? "recoveryRequired" : "ready";
|
|
800
|
-
}
|
|
801
|
-
} else if (payload.type === "item/started" || payload.type === "item/completed" || payload.type === "item/delta") {
|
|
802
|
-
const turnId = scope.turnId;
|
|
803
|
-
if (!turnId || !this.started.has(turnId) || this.ended.has(turnId))
|
|
804
|
-
throw new HarnessSdkError("PROTOCOL_ERROR", "Item outside an active turn");
|
|
805
|
-
const itemId = payload.type === "item/delta" ? payload.payload.itemId : payload.payload.item.id;
|
|
806
|
-
const key = JSON.stringify([turnId, itemId]);
|
|
807
|
-
const previous = this.items.get(key);
|
|
808
|
-
if (payload.type === "item/started") {
|
|
809
|
-
if (previous)
|
|
810
|
-
throw new HarnessSdkError("PROTOCOL_ERROR", "Duplicate item started");
|
|
811
|
-
this.items.set(key, { item: structuredClone(payload.payload.item), completed: false });
|
|
812
|
-
} else if (payload.type === "item/completed") {
|
|
813
|
-
if (previous?.completed)
|
|
814
|
-
return false;
|
|
815
|
-
this.items.set(key, { item: structuredClone(payload.payload.item), completed: true });
|
|
816
|
-
} else if (!previous || previous.completed)
|
|
817
|
-
throw new HarnessSdkError("PROTOCOL_ERROR", "Delta without an open item");
|
|
818
|
-
} else if (payload.type === "session/state") {
|
|
819
|
-
if (payload.payload.binding.sessionId !== this.binding.sessionId || payload.payload.binding.adapterId !== this.binding.adapterId || payload.payload.binding.runtimeConfigId !== this.binding.runtimeConfigId)
|
|
820
|
-
throw new HarnessSdkError("SESSION_BINDING_MISMATCH");
|
|
821
|
-
this.state = { ...structuredClone(payload.payload), metadata: { ...this.state.metadata, ...structuredClone(payload.payload.metadata) } };
|
|
822
|
-
this._capabilities = immutable(payload.payload.capabilities);
|
|
823
|
-
this._binding = immutable(payload.payload.binding);
|
|
824
|
-
if (["ready", "closed"].includes(payload.payload.status) && !this.activeTurnId)
|
|
825
|
-
this.maintenancePending = false;
|
|
826
|
-
} else if (payload.type === "session/metadata") {
|
|
827
|
-
this.state.metadata = { ...this.state.metadata, ...structuredClone(payload.payload) };
|
|
828
|
-
} else if (payload.type === "extension" && !supportsExtension(this.capabilities, payload.payload.name, payload.payload.version))
|
|
829
|
-
throw new HarnessSdkError("CAPABILITY_UNSUPPORTED");
|
|
830
|
-
return true;
|
|
831
|
-
}
|
|
832
|
-
accepted(payload) {
|
|
833
|
-
this.seq++;
|
|
834
|
-
if (payload.type === "turn/started")
|
|
835
|
-
this.deliveredStarted.add(payload.payload.turnId);
|
|
836
|
-
if (payload.type === "turn/completed")
|
|
837
|
-
this.deliveredEnded.add(payload.payload.turnId);
|
|
838
|
-
}
|
|
839
|
-
emit(payload, scope = {}) {
|
|
840
|
-
if (this.closed)
|
|
841
|
-
return Promise.reject(new HarnessSdkError("STREAM_CLOSED"));
|
|
842
|
-
if (++this.pendingEmissions > (this.options.queue?.maxWaitingWriters ?? 32)) {
|
|
843
|
-
this.pendingEmissions--;
|
|
844
|
-
const error = new HarnessSdkError("BACKPRESSURE_EXCEEDED");
|
|
845
|
-
this.fail(error);
|
|
846
|
-
return Promise.reject(error);
|
|
847
|
-
}
|
|
848
|
-
const task = this.emitter.then(async () => {
|
|
849
|
-
if (!this.prepare(payload, scope))
|
|
850
|
-
return;
|
|
851
|
-
const event = this.makeEvent(payload, scope);
|
|
852
|
-
await this.queue.write(event, () => this.accepted(payload));
|
|
853
|
-
if (payload.type === "turn/completed")
|
|
854
|
-
this.interactions.cancelAll("turn_ended", payload.payload.turnId);
|
|
855
|
-
});
|
|
856
|
-
this.emitter = task.catch(() => {});
|
|
857
|
-
return task.catch(async (cause) => {
|
|
858
|
-
await this.fail(cause);
|
|
859
|
-
throw cause;
|
|
860
|
-
}).finally(() => {
|
|
861
|
-
this.pendingEmissions--;
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
tryEmit(payload, scope = {}) {
|
|
865
|
-
try {
|
|
866
|
-
if (this.closed)
|
|
867
|
-
return false;
|
|
868
|
-
if (this.pendingEmissions > 0)
|
|
869
|
-
throw new HarnessSdkError("BACKPRESSURE_EXCEEDED");
|
|
870
|
-
const event = this.makeEvent(payload, scope);
|
|
871
|
-
this.queue.assertWritable(event);
|
|
872
|
-
if (!this.prepare(payload, scope))
|
|
873
|
-
return true;
|
|
874
|
-
this.queue.push(event);
|
|
875
|
-
this.accepted(payload);
|
|
876
|
-
if (payload.type === "turn/completed")
|
|
877
|
-
this.interactions.cancelAll("turn_ended", payload.payload.turnId);
|
|
878
|
-
return true;
|
|
879
|
-
} catch (cause) {
|
|
880
|
-
this.fail(cause);
|
|
881
|
-
return false;
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
async requestInteraction(request, responder) {
|
|
885
|
-
if (!this.capabilities.interactions?.includes(request.kind) || !this.clientCapabilities?.interactions?.includes(request.kind)) {
|
|
886
|
-
await responder(null, "unsupported").catch(() => {});
|
|
887
|
-
throw new HarnessSdkError("INTERACTION_UNSUPPORTED");
|
|
888
|
-
}
|
|
889
|
-
let result;
|
|
890
|
-
try {
|
|
891
|
-
result = this.interactions.begin(request, responder);
|
|
892
|
-
} catch (cause) {
|
|
893
|
-
await responder(null, "interaction_rejected").catch(() => {});
|
|
894
|
-
throw cause;
|
|
895
|
-
}
|
|
896
|
-
await this.emit({ type: "interaction/requested", payload: request }, { turnId: request.turnId, itemId: request.itemId });
|
|
897
|
-
return result;
|
|
898
|
-
}
|
|
899
|
-
async resolveInteraction(result) {
|
|
900
|
-
assertResult("interaction/respond", result);
|
|
901
|
-
await this.interactions.resolveExternally(result);
|
|
902
|
-
}
|
|
903
|
-
fail(cause) {
|
|
904
|
-
return this.failurePromise ??= this.performFailure(cause);
|
|
905
|
-
}
|
|
906
|
-
async performFailure(cause) {
|
|
907
|
-
if (this.closed)
|
|
908
|
-
return;
|
|
909
|
-
this.failureCause = sdkError(cause, "STREAM_FAILED", true);
|
|
910
|
-
this.closed = true;
|
|
911
|
-
this.recovering = true;
|
|
912
|
-
this.state.status = "recoveryRequired";
|
|
913
|
-
const error = wireError(cause);
|
|
914
|
-
const turnId = this.activeTurnId ?? [...this.deliveredStarted].find((id) => !this.deliveredEnded.has(id));
|
|
915
|
-
this.emergencyPublication = true;
|
|
916
|
-
await this.interactions.cancelAll("connection_lost");
|
|
917
|
-
if (turnId && this.deliveredStarted.has(turnId) && !this.deliveredEnded.has(turnId)) {
|
|
918
|
-
this.ended.add(turnId);
|
|
919
|
-
this.activeTurnId = undefined;
|
|
920
|
-
const event = this.makeEvent({ type: "turn/completed", payload: { turnId, status: "failed", error } }, { turnId });
|
|
921
|
-
this.accepted(event);
|
|
922
|
-
this.queue.endWithControl(event);
|
|
923
|
-
} else
|
|
924
|
-
this.queue.end(sdkError(cause, "STREAM_FAILED", true));
|
|
925
|
-
await this.disposeBackend().catch(() => {});
|
|
926
|
-
}
|
|
927
|
-
requireBackend(method) {
|
|
928
|
-
if (this.recovering && method !== "history/read" && method !== "turn/interrupt")
|
|
929
|
-
throw new HarnessSdkError("RECOVERY_REQUIRED");
|
|
930
|
-
if (this.closed)
|
|
931
|
-
throw new HarnessSdkError("SESSION_CLOSED");
|
|
932
|
-
if (!this.backend)
|
|
933
|
-
throw new HarnessSdkError("SESSION_NOT_READY");
|
|
934
|
-
return this.backend;
|
|
935
|
-
}
|
|
936
|
-
guard(method, params) {
|
|
937
|
-
assertRequest(method, params);
|
|
938
|
-
if (params.sessionId !== this.binding.sessionId)
|
|
939
|
-
throw new HarnessSdkError("SESSION_BINDING_MISMATCH");
|
|
940
|
-
const unsupported = () => {
|
|
941
|
-
throw new HarnessSdkError("CAPABILITY_UNSUPPORTED");
|
|
942
|
-
};
|
|
943
|
-
if (method === "turn/steer" && !supportsCapability(this.capabilities, "turn.steer"))
|
|
944
|
-
unsupported();
|
|
945
|
-
if (method === "turn/interrupt" && !supportsCapability(this.capabilities, "turn.interrupt"))
|
|
946
|
-
unsupported();
|
|
947
|
-
if (method === "session/revert" && !supportsCapability(this.capabilities, "session.revert", params.scope))
|
|
948
|
-
unsupported();
|
|
949
|
-
if (method === "history/read" && !supportsCapability(this.capabilities, "session.history"))
|
|
950
|
-
unsupported();
|
|
951
|
-
if ((method === "goal/set" || method === "goal/clear") && !supportsCapability(this.capabilities, "goal.actions", method.split("/")[1]))
|
|
952
|
-
unsupported();
|
|
953
|
-
if (method === "compact/start" && !this.capabilities.compact)
|
|
954
|
-
unsupported();
|
|
955
|
-
if (method === "review/start" && !this.capabilities.review)
|
|
956
|
-
unsupported();
|
|
957
|
-
if (method === "turn/start") {
|
|
958
|
-
const start = params;
|
|
959
|
-
if (start.outputSchema !== undefined && !this.capabilities.turn?.outputSchema)
|
|
960
|
-
unsupported();
|
|
961
|
-
const modalities = this.capabilities.turn?.inputModalities ?? ["text"];
|
|
962
|
-
if (start.input.some((block) => !modalities.includes(block.type)))
|
|
963
|
-
unsupported();
|
|
964
|
-
if (start.overrides && Object.keys(start.overrides).some((key) => !this.capabilities.turn?.overrides?.includes(key)))
|
|
965
|
-
unsupported();
|
|
966
|
-
}
|
|
967
|
-
if (method === "session/config/set") {
|
|
968
|
-
const patch = params.patch;
|
|
969
|
-
for (const [key, value] of Object.entries(patch)) {
|
|
970
|
-
const option = this.capabilities.config?.options.find((option) => option.id === key && option.scope === "session");
|
|
971
|
-
if (!option)
|
|
972
|
-
unsupported();
|
|
973
|
-
if (option && !validateJsonSchema(option.schema, value))
|
|
974
|
-
throw new HarnessSdkError("INVALID_PARAMS", "Config value does not match schema");
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
call(method, params, options = {}) {
|
|
979
|
-
if (options.signal?.aborted)
|
|
980
|
-
return Promise.reject(new HarnessSdkError("REQUEST_ABORTED"));
|
|
981
|
-
try {
|
|
982
|
-
this.guard(method, params);
|
|
983
|
-
} catch (cause) {
|
|
984
|
-
return Promise.reject(cause);
|
|
985
|
-
}
|
|
986
|
-
const key = JSON.stringify([method, params.requestId]);
|
|
987
|
-
const fingerprint = JSON.stringify(params);
|
|
988
|
-
const previous = this.requests.get(key);
|
|
989
|
-
if (previous)
|
|
990
|
-
return previous.fingerprint === fingerprint ? previous.result : Promise.reject(new HarnessSdkError("REQUEST_ID_CONFLICT"));
|
|
991
|
-
const cacheLimit = this.options.requestCacheSize ?? 1024;
|
|
992
|
-
if (this.requests.size >= cacheLimit) {
|
|
993
|
-
for (const [id, entry] of this.requests)
|
|
994
|
-
if (entry.settled) {
|
|
995
|
-
this.requests.delete(id);
|
|
996
|
-
break;
|
|
997
|
-
}
|
|
998
|
-
if (this.requests.size >= cacheLimit)
|
|
999
|
-
return Promise.reject(new HarnessSdkError("BACKPRESSURE_EXCEEDED", "Request limit exceeded"));
|
|
1000
|
-
}
|
|
1001
|
-
const result = waitForRequest(async () => {
|
|
1002
|
-
const backend = this.requireBackend(method);
|
|
1003
|
-
if (method === "interaction/respond") {
|
|
1004
|
-
const answer = params;
|
|
1005
|
-
return await this.interactions.respond(answer.interactionId, answer.requestId, answer.response);
|
|
1006
|
-
}
|
|
1007
|
-
if (method === "interaction/cancel") {
|
|
1008
|
-
const cancel = params;
|
|
1009
|
-
return await this.interactions.cancel(cancel.interactionId, cancel.reason);
|
|
1010
|
-
}
|
|
1011
|
-
const execution = method === "turn/start" || method === "review/start";
|
|
1012
|
-
const exclusive = execution || method === "session/config/set" || method === "session/revert" || method === "session/close" || method === "compact/start" || method === "goal/set" || method === "goal/clear";
|
|
1013
|
-
if (exclusive && (this.activeTurnId || this.controlBusy || this.maintenancePending))
|
|
1014
|
-
throw new HarnessSdkError("SESSION_BUSY");
|
|
1015
|
-
if (method === "turn/steer" && this.activeTurnId !== params.expectedTurnId)
|
|
1016
|
-
throw new HarnessSdkError("TURN_MISMATCH");
|
|
1017
|
-
if (method === "turn/interrupt" && this.activeTurnId !== params.turnId)
|
|
1018
|
-
throw new HarnessSdkError("TURN_MISMATCH");
|
|
1019
|
-
if (exclusive)
|
|
1020
|
-
this.controlBusy = true;
|
|
1021
|
-
if (method === "compact/start") {
|
|
1022
|
-
this.maintenancePending = true;
|
|
1023
|
-
this.state.status = "running";
|
|
1024
|
-
await this.emit({ type: "session/state", payload: this.snapshot });
|
|
1025
|
-
}
|
|
1026
|
-
const turnId = execution ? params.turnId : undefined;
|
|
1027
|
-
if (turnId) {
|
|
1028
|
-
if (this.started.has(turnId) || this.ended.has(turnId)) {
|
|
1029
|
-
this.controlBusy = false;
|
|
1030
|
-
throw new HarnessSdkError("TURN_ID_CONFLICT");
|
|
1031
|
-
}
|
|
1032
|
-
this.activeTurnId = turnId;
|
|
1033
|
-
}
|
|
1034
|
-
try {
|
|
1035
|
-
let value = await backend.execute(method, params);
|
|
1036
|
-
assertResult(method, value);
|
|
1037
|
-
value = this.secrets.result(method, value);
|
|
1038
|
-
assertResult(method, value);
|
|
1039
|
-
if (method === "compact/start" && value.status === "accepted" && !this.maintenancePending)
|
|
1040
|
-
value = { ...value, status: "completed" };
|
|
1041
|
-
if (method === "compact/start" && value.status === "completed") {
|
|
1042
|
-
this.maintenancePending = false;
|
|
1043
|
-
this.state.status = "ready";
|
|
1044
|
-
await this.emit({ type: "session/state", payload: this.snapshot });
|
|
1045
|
-
}
|
|
1046
|
-
if (method === "session/config/set")
|
|
1047
|
-
this.state = structuredClone(value);
|
|
1048
|
-
if (method === "session/revert") {
|
|
1049
|
-
const nativeRef = value.nativeRef;
|
|
1050
|
-
if (nativeRef) {
|
|
1051
|
-
this._binding = immutable({ ...this.binding, nativeRef });
|
|
1052
|
-
this.state.binding = structuredClone(this.binding);
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
if (method === "history/read") {
|
|
1056
|
-
let page = this.secrets.history(value);
|
|
1057
|
-
if (this.options.redactHistory)
|
|
1058
|
-
page = await this.options.redactHistory(this.binding, page);
|
|
1059
|
-
assertResult("history/read", page);
|
|
1060
|
-
return page;
|
|
1061
|
-
}
|
|
1062
|
-
return value;
|
|
1063
|
-
} catch (cause) {
|
|
1064
|
-
if (method === "compact/start" && !sdkError(cause).outcomeUnknown) {
|
|
1065
|
-
this.maintenancePending = false;
|
|
1066
|
-
this.state.status = "ready";
|
|
1067
|
-
await this.emit({ type: "session/state", payload: this.snapshot }).catch(() => {});
|
|
1068
|
-
}
|
|
1069
|
-
if (turnId && this.started.has(turnId) && !this.ended.has(turnId))
|
|
1070
|
-
await this.fail(cause);
|
|
1071
|
-
else if (turnId && this.activeTurnId === turnId)
|
|
1072
|
-
this.activeTurnId = undefined;
|
|
1073
|
-
throw cause;
|
|
1074
|
-
} finally {
|
|
1075
|
-
if (exclusive)
|
|
1076
|
-
this.controlBusy = false;
|
|
1077
|
-
}
|
|
1078
|
-
}, options, this.options.requestTimeoutMs).catch((cause) => {
|
|
1079
|
-
if (sdkError(cause).outcomeUnknown) {
|
|
1080
|
-
this.recovering = true;
|
|
1081
|
-
this.state.status = "recoveryRequired";
|
|
1082
|
-
}
|
|
1083
|
-
throw cause;
|
|
1084
|
-
});
|
|
1085
|
-
this.requests.set(key, { fingerprint, result, settled: false });
|
|
1086
|
-
result.finally(() => {
|
|
1087
|
-
const entry = this.requests.get(key);
|
|
1088
|
-
if (entry)
|
|
1089
|
-
entry.settled = true;
|
|
1090
|
-
}).catch(() => {});
|
|
1091
|
-
return result;
|
|
1092
|
-
}
|
|
1093
|
-
startTurn(params, options) {
|
|
1094
|
-
return this.call("turn/start", params, options);
|
|
1095
|
-
}
|
|
1096
|
-
steer(params, options) {
|
|
1097
|
-
return this.call("turn/steer", params, options);
|
|
1098
|
-
}
|
|
1099
|
-
interrupt(params, options) {
|
|
1100
|
-
return this.call("turn/interrupt", params, options);
|
|
1101
|
-
}
|
|
1102
|
-
respond(params, options) {
|
|
1103
|
-
return this.call("interaction/respond", params, options);
|
|
1104
|
-
}
|
|
1105
|
-
cancelInteraction(params, options) {
|
|
1106
|
-
return this.call("interaction/cancel", params, options);
|
|
1107
|
-
}
|
|
1108
|
-
setConfig(params, options) {
|
|
1109
|
-
return this.call("session/config/set", params, options);
|
|
1110
|
-
}
|
|
1111
|
-
revert(params, options) {
|
|
1112
|
-
return this.call("session/revert", params, options);
|
|
1113
|
-
}
|
|
1114
|
-
readHistory(params, options) {
|
|
1115
|
-
return this.call("history/read", params, options);
|
|
1116
|
-
}
|
|
1117
|
-
setGoal(params, options) {
|
|
1118
|
-
return this.call("goal/set", params, options);
|
|
1119
|
-
}
|
|
1120
|
-
clearGoal(params, options) {
|
|
1121
|
-
return this.call("goal/clear", params, options);
|
|
1122
|
-
}
|
|
1123
|
-
compact(params, options) {
|
|
1124
|
-
return this.call("compact/start", params, options);
|
|
1125
|
-
}
|
|
1126
|
-
review(params, options) {
|
|
1127
|
-
return this.call("review/start", params, options);
|
|
1128
|
-
}
|
|
1129
|
-
async close(options) {
|
|
1130
|
-
if (this.closed && !this.recovering)
|
|
1131
|
-
return;
|
|
1132
|
-
await this.call("session/close", { requestId: this.createId(), sessionId: this.binding.sessionId }, options);
|
|
1133
|
-
this.closed = true;
|
|
1134
|
-
this.state.status = "closed";
|
|
1135
|
-
await this.disposeBackend();
|
|
1136
|
-
this.queue.end();
|
|
1137
|
-
}
|
|
1138
|
-
disposeBackend() {
|
|
1139
|
-
return this.backendDisposal ??= this.backend?.dispose() ?? Promise.resolve();
|
|
1140
|
-
}
|
|
1141
|
-
dispose() {
|
|
1142
|
-
return this.disposePromise ??= (async () => {
|
|
1143
|
-
this.emergencyPublication = true;
|
|
1144
|
-
if (!this.closed)
|
|
1145
|
-
await this.interactions.cancelAll("runtime_disposed");
|
|
1146
|
-
if (this.activeTurnId && this.started.has(this.activeTurnId) && !this.ended.has(this.activeTurnId))
|
|
1147
|
-
await this.fail(new HarnessSdkError("RUNTIME_DISPOSED", "Runtime disposed", false, true));
|
|
1148
|
-
this.closed = true;
|
|
1149
|
-
this.queue.end();
|
|
1150
|
-
await this.disposeBackend();
|
|
1151
|
-
})();
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
class ManagedHarnessRuntime {
|
|
1156
|
-
adapter;
|
|
1157
|
-
options;
|
|
1158
|
-
initialized;
|
|
1159
|
-
sessions = new Map;
|
|
1160
|
-
closed = false;
|
|
1161
|
-
disposing;
|
|
1162
|
-
initialization;
|
|
1163
|
-
clientCapabilities;
|
|
1164
|
-
constructor(adapter, options = {}) {
|
|
1165
|
-
this.adapter = adapter;
|
|
1166
|
-
this.options = options;
|
|
1167
|
-
}
|
|
1168
|
-
initialize(params, options) {
|
|
1169
|
-
if (this.closed)
|
|
1170
|
-
return Promise.reject(new HarnessSdkError("RUNTIME_DISPOSED"));
|
|
1171
|
-
assertRequest("initialize", params);
|
|
1172
|
-
negotiateVersion(params.protocolVersions);
|
|
1173
|
-
return this.initialization ??= waitForRequest(async () => {
|
|
1174
|
-
const result = await this.adapter.initialize(params);
|
|
1175
|
-
assertResult("initialize", result);
|
|
1176
|
-
if (!params.protocolVersions.includes(result.protocolVersion))
|
|
1177
|
-
throw new HarnessSdkError("PROTOCOL_VERSION_UNSUPPORTED");
|
|
1178
|
-
this.initialized = immutable(result);
|
|
1179
|
-
this.clientCapabilities = immutable(params.clientCapabilities ?? {});
|
|
1180
|
-
return this.initialized;
|
|
1181
|
-
}, options, this.options.requestTimeoutMs).catch((cause) => {
|
|
1182
|
-
this.initialization = undefined;
|
|
1183
|
-
throw cause;
|
|
1184
|
-
});
|
|
1185
|
-
}
|
|
1186
|
-
async openSession(params, options) {
|
|
1187
|
-
if (options?.signal?.aborted)
|
|
1188
|
-
throw new HarnessSdkError("REQUEST_ABORTED");
|
|
1189
|
-
assertRequest("session/open", params);
|
|
1190
|
-
if (this.closed)
|
|
1191
|
-
throw new HarnessSdkError("RUNTIME_DISPOSED");
|
|
1192
|
-
if (!this.initialized)
|
|
1193
|
-
throw new HarnessSdkError("NOT_INITIALIZED");
|
|
1194
|
-
const previous = this.sessions.get(params.binding.sessionId);
|
|
1195
|
-
if (previous && !previous.isClosed)
|
|
1196
|
-
throw new HarnessSdkError("SESSION_ALREADY_OPEN");
|
|
1197
|
-
if (params.mode === "resume" && !this.initialized.capabilities.session?.resume)
|
|
1198
|
-
throw new HarnessSdkError("CAPABILITY_UNSUPPORTED");
|
|
1199
|
-
if (params.mode === "fork" && (!params.fork || !this.initialized.capabilities.session?.fork?.includes(params.fork.boundary.relation)))
|
|
1200
|
-
throw new HarnessSdkError("CAPABILITY_UNSUPPORTED");
|
|
1201
|
-
const source = params.mode === "fork" && params.fork ? this.sessions.get(params.fork.sourceSessionId) : undefined;
|
|
1202
|
-
const releaseSource = source && !source.isClosed ? source.reserveFork(params.binding) : undefined;
|
|
1203
|
-
try {
|
|
1204
|
-
const session = new ManagedHarnessSession(params.binding, this.initialized.capabilities, this.options, params, this.clientCapabilities);
|
|
1205
|
-
this.sessions.set(params.binding.sessionId, session);
|
|
1206
|
-
try {
|
|
1207
|
-
await waitForRequest(async () => {
|
|
1208
|
-
const backend = await this.adapter.openSession(params, session);
|
|
1209
|
-
await session.attach(backend);
|
|
1210
|
-
}, options, this.options.openTimeoutMs ?? 120000);
|
|
1211
|
-
return session;
|
|
1212
|
-
} catch (cause) {
|
|
1213
|
-
await session.dispose();
|
|
1214
|
-
this.sessions.delete(params.binding.sessionId);
|
|
1215
|
-
throw cause;
|
|
1216
|
-
}
|
|
1217
|
-
} finally {
|
|
1218
|
-
releaseSource?.();
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
dispose() {
|
|
1222
|
-
return this.disposing ??= (async () => {
|
|
1223
|
-
this.closed = true;
|
|
1224
|
-
const results = await Promise.allSettled([...this.sessions.values()].map((session) => session.dispose()));
|
|
1225
|
-
const [adapterResult] = await Promise.allSettled([this.adapter.dispose()]);
|
|
1226
|
-
this.sessions.clear();
|
|
1227
|
-
const failure = [...results, adapterResult].find((result) => result.status === "rejected");
|
|
1228
|
-
if (failure?.status === "rejected")
|
|
1229
|
-
throw failure.reason;
|
|
1230
|
-
})();
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
function createHarnessRuntime(adapter, options) {
|
|
1234
|
-
return new ManagedHarnessRuntime(adapter, options);
|
|
1235
|
-
}
|
|
1236
|
-
// packages/core/src/identity.ts
|
|
1237
|
-
class NativeIdentityMap {
|
|
1238
|
-
createId;
|
|
1239
|
-
turns = new Map;
|
|
1240
|
-
reverseTurns = new Map;
|
|
1241
|
-
items = new Map;
|
|
1242
|
-
constructor(createId) {
|
|
1243
|
-
this.createId = createId;
|
|
1244
|
-
}
|
|
1245
|
-
bindTurn(turnId, nativeTurnId) {
|
|
1246
|
-
const previous = this.turns.get(nativeTurnId);
|
|
1247
|
-
const reverse = this.reverseTurns.get(turnId);
|
|
1248
|
-
if (previous && previous !== turnId || reverse && reverse !== nativeTurnId)
|
|
1249
|
-
throw new HarnessSdkError("PROTOCOL_ERROR", "Conflicting native turn identity");
|
|
1250
|
-
this.turns.set(nativeTurnId, turnId);
|
|
1251
|
-
this.reverseTurns.set(turnId, nativeTurnId);
|
|
1252
|
-
}
|
|
1253
|
-
turn(nativeTurnId) {
|
|
1254
|
-
return this.turns.get(nativeTurnId);
|
|
1255
|
-
}
|
|
1256
|
-
nativeTurn(turnId) {
|
|
1257
|
-
return this.reverseTurns.get(turnId);
|
|
1258
|
-
}
|
|
1259
|
-
item(turnId, nativeItemId) {
|
|
1260
|
-
const key = JSON.stringify([turnId, nativeItemId]);
|
|
1261
|
-
let value = this.items.get(key);
|
|
1262
|
-
if (!value) {
|
|
1263
|
-
value = this.createId();
|
|
1264
|
-
this.items.set(key, value);
|
|
1265
|
-
}
|
|
1266
|
-
return value;
|
|
1267
|
-
}
|
|
1268
|
-
clear() {
|
|
1269
|
-
this.turns.clear();
|
|
1270
|
-
this.reverseTurns.clear();
|
|
1271
|
-
this.items.clear();
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
// packages/core/src/process.ts
|
|
1275
|
-
import { spawn } from "node:child_process";
|
|
1276
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
1277
|
-
|
|
1278
|
-
class ProcessPortError extends Error {
|
|
1279
|
-
code;
|
|
1280
|
-
nativeCode;
|
|
1281
|
-
constructor(code, message, options) {
|
|
1282
|
-
super(message);
|
|
1283
|
-
this.name = "ProcessPortError";
|
|
1284
|
-
this.code = code;
|
|
1285
|
-
const cause = options?.cause;
|
|
1286
|
-
if (cause && typeof cause === "object" && "code" in cause && typeof cause.code === "string" && /^[A-Z][A-Z0-9_]{0,63}$/.test(cause.code))
|
|
1287
|
-
this.nativeCode = cause.code;
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
function exists(pid) {
|
|
1291
|
-
try {
|
|
1292
|
-
process.kill(pid, 0);
|
|
1293
|
-
return true;
|
|
1294
|
-
} catch (error) {
|
|
1295
|
-
return error.code === "EPERM";
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
function signalGroup(pid, signal) {
|
|
1299
|
-
try {
|
|
1300
|
-
process.kill(-pid, signal);
|
|
1301
|
-
} catch (error) {
|
|
1302
|
-
if (error.code !== "ESRCH")
|
|
1303
|
-
throw error;
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
async function signalGroupWithExitRace(pid, signal) {
|
|
1307
|
-
for (let attempt = 0;; attempt++) {
|
|
1308
|
-
try {
|
|
1309
|
-
signalGroup(pid, signal);
|
|
1310
|
-
return;
|
|
1311
|
-
} catch (error) {
|
|
1312
|
-
if (error.code !== "EPERM" || attempt >= 4)
|
|
1313
|
-
throw error;
|
|
1314
|
-
await delay(10);
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
async function windowsKillTree(pid, force) {
|
|
1319
|
-
await new Promise((resolve, reject) => {
|
|
1320
|
-
const killer = spawn("taskkill.exe", ["/PID", String(pid), "/T", ...force ? ["/F"] : []], {
|
|
1321
|
-
stdio: "ignore",
|
|
1322
|
-
windowsHide: true,
|
|
1323
|
-
shell: false
|
|
1324
|
-
});
|
|
1325
|
-
killer.once("error", reject);
|
|
1326
|
-
killer.once("exit", (code) => {
|
|
1327
|
-
if (code === 0 || !exists(pid))
|
|
1328
|
-
resolve();
|
|
1329
|
-
else
|
|
1330
|
-
reject(new ProcessPortError("PROCESS_DISPOSE_FAILED", "Unable to terminate harness process tree"));
|
|
1331
|
-
});
|
|
1332
|
-
});
|
|
1333
|
-
}
|
|
1334
|
-
async function terminateTree(child, pid, timeoutMs, didExit) {
|
|
1335
|
-
if (!Number.isSafeInteger(pid) || pid <= 1)
|
|
1336
|
-
throw new ProcessPortError("PROCESS_DISPOSE_FAILED", "Invalid original harness PID");
|
|
1337
|
-
if (process.platform === "win32") {
|
|
1338
|
-
if (!didExit()) {
|
|
1339
|
-
try {
|
|
1340
|
-
await windowsKillTree(pid, false);
|
|
1341
|
-
} catch {
|
|
1342
|
-
if (!didExit())
|
|
1343
|
-
await windowsKillTree(pid, true);
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
const deadline = Date.now() + timeoutMs;
|
|
1347
|
-
while (!didExit() && Date.now() < deadline)
|
|
1348
|
-
await delay(10);
|
|
1349
|
-
if (!didExit())
|
|
1350
|
-
await windowsKillTree(pid, true);
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
|
-
await signalGroupWithExitRace(pid, "SIGTERM");
|
|
1354
|
-
const deadline = Date.now() + timeoutMs;
|
|
1355
|
-
while (exists(-pid) && Date.now() < deadline)
|
|
1356
|
-
await delay(10);
|
|
1357
|
-
if (exists(-pid))
|
|
1358
|
-
await signalGroupWithExitRace(pid, "SIGKILL");
|
|
1359
|
-
}
|
|
1360
|
-
async function spawnHarnessProcess(options) {
|
|
1361
|
-
if (!options.command || options.command.includes("\x00") || options.args?.some((arg) => arg.includes("\x00"))) {
|
|
1362
|
-
throw new ProcessPortError("HARNESS_UNAVAILABLE", "Invalid harness executable or argument");
|
|
1363
|
-
}
|
|
1364
|
-
const timeoutMs = options.shutdownTimeoutMs ?? 1000;
|
|
1365
|
-
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 60000) {
|
|
1366
|
-
throw new ProcessPortError("HARNESS_UNAVAILABLE", "Invalid process shutdown timeout");
|
|
1367
|
-
}
|
|
1368
|
-
const owned = options.owned ?? true;
|
|
1369
|
-
let child;
|
|
1370
|
-
try {
|
|
1371
|
-
child = spawn(options.command, [...options.args ?? []], {
|
|
1372
|
-
cwd: options.cwd,
|
|
1373
|
-
env: options.env,
|
|
1374
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1375
|
-
detached: owned && process.platform !== "win32",
|
|
1376
|
-
shell: false,
|
|
1377
|
-
windowsHide: true
|
|
1378
|
-
});
|
|
1379
|
-
} catch (cause) {
|
|
1380
|
-
throw new ProcessPortError("HARNESS_UNAVAILABLE", "Unable to start harness executable", { cause });
|
|
1381
|
-
}
|
|
1382
|
-
let didExit = false;
|
|
1383
|
-
let originalPid = child.pid;
|
|
1384
|
-
let disposePromise;
|
|
1385
|
-
const exited = new Promise((resolve) => {
|
|
1386
|
-
child.once("exit", (code, signal) => {
|
|
1387
|
-
didExit = true;
|
|
1388
|
-
resolve({ code, signal });
|
|
1389
|
-
});
|
|
1390
|
-
child.once("error", (error) => {
|
|
1391
|
-
if (!child.pid) {
|
|
1392
|
-
didExit = true;
|
|
1393
|
-
resolve({ code: null, signal: null, errorCode: error.code });
|
|
1394
|
-
}
|
|
1395
|
-
});
|
|
1396
|
-
});
|
|
1397
|
-
child.stdin.on("error", () => {});
|
|
1398
|
-
try {
|
|
1399
|
-
await new Promise((resolve, reject) => {
|
|
1400
|
-
child.once("spawn", () => {
|
|
1401
|
-
if (child.pid !== undefined && child.pid > 1)
|
|
1402
|
-
originalPid = child.pid;
|
|
1403
|
-
resolve();
|
|
1404
|
-
});
|
|
1405
|
-
child.once("error", reject);
|
|
1406
|
-
});
|
|
1407
|
-
} catch (cause) {
|
|
1408
|
-
child.stdin.destroy();
|
|
1409
|
-
child.stdout.destroy();
|
|
1410
|
-
child.stderr.destroy();
|
|
1411
|
-
throw new ProcessPortError("HARNESS_UNAVAILABLE", "Unable to start harness executable", { cause });
|
|
1412
|
-
}
|
|
1413
|
-
const pid = originalPid;
|
|
1414
|
-
const closeInput = () => new Promise((resolve, reject) => {
|
|
1415
|
-
if (child.stdin.destroyed || child.stdin.writableEnded) {
|
|
1416
|
-
resolve();
|
|
1417
|
-
return;
|
|
1418
|
-
}
|
|
1419
|
-
child.stdin.end((error) => error ? reject(new ProcessPortError("PROCESS_WRITE_FAILED", "Unable to close harness input", { cause: error })) : resolve());
|
|
1420
|
-
});
|
|
1421
|
-
return {
|
|
1422
|
-
pid,
|
|
1423
|
-
owned,
|
|
1424
|
-
stdout: child.stdout,
|
|
1425
|
-
stderr: child.stderr,
|
|
1426
|
-
exited,
|
|
1427
|
-
write(chunk) {
|
|
1428
|
-
if (disposePromise || child.stdin.destroyed || child.stdin.writableEnded || didExit) {
|
|
1429
|
-
return Promise.reject(new ProcessPortError("PROCESS_WRITE_FAILED", "Harness process input is closed"));
|
|
1430
|
-
}
|
|
1431
|
-
return new Promise((resolve, reject) => {
|
|
1432
|
-
child.stdin.write(chunk, (error) => error ? reject(new ProcessPortError("PROCESS_WRITE_FAILED", "Unable to write harness input", { cause: error })) : resolve());
|
|
1433
|
-
});
|
|
1434
|
-
},
|
|
1435
|
-
closeInput,
|
|
1436
|
-
dispose() {
|
|
1437
|
-
return disposePromise ??= (async () => {
|
|
1438
|
-
try {
|
|
1439
|
-
if (owned) {
|
|
1440
|
-
await terminateTree(child, pid, timeoutMs, () => didExit);
|
|
1441
|
-
if (!didExit) {
|
|
1442
|
-
await Promise.race([exited, delay(1000)]);
|
|
1443
|
-
if (!didExit)
|
|
1444
|
-
throw new ProcessPortError("PROCESS_DISPOSE_FAILED", "Harness process did not exit after termination");
|
|
1445
|
-
}
|
|
1446
|
-
} else
|
|
1447
|
-
child.unref();
|
|
1448
|
-
} catch (cause) {
|
|
1449
|
-
if (cause instanceof ProcessPortError)
|
|
1450
|
-
throw cause;
|
|
1451
|
-
throw new ProcessPortError("PROCESS_DISPOSE_FAILED", "Unable to dispose harness process", { cause });
|
|
1452
|
-
} finally {
|
|
1453
|
-
child.stdin.destroy();
|
|
1454
|
-
child.stdout.destroy();
|
|
1455
|
-
child.stderr.destroy();
|
|
1456
|
-
}
|
|
1457
|
-
})();
|
|
1458
|
-
}
|
|
1459
|
-
};
|
|
1460
|
-
}
|
|
1461
|
-
export {
|
|
1462
|
-
BoundedEventQueue,
|
|
1463
|
-
HarnessSdkError,
|
|
1464
|
-
InteractionRegistry,
|
|
1465
|
-
ManagedHarnessRuntime,
|
|
1466
|
-
ManagedHarnessSession,
|
|
1467
|
-
NativeIdentityMap,
|
|
1468
|
-
ProcessPortError,
|
|
1469
|
-
SecretRedactor,
|
|
1470
|
-
createHarnessRuntime,
|
|
1471
|
-
sdkError,
|
|
1472
|
-
spawnHarnessProcess,
|
|
1473
|
-
waitForRequest
|
|
1474
|
-
};
|