-
Notifications
You must be signed in to change notification settings - Fork 0
feat: rig/globals ambient call, pipeline null-skip, tsconfig baseUrl fix #339
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| /** | ||
| * Ambient workflow context helpers. | ||
| * | ||
| * Import from `"rig/globals"` to access `call`, `pipeline`, and `parallel` | ||
| * as module-level functions that automatically delegate to the active workflow | ||
| * run via `currentWorkflow()`. This keeps rig programs that port from | ||
| * Claude dynamic workflows readable without threading context explicitly. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * import { call, pipeline } from "rig/globals"; | ||
| * import { agent } from "rig"; | ||
| * | ||
| * const worker = agent({ name: "worker", instructions: "Do work." }); | ||
| * const results = await pipeline(inputs, (item) => call(worker, item)); | ||
| * ``` | ||
| * | ||
| * @module rig/globals | ||
| */ | ||
| import type { | ||
| AgentFn, | ||
| AgentInputValue, | ||
| InferSchema, | ||
| PromptBuilder, | ||
| Schema, | ||
| Workflow, | ||
| WorkflowCall, | ||
| WorkflowCallOptions, | ||
| WorkflowNestedOptions, | ||
| } from "rig"; | ||
| import { currentWorkflow, parallel, pipeline } from "rig"; | ||
|
|
||
| function requireContext(label: string): WorkflowCall { | ||
| const ctx = currentWorkflow(); | ||
| if (ctx === undefined) { | ||
| throw new Error(`${label} requires an active workflow run (call inside runWorkflow or a launcher program).`); | ||
| } | ||
| return ctx.call; | ||
| } | ||
|
|
||
| function callImpl<Input, Output>( | ||
| worker: AgentFn<Input, Output>, | ||
| input: AgentInputValue<Input>, | ||
| options?: WorkflowCallOptions, | ||
| ): Promise<Output | null> { | ||
| return requireContext("call()")(worker, input, options); | ||
| } | ||
|
|
||
| callImpl.text = (prompt: string | PromptBuilder, options?: WorkflowCallOptions): Promise<string | null> => | ||
| requireContext("call.text()").text(prompt, options); | ||
|
|
||
| callImpl.json = <const Output extends Schema>( | ||
| prompt: string | PromptBuilder, | ||
| output: Output, | ||
| options?: WorkflowCallOptions, | ||
| ): Promise<InferSchema<Output> | null> => | ||
| requireContext("call.json()").json(prompt, output, options); | ||
|
|
||
| callImpl.workflow = <Input, Output>( | ||
| child: Workflow<Input, Output>, | ||
| args?: Input, | ||
| options?: WorkflowNestedOptions, | ||
| ): Promise<Output> => | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/codebase-design] The 💡 SuggestionAssign each member explicitly so TypeScript can enforce the export const call: WorkflowCall = Object.assign(callImpl, {
text: callImpl.text,
json: callImpl.json,
workflow: callImpl.workflow,
});This avoids the double cast while keeping the code just as concise. |
||
| requireContext("call.workflow()").workflow(child, args, options); | ||
|
|
||
| /** | ||
| * Ambient workflow call. Delegates to the active `WorkflowContext.call`, | ||
| * which routes through the shared concurrency limiter and agent budget. | ||
| * Throws if called outside a workflow run. | ||
| */ | ||
| export const call: WorkflowCall = callImpl as unknown as WorkflowCall; | ||
|
|
||
| export { pipeline, parallel }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import { | |
| currentWorkflow, | ||
| log, | ||
| phase, | ||
| pipeline, | ||
| WorkflowLimitError, | ||
| parallel, | ||
| runWorkflow, | ||
|
|
@@ -13,6 +14,7 @@ import { | |
| type WorkflowEvent, | ||
| } from "rig"; | ||
| import { s } from "rig"; | ||
| import { call as ambientCall } from "rig/globals"; | ||
|
|
||
| function fakeAgent<Input, Output>( | ||
| name: string, | ||
|
|
@@ -149,6 +151,14 @@ describe("workflow primitives", () => { | |
| ])).resolves.toEqual([1, null, 3]); | ||
| }); | ||
|
|
||
| it("pipeline skips subsequent stages when a stage returns null", async () => { | ||
| const stage2 = vi.fn((_prev: unknown, item: number) => item); | ||
| await expect( | ||
| pipeline([1, 2, 3], (_item: number) => _item === 2 ? null : _item * 10, stage2), | ||
| ).resolves.toEqual([1, null, 3]); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] The 💡 SuggestionAssert the exact calls to expect(stage2).toHaveBeenCalledWith(10, 1, 0);
expect(stage2).toHaveBeenCalledWith(30, 3, 2);
expect(stage2).not.toHaveBeenCalledWith(expect.anything(), 2, expect.anything());This pins both the skipped item (index 1) and the values passed to the non-null stages. |
||
| expect(stage2).toHaveBeenCalledTimes(2); | ||
| }); | ||
|
|
||
| it("until stops on completion or repeated progress keys", async () => { | ||
| const complete = vi.fn(async (state: number | undefined) => ({ | ||
| state: (state ?? 0) + 1, | ||
|
|
@@ -333,3 +343,46 @@ describe("ambient workflow context", () => { | |
| }).not.toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("rig/globals", () => { | ||
| it("call() delegates to the active workflow context", async () => { | ||
| const worker = fakeAgent<number, number>("worker", (value) => value * 2); | ||
| const definition = workflow({ | ||
| meta: { name: "globals-call", description: "ambient call" }, | ||
| body: () => ambientCall(worker, 7), | ||
| }); | ||
|
|
||
| await expect(runWorkflow(definition)).resolves.toBe(14); | ||
| }); | ||
|
|
||
| it("call.text() delegates to the active workflow context", async () => { | ||
| configureAgent(() => ({ | ||
| ask: async () => '"pong"', | ||
| close: async () => {}, | ||
| })); | ||
| const definition = workflow({ | ||
| meta: { name: "globals-call-text", description: "ambient call.text" }, | ||
| body: () => ambientCall.text("ping"), | ||
| }); | ||
|
|
||
| await expect(runWorkflow(definition)).resolves.toBe("pong"); | ||
| }); | ||
|
|
||
| it("call.workflow() delegates to the active workflow context", async () => { | ||
| const child = workflow({ | ||
| meta: { name: "child-globals", description: "child run" }, | ||
| body: () => 42, | ||
| }); | ||
| const definition = workflow({ | ||
| meta: { name: "globals-call-workflow", description: "ambient call.workflow" }, | ||
| body: () => ambientCall.workflow(child), | ||
| }); | ||
|
|
||
| await expect(runWorkflow(definition)).resolves.toBe(42); | ||
| }); | ||
|
|
||
| it("call() throws outside a workflow run", () => { | ||
| const worker = fakeAgent<number, number>("worker", (value) => value); | ||
| expect(() => ambientCall(worker, 1)).toThrow("requires an active workflow run"); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/tdd]
requireContextis called on everycall.text(),call.json(), andcall.workflow()invocation but is never tested in isolation — only the happy path via delegation is covered.💡 Suggestion
Add a test for each sub-method throwing when called outside a run:
Only
call()is tested for the no-context path; the sub-methods could diverge silently.