Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 36 additions & 19 deletions nodejs/docs/factories.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,20 @@ Validation covers the model's `run_factory` path only. An extension calling `ses

The `run()` context provides:

- `ctx.runId`: Stable ID reused across resumed attempts.
- `ctx.args`: Invocation arguments, forwarded verbatim. When the caller omits `args`, this is `{}` rather than `undefined`.
- `ctx.agent(prompt, options?)`: Runs one factory-owned subagent. Options are exactly `label`, `schema`, `model`, `agent`, `reasoningEffort`, and `contextTier`. See [Subagent calls](#subagent-calls).
- `ctx.parallel(thunks)`: Runs thunks concurrently and awaits all of them (a barrier). A thunk that throws becomes `null` in the result array, so one failed item does not lose the rest. Cancellation and hard runtime failures (`ResponseError`, `ConnectionError`) are the exception — those propagate and reject the whole call, because they mean the run itself is in trouble rather than one item having failed. Handle them at run level; do not assume every failure arrives as a `null`. Rejects above 4096 items.
- `ctx.pipeline(items, ...stages)`: Flows each item through every stage without a barrier between stages, so one item can be in a later stage while another is still in an earlier one. Each stage is called as `(previous, item, index)`, where `previous` is the prior stage's result and `item` is the original input. A stage that throws drops that item to `null` and skips its remaining stages, with the same exception for cancellation and hard runtime failures. Rejects above 4096 items.
- `ctx.phase(title)`: Starts a named progress phase. This sets a single run-global value, so calling it from inside concurrent `parallel`/`pipeline` stages races. Call it at run-level transitions and distinguish concurrent work by `label` instead.
- `ctx.log(message)`: Appends a progress line. When a factory bounds its own coverage (top-N, sampling), log what was dropped.
- `ctx.step(key, producer, options?)`: Journals the producer's JSON result under a stable key so a resume replays it without re-running the producer. A journaled (default) producer must return a JSON-serializable value; `undefined` or a non-JSON value is rejected. Pass `{ volatile: true }` to bypass the journal and run the producer every time.
* `ctx.runId`: Stable ID reused across resumed attempts.
* `ctx.args`: Invocation arguments, forwarded verbatim. When the caller omits `args`, this is `{}` rather than `undefined`.
* `ctx.agent(prompt, options?)`: Runs one factory-owned subagent. Options are exactly `label`, `schema`, `model`, `agent`, `reasoningEffort`, and `contextTier`. See [Subagent calls](#subagent-calls).
* `ctx.parallel(thunks)`: Runs thunks concurrently and awaits all of them (a barrier). A thunk that throws becomes `null` in the result array, so one failed item does not lose the rest. Cancellation and hard runtime failures (`ResponseError`, `ConnectionError`) are the exception — those propagate and reject the whole call, because they mean the run itself is in trouble rather than one item having failed. Handle them at run level; do not assume every failure arrives as a `null`. Rejects above 4096 items.
* `ctx.pipeline(items, ...stages)`: Flows each item through every stage without a barrier between stages, so one item can be in a later stage while another is still in an earlier one. Each stage is called as `(previous, item, index)`, where `previous` is the prior stage's result and `item` is the original input. A stage that throws drops that item to `null` and skips its remaining stages, with the same exception for cancellation and hard runtime failures. Rejects above 4096 items.
* `ctx.phase(title)`: Starts a named progress phase. This sets a single run-global value, so calling it from inside concurrent `parallel`/`pipeline` stages races. Call it at run-level transitions and distinguish concurrent work by `label` instead.
* `ctx.log(message)`: Appends a progress line. When a factory bounds its own coverage (top-N, sampling), log what was dropped.
* `ctx.step(key, producer, options?)`: Journals the producer's JSON result under a stable key so a resume replays it without re-running the producer. A journaled (default) producer must return a JSON-serializable value; `undefined` or a non-JSON value is rejected. Pass `{ volatile: true }` to bypass the journal and run the producer every time.

The key is the *sole* identity: neither the producer body nor its inputs contribute to it. A resume replays the cached value for a matching key even if the producer has since changed, so version the key (`"scan-v2"`) whenever its inputs or meaning change. Journaled producers are best-effort at-least-once and may run again across crashes or concurrent same-key callers, so keep side effects idempotent.
- `ctx.session`: The session returned by `joinSession`. It refuses calls that start or resume a factory run. Call `extensions_manage` with `operation: "guide"` to read more about the session APIs.
- `ctx.signal`: Cooperative cancellation signal for extension work and subprocesses.
- `ctx.factory(...)`: Always rejects because nested factories are not supported.
* `ctx.pause(key)`: Pauses at a durable, one-shot checkpoint. The first attempt records the checkpoint, pauses, and throws `AbortError` after cooperative cancellation. When the run resumes, the factory starts again and the same checkpoint returns so execution can continue. Call it only from the main factory flow, not inside `ctx.parallel()` or `ctx.pipeline()`.
* `ctx.session`: The session returned by `joinSession`. It refuses calls that start, resume, or pause a factory run. Call `extensions_manage` with `operation: "guide"` to read more about the session APIs.
* `ctx.signal`: Cooperative cancellation signal for extension work and subprocesses.
* `ctx.factory(...)`: Always rejects because nested factories are not supported.

Factory-owned subagents are intentionally hidden from `read_agent` and `write_agent`. Use the factory observability APIs instead.

Expand Down Expand Up @@ -157,7 +158,7 @@ session.factory.run(
name: string,
options?: {
args?: JsonValue;
limits?: FactoryLimits;
limits?: FactoryLimitOverrides;
notifyOnComplete?: boolean;
logPhaseNames?: boolean;
},
Expand All @@ -180,7 +181,7 @@ The signature is:
session.factory.resume(
runId: string,
options?: {
limits?: FactoryLimits;
limits?: FactoryLimitOverrides;
notifyOnComplete?: boolean;
logPhaseNames?: boolean;
},
Expand All @@ -189,15 +190,31 @@ session.factory.resume(

Set `notifyOnComplete` to `true` for factories that are likely to be invoked by an agent, so the originating session is notified when the factory completes. Set it to `false` for factories intended to be invoked programmatically, where the caller awaits the result directly. Set `logPhaseNames` to emit factory phase names to the session transcript. Both options apply to new and resumed runs.

Both resolve with the run envelope (`FactoryRunResult`) for **every** outcome — `completed`, `error`, `halted`, and `cancelled` alike. Inspect `status` and read `result` only when the run completed; a limit breach carries a typed `failure`. SDK-initiated `run` and `resume` do not request permission, so they have no declined outcome. The model's `run_factory` tool requests permission before the durable row exists; declining it creates no run row. An SDK-initiated run is refused only when the session already has its maximum number of active top-level runs. Pre-execution resume failures throw `FactoryResumeError`, whose `code` is one of `not_found`, `non_resumable`, `already_active`, `factory_already_running`, `factory_limits_invalid`, `factory_session_disposed`, `factory_storage_unavailable`, or `factory_storage_corrupt`.
Both resolve with the run envelope (`FactoryRunResult`) for **every** outcome—`completed`, `error`, `halted`, `paused`, and `cancelled` alike. Inspect `status` and read `result` only when the run completed; a limit breach carries a typed `failure`. A `paused` envelope means that the current attempt settled, not that the durable run is permanently finished. Resume the same run ID to start another attempt with its journal and accounting intact. SDK-initiated `run` and `resume` do not request permission, so they have no declined outcome. The model's `run_factory` tool requests permission before the durable row exists; declining it creates no run row. An SDK-initiated run is refused only when the session already has its maximum number of active top-level runs. Pre-execution resume failures throw `FactoryResumeError`, whose `code` is one of `not_found`, `non_resumable`, `already_active`, `factory_already_running`, `factory_limits_invalid`, `factory_session_disposed`, `factory_storage_unavailable`, or `factory_storage_corrupt`.

An agent that no longer has a prior run's ID in context can recover it with `factories_manage` and `operation: "runs"`, which lists the session's factory runs with their IDs and statuses. This matters for resume: a run that reached a limit keeps its journal, so resuming it replays completed work for free, while restarting it from scratch pays for that work twice.

Pause a running attempt from outside its factory body:

```ts
const paused = await session.factory.pause(runId);
```

Inside a factory body, use a durable checkpoint instead:

```ts
await ctx.step("prepare", prepareInput);
await ctx.pause("review-ready");
await ctx.agent("Review the prepared input");
```

The first attempt pauses at `"review-ready"` and ends through cooperative cancellation. On resume, the factory starts from the beginning, reuses the journaled step, returns from the checkpoint, and continues.

The agent-facing `run_factory` tool has exactly two input branches:

```ts
{ name: string; args?: JsonValue; limits?: FactoryLimits }
{ resumeFromRunId: string; limits?: FactoryLimits }
{ name: string; args?: JsonValue; limits?: FactoryLimitOverrides }
{ resumeFromRunId: string; limits?: FactoryLimitOverrides }
```

## Authoring a factory from inside a session
Expand Down Expand Up @@ -253,9 +270,9 @@ const progressPage = await session.factory.getRunProgress(runId, {
- `getRunDetail(runId)` returns phases, prompt-safe agent summaries, and the latest progress page.
- `getRunProgress(runId, options?)` pages progress forward, backward, by phase, or from the latest tail.

`getRun(runId)` reads the latest run envelope, and `cancel(runId)` cancels a run and returns its terminal envelope.
`getRun(runId)` reads the latest run envelope. `pause(runId)` pauses a running attempt and returns its `paused` envelope. `cancel(runId)` cancels a run and returns its terminal envelope.

`waitForRun(runId, options?)` resolves with the terminal envelope once the run settles into `completed`, `error`, `halted`, or `cancelled`, and resolves immediately when it has already settled:
`waitForRun(runId, options?)` resolves with the current attempt's envelope once it settles into `completed`, `error`, `halted`, `paused`, or `cancelled`. It resolves immediately when the current attempt has already settled:

```ts
const settled = await session.factory.waitForRun(runId);
Expand All @@ -272,7 +289,7 @@ setTimeout(() => controller.abort(), 30_000);
const settled = await session.factory.waitForRun(runId, { signal: controller.signal });
```

Aborting rejects the wait and has no effect on the run, which keeps executinguse `cancel(runId)` to actually stop it. Because a terminal envelope is final, the resolved value never changes afterwards. `isFactoryRunTerminal(status)` exposes the same terminal-status test for callers driving their own loop.
Aborting rejects the wait and has no effect on the run, which keeps executinguse `pause(runId)` or `cancel(runId)` to stop it. The resolved object is a snapshot of that settled attempt. If its status is `paused`, a later resume updates the durable envelope under the same run ID. Call `getRun(runId)` to read the latest envelope. `isFactoryRunTerminal(status)` exposes the same current-attempt settlement test for callers driving their own loop.

Listen for the ephemeral `factory.run_updated` event. Its `{ runId, revision }` payload is an invalidation signal. Re-read the desired API when a newer monotonic revision arrives.

Expand Down
67 changes: 48 additions & 19 deletions nodejs/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {
} from "./generated/rpc.js";
import type { ContextTier } from "./generated/session-events.js";
import type { CopilotSession } from "./session.js";
import type { FactoryLimits, FactoryMeta } from "./types.js";
import type { FactoryMeta } from "./types.js";

export type { FactoryRunResult };
export type {
Expand Down Expand Up @@ -47,13 +47,15 @@ export type FactoryRunsPage = FactoryListRunsResult;
/**
* Run statuses a factory run can no longer move away from.
*
* A run is either still in flight (`pending`, `running`) or settled into one of
* these four. Terminal state is final: once written it is never reopened, so a
* caller that observes one of these can stop watching the run.
* A run is either still in flight (`pending`, `running`) or its current attempt
* has settled into one of these states. A paused run can later start a new
* attempt under the same run ID, but callers waiting on the current attempt can
* stop watching once they observe it.
*/
const FACTORY_TERMINAL_STATUSES: ReadonlySet<FactoryRunStatus> = new Set([
"completed",
"halted",
"paused",
"cancelled",
"error",
]);
Expand Down Expand Up @@ -139,6 +141,22 @@ export interface FactoryStepOptions {
volatile?: boolean;
}

/**
* Per-invocation factory resource ceiling overrides.
*
* An omitted field preserves the existing/default ceiling, a number replaces
* it, and `null` explicitly makes that dimension unlimited.
*
* @experimental Part of the experimental Agent Factories surface and may
* change or be removed in future SDK or CLI releases.
*/
export interface FactoryLimitOverrides {
maxConcurrentSubagents?: number | null;
maxTotalSubagents?: number | null;
maxAiCredits?: number | null;
timeoutSeconds?: number | null;
}

/**
* One stage in a per-item factory pipeline.
*
Expand Down Expand Up @@ -168,6 +186,13 @@ export interface FactoryContext<TArgs extends JsonValue = JsonValue> {
producer: () => Promise<JsonValue> | JsonValue,
options?: FactoryStepOptions
): Promise<JsonValue>;
/**
* Pause this run at a durable, one-shot checkpoint.
*
* The first attempt to reach a key pauses and aborts cooperatively. A
* resumed attempt returns from the same key and continues.
*/
pause(key: string): Promise<void>;
/**
* Run thunks concurrently and await all of them.
*
Expand Down Expand Up @@ -198,7 +223,7 @@ export interface FactoryContext<TArgs extends JsonValue = JsonValue> {
args: TArgs;
/**
* The session instance returned by `joinSession`. It refuses calls that
* start or resume a factory run.
* start, resume, or pause a factory run.
*/
session: CopilotSession;
/** Cooperative cancellation signal for the current factory run. */
Expand Down Expand Up @@ -259,7 +284,7 @@ export interface RunOptions<TArgs extends JsonValue = JsonValue> {
/** Input surfaced as `context.args`. */
args?: TArgs;
/** Optional per-invocation resource ceiling overrides. */
limits?: FactoryLimits;
limits?: FactoryLimitOverrides;
/** Whether to notify the originating session when the factory completes. */
notifyOnComplete?: boolean;
/** Whether to emit factory phase names to the session transcript. */
Expand All @@ -280,7 +305,7 @@ export interface RunOptions<TArgs extends JsonValue = JsonValue> {
*/
export interface ResumeOptions {
/** Optional per-invocation resource ceiling overrides. */
limits?: FactoryLimits;
limits?: FactoryLimitOverrides;
/** Whether to notify the originating session when the factory completes. */
notifyOnComplete?: boolean;
/** Whether to emit factory phase names to the session transcript. */
Expand Down Expand Up @@ -314,13 +339,14 @@ export interface SessionFactoryApi {
* Run a registered factory and resolve with its run envelope.
*
* The envelope is returned for every outcome, including `error`, `halted`,
* and `cancelled` — inspect `status` and read `result` only when the run
* completed. SDK-initiated runs do not request permission, so they have no
* declined outcome. The model's `run_factory` tool requests permission
* before a durable row exists; declining it creates no run row. Failures
* that occur before a run exists (such as an unknown factory or attempting
* to start a run while the session is at its active top-level run limit)
* still reject.
* `paused`, and `cancelled` — inspect `status` and read `result` only when
* the run completed. `paused` settles the current attempt, but the same
* durable run can later resume under its existing run ID. SDK-initiated
* runs do not request permission, so they have no declined outcome. The
* model's `run_factory` tool requests permission before a durable row
* exists; declining it creates no run row. Failures that occur before a run
* exists (such as an unknown factory or attempting to start a run while the
* session is at its active top-level run limit) still reject.
*/
run(name: string, options?: RunOptions): Promise<FactoryRunResult>;
run<TArgs extends JsonValue>(
Expand All @@ -338,12 +364,13 @@ export interface SessionFactoryApi {
/** Read the latest durable envelope for a factory run. */
getRun(runId: string): Promise<FactoryRunResult>;
/**
* Wait for a run to settle and resolve with its terminal envelope.
* Wait for the current attempt to settle and resolve with its envelope.
*
* Resolves as soon as the run reaches `completed`, `error`, `halted`, or
* `cancelled`, and resolves immediately when it has already settled. A
* terminal envelope is final, so the resolved value never changes
* afterwards.
* Resolves as soon as the run reaches `completed`, `error`, `halted`,
* `paused`, or `cancelled`, and resolves immediately when the current
* attempt has already settled. A `paused` envelope is an attempt-level
* snapshot: resuming the same durable run can later change the envelope
* returned by {@link SessionFactoryApi.getRun}.
*
* This watches the run's `factory.run_updated` invalidation events and
* periodically re-reads the durable envelope so a missed event cannot
Expand Down Expand Up @@ -375,6 +402,8 @@ export interface SessionFactoryApi {
runId: string,
options?: Omit<FactoryGetRunProgressRequest, "runId">
): Promise<FactoryProgressPage>;
/** Pause a running factory attempt and return its `paused` envelope. */
pause(runId: string): Promise<FactoryRunResult>;
/** Cancel a factory run and return its terminal envelope. */
cancel(runId: string): Promise<FactoryRunResult>;
}
Expand Down
1 change: 1 addition & 0 deletions nodejs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export type {
export type {
RunOptions,
ResumeOptions,
FactoryLimitOverrides,
FactoryResumeErrorCode,
SessionFactoryApi,
FactoryAgentOptions,
Expand Down
Loading
Loading