@github/copilot-sdk 1.0.14 → 1.0.15-preview.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/README.md CHANGED
@@ -118,6 +118,7 @@ new CopilotClient(options?: CopilotClientOptions)
118
118
  - `mode?: "empty" | "copilot-cli"` - Defaulting strategy. Use `"empty"` for multi-user server mode; defaults to `"copilot-cli"`.
119
119
  - `workingDirectory?: string` - Working directory for the runtime process (default: current process cwd).
120
120
  - `baseDirectory?: string` - Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned runtime. When not set, the runtime defaults to `~/.copilot`. Ignored when connecting via `RuntimeConnection.forUri`.
121
+ - `extensionLaunchProvider?: ExtensionLaunchProvider` - Experimental connection-level resolver for extension launch profiles. The client installs the reverse-RPC handler and registers the provider during startup before sessions can be created.
121
122
  - `logLevel?: "none" | "error" | "warning" | "info" | "debug" | "all"` - Log level. When omitted, the runtime uses its own default (currently `"info"`).
122
123
  - `env?: Record<string, string | undefined>` - Environment variables for the runtime process. When omitted, inherits `process.env`.
123
124
  - `gitHubToken?: string` - GitHub token for authentication. When provided, takes priority over other auth methods.
@@ -305,6 +306,87 @@ Send a message and wait until the session becomes idle.
305
306
 
306
307
  Returns the final assistant message event, or undefined if none was received.
307
308
 
309
+ ##### Structured output (preview)
310
+
311
+ Requires a runtime build with `responseFormat` and `originatingMessageId` support.
312
+ Pass a raw JSON Schema or a Zod schema as `responseSchema` to `send` or
313
+ `sendAndWait`. As with custom tool parameters, the SDK converts Zod schemas to
314
+ JSON Schema before sending them:
315
+
316
+ ```typescript
317
+ import { z } from "zod";
318
+
319
+ const answerSchema = z.object({ answer: z.number().int() });
320
+ const message = await session.sendAndWait({
321
+ prompt: "What is 19 + 23?",
322
+ responseSchema: answerSchema,
323
+ });
324
+ console.log(message?.data.content); // JSON text
325
+ ```
326
+
327
+ For a typed result, pass the Zod schema as the **second argument** instead:
328
+
329
+ ```typescript
330
+ const answer = await session.sendAndWait("What is 19 + 23?", answerSchema);
331
+ console.log(answer.answer); // number; TResult is inferred from answerSchema
332
+ ```
333
+
334
+ `sendAndWait<TResult>(options, schema, timeout?)` generates the JSON Schema from
335
+ the schema value, parses the final JSON, and validates it with the schema's
336
+ `parse` method. TypeScript cannot derive a runtime schema from an erased type
337
+ parameter alone. Invalid JSON, a schema mismatch, or a completed run without a
338
+ matching assistant message throws. Do not also set `options.responseSchema` when
339
+ using the typed overload.
340
+
341
+ The schema belongs to the submitted run, including its tool-call iterations.
342
+ Internally generated stop-hook corrections retain the schema and originating
343
+ message ID, so the wait returns the corrected answer. Independent subsequent
344
+ sends do not inherit it. Ordinary immediate steering inherits the active schema
345
+ and originating message ID, even when it arrives too late for the current model
346
+ request and is promoted into a follow-up run. Specifying a schema with
347
+ `mode: "immediate"` is rejected, even while idle.
348
+ The generated `session.rpc.send` and `session.rpc.sendMessages` wrappers expose
349
+ the full `responseFormat` contract when you need to set its name, description,
350
+ or strict option rather than using the convenience defaults (`name: "response"`,
351
+ `strict: true`).
352
+ Each batch starts one run: the final returned message ID is its origin, preceding
353
+ messages are context, and an empty batch has no origin. An immediate batch
354
+ steers the active run instead and retains its origin.
355
+ The schema is not a persisted session default: autonomous resume-pending work
356
+ after a restart does not restore it. A terminal tool that clears context ends
357
+ the old run; its fresh seed does not inherit the schema or origin. Such a run
358
+ can finish without a structured result, in which case the typed wait throws.
359
+ After a successful terminal tool, the runtime disables tools while the model
360
+ produces the structured result. Stop-hook corrections remain supported.
361
+ Remote sessions and known HydraFusion routes reject response formats before
362
+ admission. Schemas larger than 32 MiB when JSON-encoded are also rejected before
363
+ admission, using the runtime's existing request-size ceiling. This does not
364
+ guarantee the schema plus conversation and tools fits the provider's budget.
365
+
366
+ Structured waits select the last root-agent message whose `originatingMessageId`
367
+ matches the ID returned by their send, then return at a non-autopilot
368
+ `session.idle`. Other queued work can delay that idle, but cannot replace the
369
+ selected result. The existing unformatted overload retains its session-wide
370
+ behavior. `turnId` identifies an individual model/tool iteration, not the whole
371
+ run; telemetry interaction IDs are not unique run identifiers.
372
+
373
+ For event-driven consumption with `send`, subscribe before sending and collect
374
+ root `assistant.message` events whose `data.originatingMessageId` matches the ID
375
+ returned by `send`; events may arrive before that acknowledgement. Wait for
376
+ `session.idle`, then parse the last matching message without tool requests.
377
+ An earlier response may be superseded by a stop-hook correction. Handle
378
+ `session.error` and aborted idle events rather than returning a partial result.
379
+
380
+ Streaming still delivers ordinary text events, including intermediate messages
381
+ and tool calls. Only the final selected message is parsed by the typed overload;
382
+ not every event is necessarily a complete schema-conforming JSON document.
383
+ Provider errors, refusals, cancellation, truncation, session errors, and timeouts
384
+ can prevent a typed result. A timeout stops waiting, not the runtime's work.
385
+ Use a model and endpoint that support native structured output. An API-compatible
386
+ gateway may ignore format fields even when it accepts the request; for example,
387
+ the Claude Chat-completions compatibility route is not equivalent to Anthropic's
388
+ native `output_config.format` endpoint.
389
+
308
390
  ##### `on(eventType: string, handler: TypedSessionEventHandler): () => void`
