Understanding the Copilot SDK Factory Pattern: defineFactory, step, parallel, and pipeline Usage
The Copilot SDK Factory pattern enables developers to define reusable computation units using defineFactory(), which provides a step API for running sequential tasks, parallel fan-outs, and data-flow pipelines while enforcing JSON-serializable outputs and resource limits.
The GitHub Copilot SDK exposes a structured Factory pattern for building composable, server-side extensions. By implementing defineFactory() in nodejs/src/factory.ts, you create self-contained units of work that the runtime can orchestrate across distributed sessions. This architecture supports complex execution graphs through the step API, allowing extensions to parallelize independent operations or chain dependent transformations with built-in safety guards.
Core Factory Structure
A factory definition consists of metadata describing the operation and a run function that receives contextual state and step utilities.
Metadata and Limits
The meta object adheres to the FactoryMeta interface defined in nodejs/src/types.ts and describes the factory's purpose and constraints:
- description: Human-readable summary of the factory's behavior
- limits: An optional
FactoryLimitsobject specifyingtimeoutSeconds,creditLimit, and other execution boundaries
These limits are enforced by the runtime to prevent runaway execution.
The Run Function and Context
The run function signature receives two arguments:
FactoryContext: Containsargs(input parameters),session(the active session handle), andloggerfor structured loggingFactoryStep: An API object exposing execution methods
The implementation must return a JSON-serializable value. The SDK explicitly validates against functions, symbols, BigInt, undefined, and cyclic references, throwing a FactoryResumeError if the contract is violated.
The Step API: Execution Primitives
The step object available in the run function provides three distinct execution models defined in nodejs/src/session.ts (around line 145).
Sequential Execution with step.run()
Use step.run(thunk) to execute a single asynchronous operation. This blocks until the thunk resolves, making it suitable for dependent operations that must complete before subsequent logic.
Concurrent Fan-Out with step.runParallel()
step.runParallel(thunks[]) executes an array of async functions concurrently. The SDK automatically evaluates all thunks and returns an array of results upon completion.
The runtime protects against resource exhaustion through assertFactoryFanoutSize("parallel", ...) checks in nodejs/src/session.ts, which validate that the number of concurrent operations does not exceed configured limits. This pattern maximizes throughput when steps are independent, such as fetching data from multiple external APIs simultaneously.
Chained Data Flow with step.runPipeline()
step.runPipeline(thunks[]) executes thunks sequentially, passing the output of each step as the input to the next. Unlike parallel execution, each function in the pipeline receives the previous step's return value.
The SDK validates pipeline depth via assertFactoryFanoutSize("pipeline", ...) to prevent deeply nested execution stacks. This pattern is ideal for data transformation workflows where the output of one stage feeds directly into the next.
Execution Patterns: Parallel vs Pipeline
Understanding when to apply each execution model ensures optimal resource utilization:
-
Parallel execution launches all thunks simultaneously and waits for every promise to settle. Use this when operations have no interdependencies and you need maximum concurrency. The SDK enforces a fan-out limit to protect the runtime from overwhelming the underlying compute resources.
-
Pipeline execution processes thunks sequentially, creating a unidirectional data flow. Use this when each transformation depends on the previous result, such as parsing input, validating schema, and then formatting output. The SDK checks pipeline depth limits to avoid stack overflow conditions.
Both patterns share the same underlying RPC mechanism (FactoryRunRequest) and error handling semantics, meaning FactoryResumeError instances propagate identically regardless of execution strategy.
Practical Implementation Example
The following example demonstrates defining a factory that uses both parallel and pipeline execution:
import { defineFactory } from "@github/copilot-sdk";
const suggestionFactory = defineFactory({
meta: {
description: "Generate and process suggestions using parallel and pipeline stages",
limits: { timeoutSeconds: 30, creditLimit: 1000 },
},
async run(context, step) {
// Execute heavy computations concurrently
const [resultA, resultB] = await step.runParallel([
async () => computeEmbedding(context.args.query),
async () => fetchContextData(context.args.filePath),
]);
// Process results through a sequential pipeline
const final = await step.runPipeline([
async () => mergeResults(resultA, resultB),
async (prev) => validateSuggestions(prev),
async (prev) => formatOutput(prev, context.args.format),
]);
// Must return JSON-serializable data
return { suggestions: final, timestamp: Date.now() };
},
});
The run function can interleave any combination of step.run, step.runParallel, and step.runPipeline calls. Errors thrown within any sub-task bubble up as FactoryResumeError instances with specific error codes indicating the failure mode.
Registration and Session Orchestration
Factories must be registered with a session before invocation. The session.registerFactories([...]) method in nodejs/src/session.ts binds factory definitions to the active runtime context.
Once registered, factories become callable through:
- The Copilot UI interface
- Other factories via the step API
- Direct RPC client invocations
The session handles the wire format serialization (FactoryRunRequest), result validation, and limit enforcement. When a factory exceeds its timeoutSeconds or creditLimit, the session terminates execution and raises a FactoryResumeError with an appropriate termination code.
Testing and Validation Patterns
The SDK provides comprehensive test coverage illustrating factory behavior:
nodejs/test/factory.test.ts: Unit tests covering basic execution, parallel fan-out size validation, pipeline chaining, and error serialization scenariosnodejs/test/e2e/factory.e2e.test.ts: End-to-end tests demonstrating real session initialization, factory registration, and cross-process invocation via the RPC client
These test files demonstrate how to mock FactoryContext and verify that factories return strictly JSON-serializable data structures.
Summary
- Define factories using
defineFactory()innodejs/src/factory.ts, providing metadata and arunfunction that receivesFactoryContextandFactoryStepAPIs. - Execute work through the step API:
step.run()for sequential tasks,step.runParallel()for concurrent fan-out (with automatic fan-out limits), andstep.runPipeline()for chained data processing. - Respect constraints: Return values must be JSON-serializable (no functions, symbols, or cyclic references), and operations must stay within declared
limits(timeout, credits). - Register and run factories via
session.registerFactories()innodejs/src/session.ts, which orchestrates RPC communication and enforces runtime protections.
Frequently Asked Questions
What happens if a factory returns non-JSON-serializable data?
The SDK validates return values before transmission and throws a FactoryResumeError if the result contains functions, symbols, BigInt, undefined, or cyclic structures. This ensures all factory outputs can traverse the JSON-RPC wire format safely.
How does the Copilot SDK prevent resource exhaustion during parallel execution?
The runtime calls assertFactoryFanoutSize("parallel", ...) in nodejs/src/session.ts before launching concurrent thunks, enforcing a configurable maximum fan-out size. If you exceed this limit, the SDK throws an error before executing the steps.
Can I mix parallel and pipeline execution within the same factory?
Yes. The step API allows unlimited composition of step.runParallel() and step.runPipeline() calls. You can fan out to gather data in parallel, then feed the aggregated results into a pipeline for sequential processing, as demonstrated in nodejs/test/factory.test.ts.
What is the difference between step.runPipeline() and manually chaining await step.run() calls?
While both execute sequentially, step.runPipeline() provides structured data-flow semantics where each step automatically receives the previous output as its argument. Additionally, the SDK tracks pipeline depth separately and validates it against assertFactoryFanoutSize("pipeline", ...) limits, offering better debugging context and resource tracking than manual chaining.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →