Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b9d143b
Phase 1: Contract skeleton and feature flag
MRayermannMSFT Jul 15, 2026
d211489
Phase 2: SDK authoring surface (defineWorkflow + registration)
MRayermannMSFT Jul 15, 2026
20fc956
Phase 4: Host API context, part 1 (args, log, phase, return)
MRayermannMSFT Jul 15, 2026
69854e2
Phase 5: The one-agent executor (`agent()` via in-session subagents)
MRayermannMSFT Jul 15, 2026
d7c622b
Phase 6: Structured output (`agent({ schema })`) and retry
MRayermannMSFT Jul 15, 2026
be55827
Phase 7: Combinators (`parallel`, `pipeline`) and the per-run limiter
MRayermannMSFT Jul 16, 2026
beabf08
Phase 8: Durable `step()` and execution state in SQL
MRayermannMSFT Jul 16, 2026
00513d6
Phase 9: Limits enforcement and the pre-run approval gate
MRayermannMSFT Jul 16, 2026
d5b73aa
Phase 10: Full session and Node power (the trust-tier payoff)
MRayermannMSFT Jul 16, 2026
c6d9921
Phase 11: Invocation ergonomics: the `run_workflow` tool, commands, n…
MRayermannMSFT Jul 16, 2026
7d51f98
Phase 12: Forbid workflow nesting
MRayermannMSFT Jul 16, 2026
fc0626e
Mark Dynamic Workflows SDK APIs As Experimental
MRayermannMSFT Jul 16, 2026
713b4cb
Rename Dynamic Workflows to Agent Orchestrations
MRayermannMSFT Jul 16, 2026
41e181e
CCR 1
MRayermannMSFT Jul 17, 2026
59af1ba
Rename Agent Orchestrations to Agent Factories
MRayermannMSFT Jul 17, 2026
8eaa776
CCR 2
MRayermannMSFT Jul 17, 2026
611c7c1
CCR 3
MRayermannMSFT Jul 17, 2026
36baf44
CCR 4
MRayermannMSFT Jul 17, 2026
58a7f07
CCR 5
MRayermannMSFT Jul 17, 2026
754c28f
Factories: Propagate cancellation out of parallel/pipeline (CR-5)
MRayermannMSFT Jul 17, 2026
428ff9d
Shift 3
MRayermannMSFT Jul 18, 2026
372f740
Shift 4
MRayermannMSFT Jul 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import type {
TypedSessionLifecycleHandler,
} from "./types.js";
import { defaultJoinSessionPermissionHandler } from "./types.js";
import type { FactoryHandle } from "./factory.js";

/**
* Minimum protocol version this SDK can communicate with.
Expand Down Expand Up @@ -1663,6 +1664,23 @@ export class CopilotClient {
* ```
*/
async resumeSession(sessionId: string, config: ResumeSessionConfig): Promise<CopilotSession> {
return this.resumeSessionInternal(sessionId, config);
}

/** @internal */
async resumeSessionForExtension(
sessionId: string,
config: ResumeSessionConfig,
factories?: FactoryHandle[]
): Promise<CopilotSession> {
return this.resumeSessionInternal(sessionId, config, factories);
}

