@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,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
|
+
}
|
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
|
+
}
|