@anureo/sdk 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +162 -65
  2. package/package.json +29 -24
  3. package/src/adapter/identity.d.ts +41 -0
  4. package/src/adapter/identity.js +71 -0
  5. package/src/client-options.d.ts +71 -0
  6. package/src/client-options.js +0 -0
  7. package/src/client.d.ts +106 -0
  8. package/src/client.js +254 -0
  9. package/src/diagnostics/errors.d.ts +15 -0
  10. package/src/diagnostics/errors.js +59 -0
  11. package/src/diagnostics/events.d.ts +46 -0
  12. package/src/diagnostics/events.js +47 -0
  13. package/src/diagnostics.d.ts +20 -0
  14. package/src/diagnostics.js +56 -0
  15. package/src/errors.d.ts +48 -0
  16. package/src/errors.js +63 -0
  17. package/src/index.d.ts +27 -0
  18. package/src/index.js +23 -0
  19. package/src/lifecycle/approval.d.ts +16 -0
  20. package/src/lifecycle/approval.js +18 -0
  21. package/src/lifecycle/deferred.d.ts +10 -0
  22. package/src/lifecycle/deferred.js +12 -0
  23. package/src/lifecycle/errors.d.ts +18 -0
  24. package/src/lifecycle/errors.js +45 -0
  25. package/src/lifecycle/events.d.ts +35 -0
  26. package/src/lifecycle/events.js +141 -0
  27. package/src/lifecycle/execution-observation.d.ts +33 -0
  28. package/src/lifecycle/execution-observation.js +0 -0
  29. package/src/lifecycle/immutable.d.ts +12 -0
  30. package/src/lifecycle/immutable.js +28 -0
  31. package/src/lifecycle/input.d.ts +9 -0
  32. package/src/lifecycle/input.js +19 -0
  33. package/src/lifecycle/interactions.d.ts +129 -0
  34. package/src/lifecycle/interactions.js +282 -0
  35. package/src/lifecycle/limits.d.ts +7 -0
  36. package/src/lifecycle/limits.js +13 -0
  37. package/src/lifecycle/result.d.ts +77 -0
  38. package/src/lifecycle/result.js +213 -0
  39. package/src/lifecycle/value.d.ts +4 -0
  40. package/src/lifecycle/value.js +5 -0
  41. package/src/ports.d.ts +224 -0
  42. package/src/ports.js +0 -0
  43. package/src/privacy/file-change-limits.d.ts +20 -0
  44. package/src/privacy/file-change-limits.js +124 -0
  45. package/src/privacy/secrets.d.ts +205 -0
  46. package/src/privacy/secrets.js +564 -0
  47. package/src/process.d.ts +92 -0
  48. package/src/process.js +331 -0
  49. package/src/queue.d.ts +143 -0
  50. package/src/queue.js +266 -0
  51. package/src/request.d.ts +28 -0
  52. package/src/request.js +81 -0
  53. package/src/requests/guard.d.ts +15 -0
  54. package/src/requests/guard.js +93 -0
  55. package/src/session.d.ts +459 -0
  56. package/src/session.js +1135 -0
  57. package/src/structured-output/adapter.d.ts +8 -0
  58. package/src/structured-output/adapter.js +54 -0
  59. package/src/structured-output/bridge.d.ts +81 -0
  60. package/src/structured-output/bridge.js +274 -0
  61. package/src/structured-output/errors.d.ts +17 -0
  62. package/src/structured-output/errors.js +31 -0
  63. package/src/structured-output/limits.d.ts +19 -0
  64. package/src/structured-output/limits.js +0 -0
  65. package/src/structured-output/schema.d.ts +20 -0
  66. package/src/structured-output/schema.js +122 -0
  67. package/src/structured-output/session.d.ts +329 -0
  68. package/src/structured-output/session.js +846 -0
  69. package/src/structured-output/stdio.mjs +83 -0
  70. package/src/structured-output/tool.d.ts +38 -0
  71. package/src/structured-output/tool.js +34 -0
  72. package/src/turn.d.ts +75 -0
  73. package/src/turn.js +95 -0
  74. package/src/types.d.ts +62 -0
  75. package/src/types.js +0 -0
  76. package/dist/diagnostics.d.ts +0 -15
  77. package/dist/diagnostics.js +0 -34
  78. package/dist/errors.d.ts +0 -15
  79. package/dist/file-change-limits.d.ts +0 -5
  80. package/dist/identity.d.ts +0 -13
  81. package/dist/index.d.ts +0 -8
  82. package/dist/index.js +0 -1474
  83. package/dist/interactions.d.ts +0 -20
  84. package/dist/ports.d.ts +0 -66
  85. package/dist/process.d.ts +0 -34
  86. package/dist/process.js +0 -191
  87. package/dist/queue.d.ts +0 -36
  88. package/dist/queue.js +0 -166
  89. package/dist/runtime.d.ts +0 -85
  90. package/dist/secrets.d.ts +0 -28
