@anureo/sdk 0.0.0-stage → 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 (76) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +185 -2
  3. package/package.json +66 -4
  4. package/src/adapter/identity.d.ts +41 -0
  5. package/src/adapter/identity.js +71 -0
  6. package/src/client-options.d.ts +71 -0
  7. package/src/client-options.js +0 -0
  8. package/src/client.d.ts +106 -0
  9. package/src/client.js +254 -0
  10. package/src/diagnostics/errors.d.ts +15 -0
  11. package/src/diagnostics/errors.js +59 -0
  12. package/src/diagnostics/events.d.ts +46 -0
  13. package/src/diagnostics/events.js +47 -0
  14. package/src/diagnostics.d.ts +20 -0
  15. package/src/diagnostics.js +56 -0
  16. package/src/errors.d.ts +48 -0
  17. package/src/errors.js +63 -0
  18. package/src/index.d.ts +27 -0
  19. package/src/index.js +23 -0
  20. package/src/lifecycle/approval.d.ts +16 -0
  21. package/src/lifecycle/approval.js +18 -0
  22. package/src/lifecycle/deferred.d.ts +10 -0
  23. package/src/lifecycle/deferred.js +12 -0
  24. package/src/lifecycle/errors.d.ts +18 -0
  25. package/src/lifecycle/errors.js +45 -0
  26. package/src/lifecycle/events.d.ts +35 -0
  27. package/src/lifecycle/events.js +141 -0
  28. package/src/lifecycle/execution-observation.d.ts +33 -0
  29. package/src/lifecycle/execution-observation.js +0 -0
  30. package/src/lifecycle/immutable.d.ts +12 -0
  31. package/src/lifecycle/immutable.js +28 -0
  32. package/src/lifecycle/input.d.ts +9 -0
  33. package/src/lifecycle/input.js +19 -0
  34. package/src/lifecycle/interactions.d.ts +129 -0
  35. package/src/lifecycle/interactions.js +282 -0
  36. package/src/lifecycle/limits.d.ts +7 -0
  37. package/src/lifecycle/limits.js +13 -0
  38. package/src/lifecycle/result.d.ts +77 -0
  39. package/src/lifecycle/result.js +213 -0
  40. package/src/lifecycle/value.d.ts +4 -0
  41. package/src/lifecycle/value.js +5 -0
  42. package/src/ports.d.ts +224 -0
  43. package/src/ports.js +0 -0
  44. package/src/privacy/file-change-limits.d.ts +20 -0
  45. package/src/privacy/file-change-limits.js +124 -0
  46. package/src/privacy/secrets.d.ts +205 -0
  47. package/src/privacy/secrets.js +564 -0
  48. package/src/process.d.ts +92 -0
  49. package/src/process.js +331 -0
  50. package/src/queue.d.ts +143 -0
  51. package/src/queue.js +266 -0
  52. package/src/request.d.ts +28 -0
  53. package/src/request.js +81 -0
  54. package/src/requests/guard.d.ts +15 -0
  55. package/src/requests/guard.js +93 -0
  56. package/src/session.d.ts +459 -0
  57. package/src/session.js +1135 -0
  58. package/src/structured-output/adapter.d.ts +8 -0
  59. package/src/structured-output/adapter.js +54 -0
  60. package/src/structured-output/bridge.d.ts +81 -0
  61. package/src/structured-output/bridge.js +274 -0
  62. package/src/structured-output/errors.d.ts +17 -0
  63. package/src/structured-output/errors.js +31 -0
  64. package/src/structured-output/limits.d.ts +19 -0
  65. package/src/structured-output/limits.js +0 -0
  66. package/src/structured-output/schema.d.ts +20 -0
  67. package/src/structured-output/schema.js +122 -0
  68. package/src/structured-output/session.d.ts +329 -0
  69. package/src/structured-output/session.js +846 -0
  70. package/src/structured-output/stdio.mjs +83 -0
  71. package/src/structured-output/tool.d.ts +38 -0
  72. package/src/structured-output/tool.js +34 -0
  73. package/src/turn.d.ts +75 -0
  74. package/src/turn.js +95 -0
  75. package/src/types.d.ts +62 -0
  76. package/src/types.js +0 -0