private async resumeSessionInternal(
sessionId: string,
config: ResumeSessionConfig,
factories?: FactoryHandle[]
): Promise<CopilotSession> {
if (!this.connection) {
await this.start();
}
Expand All @@ -1679,6 +1697,7 @@ export class CopilotClient {
session.registerTools(config.tools);
session.registerCanvases(config.canvases);
session.registerCommands(config.commands);
session.registerFactories(factories);
const {
wireProvider: bearerWireProvider,
wireProviders: bearerWireProviders,
Expand Down Expand Up @@ -1750,6 +1769,7 @@ export class CopilotClient {
})),
toolSearch: config.toolSearch,
canvases: config.canvases?.map((canvas) => canvas.declaration),
factories: factories?.map((factory) => factory.meta),
requestCanvasRenderer: config.requestCanvasRenderer,
requestExtensions: config.requestExtensions,
extensionSdkPath: config.extensionSdkPath,
Expand Down
45 changes: 37 additions & 8 deletions nodejs/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { CopilotClient } from "./client.js";
import type { CopilotSession } from "./session.js";
import {
defaultJoinSessionPermissionHandler,
type ExtensionInfo,
type PermissionHandler,
type ResumeSessionConfig,
} from "./types.js";
import type { FactoryHandle } from "./factory.js";

export {
Canvas,
Expand All @@ -27,9 +27,30 @@ export type JoinSessionConfig = Omit<
"onPermissionRequest" | "extensionSdkPath"
> & {
onPermissionRequest?: PermissionHandler;
/**
* Factory handles to register when the extension joins the session.
*
* @experimental Part of the experimental Agent Factories surface and may
* change or be removed in future SDK or CLI releases.
*/
factories?: FactoryHandle[];
};

export type { ExtensionInfo };
export type { ExtensionInfo, FactoryLimits, FactoryMeta } from "./types.js";
export {
defineFactory,
FactoryRunError,
type RunOptions,
type SessionFactoryApi,
type FactoryAgentOptions,
type FactoryContext,
type FactoryDefinition,
type FactoryHandle,
type FactoryJsonSchema,
type FactoryPipelineStage,
type FactoryStepOptions,
type FactoryRunResult,
} from "./factory.js";

/**
* Joins the current foreground session.
Expand Down Expand Up @@ -58,14 +79,22 @@ export async function joinSession(config: JoinSessionConfig = {}): Promise<Copil
// at the type level — untyped (JS) callers can still slip it through, and
// honoring it here would be misleading since the extension subprocess has
// already been forked by the host with the SDK the host chose.
const { extensionSdkPath: _stripped, ...rest } = config as JoinSessionConfig & {
const {
extensionSdkPath: _stripped,
factories,
...rest
} = config as JoinSessionConfig & {
extensionSdkPath?: string;
};
void _stripped;

return client.resumeSession(sessionId, {
...rest,
onPermissionRequest: config.onPermissionRequest ?? defaultJoinSessionPermissionHandler,
suppressResumeEvent: config.suppressResumeEvent ?? true,
});
return client.resumeSessionForExtension(
sessionId,
{
...rest,
onPermissionRequest: config.onPermissionRequest ?? defaultJoinSessionPermissionHandler,
suppressResumeEvent: config.suppressResumeEvent ?? true,
},
factories
);
}
251 changes: 251 additions & 0 deletions nodejs/src/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

import type { FactoryRunResult } from "./generated/rpc.js";
import type { CopilotSession } from "./session.js";
import type { FactoryLimits, FactoryMeta } from "./types.js";

/**
* The terminal envelope describing a factory run's outcome (status, result,
* reason). Re-exported so consumers can name the type returned by
* {@link SessionFactoryApi} methods and carried on {@link FactoryRunError}.
*/
export type { FactoryRunResult } from "./generated/rpc.js";

declare const factoryHandleBrand: unique symbol;

/**
* Conservative JSON shape language accepted for structured factory agent output.
*
* This is a best-effort structural guard used to decide whether a subagent's
* structured output should be accepted or retried — **not** a full JSON Schema
* validator. Only these keywords are honored: `type`, `required`, `enum`,
* `const`, recursive `properties`/`items`, and `anyOf`/`oneOf`/`allOf`.
*
* Everything else is **ignored, not enforced**. In particular, string
* constraints (`pattern`, `minLength`, `maxLength`, `format`), numeric ranges
* (`minimum`, `maximum`), `additionalProperties`, and boolean (`true`/`false`)
* schemas do not reject non-conforming output. `oneOf` is treated like `anyOf`
* (at least one branch must match) rather than strict exactly-one. Author
* schemas within this subset; do not rely on unsupported constraints for
* correctness.
*
* @experimental Part of the experimental Agent Factories surface and may
* change or be removed in future SDK or CLI releases.
*/
export type FactoryJsonSchema = Record<string, unknown>;

/**
* Options for one factory-scoped subagent call.
*
* @experimental Part of the experimental Agent Factories surface and may
* change or be removed in future SDK or CLI releases.
*/
export interface FactoryAgentOptions {
label?: string;
schema?: FactoryJsonSchema;
model?: string;
}

/**
* Options for a durable factory step.
*
* @experimental Part of the experimental Agent Factories surface and may
* change or be removed in future SDK or CLI releases.
*/
export interface FactoryStepOptions {
/** Skip the journal and always invoke the producer. */
volatile?: boolean;
}

/**
* One stage in a per-item factory pipeline.
*
* @experimental Part of the experimental Agent Factories surface and may
* change or be removed in future SDK or CLI releases.
*/
export type FactoryPipelineStage<TInput = unknown, TResult = unknown> = (
previous: TInput,
item: unknown,
index: number
) => Promise<TResult> | TResult;

/**
* Context passed to an extension-authored factory body.
*
* @experimental Part of the experimental Agent Factories surface and may
* change or be removed in future SDK or CLI releases.
*/
export interface FactoryContext<TArgs = unknown> {
/** Spawn and await one factory-scoped subagent. */
agent(prompt: string, options?: FactoryAgentOptions): Promise<unknown>;
/** Memoize an arbitrary producer under a stable author-supplied key. */
step<TResult>(
key: string,
producer: () => Promise<TResult> | TResult,
options?: FactoryStepOptions
): Promise<TResult>;
/** Run thunks concurrently, returning null for a thunk that throws. */
parallel<TResult>(
thunks: Array<() => Promise<TResult> | TResult>
): Promise<Array<TResult | null>>;
/** Run each item through every stage without barriers between stages. */
pipeline(items: unknown[], ...stages: FactoryPipelineStage[]): Promise<unknown[]>;
/** Start a named factory progress phase. */
phase(title: string): void;
/** Emit a factory progress line. */
log(message: string): void;
/** Reject because nested factories are not supported. */
factory(name: string, args?: unknown): Promise<unknown>;
/** Caller-supplied input, forwarded verbatim. */
args: TArgs;
/** The same full session instance returned by `joinSession`. */
session: CopilotSession;
/** Cooperative cancellation signal for the current factory run. */
signal: AbortSignal;
}

/**
* Definition accepted by {@link defineFactory}.
*
* @experimental Part of the experimental Agent Factories surface and may
* change or be removed in future SDK or CLI releases.
*/
export interface FactoryDefinition<TArgs = unknown, TResult = unknown> {
meta: FactoryMeta;
run(context: FactoryContext<TArgs>): Promise<TResult>;
}

/**
* Opaque reusable reference to a defined factory.
*
* @experimental Part of the experimental Agent Factories surface and may
* change or be removed in future SDK or CLI releases.
*/
export interface FactoryHandle<TArgs = unknown, TResult = unknown> {
readonly meta: FactoryMeta;
readonly [factoryHandleBrand]: {
readonly args: TArgs;
readonly result: TResult;
};
}

/**
* Options for invoking a factory.
*
* @experimental Part of the experimental Agent Factories surface and may
* change or be removed in future SDK or CLI releases.
*/
export interface RunOptions<TArgs = unknown> {
/** Input surfaced as `context.args`. */
args?: TArgs;
/** Optional per-invocation resource ceiling overrides. */
limits?: FactoryLimits;
/** Prior run whose journal and progress should seed this run. */
resumeFromRunId?: string;
}

/**
* Friendly factory API exposed on a session.
*
* @experimental Part of the experimental Agent Factories surface and may
* change or be removed in future SDK or CLI releases.
*/
export interface SessionFactoryApi {
run<TResult = unknown>(name: string, options?: RunOptions): Promise<TResult>;
run<TArgs, TResult>(
factory: FactoryHandle<TArgs, TResult>,
options?: RunOptions<TArgs>
): Promise<TResult>;
/** Read the latest durable envelope for a factory run. */
getRun(runId: string): Promise<FactoryRunResult>;
/** Cancel a factory run and return its terminal envelope. */
cancel(runId: string): Promise<FactoryRunResult>;
}

/**
* Error thrown when a foreground factory run does not complete successfully.
*
* @experimental Part of the experimental Agent Factories surface and may
* change or be removed in future SDK or CLI releases.
*/
export class FactoryRunError extends Error {
constructor(public readonly envelope: FactoryRunResult) {
super(
envelope.error ??
envelope.reason ??
`Factory run "${envelope.runId}" ended with status "${envelope.status}"`
);
this.name = "FactoryRunError";
}
}

interface StoredFactory {
meta: FactoryMeta;
run(context: FactoryContext<unknown>): Promise<unknown>;
}

const factoryHandles = new WeakMap<object, StoredFactory>();

/**
* Maximum accepted factory timeout in milliseconds (2^31-1). Node clamps
* `setTimeout` delays above this to ~1ms, so a larger value would invert the
* declared timeout into an immediate halt.
*/
const MAX_FACTORY_TIMEOUT_MS = 2_147_483_647;

function validateLimits(meta: FactoryMeta): void {
const limits = meta.limits;
if (!limits) {
return;
}

for (const field of ["maxConcurrentSubagents", "maxTotalSubagents"] as const) {
const value = limits[field];
if (value !== undefined && (!Number.isInteger(value) || value <= 0)) {
throw new Error(`Factory limit "${field}" must be a positive integer`);
}
}

if (limits.timeout !== undefined && (!Number.isFinite(limits.timeout) || limits.timeout <= 0)) {
throw new Error('Factory limit "timeout" must be a positive number of milliseconds');
}
// Node clamps setTimeout delays above 2^31-1 ms to ~1ms, which would make a
// large timeout halt the run almost immediately. Reject it up front.
if (limits.timeout !== undefined && limits.timeout > MAX_FACTORY_TIMEOUT_MS) {
throw new Error(
`Factory limit "timeout" must not exceed ${MAX_FACTORY_TIMEOUT_MS} milliseconds (~24.8 days)`
);
}
}

/**
* Defines an extension-authored factory and returns an opaque registration handle.
*
* @experimental Part of the experimental Agent Factories surface and may
* change or be removed in future SDK or CLI releases.
*/
export function defineFactory<TArgs = unknown, TResult = unknown>(
definition: FactoryDefinition<TArgs, TResult>
): FactoryHandle<TArgs, TResult> {
validateLimits(definition.meta);

const stored: StoredFactory = {
meta: definition.meta,
run: definition.run as StoredFactory["run"],
};
const handle = Object.freeze({ meta: definition.meta }) as FactoryHandle<TArgs, TResult>;

factoryHandles.set(handle, stored);
return handle;
}

/** @internal */
export function getFactoryDefinition(handle: FactoryHandle): StoredFactory {
const definition = factoryHandles.get(handle);
if (!definition) {
throw new Error("Invalid factory handle");
}
return definition;
}
Loading
Loading