@miao-ai/client 0.1.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/dist/client.js ADDED
@@ -0,0 +1,664 @@
1
+ import { RUNTIME_PROTOCOL_VERSION, } from "@miao-ai/protocol";
2
+ import { BoundedAsyncQueue } from "./async-queue.js";
3
+ import { BackpressureError, HostCapabilityError, HostCapabilityTimeoutError, MiaoError, ProtocolError, ProtocolVersionError, RequestCancelledError, RequestTimeoutError, TransportError, UnsupportedOperationError, asTransportError, errorFromRuntime, } from "./errors.js";
4
+ import { validateSchema, } from "./host-capability.js";
5
+ const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
6
+ const DEFAULT_MAX_PENDING_REQUESTS = 256;
7
+ const DEFAULT_EVENT_BUFFER_SIZE = 256;
8
+ const DEFAULT_MAX_HOST_CALLS = 32;
9
+ const DEFAULT_REMEMBERED_HOST_CALLS = 1_024;
10
+ export class MiaoClient {
11
+ transport;
12
+ runtime;
13
+ apps;
14
+ agents;
15
+ runs;
16
+ approvals;
17
+ capabilities;
18
+ state;
19
+ memory;
20
+ artifacts;
21
+ scheduler;
22
+ skills;
23
+ mcp;
24
+ plugins;
25
+ bindings;
26
+ messages;
27
+ delegations;
28
+ #descriptor;
29
+ #state = "disconnected";
30
+ #generation = 0;
31
+ #requestCounter = 0;
32
+ #pending = new Map();
33
+ #subscriptions = new Map();
34
+ #orphanEvents = new Map();
35
+ #capabilities = new Map();
36
+ #hostResults = new Map();
37
+ #activeHostCalls = new Map();
38
+ #defaultTimeoutMs;
39
+ #maxPendingRequests;
40
+ #eventBufferSize;
41
+ #maxConcurrentHostCalls;
42
+ #maxRememberedHostCalls;
43
+ #restoreCapabilities;
44
+ #schemaValidator;
45
+ #autoDescribe;
46
+ constructor(options) {
47
+ this.transport = options.transport;
48
+ this.#defaultTimeoutMs = positive(options.defaultTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS, "defaultTimeoutMs");
49
+ this.#maxPendingRequests = positive(options.maxPendingRequests, DEFAULT_MAX_PENDING_REQUESTS, "maxPendingRequests");
50
+ this.#eventBufferSize = positive(options.eventBufferSize, DEFAULT_EVENT_BUFFER_SIZE, "eventBufferSize");
51
+ this.#maxConcurrentHostCalls = positive(options.maxConcurrentHostCalls, DEFAULT_MAX_HOST_CALLS, "maxConcurrentHostCalls");
52
+ this.#maxRememberedHostCalls = positive(options.maxRememberedHostCalls, DEFAULT_REMEMBERED_HOST_CALLS, "maxRememberedHostCalls");
53
+ this.#restoreCapabilities = options.restoreCapabilities ?? false;
54
+ this.#schemaValidator = options.schemaValidator;
55
+ this.#autoDescribe = options.autoDescribe ?? true;
56
+ this.runtime = { describe: (requestOptions) => this.describe(requestOptions) };
57
+ this.apps = {
58
+ register: (manifest, requestOptions) => this.request("apps.register", { manifest }, requestOptions),
59
+ list: (requestOptions) => this.request("apps.list", {}, requestOptions),
60
+ };
61
+ this.agents = {
62
+ list: (appId, requestOptions) => this.request("agents.list", { appId }, requestOptions),
63
+ launch: (input, requestOptions) => this.request("agents.launch", input, requestOptions),
64
+ };
65
+ this.runs = {
66
+ start: (input, requestOptions) => this.request("runs.start", input, requestOptions),
67
+ get: (runId, requestOptions) => this.request("runs.get", { runId }, requestOptions),
68
+ pause: (runId, requestOptions) => this.request("runs.pause", { runId }, requestOptions),
69
+ resume: (runId, requestOptions) => this.request("runs.resume", { runId }, requestOptions),
70
+ cancel: (runId, requestOptions) => this.request("runs.cancel", { runId }, requestOptions),
71
+ signal: (runId, name, payload = null, requestOptions) => this.request("runs.signal", { runId, name, payload }, requestOptions),
72
+ events: (runId, eventOptions) => this.#events(runId, eventOptions),
73
+ subscribe: (runId, listener, eventOptions) => this.#subscribe(runId, listener, eventOptions),
74
+ };
75
+ this.approvals = {
76
+ list: (requestOptions) => this.request("approvals.list", {}, requestOptions),
77
+ decide: (toolCallId, decision, reason, requestOptions) => this.request("approvals.decide", { toolCallId, decision, ...(reason === undefined ? {} : { reason }) }, requestOptions),
78
+ approve: (toolCallId, requestOptions) => this.request("approvals.decide", { toolCallId, decision: "approved" }, requestOptions),
79
+ deny: (toolCallId, input, requestOptions) => this.request("approvals.decide", { toolCallId, decision: "denied", ...input }, requestOptions),
80
+ cancel: (toolCallId, input, requestOptions) => this.request("approvals.decide", { toolCallId, decision: "cancelled", ...input }, requestOptions),
81
+ };
82
+ this.capabilities = {
83
+ register: (definition, requestOptions) => this.#registerCapability(definition, requestOptions),
84
+ renew: (address, leaseMs, requestOptions) => this.#renewCapability(address, leaseMs, requestOptions),
85
+ unregister: (address, requestOptions) => this.#unregisterCapability(address, requestOptions),
86
+ };
87
+ this.state = {
88
+ get: (appId, key, requestOptions) => this.request("state.get", { appId, key }, requestOptions),
89
+ put: (appId, key, value, expectedVersion, requestOptions) => this.request("state.put", { appId, key, value, ...(expectedVersion === undefined ? {} : { expectedVersion }) }, requestOptions),
90
+ };
91
+ this.memory = {
92
+ list: (appId, query, requestOptions) => this.request("memory.list", { appId, ...(query === undefined ? {} : { query }) }, requestOptions),
93
+ remember: (appId, memory, requestOptions) => this.request("memory.remember", { appId, memory }, requestOptions),
94
+ forget: (appId, memoryId, requestOptions) => this.request("memory.forget", { appId, memoryId }, requestOptions),
95
+ };
96
+ this.artifacts = {
97
+ list: (appId, runId, requestOptions) => this.request("artifacts.list", { appId, runId }, requestOptions),
98
+ get: (appId, artifactId, requestOptions) => this.request("artifacts.get", { appId, artifactId }, requestOptions),
99
+ };
100
+ this.scheduler = {
101
+ list: (status, requestOptions) => this.request("scheduler.list", status === undefined ? {} : { status }, requestOptions),
102
+ create: (input, requestOptions) => this.request("scheduler.create", input, requestOptions),
103
+ pause: (jobId, requestOptions) => this.request("scheduler.pause", { jobId }, requestOptions),
104
+ resume: (jobId, requestOptions) => this.request("scheduler.resume", { jobId }, requestOptions),
105
+ delete: (jobId, requestOptions) => this.request("scheduler.delete", { jobId }, requestOptions),
106
+ history: (jobId, requestOptions) => this.request("scheduler.history", jobId === undefined ? {} : { jobId }, requestOptions),
107
+ };
108
+ this.skills = {
109
+ list: (requestOptions) => this.request("skills.list", {}, requestOptions),
110
+ reload: (requestOptions) => this.request("skills.reload", {}, requestOptions),
111
+ };
112
+ this.mcp = {
113
+ list: (requestOptions) => this.request("mcp.list", {}, requestOptions),
114
+ reload: (requestOptions) => this.request("mcp.reload", {}, requestOptions),
115
+ };
116
+ this.plugins = {
117
+ list: (requestOptions) => this.request("plugins.list", {}, requestOptions),
118
+ install: (source, requestOptions) => this.request("plugins.install", { source }, requestOptions),
119
+ setEnabled: (pluginId, enabled, requestOptions) => this.request("plugins.set_enabled", { pluginId, enabled }, requestOptions),
120
+ enable: (pluginId, requestOptions) => this.request("plugins.set_enabled", { pluginId, enabled: true }, requestOptions),
121
+ disable: (pluginId, requestOptions) => this.request("plugins.set_enabled", { pluginId, enabled: false }, requestOptions),
122
+ };
123
+ this.bindings = {
124
+ list: (appId, requestOptions) => this.request("bindings.list", { appId }, requestOptions),
125
+ create: (input, requestOptions) => this.request("bindings.create", input, requestOptions),
126
+ };
127
+ this.messages = { deliver: (message, requestOptions) => this.request("messages.deliver", message, requestOptions) };
128
+ this.delegations = { create: (delegation, requestOptions) => this.request("delegations.create", delegation, requestOptions) };
129
+ }
130
+ get descriptor() {
131
+ return this.#descriptor;
132
+ }
133
+ get connected() {
134
+ return this.#state === "connected";
135
+ }
136
+ async connect() {
137
+ if (this.#state === "connected")
138
+ return this;
139
+ if (this.#state !== "disconnected")
140
+ throw new TransportError(`cannot connect while client is ${this.#state}`);
141
+ this.#state = "connecting";
142
+ const generation = ++this.#generation;
143
+ try {
144
+ await this.transport.connect();
145
+ this.#state = "connected";
146
+ void this.#consumeFrames(generation);
147
+ if (this.#autoDescribe)
148
+ await this.describe();
149
+ if (this.#restoreCapabilities)
150
+ await this.#restoreCapabilityRegistrations();
151
+ return this;
152
+ }
153
+ catch (error) {
154
+ const transportError = error instanceof MiaoError ? error : asTransportError(error, "failed to connect to Miao Runtime");
155
+ await this.#finishDisconnect(transportError, generation);
156
+ throw transportError;
157
+ }
158
+ }
159
+ async ready() {
160
+ if (this.#state !== "connected")
161
+ await this.connect();
162
+ return this;
163
+ }
164
+ async close() {
165
+ if (this.#state === "disconnected")
166
+ return;
167
+ this.#state = "closing";
168
+ const generation = this.#generation;
169
+ let closeError;
170
+ try {
171
+ await this.transport.close();
172
+ }
173
+ catch (error) {
174
+ closeError = error;
175
+ }
176
+ await this.#finishDisconnect(closeError === undefined
177
+ ? new TransportError("Miao Runtime transport closed", { code: "transport_closed", retryable: true })
178
+ : asTransportError(closeError, "failed to close Miao Runtime transport"), generation);
179
+ if (closeError !== undefined)
180
+ throw asTransportError(closeError, "failed to close Miao Runtime transport");
181
+ }
182
+ async describe(options) {
183
+ const descriptor = await this.request("runtime.describe", {}, options);
184
+ if (descriptor.protocolVersion !== RUNTIME_PROTOCOL_VERSION) {
185
+ throw new ProtocolVersionError(`Runtime uses ${descriptor.protocolVersion}; client requires ${RUNTIME_PROTOCOL_VERSION}`, { code: "protocol_version", details: { expected: RUNTIME_PROTOCOL_VERSION, actual: descriptor.protocolVersion } });
186
+ }
187
+ this.#descriptor = descriptor;
188
+ return descriptor;
189
+ }
190
+ supports(operation) {
191
+ return this.#descriptor?.operations.includes(operation) ?? false;
192
+ }
193
+ hasFeature(feature) {
194
+ return this.#descriptor?.features.some((candidate) => candidate === feature) ?? false;
195
+ }
196
+ require(requirements) {
197
+ if (this.#descriptor === undefined) {
198
+ throw new ProtocolError("Runtime capabilities have not been negotiated", { code: "descriptor_unavailable" });
199
+ }
200
+ const missingOperations = (requirements.operations ?? []).filter((operation) => !this.supports(operation));
201
+ const missingFeatures = (requirements.features ?? []).filter((feature) => !this.hasFeature(feature));
202
+ if (missingOperations.length > 0 || missingFeatures.length > 0) {
203
+ throw new UnsupportedOperationError("Runtime does not satisfy required capabilities", {
204
+ code: "unsupported_capability",
205
+ details: { missingOperations, missingFeatures },
206
+ });
207
+ }
208
+ }
209
+ async request(operation, payload = {}, options = {}) {
210
+ if (this.#state !== "connected") {
211
+ throw new TransportError("Miao Runtime transport is not connected", { code: "transport_disconnected", retryable: true });
212
+ }
213
+ if (this.#pending.size >= this.#maxPendingRequests) {
214
+ throw new BackpressureError(`client has reached ${this.#maxPendingRequests} pending requests`, {
215
+ code: "request_backpressure", retryable: true, details: { limit: this.#maxPendingRequests },
216
+ });
217
+ }
218
+ if (options.signal?.aborted === true) {
219
+ throw new RequestCancelledError("Miao Runtime request was cancelled before send", {
220
+ code: "request_cancelled", cause: options.signal.reason,
221
+ });
222
+ }
223
+ const requestId = this.#nextRequestId();
224
+ const timeoutMs = positive(options.timeoutMs, this.#defaultTimeoutMs, "timeoutMs");
225
+ let timer;
226
+ let abortListener;
227
+ const promise = new Promise((resolve, reject) => {
228
+ const cleanup = () => {
229
+ if (timer !== undefined)
230
+ clearTimeout(timer);
231
+ if (abortListener !== undefined)
232
+ options.signal?.removeEventListener("abort", abortListener);
233
+ };
234
+ this.#pending.set(requestId, {
235
+ resolve: (value) => resolve(value),
236
+ reject,
237
+ cleanup,
238
+ });
239
+ const fail = (error, reason) => {
240
+ const pending = this.#pending.get(requestId);
241
+ if (pending === undefined)
242
+ return;
243
+ this.#pending.delete(requestId);
244
+ pending.cleanup();
245
+ pending.reject(error);
246
+ void this.#sendBestEffort({
247
+ type: "cancel", protocol: RUNTIME_PROTOCOL_VERSION,
248
+ target: { kind: "request", requestId }, reason,
249
+ });
250
+ };
251
+ timer = setTimeout(() => fail(new RequestTimeoutError(`Miao Runtime request ${requestId} timed out after ${timeoutMs}ms`, {
252
+ code: "request_timeout", retryable: true, requestId, details: { operation, timeoutMs },
253
+ }), "client request timeout"), timeoutMs);
254
+ abortListener = () => fail(new RequestCancelledError(`Miao Runtime request ${requestId} was cancelled`, {
255
+ code: "request_cancelled", requestId, cause: options.signal?.reason,
256
+ }), "client request cancelled");
257
+ options.signal?.addEventListener("abort", abortListener, { once: true });
258
+ });
259
+ const frame = {
260
+ type: "request", protocol: RUNTIME_PROTOCOL_VERSION, requestId, operation, payload,
261
+ };
262
+ try {
263
+ await this.transport.send(frame);
264
+ }
265
+ catch (error) {
266
+ const transportError = asTransportError(error, `failed to send ${operation}`);
267
+ await this.#finishDisconnect(transportError, this.#generation);
268
+ }
269
+ return promise;
270
+ }
271
+ #nextRequestId() {
272
+ this.#requestCounter += 1;
273
+ return `req-${this.#generation.toString(36)}-${this.#requestCounter.toString(36)}`;
274
+ }
275
+ async #consumeFrames(generation) {
276
+ let failure;
277
+ try {
278
+ for await (const frame of this.transport.frames()) {
279
+ if (generation !== this.#generation)
280
+ return;
281
+ if (frame.protocol !== RUNTIME_PROTOCOL_VERSION) {
282
+ throw new ProtocolVersionError(`received unsupported protocol ${frame.protocol}`, {
283
+ code: "protocol_version", details: { expected: RUNTIME_PROTOCOL_VERSION, actual: frame.protocol },
284
+ });
285
+ }
286
+ this.#dispatchFrame(frame);
287
+ }
288
+ failure = new TransportError("Miao Runtime transport disconnected", {
289
+ code: "transport_disconnected", retryable: true,
290
+ });
291
+ }
292
+ catch (error) {
293
+ failure = error instanceof MiaoError
294
+ ? error
295
+ : new TransportError("Miao Runtime frame stream failed", {
296
+ code: "transport_error", retryable: true, cause: error,
297
+ });
298
+ }
299
+ finally {
300
+ await this.#finishDisconnect(failure, generation);
301
+ }
302
+ }
303
+ #dispatchFrame(frame) {
304
+ switch (frame.type) {
305
+ case "response": {
306
+ const pending = this.#pending.get(frame.requestId);
307
+ if (pending === undefined)
308
+ return;
309
+ this.#pending.delete(frame.requestId);
310
+ pending.cleanup();
311
+ if (frame.status === "success")
312
+ pending.resolve(frame.result);
313
+ else
314
+ pending.reject(errorFromRuntime(frame.error, frame.requestId));
315
+ break;
316
+ }
317
+ case "event":
318
+ this.#dispatchEvent(frame);
319
+ break;
320
+ case "host_call":
321
+ void this.#routeHostCall(frame);
322
+ break;
323
+ case "cancel":
324
+ if (frame.target.kind === "host_call")
325
+ this.#activeHostCalls.get(frame.target.callId)?.abort(frame.reason);
326
+ else if (frame.target.kind === "subscription")
327
+ this.#subscriptions.get(frame.target.subscriptionId)?.close();
328
+ break;
329
+ case "ping":
330
+ void this.#sendBestEffort({ type: "pong", protocol: RUNTIME_PROTOCOL_VERSION, nonce: frame.nonce });
331
+ break;
332
+ case "request":
333
+ case "host_result":
334
+ case "pong":
335
+ break;
336
+ }
337
+ }
338
+ #dispatchEvent(frame) {
339
+ const queue = this.#subscriptions.get(frame.subscriptionId);
340
+ if (queue !== undefined) {
341
+ queue.push(frame.event);
342
+ return;
343
+ }
344
+ let events = this.#orphanEvents.get(frame.subscriptionId);
345
+ if (events === undefined) {
346
+ if (this.#orphanEvents.size >= this.#maxPendingRequests) {
347
+ const oldest = this.#orphanEvents.keys().next().value;
348
+ if (oldest !== undefined)
349
+ this.#orphanEvents.delete(oldest);
350
+ }
351
+ events = [];
352
+ this.#orphanEvents.set(frame.subscriptionId, events);
353
+ }
354
+ if (events.length < this.#eventBufferSize)
355
+ events.push(frame);
356
+ }
357
+ async *#events(runId, options = {}) {
358
+ const result = await this.request("events.subscribe", { runId, fromSequence: options.fromSequence ?? 0 }, options);
359
+ const subscriptionId = result.subscriptionId;
360
+ if (typeof subscriptionId !== "string" || subscriptionId.length === 0) {
361
+ throw new ProtocolError("events.subscribe returned an invalid subscriptionId", { code: "invalid_response" });
362
+ }
363
+ const capacity = positive(options.bufferSize, this.#eventBufferSize, "bufferSize");
364
+ const queue = new BoundedAsyncQueue(capacity, () => {
365
+ void this.#unsubscribe(subscriptionId, "event consumer exceeded bounded buffer");
366
+ });
367
+ this.#subscriptions.set(subscriptionId, queue);
368
+ const orphans = this.#orphanEvents.get(subscriptionId) ?? [];
369
+ this.#orphanEvents.delete(subscriptionId);
370
+ for (const frame of orphans)
371
+ queue.push(frame.event);
372
+ const abort = () => queue.fail(new RequestCancelledError("event subscription was cancelled", {
373
+ code: "request_cancelled", cause: options.signal?.reason,
374
+ }));
375
+ if (options.signal?.aborted === true)
376
+ abort();
377
+ else
378
+ options.signal?.addEventListener("abort", abort, { once: true });
379
+ try {
380
+ yield* queue;
381
+ }
382
+ finally {
383
+ options.signal?.removeEventListener("abort", abort);
384
+ this.#subscriptions.delete(subscriptionId);
385
+ // Subscription teardown must not delay iterator completion on a slow or lost peer.
386
+ void this.#unsubscribe(subscriptionId, "event subscription closed");
387
+ }
388
+ }
389
+ #subscribe(runId, listener, options = {}) {
390
+ const controller = new AbortController();
391
+ let closed = false;
392
+ const forwardExternalAbort = () => controller.abort(options.signal?.reason);
393
+ options.signal?.addEventListener("abort", forwardExternalAbort, { once: true });
394
+ const done = (async () => {
395
+ try {
396
+ for await (const event of this.#events(runId, { ...options, signal: controller.signal })) {
397
+ await listener(event);
398
+ }
399
+ }
400
+ catch (error) {
401
+ if (!controller.signal.aborted)
402
+ options.onError?.(error);
403
+ }
404
+ finally {
405
+ closed = true;
406
+ options.signal?.removeEventListener("abort", forwardExternalAbort);
407
+ }
408
+ })();
409
+ return {
410
+ get closed() { return closed; },
411
+ done,
412
+ close() { controller.abort("callback subscription closed"); },
413
+ };
414
+ }
415
+ async #unsubscribe(subscriptionId, reason) {
416
+ if (this.#state !== "connected")
417
+ return;
418
+ try {
419
+ await this.request("events.unsubscribe", { subscriptionId }, { timeoutMs: Math.min(this.#defaultTimeoutMs, 5_000) });
420
+ }
421
+ catch {
422
+ await this.#sendBestEffort({
423
+ type: "cancel", protocol: RUNTIME_PROTOCOL_VERSION,
424
+ target: { kind: "subscription", subscriptionId }, reason,
425
+ });
426
+ }
427
+ }
428
+ async #registerCapability(definition, options) {
429
+ if (!/^service:\/\/\S+$/.test(definition.address)) {
430
+ throw new TypeError("Host Capability address must be a non-empty service:// URI");
431
+ }
432
+ if (definition.deadlineMs !== undefined)
433
+ positive(definition.deadlineMs, 1, "deadlineMs");
434
+ if (definition.leaseMs !== undefined)
435
+ positive(definition.leaseMs, 1, "leaseMs");
436
+ if (this.#capabilities.has(definition.address)) {
437
+ throw new HostCapabilityError(`Host Capability ${definition.address} is already registered`, {
438
+ code: "duplicate_host_capability",
439
+ });
440
+ }
441
+ const metadata = capabilityMetadata(definition);
442
+ const stored = {
443
+ definition, metadata, active: false,
444
+ };
445
+ this.#capabilities.set(definition.address, stored);
446
+ try {
447
+ await this.request("capabilities.register", metadata, options);
448
+ stored.active = true;
449
+ }
450
+ catch (error) {
451
+ this.#capabilities.delete(definition.address);
452
+ throw error;
453
+ }
454
+ return new ClientCapabilityRegistration(this, definition.address, metadata);
455
+ }
456
+ async #renewCapability(address, leaseMs, options) {
457
+ const stored = this.#capabilities.get(address);
458
+ await this.request("capabilities.renew", { address, ...(leaseMs === undefined ? {} : { leaseMs }) }, options);
459
+ if (stored !== undefined) {
460
+ stored.active = true;
461
+ if (leaseMs !== undefined)
462
+ stored.metadata = { ...stored.metadata, leaseMs };
463
+ }
464
+ }
465
+ async #unregisterCapability(address, options) {
466
+ const stored = this.#capabilities.get(address);
467
+ if (this.#state !== "connected") {
468
+ this.#capabilities.delete(address);
469
+ if (stored !== undefined)
470
+ stored.active = false;
471
+ return;
472
+ }
473
+ if (stored !== undefined && !stored.active) {
474
+ this.#capabilities.delete(address);
475
+ return;
476
+ }
477
+ await this.request("capabilities.unregister", { address }, options);
478
+ this.#capabilities.delete(address);
479
+ if (stored !== undefined)
480
+ stored.active = false;
481
+ }
482
+ #capabilityIsActive(address) {
483
+ return this.#capabilities.get(address)?.active ?? false;
484
+ }
485
+ async #restoreCapabilityRegistrations() {
486
+ for (const stored of this.#capabilities.values()) {
487
+ await this.request("capabilities.register", stored.metadata);
488
+ stored.active = true;
489
+ }
490
+ }
491
+ async #routeHostCall(frame) {
492
+ let result = this.#hostResults.get(frame.callId);
493
+ if (result === undefined) {
494
+ result = this.#executeHostCall(frame);
495
+ this.#hostResults.set(frame.callId, result);
496
+ this.#trimHostResults();
497
+ }
498
+ const response = await result;
499
+ this.#trimHostResults();
500
+ if (this.#state === "connected")
501
+ await this.#sendBestEffort(response);
502
+ }
503
+ #trimHostResults() {
504
+ while (this.#hostResults.size > this.#maxRememberedHostCalls) {
505
+ const removable = [...this.#hostResults.keys()].find((callId) => !this.#activeHostCalls.has(callId));
506
+ if (removable === undefined)
507
+ return;
508
+ this.#hostResults.delete(removable);
509
+ }
510
+ }
511
+ async #executeHostCall(frame) {
512
+ const stored = this.#capabilities.get(frame.capability);
513
+ if (stored === undefined || !stored.active) {
514
+ return hostError(frame.callId, "host_capability_unavailable", `Host Capability ${frame.capability} is unavailable`, true);
515
+ }
516
+ if (this.#activeHostCalls.size >= this.#maxConcurrentHostCalls) {
517
+ return hostError(frame.callId, "backpressure", "Host Capability concurrency limit reached", true);
518
+ }
519
+ const controller = new AbortController();
520
+ this.#activeHostCalls.set(frame.callId, controller);
521
+ const deadline = Date.now() + frame.deadlineMs;
522
+ const timer = setTimeout(() => controller.abort(new HostCapabilityTimeoutError(`Host Capability ${frame.capability} exceeded its ${frame.deadlineMs}ms deadline`, { code: "host_capability_timeout", retryable: true })), frame.deadlineMs);
523
+ const schemaContext = { capability: frame.capability, callId: frame.callId };
524
+ try {
525
+ await validateSchema(this.#schemaValidator, stored.definition.inputSchema, frame.input, {
526
+ ...schemaContext, direction: "input",
527
+ });
528
+ const context = hostContext(frame, deadline, controller.signal);
529
+ const output = await raceAbort(Promise.resolve(stored.definition.handler(frame.input, context)), controller.signal);
530
+ await validateSchema(this.#schemaValidator, stored.definition.outputSchema, output, {
531
+ ...schemaContext, direction: "output",
532
+ });
533
+ return { type: "host_result", protocol: RUNTIME_PROTOCOL_VERSION, callId: frame.callId, status: "success", output };
534
+ }
535
+ catch (error) {
536
+ if (controller.signal.aborted) {
537
+ const reason = controller.signal.reason;
538
+ if (reason instanceof HostCapabilityTimeoutError) {
539
+ return hostError(frame.callId, reason.code, reason.message, true, reason.details);
540
+ }
541
+ return {
542
+ type: "host_result", protocol: RUNTIME_PROTOCOL_VERSION, callId: frame.callId,
543
+ status: "cancelled", reason: reason instanceof Error ? reason.message : String(reason ?? "Host Call cancelled"),
544
+ };
545
+ }
546
+ const hostErrorValue = error instanceof MiaoError ? error : new HostCapabilityError(error instanceof Error ? error.message : "Host Capability handler failed", { code: "host_capability_error", cause: error });
547
+ return hostError(frame.callId, hostErrorValue.code, hostErrorValue.message, hostErrorValue.retryable, hostErrorValue.details);
548
+ }
549
+ finally {
550
+ clearTimeout(timer);
551
+ this.#activeHostCalls.delete(frame.callId);
552
+ }
553
+ }
554
+ async #sendBestEffort(frame) {
555
+ try {
556
+ await this.transport.send(frame);
557
+ }
558
+ catch (error) {
559
+ await this.#finishDisconnect(asTransportError(error), this.#generation);
560
+ }
561
+ }
562
+ async #finishDisconnect(error, generation) {
563
+ if (generation !== this.#generation || this.#state === "disconnected")
564
+ return;
565
+ const disconnectError = error instanceof MiaoError ? error : asTransportError(error);
566
+ this.#state = "disconnected";
567
+ this.#descriptor = undefined;
568
+ for (const pending of this.#pending.values()) {
569
+ pending.cleanup();
570
+ pending.reject(disconnectError);
571
+ }
572
+ this.#pending.clear();
573
+ for (const queue of this.#subscriptions.values())
574
+ queue.fail(disconnectError);
575
+ this.#subscriptions.clear();
576
+ this.#orphanEvents.clear();
577
+ for (const controller of this.#activeHostCalls.values())
578
+ controller.abort(disconnectError);
579
+ this.#activeHostCalls.clear();
580
+ this.#hostResults.clear();
581
+ for (const capability of this.#capabilities.values())
582
+ capability.active = false;
583
+ try {
584
+ await this.transport.close();
585
+ }
586
+ catch {
587
+ // The first transport failure is the useful terminal cause.
588
+ }
589
+ }
590
+ /** @internal Used by HostCapabilityRegistration. */
591
+ _registrationActive(address) { return this.#capabilityIsActive(address); }
592
+ /** @internal Used by HostCapabilityRegistration. */
593
+ _renewRegistration(address, leaseMs) { return this.#renewCapability(address, leaseMs); }
594
+ /** @internal Used by HostCapabilityRegistration. */
595
+ _unregisterRegistration(address) { return this.#unregisterCapability(address); }
596
+ }
597
+ class ClientCapabilityRegistration {
598
+ restoreMetadata;
599
+ #client;
600
+ address;
601
+ constructor(client, address, metadata) {
602
+ this.#client = client;
603
+ this.address = address;
604
+ this.restoreMetadata = metadata;
605
+ }
606
+ get active() { return this.#client._registrationActive(this.address); }
607
+ get description() { return this.restoreMetadata.description; }
608
+ get effects() { return this.restoreMetadata.effects; }
609
+ get approval() { return this.restoreMetadata.approval; }
610
+ get inputSchema() { return this.restoreMetadata.inputSchema; }
611
+ get outputSchema() { return this.restoreMetadata.outputSchema; }
612
+ get deadlineMs() { return this.restoreMetadata.deadlineMs; }
613
+ get leaseMs() { return this.restoreMetadata.leaseMs; }
614
+ get metadata() { return this.restoreMetadata.metadata; }
615
+ renew(leaseMs) { return this.#client._renewRegistration(this.address, leaseMs); }
616
+ unregister() { return this.#client._unregisterRegistration(this.address); }
617
+ }
618
+ function capabilityMetadata(definition) {
619
+ return {
620
+ address: definition.address,
621
+ ...(definition.description === undefined ? {} : { description: definition.description }),
622
+ ...(definition.effects === undefined ? {} : { effects: definition.effects }),
623
+ ...(definition.approval === undefined ? {} : { approval: definition.approval }),
624
+ ...(definition.inputSchema === undefined ? {} : { inputSchema: definition.inputSchema }),
625
+ ...(definition.outputSchema === undefined ? {} : { outputSchema: definition.outputSchema }),
626
+ ...(definition.deadlineMs === undefined ? {} : { deadlineMs: definition.deadlineMs }),
627
+ ...(definition.leaseMs === undefined ? {} : { leaseMs: definition.leaseMs }),
628
+ ...(definition.metadata === undefined ? {} : { metadata: definition.metadata }),
629
+ };
630
+ }
631
+ function hostContext(frame, deadline, signal) {
632
+ const context = frame.context;
633
+ return {
634
+ callId: frame.callId,
635
+ runId: context.runId,
636
+ principal: context.principal,
637
+ namespace: context.namespace,
638
+ deadline,
639
+ signal,
640
+ ...(context.traceId === undefined ? {} : { traceId: context.traceId }),
641
+ };
642
+ }
643
+ function hostError(callId, code, message, retryable, details) {
644
+ return {
645
+ type: "host_result", protocol: RUNTIME_PROTOCOL_VERSION, callId, status: "error",
646
+ error: { code, message, retryable, ...(details === undefined ? {} : { details }) },
647
+ };
648
+ }
649
+ function raceAbort(promise, signal) {
650
+ if (signal.aborted)
651
+ return Promise.reject(signal.reason);
652
+ return new Promise((resolve, reject) => {
653
+ const abort = () => reject(signal.reason);
654
+ signal.addEventListener("abort", abort, { once: true });
655
+ promise.then((value) => { signal.removeEventListener("abort", abort); resolve(value); }, (error) => { signal.removeEventListener("abort", abort); reject(error); });
656
+ });
657
+ }
658
+ function positive(value, fallback, name) {
659
+ const result = value ?? fallback;
660
+ if (!Number.isSafeInteger(result) || result <= 0)
661
+ throw new RangeError(`${name} must be a positive integer`);
662
+ return result;
663
+ }
664
+ //# sourceMappingURL=client.js.map