@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.
Files changed (90) hide show
  1. package/README.md +162 -65
  2. package/package.json +29 -24
  3. package/src/adapter/identity.d.ts +41 -0
  4. package/src/adapter/identity.js +71 -0
  5. package/src/client-options.d.ts +71 -0
  6. package/src/client-options.js +0 -0
  7. package/src/client.d.ts +106 -0
  8. package/src/client.js +254 -0
  9. package/src/diagnostics/errors.d.ts +15 -0
  10. package/src/diagnostics/errors.js +59 -0
  11. package/src/diagnostics/events.d.ts +46 -0
  12. package/src/diagnostics/events.js +47 -0
  13. package/src/diagnostics.d.ts +20 -0
  14. package/src/diagnostics.js +56 -0
  15. package/src/errors.d.ts +48 -0
  16. package/src/errors.js +63 -0
  17. package/src/index.d.ts +27 -0
  18. package/src/index.js +23 -0
  19. package/src/lifecycle/approval.d.ts +16 -0
  20. package/src/lifecycle/approval.js +18 -0
  21. package/src/lifecycle/deferred.d.ts +10 -0
  22. package/src/lifecycle/deferred.js +12 -0
  23. package/src/lifecycle/errors.d.ts +18 -0
  24. package/src/lifecycle/errors.js +45 -0
  25. package/src/lifecycle/events.d.ts +35 -0
  26. package/src/lifecycle/events.js +141 -0
  27. package/src/lifecycle/execution-observation.d.ts +33 -0
  28. package/src/lifecycle/execution-observation.js +0 -0
  29. package/src/lifecycle/immutable.d.ts +12 -0
  30. package/src/lifecycle/immutable.js +28 -0
  31. package/src/lifecycle/input.d.ts +9 -0
  32. package/src/lifecycle/input.js +19 -0
  33. package/src/lifecycle/interactions.d.ts +129 -0
  34. package/src/lifecycle/interactions.js +282 -0
  35. package/src/lifecycle/limits.d.ts +7 -0
  36. package/src/lifecycle/limits.js +13 -0
  37. package/src/lifecycle/result.d.ts +77 -0
  38. package/src/lifecycle/result.js +213 -0
  39. package/src/lifecycle/value.d.ts +4 -0
  40. package/src/lifecycle/value.js +5 -0
  41. package/src/ports.d.ts +224 -0
  42. package/src/ports.js +0 -0
  43. package/src/privacy/file-change-limits.d.ts +20 -0
  44. package/src/privacy/file-change-limits.js +124 -0
  45. package/src/privacy/secrets.d.ts +205 -0
  46. package/src/privacy/secrets.js +564 -0
  47. package/src/process.d.ts +92 -0
  48. package/src/process.js +331 -0
  49. package/src/queue.d.ts +143 -0
  50. package/src/queue.js +266 -0
  51. package/src/request.d.ts +28 -0
  52. package/src/request.js +81 -0
  53. package/src/requests/guard.d.ts +15 -0
  54. package/src/requests/guard.js +93 -0
  55. package/src/session.d.ts +459 -0
  56. package/src/session.js +1135 -0
  57. package/src/structured-output/adapter.d.ts +8 -0
  58. package/src/structured-output/adapter.js +54 -0
  59. package/src/structured-output/bridge.d.ts +81 -0
  60. package/src/structured-output/bridge.js +274 -0
  61. package/src/structured-output/errors.d.ts +17 -0
  62. package/src/structured-output/errors.js +31 -0
  63. package/src/structured-output/limits.d.ts +19 -0
  64. package/src/structured-output/limits.js +0 -0
  65. package/src/structured-output/schema.d.ts +20 -0
  66. package/src/structured-output/schema.js +122 -0
  67. package/src/structured-output/session.d.ts +329 -0
  68. package/src/structured-output/session.js +846 -0
  69. package/src/structured-output/stdio.mjs +83 -0
  70. package/src/structured-output/tool.d.ts +38 -0
  71. package/src/structured-output/tool.js +34 -0
  72. package/src/turn.d.ts +75 -0
  73. package/src/turn.js +95 -0
  74. package/src/types.d.ts +62 -0
  75. package/src/types.js +0 -0
  76. package/dist/diagnostics.d.ts +0 -15
  77. package/dist/diagnostics.js +0 -34
  78. package/dist/errors.d.ts +0 -15
  79. package/dist/file-change-limits.d.ts +0 -5
  80. package/dist/identity.d.ts +0 -13
  81. package/dist/index.d.ts +0 -8
  82. package/dist/index.js +0 -1474
  83. package/dist/interactions.d.ts +0 -20
  84. package/dist/ports.d.ts +0 -66
  85. package/dist/process.d.ts +0 -34
  86. package/dist/process.js +0 -191
  87. package/dist/queue.d.ts +0 -36
  88. package/dist/queue.js +0 -166
  89. package/dist/runtime.d.ts +0 -85
  90. package/dist/secrets.d.ts +0 -28