309
391
 
310
392
  Subscribe to a specific event type. The handler receives properly typed events.
@@ -22,7 +22,7 @@ __export(cliVersion_exports, {
22
22
  COPILOT_CLI_VERSION: () => COPILOT_CLI_VERSION
23
23
  });
24
24
  module.exports = __toCommonJS(cliVersion_exports);
25
- const COPILOT_CLI_VERSION = "1.0.85";
25
+ const COPILOT_CLI_VERSION = "1.0.87-0";
26
26
  const COPILOT_CLI_USE_NPM_PACKAGE = false;
27
27
  // Annotate the CommonJS export names for ESM import in node:
28
28
  0 && (module.exports = {
@@ -45,13 +45,11 @@ var import_cliVersion = require("./cliVersion.js");
45
45
  var import_sessionFsProvider = require("./sessionFsProvider.js");
46
46
  var import_copilotRequestHandler = require("./copilotRequestHandler.js");
47
47
  var import_telemetry = require("./telemetry.js");
48
+ var import_schema = require("./schema.js");
48
49
  var import_toolSet = require("./toolSet.js");
49
50
  var import_types = require("./types.js");
50
51
  const MIN_PROTOCOL_VERSION = 3;
51
52
  const RUNTIME_SHUTDOWN_TIMEOUT_MS = 1e4;
52
- function isZodSchema(value) {
53
- return value != null && typeof value === "object" && "toJSONSchema" in value && typeof value.toJSONSchema === "function";
54
- }
55
53
  async function withTimeout(promise, timeoutMs, message) {
56
54
  let timeout;
57
55
  try {
@@ -96,13 +94,6 @@ async function waitForChildExit(child, timeoutMs) {
96
94
  }
97
95
  });
98
96
  }
99
- function toJsonSchema(parameters) {
100
- if (!parameters) return void 0;
101
- if (isZodSchema(parameters)) {
102
- return parameters.toJSONSchema();
103
- }
104
- return parameters;
105
- }
106
97
  const DEFAULT_PROVIDER_NAME = "default";
107
98
  function extractBearerTokenProviders(provider, providers) {
108
99
  const callbacks = /* @__PURE__ */ new Map();
@@ -235,6 +226,7 @@ class CopilotClient {
235
226
  cliProcess = null;
236
227
  ffiHost = null;
237
228
  connection = null;
229
+ requestAdapter = null;
238
230
  messageWriter = null;
239
231
  connectionClosed = false;
240
232
  socket = null;
@@ -272,6 +264,7 @@ class CopilotClient {
272
264
  /** Connection-level session filesystem config, set via constructor option. */
273
265
  sessionFsConfig = null;
274
266
  requestHandler = null;
267
+ extensionLaunchProvider;
275
268
  builtinPluginDirectories = [];
276
269
  onGitHubTelemetry;
277
270
  clientGlobalHandlers = {};
@@ -429,6 +422,7 @@ class CopilotClient {
429
422
  this.onGetTraceContext = options.onGetTraceContext;
430
423
  this.sessionFsConfig = options.sessionFs ?? null;
431
424
  this.requestHandler = options.requestHandler ?? null;
425
+ this.extensionLaunchProvider = options.extensionLaunchProvider;
432
426
  this.onGitHubTelemetry = options.onGitHubTelemetry;
433
427
  this.setupClientGlobalHandlers();
434
428
  const connEnv = conn.kind === "stdio" || conn.kind === "tcp" ? conn.env : void 0;
@@ -531,14 +525,16 @@ class CopilotClient {
531
525
  }
532
526
  setupClientGlobalHandlers() {
533
527
  const handlers = {};
528
+ handlers.extensionLaunchProvider = this.extensionLaunchProvider;
534
529
  if (this.requestHandler) {
535
- handlers.llmInference = (0, import_copilotRequestHandler.createCopilotRequestAdapter)(this.requestHandler, () => {
530
+ this.requestAdapter = (0, import_copilotRequestHandler.createCopilotRequestAdapter)(this.requestHandler, () => {
536
531
  if (!this.connection) {
537
532
  return void 0;
538
533
  }
539
534
  this._rpc ??= (0, import_rpc.createServerRpc)(this.connection);
540
535
  return this._rpc;
541
536
  });
537
+ handlers.llmInference = this.requestAdapter;
542
538
  }
543
539
  if (this.onGitHubTelemetry) {
544
540
  const onGitHubTelemetry = this.onGitHubTelemetry;
@@ -643,6 +639,9 @@ class CopilotClient {
643
639
  }
644
640
  await this.connectToServer();
645
641
  await this.verifyProtocolVersion();
642
+ if (this.extensionLaunchProvider) {
643
+ await this.rpc.registerExtensionLaunchProvider();
644
+ }
646
645
  if (this.builtinPluginDirectories.length > 0) {
647
646
  try {
648
647
  await this.connection.sendRequest("plugins.builtin.set", {
@@ -731,6 +730,7 @@ class CopilotClient {
731
730
  }
732
731
  this.sessions.clear();
733
732
  this.githubTokenProviders.clear();
733
+ this.requestAdapter?.cancelPending();
734
734
  if (this.connection && !this.connectionClosed && (this.cliProcess || this.ffiHost) && !this.isExternalServer) {
735
735
  const runtimeShutdownStart = Date.now();
736
736
  const shutdownPromise = this.rpc.runtime.shutdown();
@@ -886,6 +886,7 @@ class CopilotClient {
886
886
  }
887
887
  this.sessions.clear();
888
888
  this.githubTokenProviders.clear();
889
+ this.requestAdapter?.cancelPending();
889
890
  if (this.messageWriter) {
890
891
  this.messageWriter.suppressWriteErrors = true;
891
892
  }
@@ -1188,7 +1189,7 @@ class CopilotClient {
1188
1189
  tools: config.tools?.map((tool) => ({
1189
1190
  name: tool.name,
1190
1191
  description: tool.description,
1191
- parameters: toJsonSchema(tool.parameters),
1192
+ parameters: (0, import_schema.toJsonSchema)(tool.parameters),
1192
1193
  overridesBuiltInTool: tool.overridesBuiltInTool,
1193
1194
  skipPermission: tool.skipPermission,
1194
1195
  defer: tool.defer,
@@ -1432,7 +1433,7 @@ class CopilotClient {
1432
1433
  tools: config.tools?.map((tool) => ({
1433
1434
  name: tool.name,
1434
1435
  description: tool.description,
1435
- parameters: toJsonSchema(tool.parameters),
1436
+ parameters: (0, import_schema.toJsonSchema)(tool.parameters),
1436
1437
  overridesBuiltInTool: tool.overridesBuiltInTool,
1437
1438
  skipPermission: tool.skipPermission,
1438
1439
  defer: tool.defer,
@@ -2294,6 +2295,7 @@ stderr: ${stderrOutput}` : ""}`
2294
2295
  }
2295
2296
  this.sessions.clear();
2296
2297
  this.githubTokenProviders.clear();
2298
+ this.requestAdapter?.cancelPending();
2297
2299
  };
2298
2300
  this.connection.onClose(markDisconnected);
2299
2301
  this.connection.onError(() => {
@@ -27,6 +27,7 @@ __export(copilotRequestHandler_exports, {
27
27
  module.exports = __toCommonJS(copilotRequestHandler_exports);
28
28
  const sharedTextDecoder = new TextDecoder("utf-8", { fatal: false });
29
29
  const sharedTextEncoder = new TextEncoder();
30
+ const HTTP_RESPONSE_READ_AHEAD_BYTES = 32 * 1024;
30
31
  const kBridge = /* @__PURE__ */ Symbol("copilotWebSocketResponseBridge");
31
32
  const kCompletion = /* @__PURE__ */ Symbol("copilotWebSocketCompletion");
32
33
  const kOpen = /* @__PURE__ */ Symbol("copilotWebSocketOpen");
@@ -265,7 +266,9 @@ function createCopilotRequestAdapter(handler, getServerRpc) {
265
266
  const message = err instanceof Error ? err.message : String(err);
266
267
  await finalize(exchange, 502, message);
267
268
  } finally {
268
- pending.delete(exchange.requestId);
269
+ if (pending.get(exchange.requestId) === exchange) {
270
+ pending.delete(exchange.requestId);
271
+ }
269
272
  }
270
273
  }
271
274
  return {
@@ -278,6 +281,13 @@ function createCopilotRequestAdapter(handler, getServerRpc) {
278
281
  async httpRequestChunk(params) {
279
282
  routeChunk(getOrCreate(params.requestId), params);
280
283
  return {};
284
+ },
285
+ cancelPending() {
286
+ const exchanges = [...pending.values()];
287
+ pending.clear();
288
+ for (const exchange of exchanges) {
289
+ exchange.pushCancel("RPC connection closed");
290
+ }
281
291
  }
282
292
  };
283
293
  }
@@ -526,30 +536,156 @@ async function drainAsync(stream) {
526
536
  return out;
527
537
  }
528
538
  async function streamResponse(response, exchange) {
529
- await exchange.startResponse({
530
- status: response.status,
531
- statusText: response.statusText || void 0,
532
- headers: headersToMultiMap(response.headers)
533
- });
534
- const body = response.body;
535
- if (!body) {
536
- await exchange.endResponse();
537
- return;
538
- }
539
- const reader = body.getReader();
539
+ const reader = response.body ? new HttpResponseReader(response.body, exchange.signal) : void 0;
540
540
  try {
541
- for (; ; ) {
542
- const { value, done } = await reader.read();
543
- if (done) {
544
- break;
545
- }
546
- if (value && value.byteLength > 0) {
547
- await exchange.writeResponse(value);
541
+ await exchange.startResponse({
542
+ status: response.status,
543
+ statusText: response.statusText || void 0,
544
+ headers: headersToMultiMap(response.headers)
545
+ });
546
+ if (reader) {
547
+ for (; ; ) {
548
+ const chunk = await reader.nextChunk();
549
+ if (!chunk) {
550
+ break;
551
+ }
552
+ await exchange.writeResponse(chunk);
548
553
  }
549
554
  }
550
555
  await exchange.endResponse();
551
556
  } finally {
552
- reader.releaseLock();
557
+ await reader?.dispose();
558
+ }
559
+ }
560
+ class HttpResponseReader {
561
+ #reader;
562
+ #signal;
563
+ #frames = [];
564
+ #pump;
565
+ #onAbort;
566
+ #queued = 0;
567
+ #done = false;
568
+ #cancelled = false;
569
+ #hasError = false;
570
+ #error;
571
+ #dataWaker;
572
+ #spaceWaker;
573
+ #cancelPromise;
574
+ constructor(body, signal) {
575
+ this.#reader = body.getReader();
576
+ this.#signal = signal;
577
+ this.#onAbort = () => {
578
+ void this.cancel(signal.reason);
579
+ };
580
+ signal.addEventListener("abort", this.#onAbort, { once: true });
581
+ this.#pump = this.#pumpSource();
582
+ if (signal.aborted) {
583
+ this.#onAbort();
584
+ }
585
+ }
586
+ async nextChunk() {
587
+ while (this.#frames.length === 0 && !this.#done && !this.#hasError) {
588
+ await new Promise((resolve) => {
589
+ this.#dataWaker = resolve;
590
+ });
591
+ }
592
+ if (this.#frames.length > 0) {
593
+ const chunk = this.#takeQueued();
594
+ this.#wakeSpace();
595
+ return chunk;
596
+ }
597
+ if (this.#hasError) {
598
+ this.#hasError = false;
599
+ throw this.#error;
600
+ }
601
+ return void 0;
602
+ }
603
+ #takeQueued() {
604
+ const frames = this.#frames;
605
+ const total = this.#queued;
606
+ this.#queued = 0;
607
+ if (frames.length === 1) {
608
+ return frames.pop();
609
+ }
610
+ const chunk = new Uint8Array(total);
611
+ let offset = 0;
612
+ for (const frame of frames) {
613
+ chunk.set(frame, offset);
614
+ offset += frame.byteLength;
615
+ }
616
+ frames.length = 0;
617
+ return chunk;
618
+ }
619
+ async dispose() {
620
+ this.#signal.removeEventListener("abort", this.#onAbort);
621
+ if (!this.#done) {
622
+ await this.cancel();
623
+ } else if (this.#cancelPromise) {
624
+ await this.#cancelPromise;
625
+ }
626
+ await this.#pump;
627
+ }
628
+ cancel(reason) {
629
+ if (this.#done) {
630
+ return Promise.resolve();
631
+ }
632
+ if (!this.#cancelPromise) {
633
+ this.#cancelled = true;
634
+ this.#done = true;
635
+ this.#hasError = true;
636
+ this.#error = reason instanceof Error ? reason : new Error("HTTP response body cancelled.");
637
+ this.#frames.length = 0;
638
+ this.#queued = 0;
639
+ this.#wakeData();
640
+ this.#wakeSpace();
641
+ this.#cancelPromise = this.#reader.cancel(reason).catch(() => void 0);
642
+ }
643
+ return this.#cancelPromise;
644
+ }
645
+ async #pumpSource() {
646
+ try {
647
+ while (!this.#cancelled) {
648
+ while (this.#queued >= HTTP_RESPONSE_READ_AHEAD_BYTES && !this.#cancelled) {
649
+ await new Promise((resolve) => {
650
+ this.#spaceWaker = resolve;
651
+ });
652
+ }
653
+ if (this.#cancelled) {
654
+ return;
655
+ }
656
+ const { value, done } = await this.#reader.read();
657
+ if (done) {
658
+ return;
659
+ }
660
+ if (value.byteLength > 0) {
661
+ this.#frames.push(value);
662
+ this.#queued += value.byteLength;
663
+ this.#wakeData();
664
+ continue;
665
+ }
666
+ await new Promise((resolve) => setImmediate(resolve));
667
+ }
668
+ } catch (error) {
669
+ if (!this.#cancelled) {
670
+ this.#hasError = true;
671
+ this.#error = error;
672
+ }
673
+ } finally {
674
+ this.#done = true;
675
+ this.#wakeData();
676
+ this.#wakeSpace();
677
+ this.#reader.releaseLock();
678
+ }
679
+ }
680
+ #wakeData() {
681
+ const waker = this.#dataWaker;
682
+ this.#dataWaker = void 0;
683
+ waker?.();
684
+ }
685
+ #wakeSpace() {
686
+ const waker = this.#spaceWaker;
687
+ this.#spaceWaker = void 0;
688
+ waker?.();
553
689
  }
554
690
  }
555
691
  function headersToMultiMap(headers) {