@@ -0,0 +1,8 @@
1
+ import type { HarnessAdapter } from "../ports.js";
2
+ import type { OutputLimits } from "./limits.js";
3
+ /**
4
+ * Client's constructor installs this package-internal adapter wrapper without native I/O.
5
+ * The wrapper owns the native adapter, tracked output sessions and one lazy bridge manager.
6
+ * Client initializes before opening and awaits wrapper disposal after its Session cleanup.
7
+ */
8
+ export declare function withStructuredOutput(adapter: HarnessAdapter, limits?: OutputLimits): HarnessAdapter;
@@ -0,0 +1,54 @@
1
+ // Owns adapter-wide structured-output resources.
2
+ import { HarnessSdkError, sdkError } from "../errors.js";
3
+ import { OutputBridgeManager } from "./bridge.js";
4
+ import { checkRelease } from "./errors.js";
5
+ import { OutputSession } from "./session.js";
6
+ /**
7
+ * Client's constructor installs this package-internal adapter wrapper without native I/O.
8
+ * The wrapper owns the native adapter, tracked output sessions and one lazy bridge manager.
9
+ * Client initializes before opening and awaits wrapper disposal after its Session cleanup.
10
+ */
11
+ export function withStructuredOutput(adapter, limits = {}) {
12
+ const manager = new OutputBridgeManager(limits.queue?.maxFrameBytes);
13
+ const sessions = new Set();
14
+ let nativeOutput = false;
15
+ return {
16
+ /** Client.initialize awaits native negotiation through this port and validates its result. Remember native schema support for later opens and propagate negotiation failures. */
17
+ async initialize(params) {
18
+ const result = await adapter.initialize(params);
19
+ nativeOutput = result.capabilities.turn?.outputSchema === true;
20
+ return result;
21
+ },
22
+ /**
23
+ * Client.attachSession awaits the opened wrapper before attaching it. Track the session
24
+ * before native open so adapter disposal can see it during setup. If setup fails, await
25
+ * session cleanup before rejecting with the setup cause or a safe release failure.
26
+ */
27
+ async openSession(params, sink) {
28
+ const session = new OutputSession({ params, sink, manager, limits, forget: () => sessions.delete(session) });
29
+ sessions.add(session);
30
+ try {
31
+ return await session.open({ adapter, nativeOutput });
32
+ }
33
+ catch (cause) {
34
+ try {
35
+ await session.dispose();
36
+ }
37
+ catch {
38
+ throw new HarnessSdkError('RESOURCE_RELEASE_FAILED', 'Structured output setup cleanup failed', false, sdkError(cause).outcomeUnknown);
39
+ }
40
+ throw cause;
41
+ }
42
+ },
43
+ /**
44
+ * Client.dispose awaits this after attempting Session cleanup. Settle tracked output session
45
+ * disposals before starting bridge manager and native adapter disposal. Classify rejected
46
+ * release promises after those stages settle.
47
+ */
48
+ async dispose() {
49
+ const results = await Promise.allSettled([...sessions].map(session => session.dispose()));
50
+ results.push(...await Promise.allSettled([manager.dispose(), adapter.dispose()]));
51
+ checkRelease(results);
52
+ },
53
+ };
54
+ }
@@ -0,0 +1,81 @@
1
+ import { type CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import type { McpServer } from '@anureo/protocol';
3
+ export { OUTPUT_SERVER_NAME, OUTPUT_TOOL, toolError } from "./tool.js";
4
+ /** Internal endpoint and cleanup handle retained by OutputSession.prepareBridge. The manager owns the shared listener. */
5
+ export interface OutputBridgeLease {
6
+ /** Native-reachable config containing lease credentials. Keep it out of public state and history. */
7
+ readonly config: McpServer;
8
+ /**
9
+ * Revoke admission immediately, then await active protocol closes. Reject with
10
+ * RESOURCE_RELEASE_FAILED if any close rejects. Other leases remain usable. Repeated calls
11
+ * return without waiting for an earlier disposal or retrying cleanup.
12
+ */
13
+ dispose(): Promise<void>;
14
+ }
15
+ /**
16
+ * Internal bridge owner constructed by withStructuredOutput for one Client. OutputSession.prepareBridge
17
+ * requests independent session leases. The adapter wrapper must dispose this manager after session
18
+ * cleanup to release the shared listener, even when individual lease cleanup fails.
19
+ */
20
+ export declare class OutputBridgeManager {
21
+ private readonly maxFrameBytes;
22
+ /** Set the HTTP body limit in bytes (default 8 MiB). A create call starts the listener on demand. */
23
+ constructor(maxFrameBytes?: number);
24
+ /**
25
+ * Allocate an authenticated endpoint and native MCP configuration, awaiting shared listener startup.
26
+ * OutputSession.prepareBridge supplies a serialized submitValue handler, injects config into native
27
+ * open params and on setup failure releases any acquired lease before publishing a warning. Stdio
28
+ * entry resolution happens before listener startup. Resolution and startup failures reject.
29
+ * Observing manager disposal before lease allocation rejects with CLIENT_DISPOSED. Keep credentials
30
+ * private and dispose the lease after use.
31
+ */
32
+ create(transport: 'http' | 'stdio', handler: Handler): Promise<OutputBridgeLease>;
33
+ /**
34
+ * Close admission, settle active protocol closes, then stop the listener and its connections.
35
+ * withStructuredOutput's dispose port awaits this after attempting session disposal and classifies
36
+ * shutdown rejection with other resource failures. Individual protocol close rejections are ignored
37
+ * here. Listener shutdown errors reject. Repeated calls return without awaiting an earlier disposal.
38
+ */
39
+ dispose(): Promise<void>;
40
+ private listener?;
41
+ private starting?;
42
+ private closed;
43
+ private readonly leases;
44
+ /**
45
+ * OutputBridgeManager.create awaits this port before allocating a lease. Lazily start one
46
+ * loopback HTTP listener and cache its port promise, including startup or disposal-race rejection.
47
+ */
48
+ private listen;
49
+ /**
50
+ * The listener's request callback starts this operation without awaiting it. Admit, parse and dispatch
51
+ * one request. Contain failures with a 500 status when headers permit, then release its concurrency permit.
52
+ * Accounting ends when dispatch/error handling settles, independently of response-close cleanup.
53
+ */
54
+ private handleRequest;
55
+ /**
56
+ * Apply endpoint, credential, host/origin, POST and 32-request concurrency checks in that order.
57
+ * Write the rejection response and return no lease on failure. handleRequest reserves the permit
58
+ * synchronously after success. An absent or empty Origin is allowed for native clients.
59
+ */
60
+ private admitRequest;
61
+ /**
62
+ * Read one bounded JSON body for handleRequest. Return undefined after a 413 size rejection or a
63
+ * 400 parse rejection. Stream failures reject into handleRequest's HTTP error path. Parsing waits
64
+ * for the body to end, while an oversized body is rejected immediately.
65
+ */
66
+ private readRequestBody;
67
+ /**
68
+ * Connect a fresh stateless MCP server/transport and await its HTTP dispatch for handleRequest.
69
+ * Setup or dispatch rejections reach handleRequest's HTTP error path. Response close removes
70
+ * the server and attempts best-effort close. Lease and manager disposal also close tracked servers.
71
+ */
72
+ private dispatchRequest;
73
+ /**
74
+ * The returned lease callback delegates here when OutputSession.release or prepareBridge awaits
75
+ * disposal. Revoke this endpoint before awaiting closes, clear its server registry after settlement
76
+ * and reject RESOURCE_RELEASE_FAILED on any close rejection. Revocation is permanent even on failure.
77
+ */
78
+ private releaseLease;
79
+ }
80
+ /** OutputSession.prepareBridge supplies a serialized submitValue callback whose result becomes the tool response. */
81
+ type Handler = (args: unknown) => Promise<CallToolResult>;
@@ -0,0 +1,274 @@
1
+ // Owns the shared loopback listener, authenticated session leases and request-scoped MCP transports.
2
+ import { createServer } from 'node:http';
3
+ import { randomBytes } from 'node:crypto';
4
+ import { createRequire } from 'node:module';
5
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
6
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
7
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
8
+ import { OUTPUT_SERVER_NAME, OUTPUT_TOOL, toolError } from "./tool.js";
9
+ import { HarnessSdkError } from "../errors.js";
10
+ // Preserve existing bridge imports while production callers depend on the tool contract directly.
11
+ export { OUTPUT_SERVER_NAME, OUTPUT_TOOL, toolError } from "./tool.js";
12
+ /**
13
+ * Internal bridge owner constructed by withStructuredOutput for one Client. OutputSession.prepareBridge
14
+ * requests independent session leases. The adapter wrapper must dispose this manager after session
15
+ * cleanup to release the shared listener, even when individual lease cleanup fails.
16
+ */
17
+ export class OutputBridgeManager {
18
+ maxFrameBytes;
19
+ /** Set the HTTP body limit in bytes (default 8 MiB). A create call starts the listener on demand. */
20
+ constructor(maxFrameBytes = 8 * 1024 * 1024) {
21
+ this.maxFrameBytes = maxFrameBytes;
22
+ }
23
+ /**
24
+ * Allocate an authenticated endpoint and native MCP configuration, awaiting shared listener startup.
25
+ * OutputSession.prepareBridge supplies a serialized submitValue handler, injects config into native
26
+ * open params and on setup failure releases any acquired lease before publishing a warning. Stdio
27
+ * entry resolution happens before listener startup. Resolution and startup failures reject.
28
+ * Observing manager disposal before lease allocation rejects with CLIENT_DISPOSED. Keep credentials
29
+ * private and dispose the lease after use.
30
+ */
31
+ async create(transport, handler) {
32
+ if (this.closed)
33
+ throw new HarnessSdkError('CLIENT_DISPOSED');
34
+ const resolvedTransport = transport === 'stdio'
35
+ ? { type: 'stdio', proxy: createRequire(import.meta.url).resolve('@anureo/sdk/structured-output-stdio') }
36
+ : { type: transport };
37
+ const port = await this.listen();
38
+ if (this.closed)
39
+ throw new HarnessSdkError('CLIENT_DISPOSED');
40
+ const path = `/mcp/${randomBytes(24).toString('hex')}`;
41
+ const credential = randomBytes(32).toString('hex');
42
+ const lease = { credential, handler, active: new Set(), disposed: false, requests: 0 };
43
+ this.leases.set(path, lease);
44
+ const url = `http://127.0.0.1:${port}${path}`;
45
+ return {
46
+ config: bridgeConfig({ transport: resolvedTransport, url, credential }),
47
+ dispose: () => this.releaseLease(path, lease),
48
+ };
49
+ }
50
+ /**
51
+ * Close admission, settle active protocol closes, then stop the listener and its connections.
52
+ * withStructuredOutput's dispose port awaits this after attempting session disposal and classifies
53
+ * shutdown rejection with other resource failures. Individual protocol close rejections are ignored
54
+ * here. Listener shutdown errors reject. Repeated calls return without awaiting an earlier disposal.
55
+ */
56
+ async dispose() {
57
+ if (this.closed)
58
+ return;
59
+ this.closed = true;
60
+ for (const lease of this.leases.values()) {
61
+ lease.disposed = true;
62
+ await closeLeaseServers(lease);
63
+ }
64
+ this.leases.clear();
65
+ if (this.starting)
66
+ await this.starting.catch(() => { });
67
+ const listener = this.listener;
68
+ if (listener?.listening)
69
+ await stopListener(listener);
70
+ }
71
+ listener;
72
+ starting;
73
+ closed = false;
74
+ leases = new Map();
75
+ /**
76
+ * OutputBridgeManager.create awaits this port before allocating a lease. Lazily start one
77
+ * loopback HTTP listener and cache its port promise, including startup or disposal-race rejection.
78
+ */
79
+ listen() {
80
+ return this.starting ??= new Promise((resolve, reject) => {
81
+ const listener = this.listener = createServer((req, res) => {
82
+ void this.handleRequest(req, res, listener);
83
+ });
84
+ listener.requestTimeout = 30_000;
85
+ listener.headersTimeout = 10_000;
86
+ listener.once('error', reject);
87
+ listener.listen(0, '127.0.0.1', () => {
88
+ const address = listener.address();
89
+ if (!address || typeof address === 'string') {
90
+ reject(new HarnessSdkError('STRUCTURED_OUTPUT_BRIDGE_FAILED'));
91
+ return;
92
+ }
93
+ if (this.closed) {
94
+ listener.close();
95
+ reject(new HarnessSdkError('CLIENT_DISPOSED'));
96
+ return;
97
+ }
98
+ resolve(address.port);
99
+ });
100
+ });
101
+ }
102
+ /**
103
+ * The listener's request callback starts this operation without awaiting it. Admit, parse and dispatch
104
+ * one request. Contain failures with a 500 status when headers permit, then release its concurrency permit.
105
+ * Accounting ends when dispatch/error handling settles, independently of response-close cleanup.
106
+ */
107
+ handleRequest(req, res, listener) {
108
+ let countedLease;
109
+ // Keep accounting outside dispatch so an error response precedes permit release.
110
+ return (async () => {
111
+ const lease = this.admitRequest(req, res, listener);
112
+ if (!lease)
113
+ return;
114
+ lease.requests++;
115
+ countedLease = lease;
116
+ const body = await this.readRequestBody(req, res);
117
+ if (body === undefined)
118
+ return;
119
+ if (lease.disposed) {
120
+ res.writeHead(404).end();
121
+ return;
122
+ }
123
+ await this.dispatchRequest(req, res, body, lease);
124
+ })().catch(() => {
125
+ if (!res.headersSent)
126
+ res.writeHead(500);
127
+ res.end();
128
+ }).finally(() => {
129
+ if (countedLease)
130
+ countedLease.requests--;
131
+ });
132
+ }
133
+ /**
134
+ * Apply endpoint, credential, host/origin, POST and 32-request concurrency checks in that order.
135
+ * Write the rejection response and return no lease on failure. handleRequest reserves the permit
136
+ * synchronously after success. An absent or empty Origin is allowed for native clients.
137
+ */
138
+ admitRequest(req, res, listener) {
139
+ const lease = this.leases.get(req.url ?? '');
140
+ if (this.closed || !lease || lease.disposed) {
141
+ res.writeHead(404).end();
142
+ return;
143
+ }
144
+ if (req.headers.authorization !== `Bearer ${lease.credential}`) {
145
+ res.writeHead(401).end();
146
+ return;
147
+ }
148
+ const address = listener.address();
149
+ const expectedHost = typeof address === 'object' && address ? `127.0.0.1:${address.port}` : '';
150
+ const origin = req.headers.origin;
151
+ if (req.headers.host !== expectedHost || (origin && origin !== `http://${expectedHost}`)) {
152
+ res.writeHead(403).end();
153
+ return;
154
+ }
155
+ if (req.method !== 'POST') {
156
+ res.writeHead(405).end();
157
+ return;
158
+ }
159
+ if (lease.requests >= 32) {
160
+ res.writeHead(429).end();
161
+ return;
162
+ }
163
+ return lease;
164
+ }
165
+ /**
166
+ * Read one bounded JSON body for handleRequest. Return undefined after a 413 size rejection or a
167
+ * 400 parse rejection. Stream failures reject into handleRequest's HTTP error path. Parsing waits
168
+ * for the body to end, while an oversized body is rejected immediately.
169
+ */
170
+ async readRequestBody(req, res) {
171
+ const chunks = [];
172
+ let size = 0;
173
+ for await (const chunk of req) {
174
+ size += chunk.length;
175
+ if (size > this.maxFrameBytes) {
176
+ res.writeHead(413).end();
177
+ return;
178
+ }
179
+ chunks.push(Buffer.from(chunk));
180
+ }
181
+ try {
182
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
183
+ }
184
+ catch {
185
+ res.writeHead(400).end();
186
+ return;
187
+ }
188
+ }
189
+ /**
190
+ * Connect a fresh stateless MCP server/transport and await its HTTP dispatch for handleRequest.
191
+ * Setup or dispatch rejections reach handleRequest's HTTP error path. Response close removes
192
+ * the server and attempts best-effort close. Lease and manager disposal also close tracked servers.
193
+ */
194
+ async dispatchRequest(req, res, body, lease) {
195
+ const server = createToolServer(lease);
196
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
197
+ lease.active.add(server);
198
+ res.once('close', () => {
199
+ lease.active.delete(server);
200
+ void server.close().catch(() => { });
201
+ });
202
+ await server.connect(transport);
203
+ await transport.handleRequest(req, res, body);
204
+ }
205
+ /**
206
+ * The returned lease callback delegates here when OutputSession.release or prepareBridge awaits
207
+ * disposal. Revoke this endpoint before awaiting closes, clear its server registry after settlement
208
+ * and reject RESOURCE_RELEASE_FAILED on any close rejection. Revocation is permanent even on failure.
209
+ */
210
+ async releaseLease(path, lease) {
211
+ if (lease.disposed)
212
+ return;
213
+ lease.disposed = true;
214
+ this.leases.delete(path);
215
+ const results = await closeLeaseServers(lease);
216
+ lease.active.clear();
217
+ if (results.some(result => result.status === 'rejected'))
218
+ throw new HarnessSdkError('RESOURCE_RELEASE_FAILED');
219
+ }
220
+ }
221
+ /** Build native config for create using the resolved transport, with the bearer credential in headers or subprocess env. */
222
+ function bridgeConfig({ transport, url, credential }) {
223
+ if (transport.type === 'http') {
224
+ return { type: 'http', name: OUTPUT_SERVER_NAME, url, headers: { Authorization: `Bearer ${credential}` } };
225
+ }
226
+ return {
227
+ type: 'stdio',
228
+ name: OUTPUT_SERVER_NAME,
229
+ command: process.execPath,
230
+ args: [transport.proxy],
231
+ env: { ANUREO_OUTPUT_BRIDGE_URL: url, ANUREO_OUTPUT_BRIDGE_AUTH: credential },
232
+ };
233
+ }
234
+ /**
235
+ * Register MCP tool handlers for dispatchRequest before it connects the server. The SDK invokes
236
+ * them to list the shared tool or call the lease handler after checking revocation and tool name.
237
+ * The SDK handles handler rejections as protocol failures. dispatchRequest owns connection and tracking.
238
+ */
239
+ function createToolServer(lease) {
240
+ const server = new Server({ name: OUTPUT_SERVER_NAME, version: '1.0.0' }, { capabilities: { tools: {} } });
241
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [OUTPUT_TOOL] }));
242
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
243
+ if (lease.disposed)
244
+ return toolError('STRUCTURED_OUTPUT_EXPIRED');
245
+ if (request.params.name !== OUTPUT_TOOL.name)
246
+ return toolError('UNKNOWN_TOOL');
247
+ return lease.handler(request.params.arguments);
248
+ });
249
+ return server;
250
+ }
251
+ /**
252
+ * Snapshot and close every currently active server for releaseLease or manager disposal, settling all
253
+ * rejections so each caller can apply its own failure and registry policy. Callers revoke admission first.
254
+ */
255
+ function closeLeaseServers(lease) {
256
+ return Promise.allSettled([...lease.active].map(server => server.close()));
257
+ }
258
+ /**
259
+ * Stop a listening server after OutputBridgeManager.dispose settles protocol cleanup and startup.
260
+ * Force remaining connections closed so idle HTTP peers cannot hold disposal open. Resolve after
261
+ * listener closure, ignoring an already-stopped race, and reject other listener shutdown errors.
262
+ */
263
+ function stopListener(listener) {
264
+ const stopped = new Promise((resolve, reject) => {
265
+ listener.close(error => {
266
+ if (error && error.code !== 'ERR_SERVER_NOT_RUNNING')
267
+ reject(error);
268
+ else
269
+ resolve();
270
+ });
271
+ });
272
+ listener.closeAllConnections();
273
+ return stopped;
274
+ }
@@ -0,0 +1,17 @@
1
+ import type { HarnessError } from '@anureo/protocol';
2
+ /**
3
+ * Return a nonretryable, known-outcome protocol failure using the code as its message.
4
+ * OutputSession.bind retains binding conflicts, submitCandidate returns these failures on candidate
5
+ * rejection and publishOutput embeds them in failed terminals consumed by Session and Turn.result.
6
+ * Supply safe codes and JSON-compatible details. Details are retained by reference.
7
+ */
8
+ export declare function failure(code: string, details?: Record<string, unknown>): HarnessError;
9
+ /**
10
+ * withStructuredOutput.dispose and OutputSession.release/dispose call this after owned release stages
11
+ * settle. Return void when all attempts fulfilled, throw the single normalized failure, or aggregate
12
+ * multiple failures with flattened release diagnostics. The aggregate outcome is unknown if any
13
+ * normalized cause has an unknown outcome. sdkError preserves existing SDK errors.
14
+ * Callers still own every cleanup
15
+ * attempt and decide how the resulting rejection reaches Client or Session disposal consumers.
16
+ */
17
+ export declare function checkRelease(results: PromiseSettledResult<unknown>[]): void;
@@ -0,0 +1,31 @@
1
+ import { HarnessSdkError, sdkError } from "../errors.js";
2
+ /**
3
+ * Return a nonretryable, known-outcome protocol failure using the code as its message.
4
+ * OutputSession.bind retains binding conflicts, submitCandidate returns these failures on candidate
5
+ * rejection and publishOutput embeds them in failed terminals consumed by Session and Turn.result.
6
+ * Supply safe codes and JSON-compatible details. Details are retained by reference.
7
+ */
8
+ export function failure(code, details) {
9
+ return { code, message: code, retryable: false, outcomeUnknown: false, ...(details ? { details: details } : {}) };
10
+ }
11
+ /**
12
+ * withStructuredOutput.dispose and OutputSession.release/dispose call this after owned release stages
13
+ * settle. Return void when all attempts fulfilled, throw the single normalized failure, or aggregate
14
+ * multiple failures with flattened release diagnostics. The aggregate outcome is unknown if any
15
+ * normalized cause has an unknown outcome. sdkError preserves existing SDK errors.
16
+ * Callers still own every cleanup
17
+ * attempt and decide how the resulting rejection reaches Client or Session disposal consumers.
18
+ */
19
+ export function checkRelease(results) {
20
+ const causes = results
21
+ .filter((result) => result.status === 'rejected')
22
+ .map(result => sdkError(result.reason, 'RESOURCE_RELEASE_FAILED', true));
23
+ if (causes.length === 0)
24
+ return;
25
+ if (causes.length === 1)
26
+ throw causes[0];
27
+ const errors = causes.flatMap(error => error.code === 'RESOURCE_RELEASE_FAILED' && Array.isArray(error.details?.errors)
28
+ ? error.details.errors
29
+ : [{ code: error.code, outcomeUnknown: error.outcomeUnknown }]);
30
+ throw new HarnessSdkError('RESOURCE_RELEASE_FAILED', 'Structured output resources could not be released', false, causes.some(error => error.outcomeUnknown), { errors });
31
+ }
@@ -0,0 +1,19 @@
1
+ import type { QueueLimits } from "../queue.js";
2
+ /**
3
+ * Client passes event and retention budgets through withStructuredOutput to each OutputSession.
4
+ * Client validates scalar retention limits during construction. The wrapper also uses the frame
5
+ * budget when constructing the bridge manager. Omitted values use the consuming operation's defaults.
6
+ */
7
+ export interface OutputLimits {
8
+ /**
9
+ * Queued plus held output events: maxEvents defaults to 2048 and maxBytes to 16 MiB.
10
+ * maxFrameBytes defaults to 8 MiB per event, transformed request or bridge request body.
11
+ */
12
+ queue?: QueueLimits;
13
+ /** Separate count caps for retained output submissions and terminal-version markers, each defaulting to 1024. */
14
+ maxSubmissions?: number;
15
+ /** Serialized UTF-8 byte limit for session-wide output retention and for each candidate, defaulting to 16 MiB. */
16
+ maxResultBytes?: number;
17
+ /** Distinct text-bearing item cap within each structured submission, defaulting to 2048. */
18
+ maxItems?: number;
19
+ }
File without changes
@@ -0,0 +1,20 @@
1
+ import type { JsonSchema } from '@anureo/protocol';
2
+ /**
3
+ * Prepare a detached schema and synchronous candidate checks without network I/O or source mutation.
4
+ * This package-internal compiler defaults to draft-07, supports explicit 2020-12 and ignores
5
+ * formats during value checks. Unsupported or invalid definitions throw INVALID_PARAMS. Cloning
6
+ * failures propagate directly. OutputSession.prepareOutput compiles before retaining submission state
7
+ * or dispatching input. The returned checks require no disposal.
8
+ */
9
+ export declare function compileOutputSchema(source: JsonSchema): {
10
+ schema: JsonSchema;
11
+ check: (value: unknown) => void;
12
+ /**
13
+ * Decode and check JSON, throwing STRUCTURED_OUTPUT_MISSING for blank text,
14
+ * STRUCTURED_OUTPUT_NOT_JSON for malformed JSON or check's invalid-value error for a mismatch.
15
+ * OutputSession.outputValue parses native finals; publishOutput rechecks projected JSON so
16
+ * privacy redaction cannot bypass schema constraints. publishOutput turns parse failures into a
17
+ * failed terminal, using STRUCTURED_OUTPUT_REDACTED when the projected JSON fails revalidation.
18
+ */
19
+ parse(text: string): unknown;
20
+ };
@@ -0,0 +1,122 @@
1
+ // Owns synchronous local schema compilation and value checks.
2
+ import Ajv from 'ajv';
3
+ import Ajv2020 from 'ajv/dist/2020.js';
4
+ import { HarnessSdkError } from "../errors.js";
5
+ /**
6
+ * Prepare a detached schema and synchronous candidate checks without network I/O or source mutation.
7
+ * This package-internal compiler defaults to draft-07, supports explicit 2020-12 and ignores
8
+ * formats during value checks. Unsupported or invalid definitions throw INVALID_PARAMS. Cloning
9
+ * failures propagate directly. OutputSession.prepareOutput compiles before retaining submission state
10
+ * or dispatching input. The returned checks require no disposal.
11
+ */
12
+ export function compileOutputSchema(source) {
13
+ const schema = structuredClone(source);
14
+ const validate = compileValidator(schema);
15
+ /**
16
+ * Check a value without coercion or default insertion. Throw STRUCTURED_OUTPUT_INVALID with at
17
+ * most sixteen path/keyword issues on mismatch. OutputSession.submitCandidate converts failures
18
+ * into retry decisions. OutputSession.publishOutput converts them into failed terminals before serialization.
19
+ */
20
+ const check = (value) => {
21
+ if (validate(value))
22
+ return;
23
+ const issues = validate.errors?.slice(0, 16).map(error => ({ path: error.instancePath, code: error.keyword })) ?? [];
24
+ throw new HarnessSdkError('STRUCTURED_OUTPUT_INVALID', 'Output does not match its schema', false, false, { issues });
25
+ };
26
+ return {
27
+ schema,
28
+ check,
29
+ /**
30
+ * Decode and check JSON, throwing STRUCTURED_OUTPUT_MISSING for blank text,
31
+ * STRUCTURED_OUTPUT_NOT_JSON for malformed JSON or check's invalid-value error for a mismatch.
32
+ * OutputSession.outputValue parses native finals; publishOutput rechecks projected JSON so
33
+ * privacy redaction cannot bypass schema constraints. publishOutput turns parse failures into a
34
+ * failed terminal, using STRUCTURED_OUTPUT_REDACTED when the projected JSON fails revalidation.
35
+ */
36
+ parse(text) {
37
+ if (!text.trim())
38
+ throw new HarnessSdkError('STRUCTURED_OUTPUT_MISSING');
39
+ let value;
40
+ try {
41
+ value = JSON.parse(text);
42
+ }
43
+ catch {
44
+ throw new HarnessSdkError('STRUCTURED_OUTPUT_NOT_JSON');
45
+ }
46
+ check(value);
47
+ return value;
48
+ },
49
+ };
50
+ }
51
+ /**
52
+ * Compile an already cloned schema for compileOutputSchema. Normalize definition and compilation
53
+ * failures to INVALID_PARAMS, leaving an existing HarnessSdkError intact.
54
+ */
55
+ function compileValidator(schema) {
56
+ try {
57
+ validateSchemaNode(schema, 0);
58
+ const engine = schemaEngine(schema);
59
+ const ajv = new engine({ allErrors: true, strict: false, validateFormats: false, multipleOfPrecision: 6, logger: false });
60
+ return ajv.compile(schema);
61
+ }
62
+ catch (cause) {
63
+ if (cause instanceof HarnessSdkError)
64
+ throw cause;
65
+ throw new HarnessSdkError('INVALID_PARAMS', 'Unsupported or invalid output schema');
66
+ }
67
+ }
68
+ /**
69
+ * Choose the Ajv implementation for the root dialect after traversal. An absent dialect uses
70
+ * draft-07, including for boolean schemas. compileValidator normalizes unsupported dialect errors.
71
+ */
72
+ function schemaEngine(schema) {
73
+ const dialect = typeof schema === 'object' ? schema.$schema : undefined;
74
+ switch (dialect) {
75
+ case undefined:
76
+ case 'http://json-schema.org/draft-07/schema#':
77
+ return Ajv;
78
+ case 'https://json-schema.org/draft/2020-12/schema':
79
+ return Ajv2020;
80
+ default:
81
+ throw new Error('dialect');
82
+ }
83
+ }
84
+ /**
85
+ * Reject non-fragment references and async schemas at visited nodes, and traversal beyond 128
86
+ * levels, for compileValidator. Follow the supported child keywords so example/default instance data
87
+ * stays opaque. Check depth even for absent or scalar children to preserve the traversal bound.
88
+ */
89
+ function validateSchemaNode(node, depth) {
90
+ if (depth > 128)
91
+ throw new Error('depth');
92
+ if (!node || typeof node !== 'object')
93
+ return;
94
+ const value = node;
95
+ assertLocalReference(value.$ref);
96
+ assertLocalReference(value.$dynamicRef);
97
+ if (value.$async)
98
+ throw new Error('async schema');
99
+ const visitChild = (child) => validateSchemaNode(child, depth + 1);
100
+ for (const key of ['properties', 'patternProperties', 'definitions', '$defs', 'dependentSchemas']) {
101
+ const children = value[key];
102
+ if (children && typeof children === 'object')
103
+ Object.values(children).forEach(visitChild);
104
+ }
105
+ for (const key of ['allOf', 'anyOf', 'oneOf', 'prefixItems']) {
106
+ const children = value[key];
107
+ if (Array.isArray(children))
108
+ children.forEach(visitChild);
109
+ }
110
+ for (const key of ['items', 'contains', 'additionalProperties', 'additionalItems', 'propertyNames', 'not', 'if', 'then', 'else']) {
111
+ const child = value[key];
112
+ if (Array.isArray(child))
113
+ child.forEach(visitChild);
114
+ else
115
+ visitChild(child);
116
+ }
117
+ }
118
+ /** Restrict string references to local fragments during traversal. Ajv checks malformed keyword values. */
119
+ function assertLocalReference(reference) {
120
+ if (typeof reference === 'string' && !reference.startsWith('#'))
121
+ throw new Error('external reference');
122
+ }