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
2 changes: 1 addition & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ Pre-built tasks: `InputTask`, `OutputTask`, `LambdaTask`, `DelayTask`, `FetchUrl

### `@workglow/bootstrap` — default registration

Nothing self-registers at import time, so a runtime has to install the defaults before any task runs. This package is that seam, and **the implementation lives here** — `packages/workglow/src/bootstrap.ts` is now a pure `export * from "@workglow/bootstrap"` re-export shim. Add a new default registration in `packages/bootstrap/src/bootstrap/registerAllDefaults.ts`; editing the shim does nothing.
Each registrar self-registers on the **global** registry when its module happens to be imported — `registerModelDefaults()`, `registerTabularStorageDefaults()` and the twelve others run at module scope, and default their `registry` parameter to the global one. What is populated therefore depends on which modules your import graph has pulled in, which is import-order dependent and easy to mis-diagnose. `bootstrapWorkglow()` is the guarantee: it installs the full set in dependency order, idempotently. An isolated registry gets **nothing** until `registerAllDefaults(registry)` (or `createOrchestrationContext()`) is called explicitly. This package is that seam, and **the implementation lives here** — `packages/workglow/src/bootstrap.ts` is now a pure `export * from "@workglow/bootstrap"` re-export shim. Add a new default registration in `packages/bootstrap/src/bootstrap/registerAllDefaults.ts`; editing the shim does nothing.

- `bootstrapWorkglow(opts?)` — installs defaults onto the global registry, idempotent. The normal application entry point.
- `createOrchestrationContext(opts?)` — a fresh, disposable registry backed by its own container (tests, multi-tenant servers, embedded use).
Expand Down
3 changes: 2 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 19 additions & 6 deletions packages/bootstrap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@

Runtime bootstrap for Workglow: registers every built-in default onto a service registry.

Workglow does not self-register defaults at import time. Something has to install
the logger, telemetry, worker-manager, credential, model, provider, knowledge-base,
MCP, storage, task, and transform factories before any task runs — this package is
that something, sitting below both the `workglow` meta-package and the test harness
so neither has to own its own copy.
Each registrar self-registers on the **global** registry when its module happens to
be imported — `registerModelDefaults()`, `registerTabularStorageDefaults()` and the
twelve others run at module scope, and default their `registry` parameter to the
global one. What is populated therefore depends on which modules your import graph
has pulled in, which is import-order dependent and easy to mis-diagnose.
`bootstrapWorkglow()` is the guarantee: it installs the full set — logger,
telemetry, worker-manager, credential, model, provider, knowledge-base, MCP,
storage, task, and transform factories — in dependency order, idempotently. An
isolated registry gets **nothing** until `registerAllDefaults(registry)` (or
`createOrchestrationContext()`) is called explicitly.

This package is that seam, sitting below both the `workglow` meta-package and the
test harness so neither has to own its own copy.

## Installation

Expand Down Expand Up @@ -51,12 +59,17 @@ import { createOrchestrationContext } from "@workglow/bootstrap";

const ctx = createOrchestrationContext();
try {
await task.run({ context: ctx });
await task.run({}, { registry: ctx.registry });
} finally {
await ctx.dispose();
}
```

The registry travels in the **run config** — `run()`'s second argument. The first
argument is input overrides, so passing a context there silently becomes an input
named `context`, the run uses the global registry, and `ctx.dispose()` tears down a
registry nothing ever touched.

### Registering onto a registry you already have

