@github/copilot-sdk 1.0.15-preview.0 → 1.0.15-preview.2

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/session.js CHANGED
@@ -11,6 +11,12 @@ import {
11
11
  FactoryResumeError,
12
12
  isFactoryRunTerminal
13
13
  } from "./factory.js";
14
+ import {
15
+ WORKFLOW_AGENT_OPTION_KEYS,
16
+ getWorkflowDefinition,
17
+ WorkflowResumeError,
18
+ isWorkflowRunTerminal
19
+ } from "./workflow.js";
14
20
  function isFactoryResumeErrorCode(value) {
15
21
  return value === "not_found" || value === "non_resumable" || value === "already_active" || value === "factory_already_running" || value === "factory_limits_invalid" || value === "factory_session_disposed" || value === "factory_storage_unavailable" || value === "factory_storage_corrupt";
16
22
  }
@@ -20,6 +26,15 @@ function copyDefinedFactoryAgentOption(source, target, key) {
20
26
  target[key] = value;
21
27
  }
22
28
  }
29
+ function isWorkflowResumeErrorCode(value) {
30
+ return value === "not_found" || value === "non_resumable" || value === "workflow_run_not_resumable" || value === "already_active" || value === "workflow_already_running" || value === "workflow_limits_invalid" || value === "workflow_session_disposed" || value === "workflow_storage_unavailable" || value === "workflow_storage_corrupt";
31
+ }
32
+ function copyDefinedWorkflowAgentOption(source, target, key) {
33
+ const value = source[key];
34
+ if (value !== void 0) {
35
+ target[key] = value;
36
+ }
37
+ }
23
38
  const factoryExecutionStore = new AsyncLocalStorage();
24
39
  function throwIfFactoryExecutionIsActive() {
25
40
  if (factoryExecutionStore.getStore()?.active) {
@@ -28,6 +43,18 @@ function throwIfFactoryExecutionIsActive() {
28
43
  );
29
44
  }
30
45
  }
46
+ const workflowExecutionStore = new AsyncLocalStorage();
47
+ function throwIfWorkflowExecutionIsActive() {
48
+ if (workflowExecutionStore.getStore()?.active) {
49
+ throw new Error(
50
+ "workflow.run, workflow.resume, and workflow.pause are not allowed while a workflow body is running on this call path."
51
+ );
52
+ }
53
+ }
54
+ function runInWorkflowHelperScope(helperScope, callback) {
55
+ const current = workflowExecutionStore.getStore();
56
+ return workflowExecutionStore.run({ active: current?.active ?? false, helperScope }, callback);
57
+ }
31
58
  function runInFactoryHelperScope(helperScope, callback) {
32
59
  const current = factoryExecutionStore.getStore();
33
60
  return factoryExecutionStore.run({ active: current?.active ?? false, helperScope }, callback);
@@ -109,6 +136,54 @@ async function runFactoryPipeline(items, ...stages) {
109
136
  })
110
137
  );
111
138
  }
139
+ async function runWorkflowParallel(thunks) {
140
+ if (!Array.isArray(thunks)) {
141
+ throw new Error(
142
+ "parallel() expects an array of functions, not promises. Wrap each call: () => agent(...)"
143
+ );
144
+ }
145
+ assertFactoryFanoutSize("parallel", thunks.length);
146
+ if (thunks.some((thunk) => typeof thunk !== "function")) {
147
+ throw new Error(
148
+ "parallel() expects an array of functions, not promises. Wrap each call: () => agent(...)"
149
+ );
150
+ }
151
+ return Promise.all(
152
+ thunks.map(
153
+ (thunk) => Promise.resolve().then(() => runInWorkflowHelperScope("parallel", thunk)).catch((error) => {
154
+ if (isFactoryFatalError(error)) {
155
+ throw error;
156
+ }
157
+ return null;
158
+ })
159
+ )
160
+ );
161
+ }
162
+ async function runWorkflowPipeline(items, ...stages) {
163
+ if (!Array.isArray(items)) {
164
+ throw new Error("pipeline(items, ...stages): items must be an array");
165
+ }
166
+ assertFactoryFanoutSize("pipeline", items.length);
167
+ return Promise.all(
168
+ items.map(async (item, index) => {
169
+ let previous = item;
170
+ for (const stage of stages) {
171
+ try {
172
+ previous = await runInWorkflowHelperScope(
173
+ "pipeline",
174
+ () => stage(previous, item, index)
175
+ );
176
+ } catch (error) {
177
+ if (isFactoryFatalError(error)) {
178
+ throw error;
179
+ }
180
+ return null;
181
+ }
182
+ }
183
+ return previous;
184
+ })
185
+ );
186
+ }
112
187
  class FactoryProgressBuffer {
113
188
  constructor(send) {
114
189
  this.send = send;
@@ -188,6 +263,85 @@ class FactoryProgressBuffer {
188
263
  }
189
264
  }
190
265
  }
