@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.
- package/LICENSE +201 -0
- package/README.md +185 -2
- package/package.json +66 -4
- 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
|
@@ -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
|
+
}
|