```typescript
Expand Down
5 changes: 4 additions & 1 deletion packages/bootstrap/src/bootstrap/bootstrapWorkglow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ export function bootstrapWorkglow(opts: BootstrapOptions = {}): WorkglowContext
* ```ts
* const ctx = createOrchestrationContext({ logger: new MyLogger() });
* try {
* await task.run({ ..., context: ctx });
* // The registry travels in the run config — `run()`'s FIRST argument is
* // input overrides, so a context passed there is just an input named
* // `context` and the run still uses the global registry.
* await task.run({}, { registry: ctx.registry });
* } finally {
* await ctx.dispose();
* }
Expand Down
2 changes: 1 addition & 1 deletion packages/bootstrap/src/bootstrap/registerAllDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import { registerKnowledgeBaseDefaults } from "@workglow/knowledge-base";
import { registerMcpServerDefaults } from "@workglow/mcp/util";
import { registerTabularStorageDefaults } from "@workglow/storage";
import { registerTaskDefaults, registerTransformDefaults } from "@workglow/task-graph";
import type { ServiceRegistry } from "@workglow/util";
import {
registerCredentialDefaults,
registerInputCompactorDefaults,
registerInputResolverDefaults,
registerLoggerDefaults,
registerTelemetryDefaults,
ServiceRegistry,
} from "@workglow/util";
import { registerImageDefaults } from "@workglow/util/media";
import { registerWorkerManagerDefaults } from "@workglow/util/worker";
Expand Down
1 change: 1 addition & 0 deletions packages/test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@workglow/ai": "workspace:*",
"@workglow/anthropic": "workspace:*",
"@workglow/aws": "workspace:*",
"@workglow/bootstrap": "workspace:*",
"@workglow/browser-control": "workspace:*",
"@workglow/bun-webview": "workspace:*",
"@workglow/cactus": "workspace:*",
Expand Down
95 changes: 95 additions & 0 deletions packages/test/src/test/util/BootstrapReadme.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* @license
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
* SPDX-License-Identifier: Apache-2.0
*/

/**
* Executable transcription of the "Isolated context" example in
* `packages/bootstrap/README.md` (and the matching `createOrchestrationContext`
* JSDoc example).
*
* The README used to show `await task.run({ context: ctx })`. `Task.run` takes
* input overrides first and a run config second, and neither `IRunConfig` nor
* `TaskGraphRunConfig` has a `context` key — so with a loosely typed `Input`
* that object is an input override named `context`, the run keeps the global
* registry, and `ctx.dispose()` tears down a registry the task never touched.
* Both halves are pinned below so the snippet cannot silently rot back.
*/

import { createOrchestrationContext } from "@workglow/bootstrap";
import type { IExecuteContext, TaskInput, TaskOutput } from "@workglow/task-graph";
import { Task } from "@workglow/task-graph";
import type { ServiceRegistry } from "@workglow/util";
import { globalServiceRegistry } from "@workglow/util";
import type { DataPortSchema } from "@workglow/util/schema";
import { describe, expect, it } from "vitest";

interface RegistryProbeOutput extends TaskOutput {
isGlobal: boolean;
}

/** Reports whether the run resolved against the process-wide registry. */
class RegistryProbeTask extends Task<TaskInput, RegistryProbeOutput> {
public static override readonly type = "RegistryProbeTask";
public static override readonly category = "Test";
public static override readonly title = "Registry probe";
public static override readonly description = "Reports which service registry the run used.";
public static override readonly cacheable = false;

public static override inputSchema(): DataPortSchema {
return {
type: "object",
properties: {},
additionalProperties: true,
} as const satisfies DataPortSchema;
}

public static override outputSchema(): DataPortSchema {
return {
type: "object",
properties: { isGlobal: { type: "boolean" } },
additionalProperties: false,
} as const satisfies DataPortSchema;
}

public seenRegistry: ServiceRegistry | undefined;

public override async execute(
_input: TaskInput,
context: IExecuteContext
): Promise<RegistryProbeOutput> {
this.seenRegistry = context.registry;
return { isGlobal: context.registry === globalServiceRegistry };
}
}

describe("the @workglow/bootstrap README isolated-context example", () => {
it("routes the run to the isolated registry when the context is passed in the run config", async () => {
const ctx = createOrchestrationContext();
const task = new RegistryProbeTask();
try {
const output = await task.run({}, { registry: ctx.registry });

expect(output.isGlobal).toBe(false);
expect(task.seenRegistry).toBe(ctx.registry);
} finally {
await ctx.dispose();
}
});

it("keeps the global registry when a context is passed as an input override instead", async () => {
const ctx = createOrchestrationContext();
const task = new RegistryProbeTask();
try {
// The shape the README used to document. It type-checks only because this
// task's input schema is open; the run silently ignores it.
const output = await task.run({ context: ctx });

expect(output.isGlobal).toBe(true);
expect(task.seenRegistry).not.toBe(ctx.registry);
} finally {
await ctx.dispose();
}
});
});
1 change: 1 addition & 0 deletions packages/test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
{ "path": "../indexeddb" },
{ "path": "../mcp" },
{ "path": "../browser-control" },
{ "path": "../bootstrap" },
{ "path": "../../providers/anthropic" },
{ "path": "../../providers/bun-webview" },
{ "path": "../../providers/deepseek" },
Expand Down
Loading