@@ -0,0 +1,129 @@
1
+ import { type InteractionRequested, type InteractionResponse, type InteractionResult } from "@anureo/protocol";
2
+ import type { NativeInteractionResponder } from "../ports.js";
3
+ /**
4
+ * Track native requests, settle their answers and retain bounded response replay state.
5
+ * Session uses this SDK implementation helper and supplies publication and privacy hooks.
6
+ * Direct consumers own those integrations and should await cancelAll with a teardown reason
7
+ * when releasing a session. The registry owns request expiry and native response wait timers.
8
+ */
9
+ export declare class InteractionRegistry {
10
+ private readonly publish;
11
+ private readonly limits;
12
+ private readonly beforeResponse?;
13
+ /**
14
+ * Configure settlement publication and synchronous pre-response checks without starting native
15
+ * work. Session supplies a publisher that aborts the host handler and awaits publication of
16
+ * interaction/resolved while observation is open. It also supplies SecretRedactor.rememberResponse
17
+ * to register secrets before delivery. respond runs the check for every validated attempt, including
18
+ * replays, and propagates a throw before delivery. Settlement awaits publication without a deadline.
19
+ */
20
+ constructor(publish: (result: InteractionResult) => Promise<void>, limits: {
21
+ /** Pending and settling capacity. Admission trims resolved records at maxPending + 1024. */
22
+ maxPending: number;
23
+ /** Interaction lifetime in milliseconds, shortened by an earlier request expiresAt. */
24
+ timeoutMs: number;
25
+ /** Native response/cancellation wait in milliseconds, defaulting to 30,000. Native work can continue after timeout. */
26
+ responseTimeoutMs?: number;
27
+ }, beforeResponse?: ((request: InteractionRequested, response: InteractionResponse) => void) | undefined);
28
+ /**
29
+ * Count requests that consume admission capacity in begin, including native delivery and
30
+ * cancellation in flight. Capacity becomes available when a request is marked resolved.
31
+ */
32
+ get pendingCount(): number;
33
+ /**
34
+ * List detached copies of actionable requests for manual answering. Cloning failures throw
35
+ * without changing registry state. These copies retain native fields, so callers own projection
36
+ * before exposing them to observers.
37
+ */
38
+ pending(): InteractionRequested[];
39
+ /**
40
+ * Register a unique unexpired request and an unrefed expiry timer. Duplicate IDs, exhausted
41
+ * capacity and invalid/elapsed lifetimes throw synchronously. A request-cloning failure rejects
42
+ * the returned promise after the expiry timer is scheduled. On success, the promise resolves
43
+ * with the answer after native delivery and result publication.
44
+ * Cancellation and peer settlement reject it after publication is attempted. If native delivery
45
+ * fails and publishing its unknown outcome also fails, this waiter stays unresolved; respond
46
+ * rejects the publication failure instead.
47
+ * Session.requestInteraction adopts this promise after publishing interaction/requested and
48
+ * scheduling any host handler. Adapters supply the responder through that sink method and own
49
+ * the native resources it uses.
50
+ */
51
+ begin(request: InteractionRequested, responder: NativeInteractionResponder): Promise<InteractionResponse>;
52
+ /**
53
+ * Deliver an answer or replay the retained operation for the same request ID and serialized
54
+ * response. Validation and the synchronous pre-response hook run before any replay or delivery.
55
+ * Missing IDs reject with INTERACTION_EXPIRED. Conflicting answers to settling or resolved
56
+ * requests reject with INTERACTION_ALREADY_RESOLVED. The operation clears expiry, retains
57
+ * capacity during native delivery, and settles after the publication it initiates. If another
58
+ * settlement wins during delivery, late native success rejects with RESULT_UNKNOWN.
59
+ * Session.invokeRequest returns the promise for manual requests. Session.startInteractionHandler
60
+ * awaits it for automatic answers and attempts handler_failed cancellation on rejection while
61
+ * its signal remains active.
62
+ */
63
+ respond(id: string, requestId: string, response: InteractionResponse): Promise<InteractionResult>;
64
+ /**
65
+ * Cancel or expire an unresolved request, notifying native once when pending. The reason and
66
+ * outcome both default to "cancelled". Missing IDs reject with INTERACTION_EXPIRED. Resolved IDs
67
+ * and in-flight settlement with reason "cancelled" reject with INTERACTION_ALREADY_RESOLVED.
68
+ * Other reasons mark in-flight settlement unknown without another native call. Native notification
69
+ * failure or timeout also changes the outcome to unknown.
70
+ * Await publication, reject begin's waiter with INTERACTION_CANCELLED or RESULT_UNKNOWN, and
71
+ * return the outcome. If publication fails, the cancellation caller receives that failure and
72
+ * the original waiter still receives its lifecycle error. Session.invokeRequest returns this
73
+ * promise for manual cancellation. Session.startInteractionHandler and the expiry timer observe
74
+ * and suppress its rejection. cancelAll collects its outcome during teardown.
75
+ */
76
+ cancel(id: string, reason?: string, outcome?: CancellationOutcome): Promise<InteractionResult>;
77
+ /**
78
+ * Attempt concurrent cancellation of unresolved interactions, optionally limited to turnId; await
79
+ * every attempt and suppress individual failures. Session.fail, Session.finishClose and
80
+ * Session.dispose await this before releasing the backend. Use a reason other than "cancelled"
81
+ * to settle in-flight requests. The literal reason "connection_lost" requests an unknown outcome.
82
+ */
83
+ cancelAll(reason: string, turnId?: string): Promise<void>;
84
+ /**
85
+ * Publish native-side settlement without calling the responder; ignore unknown/resolved IDs
86
+ * and reject the waiter even for responded because no value is supplied. Clear expiry and release
87
+ * capacity before publication; publication failure rejects this call but still rejects the waiter.
88
+ * An unknown outcome rejects the waiter with RESULT_UNKNOWN; other outcomes use INTERACTION_ALREADY_RESOLVED.
89
+ * Session.resolveInteraction returns this promise to adapters reporting an already-settled native request.
90
+ */
91
+ resolveExternally(result: InteractionResult): Promise<void>;
92
+ private readonly records;
93
+ /**
94
+ * begin calls after admission checks, before inserting a new request. At the replay bound, evict
95
+ * only the oldest resolved record so unresolved native work and its capacity remain accounted for.
96
+ * Duplicate checks run first: a retained ID cannot be reused by evicting its own replay guard.
97
+ */
98
+ private evictOldestResolvedRecord;
99
+ /**
100
+ * respond caches this operation after admission. Bound native delivery without retrying it, then
101
+ * publish before resolving begin's waiter. If delivery succeeds after another settlement resolves
102
+ * the record, reject with RESULT_UNKNOWN without publishing again. Normalize delivery/publication
103
+ * failures for both callers. If publishing the unknown outcome itself fails, that failure escapes
104
+ * before the original waiter is rejected. Cancellation uses publishRejectedSettlement's finally instead.
105
+ */
106
+ private deliverResponse;
107
+ /**
108
+ * cancel calls only for an actionable record. Hold capacity and clear expiry before notifying native;
109
+ * timeout or notification failure changes the requested outcome to unknown. Release capacity before
110
+ * publication, then reject begin's waiter even if publication fails. Once started, this attempt
111
+ * still publishes after its native wait, even if teardown or peer settlement has already published.
112
+ */
113
+ private cancelPending;
114
+ /**
115
+ * Wait for publication before rejecting begin's waiter with its lifecycle error. A publication
116
+ * failure still reaches cancel, cancelPending or resolveExternally. Those callers clear expiry
117
+ * and release capacity before entering this step.
118
+ */
119
+ private publishRejectedSettlement;
120
+ /**
121
+ * Bound one invocation of the adapter's registered callback. deliverResponse supplies the answer
122
+ * and cancelPending supplies null and a reason. Both await and classify native errors or timeout.
123
+ * A timeout ends the registry's wait and keeps observing late rejection. The adapter still owns
124
+ * completion and native resource cleanup.
125
+ */
126
+ private notifyNative;
127
+ }
128
+ type CancellationOutcome = "cancelled" | "expired" | "unknown";
129
+ export {};
@@ -0,0 +1,282 @@
1
+ // Owns native interaction capacity, expiry, settlement and response replay.
2
+ import { createHash } from "node:crypto";
3
+ import { assertInteractionResponse } from "@anureo/protocol";
4
+ import { HarnessSdkError, sdkError } from "../errors.js";
5
+ import { waitForRequest } from "../request.js";
6
+ /**
7
+ * Track native requests, settle their answers and retain bounded response replay state.
8
+ * Session uses this SDK implementation helper and supplies publication and privacy hooks.
9
+ * Direct consumers own those integrations and should await cancelAll with a teardown reason
10
+ * when releasing a session. The registry owns request expiry and native response wait timers.
11
+ */
12
+ export class InteractionRegistry {
13
+ publish;
14
+ limits;
15
+ beforeResponse;
16
+ /**
17
+ * Configure settlement publication and synchronous pre-response checks without starting native
18
+ * work. Session supplies a publisher that aborts the host handler and awaits publication of
19
+ * interaction/resolved while observation is open. It also supplies SecretRedactor.rememberResponse
20
+ * to register secrets before delivery. respond runs the check for every validated attempt, including
21
+ * replays, and propagates a throw before delivery. Settlement awaits publication without a deadline.
22
+ */
23
+ constructor(publish, limits, beforeResponse) {
24
+ this.publish = publish;
25
+ this.limits = limits;
26
+ this.beforeResponse = beforeResponse;
27
+ }
28
+ /**
29
+ * Count requests that consume admission capacity in begin, including native delivery and
30
+ * cancellation in flight. Capacity becomes available when a request is marked resolved.
31
+ */
32
+ get pendingCount() {
33
+ return [...this.records.values()].filter((record) => record.state !== "resolved").length;
34
+ }
35
+ /**
36
+ * List detached copies of actionable requests for manual answering. Cloning failures throw
37
+ * without changing registry state. These copies retain native fields, so callers own projection
38
+ * before exposing them to observers.
39
+ */
40
+ pending() {
41
+ return [...this.records.values()]
42
+ .filter((record) => record.state === "pending")
43
+ .map((record) => structuredClone(record.request));
44
+ }
45
+ /**
46
+ * Register a unique unexpired request and an unrefed expiry timer. Duplicate IDs, exhausted
47
+ * capacity and invalid/elapsed lifetimes throw synchronously. A request-cloning failure rejects
48
+ * the returned promise after the expiry timer is scheduled. On success, the promise resolves
49
+ * with the answer after native delivery and result publication.
50
+ * Cancellation and peer settlement reject it after publication is attempted. If native delivery
51
+ * fails and publishing its unknown outcome also fails, this waiter stays unresolved; respond
52
+ * rejects the publication failure instead.
53
+ * Session.requestInteraction adopts this promise after publishing interaction/requested and
54
+ * scheduling any host handler. Adapters supply the responder through that sink method and own
55
+ * the native resources it uses.
56
+ */
57
+ begin(request, responder) {
58
+ if (this.records.has(request.interactionId))
59
+ throw new HarnessSdkError("INTERACTION_ALREADY_EXISTS");
60
+ if (this.pendingCount >= this.limits.maxPending)
61
+ throw new HarnessSdkError("BACKPRESSURE_EXCEEDED", "Pending interaction limit exceeded");
62
+ const ttl = interactionTtl(request, this.limits.timeoutMs);
63
+ if (!Number.isFinite(ttl) || ttl <= 0)
64
+ throw new HarnessSdkError("INTERACTION_EXPIRED");
65
+ this.evictOldestResolvedRecord();
66
+ const result = new Promise((resolve, reject) => {
67
+ // Expiry has no caller to receive cancellation failure. Settlement owns the request waiter.
68
+ const timer = setTimeout(() => { void this.cancel(request.interactionId, "timeout", "expired").catch(() => { }); }, ttl);
69
+ timer.unref?.();
70
+ this.records.set(request.interactionId, { request: structuredClone(request), responder, state: "pending", timer, resolve, reject });
71
+ });
72
+ // An adapter can still attach its await after the event publication; retain rejection observation meanwhile.
73
+ void result.catch(() => { });
74
+ return result;
75
+ }
76
+ /**
77
+ * Deliver an answer or replay the retained operation for the same request ID and serialized
78
+ * response. Validation and the synchronous pre-response hook run before any replay or delivery.
79
+ * Missing IDs reject with INTERACTION_EXPIRED. Conflicting answers to settling or resolved
80
+ * requests reject with INTERACTION_ALREADY_RESOLVED. The operation clears expiry, retains
81
+ * capacity during native delivery, and settles after the publication it initiates. If another
82
+ * settlement wins during delivery, late native success rejects with RESULT_UNKNOWN.
83
+ * Session.invokeRequest returns the promise for manual requests. Session.startInteractionHandler
84
+ * awaits it for automatic answers and attempts handler_failed cancellation on rejection while
85
+ * its signal remains active.
86
+ */
87
+ async respond(id, requestId, response) {
88
+ const record = this.records.get(id);
89
+ if (!record)
90
+ throw new HarnessSdkError("INTERACTION_EXPIRED");
91
+ assertInteractionResponse(record.request, response);
92
+ this.beforeResponse?.(record.request, response);
93
+ const fingerprint = responseFingerprint(response);
94
+ const replay = record.responseReplay;
95
+ if (replay && replay.requestId === requestId && replay.fingerprint === fingerprint) {
96
+ return replay.operation;
97
+ }
98
+ if (record.state !== "pending")
99
+ throw new HarnessSdkError("INTERACTION_ALREADY_RESOLVED");
100
+ // The responder can reenter synchronously. Mark the request settling before invoking it.
101
+ startSettlement(record);
102
+ const operation = this.deliverResponse({ id, record, response });
103
+ record.responseReplay = { requestId, fingerprint, operation };
104
+ return operation;
105
+ }
106
+ /**
107
+ * Cancel or expire an unresolved request, notifying native once when pending. The reason and
108
+ * outcome both default to "cancelled". Missing IDs reject with INTERACTION_EXPIRED. Resolved IDs
109
+ * and in-flight settlement with reason "cancelled" reject with INTERACTION_ALREADY_RESOLVED.
110
+ * Other reasons mark in-flight settlement unknown without another native call. Native notification
111
+ * failure or timeout also changes the outcome to unknown.
112
+ * Await publication, reject begin's waiter with INTERACTION_CANCELLED or RESULT_UNKNOWN, and
113
+ * return the outcome. If publication fails, the cancellation caller receives that failure and
114
+ * the original waiter still receives its lifecycle error. Session.invokeRequest returns this
115
+ * promise for manual cancellation. Session.startInteractionHandler and the expiry timer observe
116
+ * and suppress its rejection. cancelAll collects its outcome during teardown.
117
+ */
118
+ async cancel(id, reason = "cancelled", outcome = "cancelled") {
119
+ const record = this.records.get(id);
120
+ if (!record)
121
+ throw new HarnessSdkError("INTERACTION_EXPIRED");
122
+ if (record.state === "resolved")
123
+ throw new HarnessSdkError("INTERACTION_ALREADY_RESOLVED");
124
+ if (record.state === "pending")
125
+ return this.cancelPending({ id, record, reason, outcome });
126
+ if (reason === "cancelled")
127
+ throw new HarnessSdkError("INTERACTION_ALREADY_RESOLVED");
128
+ record.state = "resolved";
129
+ clearTimeout(record.timer);
130
+ const result = { interactionId: id, outcome: "unknown", reason };
131
+ await this.publishRejectedSettlement({ record, result, error: endedInFlight() });
132
+ return result;
133
+ }
134
+ /**
135
+ * Attempt concurrent cancellation of unresolved interactions, optionally limited to turnId; await
136
+ * every attempt and suppress individual failures. Session.fail, Session.finishClose and
137
+ * Session.dispose await this before releasing the backend. Use a reason other than "cancelled"
138
+ * to settle in-flight requests. The literal reason "connection_lost" requests an unknown outcome.
139
+ */
140
+ async cancelAll(reason, turnId) {
141
+ const ids = [...this.records]
142
+ .filter(([, record]) => record.state !== "resolved" && (turnId === undefined || record.request.turnId === turnId))
143
+ .map(([id]) => id);
144
+ const outcome = reason === "connection_lost" ? "unknown" : "cancelled";
145
+ await Promise.allSettled(ids.map((id) => this.cancel(id, reason, outcome)));
146
+ }
147
+ /**
148
+ * Publish native-side settlement without calling the responder; ignore unknown/resolved IDs
149
+ * and reject the waiter even for responded because no value is supplied. Clear expiry and release
150
+ * capacity before publication; publication failure rejects this call but still rejects the waiter.
151
+ * An unknown outcome rejects the waiter with RESULT_UNKNOWN; other outcomes use INTERACTION_ALREADY_RESOLVED.
152
+ * Session.resolveInteraction returns this promise to adapters reporting an already-settled native request.
153
+ */
154
+ async resolveExternally(result) {
155
+ const record = this.records.get(result.interactionId);
156
+ if (!record || record.state === "resolved")
157
+ return;
158
+ record.state = "resolved";
159
+ clearTimeout(record.timer);
160
+ await this.publishRejectedSettlement({
161
+ record, result,
162
+ error: new HarnessSdkError(result.outcome === "unknown" ? "RESULT_UNKNOWN" : "INTERACTION_ALREADY_RESOLVED", "Interaction was resolved by the native peer", false, result.outcome === "unknown"),
163
+ });
164
+ }
165
+ records = new Map();
166
+ /**
167
+ * begin calls after admission checks, before inserting a new request. At the replay bound, evict
168
+ * only the oldest resolved record so unresolved native work and its capacity remain accounted for.
169
+ * Duplicate checks run first: a retained ID cannot be reused by evicting its own replay guard.
170
+ */
171
+ evictOldestResolvedRecord() {
172
+ const atReplayLimit = this.records.size >= this.limits.maxPending + 1024;
173
+ if (!atReplayLimit)
174
+ return;
175
+ for (const [id, record] of this.records) {
176
+ if (record.state === "resolved") {
177
+ this.records.delete(id);
178
+ return;
179
+ }
180
+ }
181
+ }
182
+ /**
183
+ * respond caches this operation after admission. Bound native delivery without retrying it, then
184
+ * publish before resolving begin's waiter. If delivery succeeds after another settlement resolves
185
+ * the record, reject with RESULT_UNKNOWN without publishing again. Normalize delivery/publication
186
+ * failures for both callers. If publishing the unknown outcome itself fails, that failure escapes
187
+ * before the original waiter is rejected. Cancellation uses publishRejectedSettlement's finally instead.
188
+ */
189
+ async deliverResponse({ id, record, response }) {
190
+ try {
191
+ await this.notifyNative(record, response);
192
+ if (record.state === "resolved")
193
+ throw endedInFlight();
194
+ const result = { interactionId: id, outcome: "responded" };
195
+ // Release capacity before publication, but never release the waiter before publication finishes.
196
+ record.state = "resolved";
197
+ await this.publish(result);
198
+ record.resolve(response);
199
+ return result;
200
+ }
201
+ catch (cause) {
202
+ // This also catches publication failures, not just responder failures.
203
+ const error = sdkError(cause, "RESULT_UNKNOWN", true);
204
+ if (record.state !== "resolved") {
205
+ record.state = "resolved";
206
+ await this.publish({ interactionId: id, outcome: "unknown", reason: error.code });
207
+ }
208
+ record.reject(error);
209
+ throw error;
210
+ }
211
+ }
212
+ /**
213
+ * cancel calls only for an actionable record. Hold capacity and clear expiry before notifying native;
214
+ * timeout or notification failure changes the requested outcome to unknown. Release capacity before
215
+ * publication, then reject begin's waiter even if publication fails. Once started, this attempt
216
+ * still publishes after its native wait, even if teardown or peer settlement has already published.
217
+ */
218
+ async cancelPending({ id, record, reason, outcome }) {
219
+ startSettlement(record);
220
+ try {
221
+ await this.notifyNative(record, null, reason);
222
+ }
223
+ catch {
224
+ outcome = "unknown";
225
+ }
226
+ record.state = "resolved";
227
+ const result = { interactionId: id, outcome, reason };
228
+ const outcomeUnknown = outcome === "unknown";
229
+ await this.publishRejectedSettlement({
230
+ record, result,
231
+ error: new HarnessSdkError(outcomeUnknown ? "RESULT_UNKNOWN" : "INTERACTION_CANCELLED", "Interaction is no longer actionable", false, outcomeUnknown),
232
+ });
233
+ return result;
234
+ }
235
+ /**
236
+ * Wait for publication before rejecting begin's waiter with its lifecycle error. A publication
237
+ * failure still reaches cancel, cancelPending or resolveExternally. Those callers clear expiry
238
+ * and release capacity before entering this step.
239
+ */
240
+ async publishRejectedSettlement({ record, result, error }) {
241
+ try {
242
+ await this.publish(result);
243
+ }
244
+ finally {
245
+ record.reject(error);
246
+ }
247
+ }
248
+ /**
249
+ * Bound one invocation of the adapter's registered callback. deliverResponse supplies the answer
250
+ * and cancelPending supplies null and a reason. Both await and classify native errors or timeout.
251
+ * A timeout ends the registry's wait and keeps observing late rejection. The adapter still owns
252
+ * completion and native resource cleanup.
253
+ */
254
+ notifyNative(record, ...args) {
255
+ return waitForRequest(() => record.responder(...args), { timeoutMs: this.limits.responseTimeoutMs ?? 30_000 });
256
+ }
257
+ }
258
+ /** Hold admission capacity and clear expiry before respond or cancelPending invokes the adapter. */
259
+ function startSettlement(record) {
260
+ record.state = "settling";
261
+ clearTimeout(record.timer);
262
+ }
263
+ /** Share the unknown-outcome error between in-flight teardown and a superseded response delivery. */
264
+ function endedInFlight() {
265
+ return new HarnessSdkError("RESULT_UNKNOWN", "Interaction ended while response was in flight", false, true);
266
+ }
267
+ /**
268
+ * InteractionRegistry.begin uses the returned duration before allocating its expiry timer. Clamp a
269
+ * nonempty request expiry to the configured interaction lifetime; omitted/empty expiry uses the full
270
+ * lifetime. begin rejects a non-finite or non-positive result before allocating its timer.
271
+ */
272
+ function interactionTtl(request, timeoutMs) {
273
+ return request.expiresAt ? Math.min(timeoutMs, new Date(request.expiresAt).getTime() - Date.now()) : timeoutMs;
274
+ }
275
+ /**
276
+ * InteractionRegistry.respond stores and compares this fingerprint before deciding whether to replay
277
+ * the response promise. Hash the serialized payload for replay detection; the request ID alone
278
+ * cannot distinguish conflicting retries.
279
+ */
280
+ function responseFingerprint(response) {
281
+ return createHash("sha256").update(JSON.stringify(response)).digest("hex");
282
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Validate the selected retention budget as a positive safe integer, using the caller's fallback
3
+ * for an omitted value. Throw INVALID_PARAMS on failure. assertClientOptions checks configured
4
+ * limits at construction. Session.context, getOrCreateSubmission and retainEvent use the returned
5
+ * budget for allocation or accounting in units chosen at each call site.
6
+ */
7
+ export declare function positive(value: number | undefined, fallback: number): number;
@@ -0,0 +1,13 @@
1
+ import { HarnessSdkError } from "../errors.js";
2
+ /**
3
+ * Validate the selected retention budget as a positive safe integer, using the caller's fallback
4
+ * for an omitted value. Throw INVALID_PARAMS on failure. assertClientOptions checks configured
5
+ * limits at construction. Session.context, getOrCreateSubmission and retainEvent use the returned
6
+ * budget for allocation or accounting in units chosen at each call site.
7
+ */
8
+ export function positive(value, fallback) {
9
+ const result = value ?? fallback;
10
+ if (!Number.isSafeInteger(result) || result <= 0)
11
+ throw new HarnessSdkError('INVALID_PARAMS');
12
+ return result;
13
+ }
@@ -0,0 +1,77 @@
1
+ import type { AhpEvent } from '@anureo/protocol';
2
+ import type { TurnResult } from "../types.js";
3
+ /**
4
+ * Session.context creates one accumulator per execution. Session.retainEvent supplies privacy-projected
5
+ * events in publication order and uses the terminal snapshot to resolve the promise read by Turn.result.
6
+ * Session owns event validation, publication, shared budgets and reclamation.
7
+ */
8
+ export declare class ResultAccumulator {
9
+ private readonly maxBytes;
10
+ private readonly maxItems;
11
+ /**
12
+ * Session.context supplies positive safe integers for serialized bytes and distinct items.
13
+ * Omitted session limits default to 16 MiB and 2,048 items before construction.
14
+ */
15
+ constructor(maxBytes: number, maxItems: number);
16
+ /**
17
+ * Return the UTF-8 JSON byte total of the individual items as if buffered text were materialized.
18
+ * Session.retainEvent uses the difference across an update for its shared budget, and
19
+ * Session.reclaimExecution subtracts the total on eviction. Usage and envelope bytes are excluded.
20
+ */
21
+ get size(): number;
22
+ /**
23
+ * Retain detached turn usage and item content before Session.retainEvent publishes the event.
24
+ * Snapshots replace buffered content and completed items ignore subsequent deltas. A conflicting
25
+ * patch throws PROTOCOL_ERROR. Exceeding either limit throws RESULT_BUDGET_EXCEEDED. These throws
26
+ * reject Session.emit and reach Session.fail, which rejects unfinished Turn.result waits.
27
+ * Serialization and clone failures also propagate to Session.emit.
28
+ */
29
+ accept(event: AhpEvent): void;
30
+ /**
31
+ * Build a detached, frozen snapshot from the current retention. Turn-scoped usage in the terminal
32
+ * event takes precedence over the latest retained turn usage. Final messages are joined with
33
+ * newlines in first-seen order. Failed results without native error details get TURN_FAILED.
34
+ * Session.retainEvent resolves the promise read by Turn.result with this snapshot for every native
35
+ * terminal status, then publishes the terminal event and ends the execution stream. If cloning or
36
+ * freezing throws, Session.emit rejects before the snapshot or event is delivered.
37
+ */
38
+ finish(payload: Extract<AhpEvent, {
39
+ type: 'turn/completed';
40
+ }>['payload']): TurnResult;
41
+ /**
42
+ * Reset retained items, completion markers, usage and bytes for reuse. Previously returned snapshots
43
+ * stay independent.
44
+ */
45
+ clear(): void;
46
+ private readonly items;
47
+ private readonly completed;
48
+ private bytes;
49
+ private usage?;
50
+ /**
51
+ * Apply a delta for an item without a completion marker. Snapshot patches take the replacement
52
+ * path. Text deltas buffer only their matching field on a retained item. Retention failures
53
+ * propagate to accept.
54
+ */
55
+ private acceptDelta;
56
+ /**
57
+ * acceptDelta passes patch values here after Session.publish has projected them.
58
+ * Only a patch object with a type discriminator can replace a snapshot. SecretRedactor.deltaValue
59
+ * validates its shape upstream. Throw PROTOCOL_ERROR on a missing base item or a mismatch in ID
60
+ * or family. A valid patch replaces the snapshot and discards its text buffer.
61
+ */
62
+ private applySnapshotPatch;
63
+ /**
64
+ * acceptDelta supplies a matching field and its snapshot value. Check the exact JSON byte change
65
+ * before updating the buffer or accounting. Throw RESULT_BUDGET_EXCEEDED without retaining the
66
+ * chunk on overflow. An empty chunk can establish an absent or non-string field. Preserve a
67
+ * buffered surrogate tail across empty chunks so a later chunk can complete the pair.
68
+ */
69
+ private append;
70
+ /**
71
+ * accept and applySnapshotPatch store detached snapshots here. Check capacity and clone before
72
+ * mutation. Budget failure throws RESULT_BUDGET_EXCEEDED and serialization or clone failures
73
+ * propagate. Replacing an existing entry discards its provisional buffer while preserving its
74
+ * first-seen position for finish.
75
+ */
76
+ private retainSnapshot;
77
+ }