Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
8 changes: 4 additions & 4 deletions nodejs/docs/factories.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@ 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`, and `model`. See [Subagent calls](#subagent-calls).
- `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 full session returned by `joinSession`.
- `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.

Expand Down Expand Up @@ -156,7 +156,7 @@ session.factory.resume(
): Promise<FactoryRunResult>;
```

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`. A declined fresh run is not a pre-execution failure: the run row already exists by the time the prompt is answered, so it resolves with a terminal `cancelled` envelope carrying the run ID. Only failures that occur *before* a run exists reject: an unknown factory name or an already-active session. Pre-execution resume failures, including a declined reapproval, throw `FactoryResumeError`, whose `code` is one of `not_found`, `non_resumable`, `already_active`, `reapproval_declined`, or `no_approval_provider`.
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`.

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.

Expand Down Expand Up @@ -210,7 +210,7 @@ const page = await session.factory.getRunProgress(runId, {
});
```

- `listRuns()` returns summaries in durable creation order.
- `listRuns()` returns the newest default page of this session's durable factory runs.
- `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.

Expand Down
72 changes: 36 additions & 36 deletions nodejs/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,15 @@ import type {
FactoryGetRunProgressRequest,
FactoryProgressPage,
FactoryRunDetail,
FactoryRunResult as WireFactoryRunResult,
FactoryRunResult,
FactoryRunStatus,
FactoryRunSummary,
} 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";

/**
* The envelope describing a factory run: its identity, status, and — once it
* has completed — its result. `getRun` returns this for an in-flight run too,
* so `status` may be `pending` or `running` and the outcome fields absent.
*
* `result` is re-typed here rather than taken from the generated wire type. The
* runtime returns any JSON value — including `null`, a string, a number, or an
* array — but the schema models the field as an opaque node, which the
* generator renders as an object. Narrowing the correction to this surface
* keeps the `x-opaque-json` handling unchanged for every other consumer.
*
* This override is temporary. Once the schema distinguishes an opaque JSON
* value from an opaque in-process value and that ships in a CLI release,
* regenerating produces the right type directly, and this declaration, the
* `toPublicFactoryRunResult` boundary helper, and the casts around it should
* all be deleted. Tracked by github/copilot-agent-runtime#14122.
*
* @experimental Part of the experimental Agent Factories surface and may
* change or be removed in future SDK or CLI releases.
*/
export type FactoryRunResult = Omit<WireFactoryRunResult, "result"> & {
/** Completed factory result. */
result?: JsonValue;
};

export type { FactoryRunResult };
export type {
FactoryAgentSummary,
FactoryPhaseStatus,
Expand Down Expand Up @@ -115,8 +92,20 @@ export interface FactoryAgentOptions {
label?: string;
schema?: FactoryJsonSchema;
model?: string;
reasoningEffort?: string;
contextTier?: ContextTier;
agent?: string;
}

export const FACTORY_AGENT_OPTION_KEYS = [
"label",
"schema",
"model",
"reasoningEffort",
"contextTier",
"agent",
] as const;

/**
* Options for a durable factory step.
*
Expand Down Expand Up @@ -185,7 +174,10 @@ export interface FactoryContext<TArgs extends JsonValue = JsonValue> {
factory(name: string, args?: JsonValue): Promise<JsonValue | void>;
/** Caller-supplied input, forwarded verbatim. */
args: TArgs;
/** The same full session instance returned by `joinSession`. */
/**
* The session instance returned by `joinSession`. It refuses calls that
* start or resume a factory run.
*/
session: CopilotSession;
Comment thread
MRayermannMSFT marked this conversation as resolved.
/** Cooperative cancellation signal for the current factory run. */
signal: AbortSignal;
Expand Down Expand Up @@ -275,8 +267,11 @@ export type FactoryResumeErrorCode =
| "not_found"
| "non_resumable"
| "already_active"
| "reapproval_declined"
| "no_approval_provider";
| "factory_already_running"
| "factory_limits_invalid"
| "factory_session_disposed"
| "factory_storage_unavailable"
| "factory_storage_corrupt";

/**
* Friendly factory API exposed on a session.
Expand All @@ -290,9 +285,12 @@ export interface SessionFactoryApi {
*
* The envelope is returned for every outcome, including `error`, `halted`,
* and `cancelled` — inspect `status` and read `result` only when the run
* completed. A declined fresh run resolves with a terminal `cancelled`
* envelope. Failures that occur before a run exists (such as an unknown
* factory or an already-active session) still reject.
* 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.
*/
run(name: string, options?: RunOptions): Promise<FactoryRunResult>;
run<TArgs extends JsonValue>(
Expand All @@ -302,9 +300,9 @@ export interface SessionFactoryApi {
/**
* Resume a run from its persisted factory name, arguments, journal, and accounting.
*
* Resolves with the run envelope like {@link SessionFactoryApi.run}. A
* pre-execution failure, including declined reapproval, rejects with
* {@link FactoryResumeError}.
* Resolves with the run envelope like {@link SessionFactoryApi.run}.
* SDK-initiated resumes do not request permission. A pre-execution failure
* with a documented resume code rejects with {@link FactoryResumeError}.
*/
resume(runId: string, options?: ResumeOptions): Promise<FactoryRunResult>;
/** Read the latest durable envelope for a factory run. */
Expand All @@ -324,7 +322,9 @@ export interface SessionFactoryApi {
* {@link SessionFactoryApi.cancel} to actually stop it.
*/
waitForRun(runId: string, options?: { signal?: AbortSignal }): Promise<FactoryRunResult>;
/** List this session's durable factory runs in creation order. */
/**
* List the newest default page of this session's durable factory runs.
*/
listRuns(): Promise<FactoryRunSummary[]>;
/** Read durable phases, direct agents, and the latest progress tail for a run. */
getRunDetail(runId: string): Promise<FactoryRunDetail>;
Expand Down
Loading
Loading