Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,27 @@ var session = await client.CreateSessionAsync(new SessionConfig
});
```

### Fix: Agent Factories types and behavior now match the wire contract

The `@experimental` Agent Factories surface described several things the runtime does not do, and the TypeScript generator was the root cause. The schema marks an opaque value that travels as JSON with `x-opaque-json`, and one that never serializes with `x-opaque-in-process`. The generator read neither marker, so both kinds of value rendered as an object index signature.

`FactoryRunResult.result` and factory arguments are now `JsonValue`, so an array, a string, a number, or `null` fits the type the runtime already sent. `ctx.agent()` gains `agent`, `reasoningEffort`, and `contextTier`, which the SDK previously dropped before sending.

`FactoryResumeErrorCode` now names the eight codes the runtime raises before a resumed run starts. `reapproval_declined` and `no_approval_provider` are removed, because no runtime path raises them. `factory_already_running`, `factory_limits_invalid`, `factory_session_disposed`, `factory_storage_unavailable`, and `factory_storage_corrupt` are added.

A factory body can no longer start a second top-level run. `factory.run` and `factory.resume` are refused while a factory body runs on the same call path, through any session reference the body reaches. A run started elsewhere in the extension is unaffected. A background progress-flush error no longer turns a completed run into an errored run.

```ts
const run = await session.factory.run("collect-findings");
if (run.status === "completed" && Array.isArray(run.result)) {
for (const finding of run.result) {
console.log(finding);
}
}
```

Correcting the two markers also retypes declarations outside the factory surface. `CanvasJsonSchema`, `CanvasActionInvokeResult`, `ElicitationCompletedContent`, and `CustomNotificationPayload` change from interfaces to type aliases, and `ElicitationCompletedContent` becomes optional. `ToolTelemetry` narrows its inner record to `Record<string, JsonValue>`, which is what the wire accepts.

## [v1.0.7](https://github.com/github/copilot-sdk/releases/tag/v1.0.7) (2026-07-16)

### Feature: in-process (FFI) transport
Expand Down
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`, without the APIs that start and resume factory runs. Call `extensions_manage` with `operation: "guide"` to read more about the session APIs.
Comment thread
MRayermannMSFT marked this conversation as resolved.
Outdated
- `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`, without the APIs that
* start and resume factory runs.
*/
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