266
+ class WorkflowProgressBuffer {
267
+ constructor(send) {
268
+ this.send = send;
269
+ }
270
+ send;
271
+ nextSeq = 0;
272
+ pending = [];
273
+ flushTimer;
274
+ flushTail = Promise.resolve();
275
+ flushError;
276
+ flushFailed = false;
277
+ closed = false;
278
+ enqueue(kind, text) {
279
+ if (this.closed) {
280
+ throw new Error("Cannot log after the workflow run has settled");
281
+ }
282
+ this.pending.push({ seq: this.nextSeq++, kind, text });
283
+ this.scheduleFlush();
284
+ }
285
+ async flush() {
286
+ this.clearFlushTimer();
287
+ const lines = this.pending.splice(0);
288
+ if (lines.length > 0) {
289
+ this.flushTail = this.flushTail.then(async () => {
290
+ try {
291
+ await this.send(lines);
292
+ } catch (error) {
293
+ if (!this.flushFailed) {
294
+ this.flushFailed = true;
295
+ this.flushError = error;
296
+ }
297
+ }
298
+ });
299
+ }
300
+ await this.flushTail;
301
+ if (this.flushFailed) {
302
+ throw this.flushError;
303
+ }
304
+ }
305
+ async close() {
306
+ this.closed = true;
307
+ this.clearFlushTimer();
308
+ const lines = this.pending.splice(0);
309
+ await this.flushTail;
310
+ if (this.flushFailed) {
311
+ console.warn(
312
+ "Ignoring a background workflow progress flush failure after the workflow body settled",
313
+ this.flushError
314
+ );
315
+ }
316
+ if (lines.length > 0) {
317
+ try {
318
+ await this.send(lines);
319
+ } catch (error) {
320
+ console.warn(
321
+ "Failed to flush final workflow progress after the workflow body settled",
322
+ error
323
+ );
324
+ }
325
+ }
326
+ }
327
+ scheduleFlush() {
328
+ if (this.flushTimer !== void 0) {
329
+ return;
330
+ }
331
+ this.flushTimer = setTimeout(() => {
332
+ this.flushTimer = void 0;
333
+ void this.flush().catch(() => {
334
+ });
335
+ }, FACTORY_LOG_FLUSH_DELAY_MS);
336
+ this.flushTimer.unref?.();
337
+ }
338
+ clearFlushTimer() {
339
+ if (this.flushTimer !== void 0) {
340
+ clearTimeout(this.flushTimer);
341
+ this.flushTimer = void 0;
342
+ }
343
+ }
344
+ }
191
345
  async function awaitFactoryOperation(operation, signal) {
192
346
  let rejectAbort;
193
347
  const abortPromise = new Promise((_resolve, reject) => {
@@ -202,11 +356,19 @@ async function awaitFactoryOperation(operation, signal) {
202
356
  signal.removeEventListener("abort", onAbort);
203
357
  }
204
358
  }
359
+ async function awaitWorkflowOperation(operation, signal) {
360
+ return awaitFactoryOperation(operation, signal);
361
+ }
205
362
  function throwIfFactoryAborted(signal) {
206
363
  if (signal.aborted) {
207
364
  throw signal.reason ?? new DOMException("Factory run was aborted", "AbortError");
208
365
  }
209
366
  }
367
+ function throwIfWorkflowAborted(signal) {
368
+ if (signal.aborted) {
369
+ throw signal.reason ?? new DOMException("Workflow run was aborted", "AbortError");
370
+ }
371
+ }
210
372
  function isFactoryAbortError(error) {
211
373
  return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
212
374
  }
@@ -245,6 +407,8 @@ class CopilotSession {
245
407
  commandHandlers = /* @__PURE__ */ new Map();
246
408
  factories = /* @__PURE__ */ new Map();
247
409
  factoryAbortControllers = /* @__PURE__ */ new Map();
410
+ workflows = /* @__PURE__ */ new Map();
411
+ workflowAbortControllers = /* @__PURE__ */ new Map();
248
412
  permissionHandler;
249
413
  mcpAuthHandler;
250
414
  userInputHandler;
@@ -328,6 +492,56 @@ class CopilotSession {
328
492
  },
329
493
  cancel: async (runId) => this.rpc.factory.cancel({ runId })
330
494
  };
495
+ workflow = {
496
+ run: (async (nameOrHandle, options) => {
497
+ throwIfWorkflowExecutionIsActive();
498
+ const name = typeof nameOrHandle === "string" ? nameOrHandle : getWorkflowDefinition(nameOrHandle).meta.name;
499
+ const envelope = await this.rpc.workflow.run({
500
+ name,
501
+ args: options?.args === void 0 ? {} : options.args,
502
+ options: {
503
+ limits: options?.limits,
504
+ notifyOnComplete: options?.notifyOnComplete,
505
+ logPhaseNames: options?.logPhaseNames
506
+ }
507
+ });
508
+ return this.settleWorkflowRun(envelope);
509
+ }),
510
+ resume: (async (runId, options) => {
511
+ throwIfWorkflowExecutionIsActive();
512
+ let response;
513
+ try {
514
+ response = await this.rpc.workflow.resume({
515
+ runId,
516
+ limits: options?.limits,
517
+ notifyOnComplete: options?.notifyOnComplete,
518
+ logPhaseNames: options?.logPhaseNames
519
+ });
520
+ } catch (error) {
521
+ if (error instanceof ResponseError && typeof error.data === "object" && error.data !== null) {
522
+ const code = error.data.code;
523
+ if (isWorkflowResumeErrorCode(code)) {
524
+ throw new WorkflowResumeError(code, error.message);
525
+ }
526
+ }
527
+ throw error;
528
+ }
529
+ return this.settleWorkflowRun(response.run);
530
+ }),
531
+ getRun: async (runId) => this.rpc.workflow.getRun({ runId }),
532
+ waitForRun: (runId, options) => this.waitForWorkflowRun(runId, options?.signal),
533
+ listRuns: (async (options) => {
534
+ const page = await this.rpc.workflow.listRuns(options ?? {});
535
+ return options === void 0 ? page.runs : page;
536
+ }),
537
+ getRunDetail: (runId) => this.rpc.workflow.getRunDetail({ runId }),
538
+ getRunProgress: (runId, options = {}) => this.rpc.workflow.getRunProgress({ runId, ...options }),
539
+ pause: async (runId) => {
540
+ throwIfWorkflowExecutionIsActive();
541
+ return this.rpc.workflow.pause({ runId });
542
+ },
543
+ cancel: async (runId) => this.rpc.workflow.cancel({ runId })
544
+ };
331
545
  /**
332
546
  * Resolve a start/resume envelope into the terminal envelope callers expect.
333
547
  *
@@ -415,6 +629,86 @@ class CopilotSession {
415
629
  void read();
416
630
  });
417
631
  }
632
+ settleWorkflowRun(envelope) {
633
+ if (isWorkflowRunTerminal(envelope.status)) {
634
+ return Promise.resolve(envelope);
635
+ }
636
+ return this.waitForWorkflowRun(envelope.runId);
637
+ }
638
+ /**
639
+ * Resolve when a workflow run reaches a terminal status.
640
+ *
641
+ * The subscription is installed *before* the first read so a transition
642
+ * landing between the two cannot be missed, and re-reads are serialized so
643
+ * overlapping invalidation events cannot interleave — the run's revision
644
+ * advances once per operation, so a burst of events is common and must
645
+ * collapse into a single in-flight read. A bounded periodic re-read keeps a
646
+ * dropped invalidation from leaving the wait pending forever.
647
+ */
648
+ waitForWorkflowRun(runId, signal) {
649
+ const abortError = () => signal?.reason ?? new DOMException("Workflow run wait was aborted", "AbortError");
650
+ if (signal?.aborted === true) {
651
+ return Promise.reject(abortError());
652
+ }
653
+ return new Promise((resolve, reject) => {
654
+ let settled = false;
655
+ let reading = false;
656
+ let rereadRequested = false;
657
+ let pollHandle;
658
+ let unsubscribe;
659
+ let onAbort;
660
+ const finish = (complete) => {
661
+ if (settled) {
662
+ return;
663
+ }
664
+ settled = true;
665
+ if (pollHandle !== void 0) {
666
+ clearInterval(pollHandle);
667
+ }
668
+ unsubscribe?.();
669
+ if (onAbort !== void 0) {
670
+ signal?.removeEventListener("abort", onAbort);
671
+ }
672
+ complete();
673
+ };
674
+ const read = async () => {
675
+ if (settled) {
676
+ return;
677
+ }
678
+ if (reading) {
679
+ rereadRequested = true;
680
+ return;
681
+ }
682
+ reading = true;
683
+ try {
684
+ do {
685
+ rereadRequested = false;
686
+ const envelope = await this.rpc.workflow.getRun({ runId });
687
+ if (isWorkflowRunTerminal(envelope.status)) {
688
+ finish(() => resolve(envelope));
689
+ return;
690
+ }
691
+ } while (rereadRequested && !settled);
692
+ } catch (error) {
693
+ finish(() => reject(error));
694
+ } finally {
695
+ reading = false;
696
+ }
697
+ };
698
+ if (signal !== void 0) {
699
+ onAbort = () => finish(() => reject(abortError()));
700
+ signal.addEventListener("abort", onAbort, { once: true });
701
+ }
702
+ unsubscribe = this.on("factory.run_updated", (event) => {
703
+ if (event.data.runId === runId) {
704
+ void read();
705
+ }
706
+ });
707
+ pollHandle = setInterval(() => void read(), 5e3);
708
+ pollHandle.unref?.();
709
+ void read();
710
+ });
711
+ }
418
712
  /**
419
713
  * Typed session-scoped RPC methods.
420
714
  */
@@ -677,6 +971,13 @@ class CopilotSession {
677
971
  }
678
972
  }
679
973
  this.factoryAbortControllers.clear();
974
+ this.workflows.clear();
975
+ for (const controllersForRun of this.workflowAbortControllers.values()) {
976
+ for (const controller of controllersForRun.values()) {
977
+ controller.abort();
978
+ }
979
+ }
980
+ this.workflowAbortControllers.clear();
680
981
  this.transformCallbacks?.clear();
681
982
  }
