@github/copilot-sdk 1.0.14 → 1.0.15-preview.1
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 +116 -11
- package/dist/cjs/cliVersion.js +1 -1
- package/dist/cjs/client.js +24 -17
- package/dist/cjs/copilotRequestHandler.js +156 -20
- package/dist/cjs/extension.js +12 -1
- package/dist/cjs/generated/rpc.js +349 -8
- package/dist/cjs/index.js +9 -2
- package/dist/cjs/schema.js +40 -0
- package/dist/cjs/session.js +625 -3
- package/dist/cjs/workflow.js +134 -0
- package/dist/cliVersion.d.ts +1 -1
- package/dist/cliVersion.js +1 -1
- package/dist/client.d.ts +2 -0
- package/dist/client.js +22 -15
- package/dist/copilotRequestHandler.js +156 -20
- package/dist/extension.d.ts +22 -8
- package/dist/extension.js +13 -1
- package/dist/generated/rpc.d.ts +2824 -315
- package/dist/generated/rpc.js +349 -8
- package/dist/generated/session-events.d.ts +565 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/schema.d.ts +4 -0
- package/dist/schema.js +14 -0
- package/dist/session.d.ts +26 -3
- package/dist/session.js +630 -3
- package/dist/types.d.ts +37 -5
- package/dist/workflow.d.ts +364 -0
- package/dist/workflow.js +106 -0
- package/docs/extensions.md +1 -0
- package/docs/workflows.md +257 -0
- package/package.json +17 -14
package/dist/session.js
CHANGED
|
@@ -3,6 +3,7 @@ import { ConnectionError, ErrorCodes, ResponseError } from "vscode-jsonrpc/node.
|
|
|
3
3
|
import { createInternalSessionRpc, createSessionRpc } from "./generated/rpc.js";
|
|
4
4
|
import { CanvasError } from "./canvas.js";
|
|
5
5
|
import { getTraceContext } from "./telemetry.js";
|
|
6
|
+
import { isResponseSchema, toJsonSchema } from "./schema.js";
|
|
6
7
|
import { isAttributedPermissionResult } from "./types.js";
|
|
7
8
|
import {
|
|
8
9
|
FACTORY_AGENT_OPTION_KEYS,
|
|
@@ -10,6 +11,12 @@ import {
|
|
|
10
11
|
FactoryResumeError,
|
|
11
12
|
isFactoryRunTerminal
|
|
12
13
|
} from "./factory.js";
|
|
14
|
+
import {
|
|
15
|
+
WORKFLOW_AGENT_OPTION_KEYS,
|
|
16
|
+
getWorkflowDefinition,
|
|
17
|
+
WorkflowResumeError,
|
|
18
|
+
isWorkflowRunTerminal
|
|
19
|
+
} from "./workflow.js";
|
|
13
20
|
function isFactoryResumeErrorCode(value) {
|
|
14
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";
|
|
15
22
|
}
|
|
@@ -19,6 +26,15 @@ function copyDefinedFactoryAgentOption(source, target, key) {
|
|
|
19
26
|
target[key] = value;
|
|
20
27
|
}
|
|
21
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
|
+
}
|
|
22
38
|
const factoryExecutionStore = new AsyncLocalStorage();
|
|
23
39
|
function throwIfFactoryExecutionIsActive() {
|
|
24
40
|
if (factoryExecutionStore.getStore()?.active) {
|
|
@@ -27,6 +43,18 @@ function throwIfFactoryExecutionIsActive() {
|
|
|
27
43
|
);
|
|
28
44
|
}
|
|
29
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
|
+
}
|
|
30
58
|
function runInFactoryHelperScope(helperScope, callback) {
|
|
31
59
|
const current = factoryExecutionStore.getStore();
|
|
32
60
|
return factoryExecutionStore.run({ active: current?.active ?? false, helperScope }, callback);
|
|
@@ -108,6 +136,54 @@ async function runFactoryPipeline(items, ...stages) {
|
|
|
108
136
|
})
|
|
109
137
|
);
|
|
110
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
|
+
}
|
|
111
187
|
class FactoryProgressBuffer {
|
|
112
188
|
constructor(send) {
|
|
113
189
|
this.send = send;
|
|
@@ -187,6 +263,85 @@ class FactoryProgressBuffer {
|
|
|
187
263
|
}
|
|
188
264
|
}
|
|
189
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
|
+
}
|
|
190
345
|
async function awaitFactoryOperation(operation, signal) {
|
|
191
346
|
let rejectAbort;
|
|
192
347
|
const abortPromise = new Promise((_resolve, reject) => {
|
|
@@ -201,11 +356,19 @@ async function awaitFactoryOperation(operation, signal) {
|
|
|
201
356
|
signal.removeEventListener("abort", onAbort);
|
|
202
357
|
}
|
|
203
358
|
}
|
|
359
|
+
async function awaitWorkflowOperation(operation, signal) {
|
|
360
|
+
return awaitFactoryOperation(operation, signal);
|
|
361
|
+
}
|
|
204
362
|
function throwIfFactoryAborted(signal) {
|
|
205
363
|
if (signal.aborted) {
|
|
206
364
|
throw signal.reason ?? new DOMException("Factory run was aborted", "AbortError");
|
|
207
365
|
}
|
|
208
366
|
}
|
|
367
|
+
function throwIfWorkflowAborted(signal) {
|
|
368
|
+
if (signal.aborted) {
|
|
369
|
+
throw signal.reason ?? new DOMException("Workflow run was aborted", "AbortError");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
209
372
|
function isFactoryAbortError(error) {
|
|
210
373
|
return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
|
|
211
374
|
}
|
|
@@ -244,6 +407,8 @@ class CopilotSession {
|
|
|
244
407
|
commandHandlers = /* @__PURE__ */ new Map();
|
|
245
408
|
factories = /* @__PURE__ */ new Map();
|
|
246
409
|
factoryAbortControllers = /* @__PURE__ */ new Map();
|
|
410
|
+
workflows = /* @__PURE__ */ new Map();
|
|
411
|
+
workflowAbortControllers = /* @__PURE__ */ new Map();
|
|
247
412
|
permissionHandler;
|
|
248
413
|
mcpAuthHandler;
|
|
249
414
|
userInputHandler;
|
|
@@ -259,6 +424,7 @@ class CopilotSession {
|
|
|
259
424
|
_capabilities = {};
|
|
260
425
|
openCanvasInstances = [];
|
|
261
426
|
disconnected = false;
|
|
427
|
+
pendingStructuredWaits = /* @__PURE__ */ new Set();
|
|
262
428
|
disconnecting = false;
|
|
263
429
|
onDisconnected;
|
|
264
430
|
/** @internal Client session API handlers, populated by CopilotClient during create/resume. */
|
|
@@ -326,6 +492,56 @@ class CopilotSession {
|
|
|
326
492
|
},
|
|
327
493
|
cancel: async (runId) => this.rpc.factory.cancel({ runId })
|
|
328
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
|
+
};
|
|
329
545
|
/**
|
|
330
546
|
* Resolve a start/resume envelope into the terminal envelope callers expect.
|
|
331
547
|
*
|
|
@@ -413,6 +629,86 @@ class CopilotSession {
|
|
|
413
629
|
void read();
|
|
414
630
|
});
|
|
415
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
|
+
}
|
|
416
712
|
/**
|
|
417
713
|
* Typed session-scoped RPC methods.
|
|
418
714
|
*/
|
|
@@ -475,13 +771,54 @@ class CopilotSession {
|
|
|
475
771
|
attachments: options.attachments,
|
|
476
772
|
mode: options.mode,
|
|
477
773
|
agentMode: options.agentMode,
|
|
478
|
-
requestHeaders: options.requestHeaders
|
|
774
|
+
requestHeaders: options.requestHeaders,
|
|
775
|
+
...options.responseSchema ? {
|
|
776
|
+
responseFormat: {
|
|
777
|
+
type: "json_schema",
|
|
778
|
+
jsonSchema: {
|
|
779
|
+
name: "response",
|
|
780
|
+
strict: true,
|
|
781
|
+
schema: toJsonSchema(options.responseSchema)
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
} : {}
|
|
479
785
|
});
|
|
480
786
|
return response.messageId;
|
|
481
787
|
}
|
|
482
|
-
async sendAndWait(optionsOrPrompt, timeout) {
|
|
788
|
+
async sendAndWait(optionsOrPrompt, schemaOrTimeout, timeout) {
|
|
483
789
|
const options = typeof optionsOrPrompt === "string" ? { prompt: optionsOrPrompt } : optionsOrPrompt;
|
|
484
|
-
const
|
|
790
|
+
const typedSchema = isResponseSchema(schemaOrTimeout) ? schemaOrTimeout : void 0;
|
|
791
|
+
if (schemaOrTimeout !== void 0 && typeof schemaOrTimeout !== "number" && !typedSchema) {
|
|
792
|
+
throw new TypeError(
|
|
793
|
+
"The second argument must be a timeout or a schema with toJSONSchema() and parse(). Pass raw JSON Schema in options.responseSchema instead."
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
const effectiveTimeout = (typeof schemaOrTimeout === "number" ? schemaOrTimeout : timeout) ?? 6e4;
|
|
797
|
+
if (typedSchema && options.responseSchema) {
|
|
798
|
+
throw new Error(
|
|
799
|
+
"Do not specify responseSchema in options when requesting a typed response."
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
if (typedSchema && options.mode === "immediate") {
|
|
803
|
+
throw new Error(
|
|
804
|
+
"Structured output cannot be requested on an immediate steering message."
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
if (typedSchema || options.responseSchema) {
|
|
808
|
+
const message = await this.sendAndWaitForStructuredMessage(
|
|
809
|
+
typedSchema ? { ...options, responseSchema: typedSchema } : options,
|
|
810
|
+
effectiveTimeout
|
|
811
|
+
);
|
|
812
|
+
if (typedSchema) {
|
|
813
|
+
if (!message) {
|
|
814
|
+
throw new Error(
|
|
815
|
+
"The requested run completed without a structured assistant response."
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
return typedSchema.parse(JSON.parse(message.data.content));
|
|
819
|
+
}
|
|
820
|
+
return message;
|
|
821
|
+
}
|
|
485
822
|
let resolveOutcome;
|
|
486
823
|
const outcomePromise = new Promise((resolve) => {
|
|
487
824
|
resolveOutcome = resolve;
|
|
@@ -523,12 +860,95 @@ class CopilotSession {
|
|
|
523
860
|
unsubscribe();
|
|
524
861
|
}
|
|
525
862
|
}
|
|
863
|
+
async sendAndWaitForStructuredMessage(options, timeout) {
|
|
864
|
+
if (this.disconnected) {
|
|
865
|
+
throw new Error("Session is disconnected");
|
|
866
|
+
}
|
|
867
|
+
let resolveOutcome;
|
|
868
|
+
const outcomePromise = new Promise((resolve) => {
|
|
869
|
+
resolveOutcome = resolve;
|
|
870
|
+
});
|
|
871
|
+
const fail = (error) => resolveOutcome({ kind: "error", error });
|
|
872
|
+
let messageId;
|
|
873
|
+
let consumed = false;
|
|
874
|
+
let lastMessage;
|
|
875
|
+
const buffered = [];
|
|
876
|
+
const observe = (event) => {
|
|
877
|
+
if (event.agentId) return;
|
|
878
|
+
if (event.type === "user.message" && event.data.messageId === messageId) {
|
|
879
|
+
consumed = true;
|
|
880
|
+
} else if (event.type === "assistant.message" && event.data.originatingMessageId === messageId) {
|
|
881
|
+
consumed = true;
|
|
882
|
+
lastMessage = event.data.toolRequests?.length ? void 0 : event;
|
|
883
|
+
} else if (consumed && event.type === "session.idle" && event.data.mode !== "autopilot") {
|
|
884
|
+
if (event.data.aborted) {
|
|
885
|
+
fail(
|
|
886
|
+
new Error(
|
|
887
|
+
"The requested run was aborted before a structured result was completed."
|
|
888
|
+
)
|
|
889
|
+
);
|
|
890
|
+
} else {
|
|
891
|
+
resolveOutcome({ kind: "idle", message: lastMessage });
|
|
892
|
+
}
|
|
893
|
+
} else if (consumed && event.type === "session.error") {
|
|
894
|
+
const error = new Error(event.data.message);
|
|
895
|
+
error.stack = event.data.stack;
|
|
896
|
+
fail(error);
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
const unsubscribe = this.on((event) => {
|
|
900
|
+
if (event.type !== "user.message" && event.type !== "assistant.message" && event.type !== "session.idle" && event.type !== "session.error") {
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (messageId === void 0) {
|
|
904
|
+
buffered.push(event);
|
|
905
|
+
} else {
|
|
906
|
+
observe(event);
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
this.pendingStructuredWaits.add(fail);
|
|
910
|
+
const timer = setTimeout(
|
|
911
|
+
() => fail(new Error(`Timeout after ${timeout}ms waiting for the structured response`)),
|
|
912
|
+
timeout
|
|
913
|
+
);
|
|
914
|
+
try {
|
|
915
|
+
const sendOutcome = this.send(options).then(
|
|
916
|
+
(id) => {
|
|
917
|
+
if (!id) {
|
|
918
|
+
throw new Error(
|
|
919
|
+
"The runtime did not return a message ID for the structured send."
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
messageId = id;
|
|
923
|
+
for (const event of buffered) observe(event);
|
|
924
|
+
buffered.length = 0;
|
|
925
|
+
return outcomePromise;
|
|
926
|
+
},
|
|
927
|
+
(error) => ({
|
|
928
|
+
kind: "error",
|
|
929
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
930
|
+
})
|
|
931
|
+
);
|
|
932
|
+
const outcome = await Promise.race([sendOutcome, outcomePromise]);
|
|
933
|
+
if (outcome.kind === "error") throw outcome.error;
|
|
934
|
+
return outcome.message;
|
|
935
|
+
} finally {
|
|
936
|
+
clearTimeout(timer);
|
|
937
|
+
buffered.length = 0;
|
|
938
|
+
unsubscribe();
|
|
939
|
+
this.pendingStructuredWaits.delete(fail);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
526
942
|
/** @internal */
|
|
527
943
|
_markDisconnected() {
|
|
528
944
|
if (this.disconnected) {
|
|
529
945
|
return;
|
|
530
946
|
}
|
|
531
947
|
this.disconnected = true;
|
|
948
|
+
for (const fail of this.pendingStructuredWaits) {
|
|
949
|
+
fail(new Error("Session disconnected while waiting for a structured response"));
|
|
950
|
+
}
|
|
951
|
+
this.pendingStructuredWaits.clear();
|
|
532
952
|
for (const controller of this.pendingExternalTools.values()) {
|
|
533
953
|
controller.abort();
|
|
534
954
|
}
|
|
@@ -551,6 +971,13 @@ class CopilotSession {
|
|
|
551
971
|
}
|
|
552
972
|
}
|
|
553
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();
|
|
554
981
|
this.transformCallbacks?.clear();
|
|
555
982
|
}
|
|
556
983
|
/** @internal */
|
|
@@ -1184,6 +1611,192 @@ class CopilotSession {
|
|
|
1184
1611
|
}
|
|
1185
1612
|
};
|
|
1186
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
|
+
}
|
|
1187
1800
|
/**
|
|
1188
1801
|
* Registers per-provider {@link BearerTokenProvider} callbacks for BYOK providers
|
|
1189
1802
|
* configured with managed-identity / on-demand bearer-token auth.
|
|
@@ -1878,6 +2491,20 @@ function assertFactoryStepResult(value, key) {
|
|
|
1878
2491
|
allowTopLevelUndefined: false
|
|
1879
2492
|
});
|
|
1880
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
|
+
}
|
|
1881
2508
|
export {
|
|
1882
2509
|
CopilotSession
|
|
1883
2510
|
};
|