@bastani/atomic 0.5.12-4 → 0.5.12-5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/workflow-creator/SKILL.md +24 -17
- package/.agents/skills/workflow-creator/references/agent-sessions.md +67 -24
- package/.agents/skills/workflow-creator/references/computation-and-validation.md +5 -3
- package/.agents/skills/workflow-creator/references/control-flow.md +25 -11
- package/.agents/skills/workflow-creator/references/discovery-and-verification.md +3 -2
- package/.agents/skills/workflow-creator/references/failure-modes.md +35 -36
- package/.agents/skills/workflow-creator/references/getting-started.md +25 -12
- package/.agents/skills/workflow-creator/references/session-config.md +26 -5
- package/.agents/skills/workflow-creator/references/state-and-data-flow.md +3 -3
- package/.agents/skills/workflow-creator/references/workflow-inputs.md +52 -47
- package/README.md +63 -41
- package/package.json +2 -2
- package/src/sdk/components/workflow-picker-panel.tsx +15 -21
- package/src/sdk/define-workflow.test.ts +58 -0
- package/src/sdk/define-workflow.ts +48 -30
- package/src/sdk/providers/claude.ts +234 -233
- package/src/sdk/runtime/discovery.ts +1 -2
- package/src/sdk/runtime/executor.ts +6 -1
- package/src/sdk/types.ts +24 -19
- package/src/sdk/workflows/builtin/deep-research-codebase/claude/index.ts +11 -30
- package/src/sdk/workflows/builtin/deep-research-codebase/copilot/index.ts +7 -4
- package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +6 -2
- package/src/sdk/workflows/builtin/ralph/claude/index.ts +32 -38
- package/src/sdk/workflows/builtin/ralph/copilot/index.ts +5 -1
- package/src/sdk/workflows/builtin/ralph/opencode/index.ts +5 -1
- package/src/sdk/workflows/index.ts +2 -2
- package/src/sdk/workflow-inputs.ts +0 -54
|
@@ -43,11 +43,6 @@ import { useLatest } from "./hooks.ts";
|
|
|
43
43
|
import { resolveTheme, type TerminalTheme } from "../runtime/theme.ts";
|
|
44
44
|
import type { AgentType, WorkflowInput } from "../types.ts";
|
|
45
45
|
import type { WorkflowWithMetadata } from "../runtime/discovery.ts";
|
|
46
|
-
import {
|
|
47
|
-
DEFAULT_PROMPT_FIELDS,
|
|
48
|
-
isFreeformPromptSchema,
|
|
49
|
-
normalizePickerInputs,
|
|
50
|
-
} from "../workflow-inputs.ts";
|
|
51
46
|
import { ErrorBoundary } from "./error-boundary.tsx";
|
|
52
47
|
|
|
53
48
|
// ─── Theme ──────────────────────────────────────
|
|
@@ -470,7 +465,7 @@ const Preview = memo(function Preview({
|
|
|
470
465
|
wf: WorkflowWithMetadata;
|
|
471
466
|
}) {
|
|
472
467
|
const theme = usePickerTheme();
|
|
473
|
-
const args =
|
|
468
|
+
const args = wf.inputs;
|
|
474
469
|
|
|
475
470
|
return (
|
|
476
471
|
<box
|
|
@@ -504,13 +499,16 @@ const Preview = memo(function Preview({
|
|
|
504
499
|
</span>
|
|
505
500
|
</text>
|
|
506
501
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
502
|
+
{args.length > 0 && (
|
|
503
|
+
<>
|
|
504
|
+
<box height={2} />
|
|
505
|
+
<SectionLabel label="ARGUMENTS" />
|
|
506
|
+
<box height={1} />
|
|
507
|
+
{args.map((f) => (
|
|
508
|
+
<ArgumentRow key={f.name} field={f} />
|
|
509
|
+
))}
|
|
510
|
+
</>
|
|
511
|
+
)}
|
|
514
512
|
</box>
|
|
515
513
|
);
|
|
516
514
|
});
|
|
@@ -643,7 +641,7 @@ function EnumContent({
|
|
|
643
641
|
selected,
|
|
644
642
|
focused,
|
|
645
643
|
}: {
|
|
646
|
-
values: string[];
|
|
644
|
+
values: readonly string[];
|
|
647
645
|
selected: string;
|
|
648
646
|
focused: boolean;
|
|
649
647
|
}) {
|
|
@@ -782,7 +780,7 @@ function InputPhase({
|
|
|
782
780
|
onTextChangeRef: React.RefObject<((value: string) => void) | null>;
|
|
783
781
|
}) {
|
|
784
782
|
const theme = usePickerTheme();
|
|
785
|
-
const isStructured =
|
|
783
|
+
const isStructured = workflow.inputs.length > 0;
|
|
786
784
|
const scrollboxRef = useRef<ScrollBoxRenderable>(null);
|
|
787
785
|
const [scrollTop, setScrollTop] = useState(0);
|
|
788
786
|
|
|
@@ -1241,9 +1239,8 @@ function usePickerKeyboard(state: PickerKeyboardState): void {
|
|
|
1241
1239
|
key.stopPropagation();
|
|
1242
1240
|
const wf = focusedWfRef.current;
|
|
1243
1241
|
if (wf) {
|
|
1244
|
-
const inputs = normalizePickerInputs(wf.inputs);
|
|
1245
1242
|
const initial: Record<string, string> = {};
|
|
1246
|
-
for (const f of inputs) {
|
|
1243
|
+
for (const f of wf.inputs) {
|
|
1247
1244
|
initial[f.name] =
|
|
1248
1245
|
f.default ??
|
|
1249
1246
|
(f.type === "enum" ? (f.values?.[0] ?? "") : "");
|
|
@@ -1357,10 +1354,7 @@ export function WorkflowPicker({
|
|
|
1357
1354
|
const focusedWf = entries[clampedEntryIdx]?.workflow;
|
|
1358
1355
|
|
|
1359
1356
|
const currentFields = useMemo<readonly WorkflowInput[]>(
|
|
1360
|
-
() =>
|
|
1361
|
-
focusedWf
|
|
1362
|
-
? normalizePickerInputs(focusedWf.inputs)
|
|
1363
|
-
: DEFAULT_PROMPT_FIELDS,
|
|
1357
|
+
() => focusedWf?.inputs ?? [],
|
|
1364
1358
|
[focusedWf],
|
|
1365
1359
|
);
|
|
1366
1360
|
const currentField = currentFields[focusedFieldIdx];
|
|
@@ -170,3 +170,61 @@ describe("WorkflowBuilder.compile()", () => {
|
|
|
170
170
|
expect(() => builder.compile()).toThrow("has no run callback");
|
|
171
171
|
});
|
|
172
172
|
});
|
|
173
|
+
|
|
174
|
+
describe("WorkflowBuilder.for()", () => {
|
|
175
|
+
test("returns the same builder instance (type-only narrowing)", () => {
|
|
176
|
+
const builder = defineWorkflow({ name: "test" });
|
|
177
|
+
const narrowed = builder.for<"copilot">();
|
|
178
|
+
// Same instance — .for() only changes the TypeScript type, not the value
|
|
179
|
+
expect(narrowed === (builder as unknown)).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("chains with run and compile", () => {
|
|
183
|
+
const def = defineWorkflow({
|
|
184
|
+
name: "test",
|
|
185
|
+
inputs: [{ name: "greeting", type: "string" }],
|
|
186
|
+
})
|
|
187
|
+
.for<"copilot">()
|
|
188
|
+
.run(async () => {})
|
|
189
|
+
.compile();
|
|
190
|
+
expect(def.__brand).toBe("WorkflowDefinition");
|
|
191
|
+
expect(def.inputs[0]?.name).toBe("greeting");
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("typed inputs (compile-time)", () => {
|
|
196
|
+
test("structured inputs restrict ctx.inputs keys", () => {
|
|
197
|
+
// This test validates that the type system correctly narrows
|
|
198
|
+
// ctx.inputs to only declared field names. The assertions below
|
|
199
|
+
// are runtime no-ops — the real check is that tsc compiles this
|
|
200
|
+
// file without errors (or produces errors only where expected).
|
|
201
|
+
defineWorkflow({
|
|
202
|
+
name: "typed-test",
|
|
203
|
+
inputs: [
|
|
204
|
+
{ name: "greeting", type: "string", required: true },
|
|
205
|
+
{ name: "style", type: "enum", values: ["formal", "casual"] },
|
|
206
|
+
],
|
|
207
|
+
})
|
|
208
|
+
.for<"copilot">()
|
|
209
|
+
.run(async (ctx) => {
|
|
210
|
+
// Declared keys are valid
|
|
211
|
+
const _g: string | undefined = ctx.inputs.greeting;
|
|
212
|
+
const _s: string | undefined = ctx.inputs.style;
|
|
213
|
+
// Undeclared key — would be a compile error without @ts-expect-error
|
|
214
|
+
// @ts-expect-error — "prompt" is not a declared input
|
|
215
|
+
ctx.inputs.prompt;
|
|
216
|
+
expect(true).toBe(true);
|
|
217
|
+
})
|
|
218
|
+
.compile();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("free-form workflows allow any key", () => {
|
|
222
|
+
defineWorkflow({ name: "freeform-test" })
|
|
223
|
+
.for<"copilot">()
|
|
224
|
+
.run(async (ctx) => {
|
|
225
|
+
const _p: string | undefined = ctx.inputs.prompt;
|
|
226
|
+
expect(true).toBe(true);
|
|
227
|
+
})
|
|
228
|
+
.compile();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Workflow Builder — defines a workflow with a single `.run()` entry point.
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
|
-
* defineWorkflow
|
|
5
|
+
* defineWorkflow({ name: "my-workflow", inputs: [...] })
|
|
6
|
+
* .for<"copilot">()
|
|
6
7
|
* .run(async (ctx) => {
|
|
7
8
|
* await ctx.stage({ name: "research" }, {}, {}, async (s) => { ... });
|
|
8
9
|
* await ctx.stage({ name: "plan" }, {}, {}, async (s) => { ... });
|
|
@@ -58,16 +59,42 @@ function validateWorkflowInput(input: WorkflowInput, workflowName: string): void
|
|
|
58
59
|
* Chainable workflow builder. Records the run callback,
|
|
59
60
|
* then .compile() seals it into a WorkflowDefinition.
|
|
60
61
|
*/
|
|
61
|
-
export class WorkflowBuilder<A extends AgentType = AgentType> {
|
|
62
|
+
export class WorkflowBuilder<A extends AgentType = AgentType, N extends string = string> {
|
|
62
63
|
/** @internal Brand for detection across package boundaries */
|
|
63
64
|
readonly __brand = "WorkflowBuilder" as const;
|
|
64
65
|
private readonly options: WorkflowOptions;
|
|
65
|
-
private runFn: ((ctx: WorkflowContext<A>) => Promise<void>) | null = null;
|
|
66
|
+
private runFn: ((ctx: WorkflowContext<A, N>) => Promise<void>) | null = null;
|
|
66
67
|
|
|
67
68
|
constructor(options: WorkflowOptions) {
|
|
68
69
|
this.options = options;
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Narrow the agent type for this workflow while preserving typed inputs.
|
|
74
|
+
*
|
|
75
|
+
* Use `.for<"copilot">()` **before** `.run()` instead of passing the
|
|
76
|
+
* agent as a type parameter to `defineWorkflow`. This allows TypeScript
|
|
77
|
+
* to infer input names from the `inputs` array AND narrow the agent
|
|
78
|
+
* type for `stage()` callbacks.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* defineWorkflow({
|
|
83
|
+
* name: "my-workflow",
|
|
84
|
+
* inputs: [{ name: "greeting", type: "string" }],
|
|
85
|
+
* })
|
|
86
|
+
* .for<"copilot">()
|
|
87
|
+
* .run(async (ctx) => {
|
|
88
|
+
* ctx.inputs.greeting; // ✓ typed
|
|
89
|
+
* ctx.inputs.prompt; // ✗ compile error
|
|
90
|
+
* })
|
|
91
|
+
* .compile();
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
for<B extends AgentType>(): WorkflowBuilder<B, N> {
|
|
95
|
+
return this as unknown as WorkflowBuilder<B, N>;
|
|
96
|
+
}
|
|
97
|
+
|
|
71
98
|
/**
|
|
72
99
|
* Set the workflow's entry point.
|
|
73
100
|
*
|
|
@@ -76,7 +103,7 @@ export class WorkflowBuilder<A extends AgentType = AgentType> {
|
|
|
76
103
|
* reading completed session outputs. Use native TypeScript control flow
|
|
77
104
|
* (loops, conditionals, `Promise.all()`) for orchestration.
|
|
78
105
|
*/
|
|
79
|
-
run(fn: (ctx: WorkflowContext<A>) => Promise<void>): this {
|
|
106
|
+
run(fn: (ctx: WorkflowContext<A, N>) => Promise<void>): this {
|
|
80
107
|
if (this.runFn) {
|
|
81
108
|
throw new Error("run() can only be called once per workflow.");
|
|
82
109
|
}
|
|
@@ -93,7 +120,7 @@ export class WorkflowBuilder<A extends AgentType = AgentType> {
|
|
|
93
120
|
* After calling compile(), the returned object is consumed by the
|
|
94
121
|
* Atomic CLI runtime.
|
|
95
122
|
*/
|
|
96
|
-
compile(): WorkflowDefinition<A> {
|
|
123
|
+
compile(): WorkflowDefinition<A, N> {
|
|
97
124
|
if (!this.runFn) {
|
|
98
125
|
throw new Error(
|
|
99
126
|
`Workflow "${this.options.name}" has no run callback. ` +
|
|
@@ -133,45 +160,36 @@ export class WorkflowBuilder<A extends AgentType = AgentType> {
|
|
|
133
160
|
/**
|
|
134
161
|
* Entry point for defining a workflow.
|
|
135
162
|
*
|
|
136
|
-
*
|
|
163
|
+
* Write the `inputs` array inline so TypeScript infers literal field
|
|
164
|
+
* names and enforces them on `ctx.inputs`. Use `.for<Agent>()` to
|
|
165
|
+
* narrow the agent type while keeping typed inputs:
|
|
137
166
|
*
|
|
138
167
|
* @example
|
|
139
168
|
* ```typescript
|
|
140
169
|
* import { defineWorkflow } from "@bastani/atomic/workflows";
|
|
141
170
|
*
|
|
142
|
-
* export default defineWorkflow
|
|
171
|
+
* export default defineWorkflow({
|
|
143
172
|
* name: "hello",
|
|
144
173
|
* description: "Two-session demo",
|
|
174
|
+
* inputs: [
|
|
175
|
+
* { name: "greeting", type: "string", required: true },
|
|
176
|
+
* ],
|
|
145
177
|
* })
|
|
178
|
+
* .for<"copilot">()
|
|
146
179
|
* .run(async (ctx) => {
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* {},
|
|
150
|
-
* {},
|
|
151
|
-
* async (s) => {
|
|
152
|
-
* // s.client: CopilotClient, s.session: CopilotSession
|
|
153
|
-
* await s.session.send({ prompt: s.inputs.prompt ?? "" });
|
|
154
|
-
* s.save(await s.session.getMessages());
|
|
155
|
-
* },
|
|
156
|
-
* );
|
|
157
|
-
* await ctx.stage(
|
|
158
|
-
* { name: "summarize" },
|
|
159
|
-
* {},
|
|
160
|
-
* {},
|
|
161
|
-
* async (s) => {
|
|
162
|
-
* const research = await s.transcript(describe);
|
|
163
|
-
* // ...
|
|
164
|
-
* },
|
|
165
|
-
* );
|
|
180
|
+
* ctx.inputs.greeting; // ✓ string | undefined
|
|
181
|
+
* ctx.inputs.prompt; // ✗ compile error — not declared
|
|
166
182
|
* })
|
|
167
183
|
* .compile();
|
|
168
184
|
* ```
|
|
169
185
|
*/
|
|
170
|
-
export function defineWorkflow<
|
|
171
|
-
|
|
172
|
-
|
|
186
|
+
export function defineWorkflow<
|
|
187
|
+
const I extends readonly WorkflowInput[] = readonly WorkflowInput[],
|
|
188
|
+
>(
|
|
189
|
+
options: WorkflowOptions<I>,
|
|
190
|
+
): WorkflowBuilder<AgentType, I[number]["name"]> {
|
|
173
191
|
if (!options.name || options.name.trim() === "") {
|
|
174
192
|
throw new Error("Workflow name is required.");
|
|
175
193
|
}
|
|
176
|
-
return new WorkflowBuilder<
|
|
194
|
+
return new WorkflowBuilder<AgentType, I[number]["name"]>(options);
|
|
177
195
|
}
|