682
983
  /** @internal */
@@ -1310,6 +1611,192 @@ class CopilotSession {
1310
1611
  }
1311
1612
  };
1312
1613
  }
1614
+ /**
1615
+ * Registers workflow closures and reverse-RPC handlers for this session.
1616
+ *
1617
+ * @param workflows - Workflow handles declared by the joining extension.
1618
+ * @internal Called by the SDK when an extension joins a session.
1619
+ */
1620
+ registerWorkflows(workflows) {
1621
+ this.workflows.clear();
1622
+ if (!workflows || workflows.length === 0) {
1623
+ delete this.clientSessionApis.workflow;
1624
+ return;
1625
+ }
1626
+ for (const handle of workflows) {
1627
+ const definition = getWorkflowDefinition(handle);
1628
+ if (this.workflows.has(definition.meta.name)) {
1629
+ throw new Error(
1630
+ `Duplicate workflow name "${definition.meta.name}". Workflow names must be unique within a joinSession call.`
1631
+ );
1632
+ }
1633
+ this.workflows.set(definition.meta.name, definition);
1634
+ }
1635
+ const self = this;
1636
+ this.clientSessionApis.workflow = {
1637
+ async execute(params) {
1638
+ const definition = self.workflows.get(params.name);
1639
+ if (!definition) {
1640
+ const message = `No workflow registered with name "${params.name}"`;
1641
+ throw new ResponseError(ErrorCodes.InvalidParams, message, {
1642
+ code: "workflow_not_found",
1643
+ name: params.name
1644
+ });
1645
+ }
1646
+ const controller = new AbortController();
1647
+ let controllersForRun = self.workflowAbortControllers.get(params.runId);
1648
+ if (controllersForRun === void 0) {
1649
+ controllersForRun = /* @__PURE__ */ new Map();
1650
+ self.workflowAbortControllers.set(params.runId, controllersForRun);
1651
+ }
1652
+ controllersForRun.set(params.executionToken, controller);
1653
+ const progress = new WorkflowProgressBuffer(async (lines) => {
1654
+ await self.rpc.workflow.log({
1655
+ runId: params.runId,
1656
+ executionToken: params.executionToken,
1657
+ lines
1658
+ });
1659
+ });
1660
+ try {
1661
+ const context = {
1662
+ runId: params.runId,
1663
+ args: params.args,
1664
+ session: self,
1665
+ signal: controller.signal,
1666
+ phase: (title) => {
1667
+ throwIfWorkflowAborted(controller.signal);
1668
+ progress.enqueue("phase", title);
1669
+ },
1670
+ log: (message) => {
1671
+ throwIfWorkflowAborted(controller.signal);
1672
+ progress.enqueue("log", message);
1673
+ },
1674
+ agent: async (prompt, options = {}) => {
1675
+ await progress.flush();
1676
+ const opts = {};
1677
+ for (const key of WORKFLOW_AGENT_OPTION_KEYS) {
1678
+ copyDefinedWorkflowAgentOption(options, opts, key);
1679
+ }
1680
+ const response = await awaitWorkflowOperation(
1681
+ () => self.rpc.workflow.agent({
1682
+ workflowRunId: params.runId,
1683
+ executionToken: params.executionToken,
1684
+ prompt,
1685
+ opts
1686
+ }),
1687
+ controller.signal
1688
+ );
1689
+ return response.result ?? null;
1690
+ },
1691
+ step: async (key, producer, options = {}) => {
1692
+ await progress.flush();
1693
+ if (options.volatile) {
1694
+ throwIfWorkflowAborted(controller.signal);
1695
+ return producer();
1696
+ }
1697
+ const cached = await awaitWorkflowOperation(
1698
+ () => self.rpc.workflow.journal.get({
1699
+ runId: params.runId,
1700
+ executionToken: params.executionToken,
1701
+ key
1702
+ }),
1703
+ controller.signal
1704
+ );
1705
+ if (cached.hit) {
1706
+ if (cached.resultJson === void 0) {
1707
+ throw new Error(
1708
+ `step("${key}") journal returned a hit without a result`
1709
+ );
1710
+ }
1711
+ assertWorkflowStepResult(cached.resultJson, key);
1712
+ return cached.resultJson;
1713
+ }
1714
+ const result2 = await producer();
1715
+ assertWorkflowStepResult(result2, key);
1716
+ await awaitWorkflowOperation(
1717
+ () => self.rpc.workflow.journal.put({
1718
+ runId: params.runId,
1719
+ executionToken: params.executionToken,
1720
+ key,
1721
+ resultJson: result2
1722
+ }),
1723
+ controller.signal
1724
+ );
1725
+ return result2;
1726
+ },
1727
+ pause: async (key) => {
1728
+ if (typeof key !== "string" || key.length === 0) {
1729
+ throw new Error("Workflow pause checkpoint key must not be empty");
1730
+ }
1731
+ const helperScope = workflowExecutionStore.getStore()?.helperScope;
1732
+ if (helperScope !== void 0) {
1733
+ throw new Error(
1734
+ `Workflow pause checkpoints are not allowed inside ${helperScope}() branches`
1735
+ );
1736
+ }
1737
+ await progress.flush();
1738
+ const response = await awaitWorkflowOperation(
1739
+ () => self.internalRpc.workflow.pauseAtCheckpoint({
1740
+ runId: params.runId,
1741
+ executionToken: params.executionToken,
1742
+ key
1743
+ }),
1744
+ controller.signal
1745
+ );
1746
+ switch (response.action) {
1747
+ case "continue":
1748
+ return;
1749
+ case "pause":
1750
+ await awaitWorkflowOperation(
1751
+ () => new Promise(() => {
1752
+ }),
1753
+ controller.signal
1754
+ );
1755
+ }
1756
+ },
1757
+ parallel: runWorkflowParallel,
1758
+ pipeline: runWorkflowPipeline,
1759
+ workflow: async () => {
1760
+ throw new Error("nested workflows are not supported");
1761
+ }
1762
+ };
1763
+ const execution = { active: true };
1764
+ const result = await workflowExecutionStore.run(execution, async () => {
1765
+ try {
1766
+ return await definition.run(context);
1767
+ } finally {
1768
+ execution.active = false;
1769
+ }
1770
+ });
1771
+ if (result === void 0) {
1772
+ return {};
1773
+ }
1774
+ assertWorkflowResult(result);
1775
+ return { result };
1776
+ } finally {
1777
+ try {
1778
+ await progress.close();
1779
+ } finally {
1780
+ const controllersForRun2 = self.workflowAbortControllers.get(params.runId);
1781
+ if (controllersForRun2?.get(params.executionToken) === controller) {
1782
+ controllersForRun2.delete(params.executionToken);
1783
+ if (controllersForRun2.size === 0) {
1784
+ self.workflowAbortControllers.delete(params.runId);
1785
+ }
1786
+ }
1787
+ }
1788
+ }
1789
+ },
1790
+ async abort(params) {
1791
+ const controllersForRun = self.workflowAbortControllers.get(params.runId);
1792
+ const controller = controllersForRun?.get(params.executionToken);
1793
+ if (controller !== void 0) {
1794
+ controller.abort(new DOMException("Workflow run was aborted", "AbortError"));
1795
+ }
1796
+ return {};
1797
+ }
1798
+ };
1799
+ }
1313
1800
  /**
1314
1801
  * Registers per-provider {@link BearerTokenProvider} callbacks for BYOK providers
1315
1802
  * configured with managed-identity / on-demand bearer-token auth.
@@ -2004,6 +2491,20 @@ function assertFactoryStepResult(value, key) {
2004
2491
  allowTopLevelUndefined: false
2005
2492
  });
2006
2493
  }
2494
+ function assertWorkflowResult(value) {
2495
+ assertStrictJson(value, {
2496
+ code: "workflow_result_not_json",
2497
+ label: "Workflow result",
2498
+ allowTopLevelUndefined: true
2499
+ });
2500
+ }
2501
+ function assertWorkflowStepResult(value, key) {
2502
+ assertStrictJson(value, {
2503
+ code: "workflow_step_not_json",
2504
+ label: `Workflow step "${key}" result`,
2505
+ allowTopLevelUndefined: false
2506
+ });
2507
+ }
2007
2508
  export {
2008
2509
  CopilotSession
2009
2510
  };