@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,213 @@
1
+ import { HarnessSdkError } from "../errors.js";
2
+ import { immutable } from "./immutable.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 class ResultAccumulator {
9
+ maxBytes;
10
+ 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, maxItems) {
16
+ this.maxBytes = maxBytes;
17
+ this.maxItems = maxItems;
18
+ }
19
+ /**
20
+ * Return the UTF-8 JSON byte total of the individual items as if buffered text were materialized.
21
+ * Session.retainEvent uses the difference across an update for its shared budget, and
22
+ * Session.reclaimExecution subtracts the total on eviction. Usage and envelope bytes are excluded.
23
+ */
24
+ get size() { return this.bytes; }
25
+ /**
26
+ * Retain detached turn usage and item content before Session.retainEvent publishes the event.
27
+ * Snapshots replace buffered content and completed items ignore subsequent deltas. A conflicting
28
+ * patch throws PROTOCOL_ERROR. Exceeding either limit throws RESULT_BUDGET_EXCEEDED. These throws
29
+ * reject Session.emit and reach Session.fail, which rejects unfinished Turn.result waits.
30
+ * Serialization and clone failures also propagate to Session.emit.
31
+ */
32
+ accept(event) {
33
+ switch (event.type) {
34
+ case 'usage/updated':
35
+ if (event.payload.scope === 'turn')
36
+ this.usage = structuredClone(event.payload);
37
+ break;
38
+ case 'item/started':
39
+ case 'item/completed':
40
+ this.retainSnapshot(event.payload.item);
41
+ // Mark completion only after retention succeeds, so a rejected snapshot cannot suppress deltas.
42
+ if (event.type === 'item/completed')
43
+ this.completed.add(event.payload.item.id);
44
+ break;
45
+ case 'item/delta':
46
+ if (!this.completed.has(event.payload.itemId))
47
+ this.acceptDelta(event.payload);
48
+ break;
49
+ }
50
+ }
51
+ /**
52
+ * Build a detached, frozen snapshot from the current retention. Turn-scoped usage in the terminal
53
+ * event takes precedence over the latest retained turn usage. Final messages are joined with
54
+ * newlines in first-seen order. Failed results without native error details get TURN_FAILED.
55
+ * Session.retainEvent resolves the promise read by Turn.result with this snapshot for every native
56
+ * terminal status, then publishes the terminal event and ends the execution stream. If cloning or
57
+ * freezing throws, Session.emit rejects before the snapshot or event is delivered.
58
+ */
59
+ finish(payload) {
60
+ const items = [...this.items.values()].map(materializeItem);
61
+ const usage = payload.usage?.scope === 'turn' ? payload.usage : this.usage;
62
+ const common = { text: finalMessageText(items), items, ...(usage ? { usage } : {}) };
63
+ if (payload.status === 'failed') {
64
+ const error = payload.error ?? {
65
+ code: 'TURN_FAILED',
66
+ message: 'Turn failed',
67
+ retryable: false,
68
+ outcomeUnknown: false,
69
+ };
70
+ return immutable({ ...common, status: 'failed', error });
71
+ }
72
+ if (payload.status === 'interrupted') {
73
+ return immutable({ ...common, status: 'interrupted', ...(payload.error ? { error: payload.error } : {}) });
74
+ }
75
+ return immutable({ ...common, status: payload.status });
76
+ }
77
+ /**
78
+ * Reset retained items, completion markers, usage and bytes for reuse. Previously returned snapshots
79
+ * stay independent.
80
+ */
81
+ clear() {
82
+ this.items.clear();
83
+ this.completed.clear();
84
+ this.usage = undefined;
85
+ this.bytes = 0;
86
+ }
87
+ items = new Map();
88
+ completed = new Set();
89
+ bytes = 0;
90
+ usage;
91
+ /**
92
+ * Apply a delta for an item without a completion marker. Snapshot patches take the replacement
93
+ * path. Text deltas buffer only their matching field on a retained item. Retention failures
94
+ * propagate to accept.
95
+ */
96
+ acceptDelta({ itemId, kind, value, text }) {
97
+ const entry = this.items.get(itemId);
98
+ if (kind === 'patch') {
99
+ this.applySnapshotPatch(entry?.snapshot, value);
100
+ return;
101
+ }
102
+ if (!entry || text === undefined)
103
+ return;
104
+ const item = entry.snapshot;
105
+ if (item.type === 'agentMessage' && kind === 'text')
106
+ this.append(entry, 'text', item.text, text);
107
+ else if (item.type === 'reasoning' && kind === 'reasoningSummary')
108
+ this.append(entry, 'summary', item.summary, text);
109
+ else if (item.type === 'toolCall' && kind === 'output')
110
+ this.append(entry, 'output', item.output, text);
111
+ }
112
+ /**
113
+ * acceptDelta passes patch values here after Session.publish has projected them.
114
+ * Only a patch object with a type discriminator can replace a snapshot. SecretRedactor.deltaValue
115
+ * validates its shape upstream. Throw PROTOCOL_ERROR on a missing base item or a mismatch in ID
116
+ * or family. A valid patch replaces the snapshot and discards its text buffer.
117
+ */
118
+ applySnapshotPatch(current, value) {
119
+ if (!value || typeof value !== 'object' || Array.isArray(value) || !('type' in value))
120
+ return;
121
+ if (!current || value.id !== current.id || value.type !== current.type) {
122
+ throw new HarnessSdkError('PROTOCOL_ERROR', 'Conflicting item snapshot');
123
+ }
124
+ this.retainSnapshot(value);
125
+ }
126
+ /**
127
+ * acceptDelta supplies a matching field and its snapshot value. Check the exact JSON byte change
128
+ * before updating the buffer or accounting. Throw RESULT_BUDGET_EXCEEDED without retaining the
129
+ * chunk on overflow. An empty chunk can establish an absent or non-string field. Preserve a
130
+ * buffered surrogate tail across empty chunks so a later chunk can complete the pair.
131
+ */
132
+ append(entry, field, previous, chunk) {
133
+ const pending = entry.bufferedText;
134
+ if (!chunk.length && (pending || typeof previous === 'string'))
135
+ return;
136
+ const serializedByteDelta = serializedTextAppendByteDelta({ field, previous, chunk, bufferedText: pending });
137
+ if (this.bytes + serializedByteDelta > this.maxBytes)
138
+ throw new HarnessSdkError('RESULT_BUDGET_EXCEEDED');
139
+ const bufferedText = pending ?? {
140
+ field,
141
+ chunks: [typeof previous === 'string' ? previous : ''],
142
+ };
143
+ bufferedText.chunks.push(chunk);
144
+ if (chunk.length)
145
+ bufferedText.lastCodeUnit = chunk.charCodeAt(chunk.length - 1);
146
+ entry.bufferedText = bufferedText;
147
+ entry.serializedBytes += serializedByteDelta;
148
+ this.bytes += serializedByteDelta;
149
+ }
150
+ /**
151
+ * accept and applySnapshotPatch store detached snapshots here. Check capacity and clone before
152
+ * mutation. Budget failure throws RESULT_BUDGET_EXCEEDED and serialization or clone failures
153
+ * propagate. Replacing an existing entry discards its provisional buffer while preserving its
154
+ * first-seen position for finish.
155
+ */
156
+ retainSnapshot(item) {
157
+ const size = Buffer.byteLength(JSON.stringify(item));
158
+ const previous = this.items.get(item.id);
159
+ const bytes = this.bytes - (previous?.serializedBytes ?? 0) + size;
160
+ if (bytes > this.maxBytes || (!previous && this.items.size >= this.maxItems))
161
+ throw new HarnessSdkError('RESULT_BUDGET_EXCEEDED');
162
+ this.items.set(item.id, { snapshot: structuredClone(item), serializedBytes: size });
163
+ this.bytes = bytes;
164
+ }
165
+ }
166
+ /**
167
+ * Return the JSON byte change that append uses to check capacity, including the structural cost of
168
+ * establishing a text field and any surrogate pair formed with the end of the existing text.
169
+ */
170
+ function serializedTextAppendByteDelta({ field, previous, chunk, bufferedText }) {
171
+ const prefix = typeof previous === 'string' ? previous : '';
172
+ const lastCodeUnit = bufferedText ? bufferedText.lastCodeUnit : prefix.charCodeAt(prefix.length - 1);
173
+ const chunkContentBytes = Buffer.byteLength(JSON.stringify(chunk)) - 2; // Exclude string quotes.
174
+ const surrogatePairByteDelta = splitSurrogateByteAdjustment(lastCodeUnit, chunk);
175
+ let fieldStructureByteDelta = 0;
176
+ if (!bufferedText && typeof previous !== 'string') {
177
+ fieldStructureByteDelta = previous === undefined
178
+ ? Buffer.byteLength(JSON.stringify(field)) + 4 // Comma, colon and empty string quotes.
179
+ : 2 - Buffer.byteLength(JSON.stringify(previous)); // Replace the previous value with empty string quotes.
180
+ }
181
+ return chunkContentBytes + surrogatePairByteDelta + fieldStructureByteDelta;
182
+ }
183
+ /**
184
+ * ResultAccumulator.finish maps retained entries through this callback in first-seen item order.
185
+ * Overlay provisional text without consuming its buffer; finish still owns detaching/freezing the
186
+ * result, since entries without buffers return their retained snapshot directly.
187
+ */
188
+ function materializeItem({ snapshot, bufferedText }) {
189
+ return bufferedText
190
+ ? { ...snapshot, [bufferedText.field]: bufferedText.chunks.join('') }
191
+ : snapshot;
192
+ }
193
+ /**
194
+ * ResultAccumulator.finish derives host-visible text from materialized items. Keep final messages in
195
+ * retained order, including empty messages and their separators; interim messages remain in items.
196
+ */
197
+ function finalMessageText(items) {
198
+ return items
199
+ .filter((item) => item.type === 'agentMessage' && item.phase === 'final')
200
+ .map(item => item.text)
201
+ .join('\n');
202
+ }
203
+ /**
204
+ * Correct append accounting when a chunk joins a trailing high surrogate to a leading low surrogate.
205
+ * JSON replaces two isolated escapes (6 bytes each) with one UTF-8 pair (4 bytes); empty chunks and
206
+ * non-pair boundaries need no adjustment.
207
+ */
208
+ function splitSurrogateByteAdjustment(previousCodeUnit, chunk) {
209
+ const endsWithHighSurrogate = previousCodeUnit !== undefined && previousCodeUnit >= 0xd800 && previousCodeUnit <= 0xdbff;
210
+ const firstCodeUnit = chunk.charCodeAt(0);
211
+ const startsWithLowSurrogate = firstCodeUnit >= 0xdc00 && firstCodeUnit <= 0xdfff;
212
+ return endsWithHighSurrogate && startsWithLowSurrogate ? 4 - 2 * 6 : 0;
213
+ }
@@ -0,0 +1,4 @@
1
+ export { immutable } from "./immutable.js";
2
+ export { inputBlocks } from "./input.js";
3
+ export { positive } from "./limits.js";
4
+ export { deferred } from "./deferred.js";
@@ -0,0 +1,5 @@
1
+ // Compatibility re-exports for internal lifecycle helper imports.
2
+ export { immutable } from "./immutable.js";
3
+ export { inputBlocks } from "./input.js";
4
+ export { positive } from "./limits.js";
5
+ export { deferred } from "./deferred.js";
package/src/ports.d.ts ADDED
@@ -0,0 +1,224 @@
1
+ import type { AhpEventPayload, AhpMethod, AhpRequestMap, AhpResultMap, Capabilities, HarnessError, InitializeParams, InitializeResult, InteractionRequested, InteractionResponse, JsonSchema, SessionBinding, SessionOpenParams } from '@anureo/protocol';
2
+ import type { QueueLimits } from "./queue.js";
3
+ /**
4
+ * Deliver an interaction answer or cancellation to the native peer. Adapters supply this callback to
5
+ * the sink's requestInteraction. InteractionRegistry.deliverResponse invokes it with an answer;
6
+ * cancelPending invokes it with null and a reason on expiry/cancellation/teardown.
7
+ * Session.requestInteraction also calls it directly when rejecting closed, unsupported or handler
8
+ * capacity admission. Resolve once native delivery or cancellation is confirmed by the adapter.
9
+ * Reject delivery failures and mark uncertain outcomes so the SDK can report them without retrying
10
+ * a response that might already have been accepted.
11
+ */
12
+ export type NativeInteractionResponder = (response: InteractionResponse | null, reason?: string) => Promise<void>;
13
+ /** Session.request and BackendSession.execute use these methods; Client handles initialize/session-open separately through HarnessAdapter. */
14
+ export type SessionMethod = Exclude<AhpMethod, 'initialize' | 'session/open'>;
15
+ /** Supply the execution and item identities for a native event. IDs in scope and payload must agree. */
16
+ export interface EventScope {
17
+ /** Execution identity needed for item/usage events whose payload carries no turn ID. */
18
+ turnId?: string;
19
+ /** Optional item identity; must agree with an item payload's own ID. */
20
+ itemId?: string;
21
+ }
22
+ /**
23
+ * Publish events and settle interactions through the sink received by HarnessAdapter.openSession.
24
+ * OutputSession.open supplies adapter callbacks that route events through structured-output policy
25
+ * before delivering them to Session. Await publication calls to preserve source order. Report
26
+ * observation failures through fail and release native resources through the adapter's disposal.
27
+ */
28
+ export interface SessionEventSink {
29
+ /**
30
+ * Project final JSON synchronously for privacy. Session supplies this hook to
31
+ * OutputSession.publishOutput, which revalidates the returned text against the schema.
32
+ * Projection or validation failure turns the native success into a failed terminal result.
33
+ */
34
+ projectOutput?(text: string): string;
35
+ /**
36
+ * Publish an association between a submission and its native execution. Successful publication
37
+ * can resolve Session.submit before the native receipt. Invalid or conflicting bindings fail
38
+ * publication. PiBackendSession.beginTurn awaits this before processing later turn events.
39
+ */
40
+ bindSubmission(submissionId: string, turnId: string): Promise<void>;
41
+ /**
42
+ * Admit a detached event for ordered processing. Await this call before sending a dependent event.
43
+ * Structured-output policy can hold events until a binding arrives. Admission can throw or reject.
44
+ * Queued processing failures reject and end observation.
45
+ */
46
+ emit(event: AhpEventPayload, scope?: EventScope): Promise<void>;
47
+ /** Attempt event admission without waiting. False reports immediate failure. True allows later processing failure to end observation. */
48
+ tryEmit(event: AhpEventPayload, scope?: EventScope): boolean;
49
+ /**
50
+ * Register a native request and the callback that delivers its answer or cancellation. Session
51
+ * publishes interaction/requested before scheduling a configured host handler. Manual answers
52
+ * arrive through Session.request. Resolve with the answer after delivery and settlement
53
+ * publication. Admission failures, cancellation and expiry reject. If native delivery and
54
+ * publication of its unknown outcome both fail, the pending answer can remain unresolved.
55
+ * Keep native callback resources available through settlement or adapter teardown.
56
+ */
57
+ requestInteraction(request: InteractionRequested, responder: NativeInteractionResponder): Promise<InteractionResponse>;
58
+ /**
59
+ * Report settlement already completed by the native peer. Session publishes the result without
60
+ * invoking the responder and rejects the pending requestInteraction, including for a responded
61
+ * outcome, because there is no answer value. Unknown or resolved IDs are ignored. A publication
62
+ * failure rejects this call.
63
+ */
64
+ resolveInteraction(result: AhpResultMap['interaction/respond']): Promise<void>;
65
+ /**
66
+ * Report observation loss. The first call on an open Session ends observation, cancels pending
67
+ * interactions and attempts backend cleanup. Session suppresses backend disposal rejection.
68
+ * Native execution stop remains unconfirmed.
69
+ */
70
+ fail(cause: unknown): Promise<void>;
71
+ }
72
+ /**
73
+ * Inspect the candidate decision returned by StructuredOutputToolDefinition.execute to native tool
74
+ * adapters. OutputSession.submitCandidate checks and retains accepted candidates. Acceptance is
75
+ * provisional: final privacy/schema validation occurs at terminal publication. Rejection carries
76
+ * safe retry/terminal policy; infrastructure failure can reject the callback promise instead.
77
+ */
78
+ export type StructuredOutputToolResult = {
79
+ accepted: true;
80
+ } | {
81
+ accepted: false;
82
+ error: HarnessError;
83
+ };
84
+ /**
85
+ * Present this tool to the native agent and invoke execute for candidate calls. OutputSession.prepareOutput
86
+ * passes the definition to a native StructuredOutputToolPort before input dispatch. The adapter
87
+ * consumes acceptance or safe failure. Each callback captures its submission, so a late call cannot
88
+ * satisfy a replacement input. Native tool registration/cleanup remains the adapter's responsibility.
89
+ */
90
+ export interface StructuredOutputToolDefinition {
91
+ /** Native tool name supplied by the output wrapper; keep it stable when presenting the tool. */
92
+ name: string;
93
+ /** Model-facing instructions supplied by the output wrapper. */
94
+ description: string;
95
+ /** Candidate schema whose constraints the native tool presentation must preserve. */
96
+ schema: JsonSchema;
97
+ /** Native tool calls await candidate validation/retention; resolve a decision, or reject an infrastructure failure. Acceptance does not settle Turn.result. */
98
+ execute(value: unknown): Promise<StructuredOutputToolResult>;
99
+ }
100
+ /**
101
+ * Implement session-scoped native tool registration on a BackendSession. OutputSession.prepareOutput
102
+ * awaits installation before dispatch. OutputSession.completeTurn, reject and release disable the
103
+ * tool on completion, definite rejection or teardown. Finish changes before resolving.
104
+ */
105
+ export interface StructuredOutputToolPort {
106
+ /**
107
+ * OutputSession replaces the tool or disables it with undefined. Resolve when native registration
108
+ * changes complete; setup rejection prevents input dispatch, while teardown rejection reports cleanup failure.
109
+ */
110
+ configure(tool?: StructuredOutputToolDefinition): Promise<void>;
111
+ }
112
+ /**
113
+ * Return an owned native session from HarnessAdapter.openSession. OutputSession.open adopts it; Client then
114
+ * attaches the wrapper to Session. Session.executeRequest awaits wrapper execute and
115
+ * Session.disposeBackend awaits wrapper disposal, which releases the native backend. Receipt promises
116
+ * and sink events can settle independently; adapters must preserve that distinction.
117
+ */
118
+ export interface BackendSession {
119
+ /** Optional native tool registration used by OutputSession.prepareOutput when native outputSchema support is absent. */
120
+ readonly structuredOutputTool?: StructuredOutputToolPort;
121
+ /** Optional attachment snapshot; Session.attach restores its active execution observations after checking its binding. */
122
+ readonly state?: import('@anureo/protocol').SessionState;
123
+ /** Session.attach requires the requested session/adapter/configuration IDs, then adopts this binding's nativeRef. */
124
+ readonly binding: SessionBinding;
125
+ /** Session-specific capabilities adopted at attachment, after output projection; may differ from initialization capabilities. */
126
+ readonly capabilities: Capabilities;
127
+ /**
128
+ * OutputSession.execute invokes the native backend after policy setup and request validation.
129
+ * Resolve a native receipt/result or reject a safe failure with unknown-outcome metadata when
130
+ * acceptance is uncertain; neither wrapper nor Session automatically retries native dispatch.
131
+ */
132
+ execute<M extends SessionMethod>(method: M, params: AhpRequestMap[M]): Promise<AhpResultMap[M]>;
133
+ /** OutputSession.dispose awaits native handle release; implementations must be idempotent and reject cleanup failure without claiming execution stopped. */
134
+ dispose(): Promise<void>;
135
+ }
136
+ /**
137
+ * Implement native negotiation, session acquisition and client-wide cleanup for ClientOptions.adapter.
138
+ * Client invokes these operations through its structured-output wrapper. Release partially acquired
139
+ * native resources if setup fails. Be prepared to dispose after failed negotiation or while
140
+ * negotiation or a native open is still in flight, since a host can stop waiting first.
141
+ */
142
+ export interface HarnessAdapter {
143
+ /**
144
+ * Select a bridge transport reachable by the native session, or return undefined to disable MCP
145
+ * fallback. OutputSession.open calls this synchronously when initialization lacks native
146
+ * outputSchema support. Opt in when native open can consume session-scoped MCP configuration.
147
+ * A throw rejects session setup.
148
+ */
149
+ outputBridgeTransport?(params: SessionOpenParams): 'http' | 'stdio' | undefined;
150
+ /** Client.initialize awaits negotiation through its wrapper, validates the result and stores capabilities; reject incompatible contracts/native setup failure. */
151
+ initialize(params: InitializeParams): Promise<InitializeResult>;
152
+ /**
153
+ * OutputSession.open supplies detached params and forwarding sink callbacks. Resolve an owned
154
+ * backend; callbacks may run before it resolves. Reject setup failure after releasing partially
155
+ * acquired native resources; the wrapper/Session release backends that arrive after caller teardown.
156
+ */
157
+ openSession(params: SessionOpenParams, sink: SessionEventSink): Promise<BackendSession>;
158
+ /** The output wrapper invokes this after session cleanup attempts; release client-wide native resources or reject release failure. */
159
+ dispose(): Promise<void>;
160
+ }
161
+ /**
162
+ * Hosts supply these hooks/budgets through ClientOptions.runtime. Client consumes ID/wait policy;
163
+ * Session consumes publication, interaction and privacy policy. Hooks can perform host-owned effects,
164
+ * but do not transfer native execution ownership. Callback failures propagate through their invoking
165
+ * operation; hosts must arrange any cleanup needed by their callbacks.
166
+ */
167
+ export interface RuntimeOptions {
168
+ /**
169
+ * Generate nonempty, collision-free IDs synchronously; defaults to randomUUID. Client session helpers
170
+ * and Session request/stream allocation invoke this; allocation failures propagate to their callers.
171
+ */
172
+ createId?: () => string;
173
+ /**
174
+ * Supply public ISO timestamps for Session.publish. Defaults to new Date().toISOString().
175
+ * An invalid timestamp or throw fails publication. Interaction expiry uses wall-clock time.
176
+ */
177
+ now?: () => string;
178
+ /**
179
+ * Default wait for initialization, session requests, submission association and native interaction
180
+ * responders, in milliseconds. Defaults to 30_000. Per-call waits can override the operation
181
+ * wait. Turn.result has a separate 30-second default.
182
+ */
183
+ requestTimeoutMs?: number;
184
+ /**
185
+ * Default session attachment wait in milliseconds; defaults to 120_000. Per-call waits override it;
186
+ * abort/expiry does not abandon late backend cleanup.
187
+ */
188
+ openTimeoutMs?: number;
189
+ /**
190
+ * Bound actionable interaction lifetime in milliseconds. Defaults to 600_000.
191
+ * An earlier native expiry takes precedence.
192
+ */
193
+ interactionTimeoutMs?: number;
194
+ /**
195
+ * Separate caps for pending/settling interactions and live automatic handlers; each defaults to 32.
196
+ * Aborted host work retains its slot until settled.
197
+ */
198
+ maxPendingInteractions?: number;
199
+ /**
200
+ * Retained idempotency-outcome count per session; defaults to 1024.
201
+ * Session.admitRequestCapacity never evicts unsettled requests.
202
+ */
203
+ requestCacheSize?: number;
204
+ /**
205
+ * Set maxEvents, maxBytes and maxFrameBytes for session and structured-output event buffering.
206
+ * Their defaults are 2048 events, 16 MiB buffered and 8 MiB per frame. Observer queues use
207
+ * EventSubscriptionOptions.
208
+ */
209
+ queue?: QueueLimits;
210
+ /**
211
+ * Persist irreversible privacy metadata synchronously before accepting a new marked secret.
212
+ * Session registers this hook with SecretRedactor. SecretRedactor.rememberScalar invokes it before
213
+ * retaining a new marked value during InteractionRegistry.respond's pre-delivery check.
214
+ * A throw prevents native response delivery and leaves earlier persisted values in place.
215
+ */
216
+ onMarkedSecret?: (binding: Readonly<SessionBinding>, value: string) => void;
217
+ /**
218
+ * Apply host privacy metadata, including secrets retained across processes, to a native history page.
219
+ * Session.executeRequest invokes/awaits this after local history redaction, then validates/freezes
220
+ * the returned history/read page for its caller. Host I/O is allowed; throws/rejection fail the
221
+ * history request without SDK retries.
222
+ */
223
+ redactHistory?: (binding: Readonly<SessionBinding>, page: AhpResultMap["history/read"]) => AhpResultMap["history/read"] | Promise<AhpResultMap["history/read"]>;
224
+ }
package/src/ports.js ADDED
File without changes
@@ -0,0 +1,20 @@
1
+ import type { Item } from '@anureo/protocol';
2
+ /**
3
+ * boundMaterial caps available materials across every file in one tool-call item. Existing
4
+ * unavailable metadata does not consume this count; excess available materials are omitted.
5
+ */
6
+ export declare const FILE_CHANGE_MATERIAL_LIMIT = 50;
7
+ /**
8
+ * Per-item UTF-8 text budget used by boundFileChangeItem.
9
+ * Canonical material reserves bytes before mirrors; JSON keys and item metadata are outside the cap.
10
+ */
11
+ export declare const FILE_CHANGE_TEXT_LIMIT: number;
12
+ /**
13
+ * Bound a tool call's file-change evidence without mutating the input. Items without fileChanges
14
+ * and non-tool items pass through by identity. Canonical material consumes the shared byte budget
15
+ * before input, output, raw and extensions, in that order. Overflow sets x-anureo/fileChangesTruncated.
16
+ * Only loss of canonical material downgrades a change's completeness and existing stat accuracy.
17
+ * SecretRedactor.toolCall calls this after redaction, so live events and history get the same
18
+ * retention rules. Callers own protocol validation and publication.
19
+ */
20
+ export declare function boundFileChangeItem(item: Item): Item;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * boundMaterial caps available materials across every file in one tool-call item. Existing
3
+ * unavailable metadata does not consume this count; excess available materials are omitted.
4
+ */
5
+ export const FILE_CHANGE_MATERIAL_LIMIT = 50;
6
+ /**
7
+ * Per-item UTF-8 text budget used by boundFileChangeItem.
8
+ * Canonical material reserves bytes before mirrors; JSON keys and item metadata are outside the cap.
9
+ */
10
+ export const FILE_CHANGE_TEXT_LIMIT = 256 * 1024;
11
+ /**
12
+ * Bound a tool call's file-change evidence without mutating the input. Items without fileChanges
13
+ * and non-tool items pass through by identity. Canonical material consumes the shared byte budget
14
+ * before input, output, raw and extensions, in that order. Overflow sets x-anureo/fileChangesTruncated.
15
+ * Only loss of canonical material downgrades a change's completeness and existing stat accuracy.
16
+ * SecretRedactor.toolCall calls this after redaction, so live events and history get the same
17
+ * retention rules. Callers own protocol validation and publication.
18
+ */
19
+ export function boundFileChangeItem(item) {
20
+ if (item.type !== 'toolCall' || item.fileChanges === undefined)
21
+ return item;
22
+ const budget = {
23
+ bytes: FILE_CHANGE_TEXT_LIMIT,
24
+ materials: 0,
25
+ truncated: false,
26
+ encoder: new TextEncoder(),
27
+ };
28
+ const fileChanges = item.fileChanges.map(change => boundChange(change, budget));
29
+ const result = { ...item, fileChanges };
30
+ if (item.input !== undefined)
31
+ result.input = boundMirror(item.input, budget);
32
+ if (item.output !== undefined)
33
+ result.output = boundMirror(item.output, budget);
34
+ if (item.raw !== undefined)
35
+ result.raw = boundMirror(item.raw, budget);
36
+ if (item.extensions)
37
+ result.extensions = boundMirror(item.extensions, budget);
38
+ if (budget.truncated)
39
+ result.extensions = { ...result.extensions, 'x-anureo/fileChangesTruncated': true };
40
+ return result;
41
+ }
42
+ /**
43
+ * boundMaterial and boundMirror use this shared accounting rather than independent budgets. Return
44
+ * text within the remaining UTF-8 allowance and debit it. Overflow sets the sticky truncation flag
45
+ * and keeps only complete newline-terminated lines, dropping any split UTF-8 character with its line.
46
+ * Debit only the retained prefix, leaving unused bytes available to later material or mirrors.
47
+ */
48
+ function takeText(budget, value) {
49
+ const encoded = budget.encoder.encode(value);
50
+ if (encoded.byteLength <= budget.bytes) {
51
+ budget.bytes -= encoded.byteLength;
52
+ return { value, complete: true };
53
+ }
54
+ const decoded = new TextDecoder().decode(encoded.subarray(0, budget.bytes));
55
+ const prefix = decoded.slice(0, decoded.lastIndexOf('\n') + 1);
56
+ budget.bytes -= budget.encoder.encode(prefix).byteLength;
57
+ budget.truncated = true;
58
+ return { value: prefix, complete: false };
59
+ }
60
+ /**
61
+ * boundChange calls this in source order with the item-wide budget. Return retained material and a
62
+ * partial flag, omitting material beyond the count cap. An empty retained patch becomes unavailable.
63
+ * Replacement sides must both fit to remain usable. Their consumed bytes stay reserved even when
64
+ * the replacement becomes unavailable metadata.
65
+ */
66
+ function boundMaterial(material, budget) {
67
+ if (material.format === 'unavailable')
68
+ return { material, partial: false };
69
+ if (++budget.materials > FILE_CHANGE_MATERIAL_LIMIT) {
70
+ budget.truncated = true;
71
+ return { partial: true };
72
+ }
73
+ if (material.format === 'unified') {
74
+ const patch = takeText(budget, material.patch);
75
+ return {
76
+ material: patch.value ? { ...material, patch: patch.value } : { format: 'unavailable', reason: 'tooLarge' },
77
+ partial: !patch.complete,
78
+ };
79
+ }
80
+ const oldText = material.oldText === null ? null : takeText(budget, material.oldText);
81
+ const newText = takeText(budget, material.newText);
82
+ if ((oldText !== null && !oldText.complete) || !newText.complete) {
83
+ return { material: { format: 'unavailable', reason: 'tooLarge' }, partial: true };
84
+ }
85
+ return { material: { ...material, oldText: oldText === null ? null : oldText.value, newText: newText.value }, partial: false };
86
+ }
87
+ /**
88
+ * boundFileChangeItem maps canonical changes through this before traversing mirrors. Retain material
89
+ * in order and downgrade completeness/stat accuracy when a cap discards content; keep a metadata placeholder
90
+ * when all supplied material was omitted. Only the shared budget is mutated, never the source change.
91
+ */
92
+ function boundChange(change, budget) {
93
+ let partial = false;
94
+ const materials = [];
95
+ for (const material of change.materials) {
96
+ const retained = boundMaterial(material, budget);
97
+ partial ||= retained.partial;
98
+ if (retained.material)
99
+ materials.push(retained.material);
100
+ }
101
+ if (!materials.length && change.materials.length)
102
+ materials.push({ format: 'unavailable', reason: 'tooLarge' });
103
+ const result = { ...change, materials };
104
+ if (partial) {
105
+ result.completeness = 'partial';
106
+ if (change.stats)
107
+ result.stats = { ...change.stats, accuracy: 'partial' };
108
+ }
109
+ return result;
110
+ }
111
+ /**
112
+ * boundFileChangeItem invokes this for input, output, raw and extensions after canonical material.
113
+ * Return recursively bounded JSON without changing keys or scalar types. String values consume the
114
+ * remaining budget in traversal order; only the shared budget is mutated.
115
+ */
116
+ function boundMirror(value, budget) {
117
+ if (typeof value === 'string')
118
+ return takeText(budget, value).value;
119
+ if (Array.isArray(value))
120
+ return value.map(entry => boundMirror(entry, budget));
121
+ if (value && typeof value === 'object')
122
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, boundMirror(child, budget)]));
123
+ return value;
124
+ }