package/src/queue.js ADDED
@@ -0,0 +1,266 @@
1
+ // Owns bounded admission, buffered drain and exclusive iterator ownership for one producer stream.
2
+ import { HarnessSdkError } from "./errors.js";
3
+ /**
4
+ * Buffer a producer stream within event and byte budgets, or await capacity with write.
5
+ * One iterator owns reads at a time. Returning it releases ownership while the producer stays open.
6
+ * Producers own closure and consumers own iterator release. Buffered regular events drain before
7
+ * buffered controls, then readers observe completion or failure. Values are retained by reference;
8
+ * keep their measured size stable while queued. EventHub uses a private queue per observer to
9
+ * isolate overflow, while adapter transports use queues for incoming frames or native input.
10
+ */
11
+ export class BoundedEventQueue {
12
+ sizeOf;
13
+ /**
14
+ * Configure storage without claiming a reader. Invalid resolved budgets throw INVALID_PARAMS.
15
+ * Omitted or nullish budget fields select the QueueLimits defaults. The queue calls sizeOf during
16
+ * admission and propagates measurement failures. It defaults to measuring the UTF-8 byte length
17
+ * of the JSON serialization. A custom measurement must return a consistent, nonnegative byte
18
+ * count because a value can be measured more than once.
19
+ */
20
+ constructor(limits = {}, sizeOf = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength) {
21
+ this.sizeOf = sizeOf;
22
+ this.limits = {
23
+ maxEvents: limits.maxEvents ?? 2048,
24
+ maxBytes: limits.maxBytes ?? 16 * 1024 * 1024,
25
+ maxFrameBytes: limits.maxFrameBytes ?? 8 * 1024 * 1024,
26
+ maxWaitingWriters: limits.maxWaitingWriters ?? 32,
27
+ maxControlEvents: limits.maxControlEvents ?? 33,
28
+ };
29
+ if (Object.values(this.limits).some((n) => !Number.isSafeInteger(n) || n <= 0))
30
+ throw new HarnessSdkError("INVALID_PARAMS", "Queue limits must be positive integers");
31
+ }
32
+ /** Resolved construction-time budgets exposed for producers; callers must treat this object as read-only. */
33
+ limits;
34
+ /** Count regular events and controls currently in the buffer. */
35
+ get bufferedEvents() { return this.entries.length + this.controls.length; }
36
+ /** Count bytes reserved by buffered regular events. Controls consume their own event budget. */
37
+ get bufferedBytes() { return this.bytes; }
38
+ /**
39
+ * Check regular-event admission without reserving capacity. A later push can still fail.
40
+ * Throw the stored non-nullish closure cause (or STREAM_CLOSED), FRAME_TOO_LARGE, a measurement
41
+ * failure, or BACKPRESSURE_EXCEEDED for a full regular buffer.
42
+ */
43
+ assertWritable(value) {
44
+ this.assertOpen();
45
+ const bytes = this.checkedSize(value);
46
+ if (this.isRegularBufferFull(bytes))
47
+ throw new HarnessSdkError("BACKPRESSURE_EXCEEDED");
48
+ }
49
+ /**
50
+ * Deliver to a waiting read or buffer synchronously, throwing on closure, capacity or
51
+ * size/measurement failure without evicting data. Producers handle failures and own closure.
52
+ * EventHub.publish ends only the affected observer on failure. The message listener in
53
+ * createSocketTransport also closes the native socket.
54
+ */
55
+ push(value) {
56
+ // Run the overridable admission probe before measuring the value for storage.
57
+ this.assertWritable(value);
58
+ const bytes = this.checkedSize(value);
59
+ if (this.deliverToWaitingReader(value))
60
+ return;
61
+ this.entries.push({ value, bytes });
62
+ this.bytes += bytes;
63
+ }
64
+ /**
65
+ * Await capacity and resolve once delivered or buffered, without waiting for consumption.
66
+ * Reject on closure, size/measurement failure or exhausted writer capacity.
67
+ * Waking writers compete for capacity and recheck it before admitting a value. Pass accepted
68
+ * to run a synchronous callback after admission. If it throws, the promise rejects and the value
69
+ * remains admitted. Producers can end the queue to wake all blocked writers and reject admission.
70
+ */
71
+ async write(value, accepted) {
72
+ const bytes = this.checkedSize(value);
73
+ // Resuming notifies all waiting writers to recheck capacity; it reserves no slot.
74
+ while (this.isRegularBufferFull(bytes)) {
75
+ this.assertOpen();
76
+ if (this.waitingWriterResumes.size >= this.limits.maxWaitingWriters)
77
+ throw new HarnessSdkError("BACKPRESSURE_EXCEEDED");
78
+ await new Promise((resolve) => this.waitingWriterResumes.add(resolve));
79
+ }
80
+ this.push(value);
81
+ accepted?.();
82
+ }
83
+ /**
84
+ * Admit a control event even when the regular buffer is full. Each event still obeys
85
+ * maxFrameBytes and maxBytes; buffered controls have a separate maxControlEvents limit
86
+ * and do not contribute to bufferedBytes.
87
+ * Buffered regular events are delivered before buffered control events. Throws on closure,
88
+ * measurement/size failure or exhausted control capacity; callers still own producer closure.
89
+ */
90
+ pushControl(value) {
91
+ this.assertOpen();
92
+ this.checkedSize(value);
93
+ if (this.deliverToWaitingReader(value))
94
+ return;
95
+ if (this.controls.length >= this.limits.maxControlEvents)
96
+ throw new HarnessSdkError("BACKPRESSURE_EXCEEDED", "Control event limit exceeded");
97
+ this.controls.push(value);
98
+ }
99
+ /**
100
+ * Admit a final control under the usual control budget, then close admission. Readers can
101
+ * receive the control before completion. Already-ended queues ignore the value. If admission
102
+ * throws, the producer still owns recovery or closure.
103
+ */
104
+ endWithControl(value) {
105
+ if (this.ended)
106
+ return;
107
+ this.pushControl(value);
108
+ this.end();
109
+ }
110
+ /**
111
+ * Idempotently close admission, wake blocked writers so they can reject, and settle any pending read.
112
+ * Buffered values drain before a truthy cause rejects reads; falsy causes complete reads normally.
113
+ * Closure checks throw a non-nullish stored cause, otherwise STREAM_CLOSED.
114
+ * Reader ownership remains until next observes drained closure or the consumer calls return or
115
+ * throw on the iterator. The caller still owns stopping any native producer.
116
+ */
117
+ end(cause) {
118
+ if (this.ended)
119
+ return;
120
+ this.ended = true;
121
+ this.failure = cause;
122
+ this.resumeWaitingWriters();
123
+ const waiter = this.takePendingRead();
124
+ if (!waiter)
125
+ return;
126
+ if (cause)
127
+ waiter.reject(cause);
128
+ else
129
+ waiter.resolve(completedRead());
130
+ }
131
+ /**
132
+ * Claim exclusive reader ownership, throwing EVENT_STREAM_ALREADY_OWNED until the prior reader
133
+ * releases it. next drains regular values then controls before terminal completion or rejection.
134
+ * A second next while a read is blocked rejects with CONCURRENT_EVENT_READ. return and throw
135
+ * release ownership and complete a blocked read while leaving the producer open. throw also
136
+ * rejects its own promise with the supplied cause. A pending read settled by end still needs
137
+ * a subsequent next, return or throw to release ownership.
138
+ */
139
+ [Symbol.asyncIterator]() {
140
+ if (this.activeReaderToken)
141
+ throw new HarnessSdkError("EVENT_STREAM_ALREADY_OWNED");
142
+ const readerToken = Symbol("reader");
143
+ this.activeReaderToken = readerToken;
144
+ return {
145
+ next: () => this.readNext(readerToken),
146
+ return: async () => {
147
+ this.detachReader(readerToken);
148
+ return completedRead();
149
+ },
150
+ throw: async (cause) => {
151
+ this.detachReader(readerToken);
152
+ throw cause;
153
+ },
154
+ };
155
+ }
156
+ entries = [];
157
+ bytes = 0;
158
+ activeReaderToken;
159
+ pendingRead;
160
+ controls = [];
161
+ ended = false;
162
+ failure;
163
+ waitingWriterResumes = new Set();
164
+ /**
165
+ * end, readBuffered and readNext wake blocked writers when they close the queue, free capacity
166
+ * or register a reader for direct delivery. Writers recheck admission on resumption.
167
+ */
168
+ resumeWaitingWriters() {
169
+ for (const resume of this.waitingWriterResumes)
170
+ resume();
171
+ this.waitingWriterResumes.clear();
172
+ }
173
+ /**
174
+ * assertWritable and write share this admission rule. A pending read permits direct delivery even
175
+ * at the buffer budget; checking capacity neither reserves a slot nor wakes a producer.
176
+ */
177
+ isRegularBufferFull(bytes) {
178
+ return !this.pendingRead && (this.entries.length >= this.limits.maxEvents || this.bytes + bytes > this.limits.maxBytes);
179
+ }
180
+ /**
181
+ * push and pushControl try direct delivery after checking size. Report whether this settled
182
+ * a pending read so the caller can skip buffering.
183
+ */
184
+ deliverToWaitingReader(value) {
185
+ const waiter = this.takePendingRead();
186
+ if (!waiter)
187
+ return false;
188
+ waiter.resolve({ done: false, value });
189
+ return true;
190
+ }
191
+ /** Remove the pending read before its caller delivers a value, completes it or rejects it. */
192
+ takePendingRead() {
193
+ const read = this.pendingRead;
194
+ this.pendingRead = undefined;
195
+ return read;
196
+ }
197
+ /**
198
+ * Producer admission uses this to check for closure. A closed queue throws the stored
199
+ * non-nullish cause or STREAM_CLOSED and keeps the reader attached.
200
+ */
201
+ assertOpen() { if (this.ended)
202
+ throw this.failure ?? new HarnessSdkError("STREAM_CLOSED"); }
203
+ /**
204
+ * assertWritable, push, write and pushControl call before admission. Measure without reserving
205
+ * capacity; throw FRAME_TOO_LARGE above either single-frame or regular-byte limits, including for
206
+ * controls. Propagate sizeOf failures unchanged so the producer can handle admission failure.
207
+ */
208
+ checkedSize(value) {
209
+ const bytes = this.sizeOf(value);
210
+ if (bytes > this.limits.maxFrameBytes || bytes > this.limits.maxBytes)
211
+ throw new HarnessSdkError("FRAME_TOO_LARGE");
212
+ return bytes;
213
+ }
214
+ /**
215
+ * Settle the iterator's next call from buffered data or drained closure. Otherwise register a
216
+ * pending read and wake writers because they can now deliver directly. A detached reader
217
+ * completes immediately, and an overlapping blocked read rejects without replacing the first.
218
+ */
219
+ readNext(readerToken) {
220
+ if (this.activeReaderToken !== readerToken)
221
+ return Promise.resolve(completedRead());
222
+ const buffered = this.readBuffered();
223
+ if (buffered)
224
+ return Promise.resolve(buffered);
225
+ if (this.ended) {
226
+ this.detachReader(readerToken);
227
+ return this.failure ? Promise.reject(this.failure) : Promise.resolve(completedRead());
228
+ }
229
+ if (this.pendingRead)
230
+ return Promise.reject(new HarnessSdkError("CONCURRENT_EVENT_READ"));
231
+ return new Promise((resolve, reject) => {
232
+ this.pendingRead = { resolve, reject };
233
+ this.resumeWaitingWriters();
234
+ });
235
+ }
236
+ /**
237
+ * readNext on drained closure and the iterator's return and throw release ownership here.
238
+ * createSubscription also calls return when cancelled. Only the active reader can have a pending
239
+ * read, so checking its token protects a replacement reader from stale calls.
240
+ */
241
+ detachReader(readerToken) {
242
+ if (this.activeReaderToken !== readerToken)
243
+ return;
244
+ this.activeReaderToken = undefined;
245
+ this.takePendingRead()?.resolve(completedRead());
246
+ }
247
+ /**
248
+ * Give readNext regular values before controls and release regular bytes before waking writers.
249
+ * Return undefined only when neither buffer has data, leaving terminal settlement to readNext.
250
+ */
251
+ readBuffered() {
252
+ const entry = this.entries.shift();
253
+ if (entry) {
254
+ this.bytes -= entry.bytes;
255
+ this.resumeWaitingWriters();
256
+ return { done: false, value: entry.value };
257
+ }
258
+ if (this.controls.length)
259
+ return { done: false, value: this.controls.shift() };
260
+ return undefined;
261
+ }
262
+ }
263
+ /** Give each completed read its own result object. */
264
+ function completedRead() {
265
+ return { done: true, value: undefined };
266
+ }
@@ -0,0 +1,28 @@
1
+ import type { RequestOptions } from "./types.js";
2
+ /**
3
+ * Package-internal validation of explicit Client and Session wait durations in milliseconds.
4
+ * Return true for non-finite, non-positive or above 2_147_483_647. Omission and positive fractions
5
+ * within the limit pass. Client.initialize and Client.validateOpen throw INVALID_PARAMS on
6
+ * failure. Session.request, Session.submitExecution and Session.closeRequest return a rejection.
7
+ */
8
+ export declare function invalidWait(wait?: RequestOptions): boolean;
9
+ /**
10
+ * Await one operation with a caller-scoped deadline and optional abort signal.
11
+ * A pre-aborted signal rejects with REQUEST_ABORTED before timeout validation or invocation.
12
+ * A nullish timeout uses defaultTimeoutMs, which defaults to 30,000 ms. A non-finite or non-positive
13
+ * effective timeout rejects with INVALID_PARAMS. Rounding and timer range follow the host timer.
14
+ * The operation is invoked synchronously once. Its value or original failure, including a
15
+ * synchronous throw, settles the wait if it wins the race. Abort or expiry after invocation
16
+ * rejects with REQUEST_ABORTED or REQUEST_TIMEOUT and outcomeUnknown=true. Keep options.signal
17
+ * stable until settlement so the wait can remove its listener. Settlement clears the timer and
18
+ * the operation's rejection remains observed. Callers own any native cancellation, resource
19
+ * release or decision to retry.
20
+ *
21
+ * Turn.result returns this wait for the shared terminal snapshot. Session.submitExecution
22
+ * returns it for execution binding after dispatch, and Session.closeRequest uses it to
23
+ * observe close and backend cleanup.
24
+ * InteractionRegistry.deliverResponse and cancelPending await the supplied native responder and
25
+ * translate failures into interaction outcomes. AcpAdapter.call returns the native result or
26
+ * normalizes the rejection while preserving SDK errors.
27
+ */
28
+ export declare function waitForRequest<T>(operation: () => Promise<T>, options?: RequestOptions, defaultTimeoutMs?: number): Promise<T>;
package/src/request.js ADDED
@@ -0,0 +1,81 @@
1
+ // Owns caller deadlines and cancellation for observing one operation.
2
+ import { HarnessSdkError } from "./errors.js";
3
+ /**
4
+ * Package-internal validation of explicit Client and Session wait durations in milliseconds.
5
+ * Return true for non-finite, non-positive or above 2_147_483_647. Omission and positive fractions
6
+ * within the limit pass. Client.initialize and Client.validateOpen throw INVALID_PARAMS on
7
+ * failure. Session.request, Session.submitExecution and Session.closeRequest return a rejection.
8
+ */
9
+ export function invalidWait(wait) {
10
+ return wait?.timeoutMs !== undefined
11
+ && (!Number.isFinite(wait.timeoutMs) || wait.timeoutMs <= 0 || wait.timeoutMs > 0x7fffffff);
12
+ }
13
+ /**
14
+ * Await one operation with a caller-scoped deadline and optional abort signal.
15
+ * A pre-aborted signal rejects with REQUEST_ABORTED before timeout validation or invocation.
16
+ * A nullish timeout uses defaultTimeoutMs, which defaults to 30,000 ms. A non-finite or non-positive
17
+ * effective timeout rejects with INVALID_PARAMS. Rounding and timer range follow the host timer.
18
+ * The operation is invoked synchronously once. Its value or original failure, including a
19
+ * synchronous throw, settles the wait if it wins the race. Abort or expiry after invocation
20
+ * rejects with REQUEST_ABORTED or REQUEST_TIMEOUT and outcomeUnknown=true. Keep options.signal
21
+ * stable until settlement so the wait can remove its listener. Settlement clears the timer and
22
+ * the operation's rejection remains observed. Callers own any native cancellation, resource
23
+ * release or decision to retry.
24
+ *
25
+ * Turn.result returns this wait for the shared terminal snapshot. Session.submitExecution
26
+ * returns it for execution binding after dispatch, and Session.closeRequest uses it to
27
+ * observe close and backend cleanup.
28
+ * InteractionRegistry.deliverResponse and cancelPending await the supplied native responder and
29
+ * translate failures into interaction outcomes. AcpAdapter.call returns the native result or
30
+ * normalizes the rejection while preserving SDK errors.
31
+ */
32
+ export function waitForRequest(operation, options = {}, defaultTimeoutMs = 30_000) {
33
+ if (options.signal?.aborted)
34
+ return Promise.reject(new HarnessSdkError("REQUEST_ABORTED", "Request aborted before dispatch"));
35
+ const timeoutMs = options.timeoutMs ?? defaultTimeoutMs;
36
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
37
+ return Promise.reject(new HarnessSdkError("INVALID_PARAMS", "timeoutMs must be positive"));
38
+ return observeRequest({ operation, options, timeoutMs });
39
+ }
40
+ /**
41
+ * Dispatch the operation after waitForRequest validates admission. Own the timer and listener
42
+ * for this promise and attach the operation's continuations even if invocation triggers abort.
43
+ */
44
+ function observeRequest({ operation, options, timeoutMs }) {
45
+ return new Promise((resolve, reject) => {
46
+ let settled = false;
47
+ /**
48
+ * The operation continuations and abandon release wait resources before delivering the first
49
+ * outcome. Later outcomes leave the original settlement intact.
50
+ */
51
+ const finish = (callback) => {
52
+ if (settled)
53
+ return;
54
+ settled = true;
55
+ clearTimeout(timer);
56
+ options.signal?.removeEventListener("abort", abort);
57
+ callback();
58
+ };
59
+ /**
60
+ * The abort listener and deadline callback reject the same wait with an unknown outcome.
61
+ * Build the error only if this callback wins settlement.
62
+ */
63
+ const abandon = (code) => {
64
+ finish(() => reject(new HarnessSdkError(code, "Request outcome is unknown after dispatch", false, true)));
65
+ };
66
+ /** The caller signal invokes this listener; finish removes this same reference on any settlement. */
67
+ const abort = () => abandon("REQUEST_ABORTED");
68
+ /** Forward invocation failures and promise rejections through the same cleanup. */
69
+ const rejectOperation = (cause) => finish(() => reject(cause));
70
+ const timer = setTimeout(() => abandon("REQUEST_TIMEOUT"), timeoutMs);
71
+ // Install before invoking operation: it may synchronously trigger the caller's abort signal.
72
+ options.signal?.addEventListener("abort", abort, { once: true });
73
+ // Keep the rejection handler attached after abandonment so late failures stay observed.
74
+ try {
75
+ operation().then((value) => finish(() => resolve(value)), rejectOperation);
76
+ }
77
+ catch (cause) {
78
+ rejectOperation(cause);
79
+ }
80
+ });
81
+ }
@@ -0,0 +1,15 @@
1
+ import { type AhpRequestMap, type Capabilities } from "@anureo/protocol";
2
+ import type { SessionMethod } from "../ports.js";
3
+ /**
4
+ * Synchronously admit a typed session request in precedence order: protocol shape, session binding,
5
+ * operation capability, submission policy, then config values. Return void on admission; propagate
6
+ * assertRequest failures, throw SESSION_BINDING_MISMATCH for a foreign session, or throw
7
+ * CAPABILITY_UNSUPPORTED/INVALID_PARAMS for capability/config violations. No dispatch or state change.
8
+ * Session.submitExecution checks before allocating submission records; Session.request checks close
9
+ * requests before shared teardown; dispatchOperation checks before replay/capacity bookkeeping. They
10
+ * expose failures as rejected caller promises. OutputSession.execute checks transformed fallback
11
+ * requests against native capabilities and cleans up definite submission failures before propagating
12
+ * them (adding safe diagnostics if that cleanup also fails).
13
+ * Callers still own lifecycle/wait admission, dispatch deduplication and resource cleanup.
14
+ */
15
+ export declare function guardRequest<M extends SessionMethod>(method: M, params: AhpRequestMap[M], sessionId: string, capabilities: Capabilities): void;
@@ -0,0 +1,93 @@
1
+ // Enforces request shape, binding and capability policy; must not dispatch native operations.
2
+ import { assertRequest, supportsCapability, validateJsonSchema } from "@anureo/protocol";
3
+ import { HarnessSdkError } from "../errors.js";
4
+ /**
5
+ * Synchronously admit a typed session request in precedence order: protocol shape, session binding,
6
+ * operation capability, submission policy, then config values. Return void on admission; propagate
7
+ * assertRequest failures, throw SESSION_BINDING_MISMATCH for a foreign session, or throw
8
+ * CAPABILITY_UNSUPPORTED/INVALID_PARAMS for capability/config violations. No dispatch or state change.
9
+ * Session.submitExecution checks before allocating submission records; Session.request checks close
10
+ * requests before shared teardown; dispatchOperation checks before replay/capacity bookkeeping. They
11
+ * expose failures as rejected caller promises. OutputSession.execute checks transformed fallback
12
+ * requests against native capabilities and cleans up definite submission failures before propagating
13
+ * them (adding safe diagnostics if that cleanup also fails).
14
+ * Callers still own lifecycle/wait admission, dispatch deduplication and resource cleanup.
15
+ */
16
+ export function guardRequest(method, params, sessionId, capabilities) {
17
+ assertRequest(method, params);
18
+ if (params.sessionId !== sessionId)
19
+ throw new HarnessSdkError("SESSION_BINDING_MISMATCH");
20
+ if (!supportsOperation(method, params, capabilities))
21
+ throwCapabilityUnsupported();
22
+ if (method === "input/submit")
23
+ guardSubmissionCapability(params, capabilities);
24
+ if (method === "session/config/set")
25
+ guardSessionConfigCapability(params.patch, capabilities);
26
+ }
27
+ /**
28
+ * guardRequest, guardSubmissionCapability and guardSessionConfigCapability throw through
29
+ * this shared policy to guardRequest. Always throw a code-only SDK failure without exposing rejected
30
+ * capability details; the calling admission path owns propagation and any prior resource cleanup.
31
+ */
32
+ function throwCapabilityUnsupported() {
33
+ throw new HarnessSdkError("CAPABILITY_UNSUPPORTED");
34
+ }
35
+ /**
36
+ * guardRequest checks operation support after shape and binding, before submission or config policy.
37
+ * Unrestricted methods pass. Session interruption also requires opt-in support for cancelling queued
38
+ * inputs when the caller requests it. A false result becomes CAPABILITY_UNSUPPORTED in guardRequest.
39
+ */
40
+ function supportsOperation(method, params, capabilities) {
41
+ switch (method) {
42
+ case "turn/steer":
43
+ return Boolean(capabilities.input?.steer?.atomicMatch);
44
+ case "turn/interrupt":
45
+ return capabilities.input?.interrupt?.targets.includes("turn") ?? false;
46
+ case "session/revert":
47
+ return supportsCapability(capabilities, "session.revert", params.scope);
48
+ case "history/read":
49
+ return supportsCapability(capabilities, "session.history");
50
+ case "goal/set":
51
+ case "goal/clear":
52
+ return supportsCapability(capabilities, "goal.actions", method.split("/")[1]);
53
+ case "compact/start":
54
+ return Boolean(capabilities.compact);
55
+ case "review/start":
56
+ return Boolean(capabilities.review);
57
+ case "session/interrupt":
58
+ return Boolean(capabilities.input?.interrupt?.targets.includes("session")
59
+ && (!params.cancelQueued || capabilities.input?.interrupt?.cancelQueued));
60
+ default:
61
+ return true;
62
+ }
63
+ }
64
+ /**
65
+ * guardRequest checks input/submit delivery, output schema, modalities and override names in that
66
+ * order. Omitted input modalities allow text only. An unsupported option throws CAPABILITY_UNSUPPORTED.
67
+ */
68
+ function guardSubmissionCapability(submission, capabilities) {
69
+ if (!capabilities.input?.delivery.includes(submission.delivery))
70
+ throwCapabilityUnsupported();
71
+ if (submission.outputSchema !== undefined && !capabilities.turn?.outputSchema)
72
+ throwCapabilityUnsupported();
73
+ const modalities = capabilities.turn?.inputModalities ?? ["text"];
74
+ if (submission.input.some((block) => !modalities.includes(block.type)))
75
+ throwCapabilityUnsupported();
76
+ if (submission.overrides && Object.keys(submission.overrides).some((key) => !capabilities.turn?.overrides?.includes(key)))
77
+ throwCapabilityUnsupported();
78
+ }
79
+ /**
80
+ * guardRequest calls this last. For session/config/set, visit patch entries in property order and
81
+ * require a session-scoped advertised option before validating its value. Throw CAPABILITY_UNSUPPORTED
82
+ * for an unknown/non-session option or INVALID_PARAMS for a schema mismatch; the first failing entry
83
+ * wins. Applying the validated patch remains the responsibility of the request implementation.
84
+ */
85
+ function guardSessionConfigCapability(patch, capabilities) {
86
+ for (const [key, value] of Object.entries(patch)) {
87
+ const option = capabilities.config?.options.find((entry) => entry.id === key && entry.scope === "session");
88
+ if (!option)
89
+ throwCapabilityUnsupported();
90
+ if (!validateJsonSchema(option.schema, value))
91
+ throw new HarnessSdkError("INVALID_PARAMS", "Config value does not match schema");
92
+ }
93
+ }