chore: add ion package, codesight wiki, work plans, ascli config
New @boocode/ion package (v0.0.1) for inference optimization network. .codesight/ wiki artifacts for codebase documentation. .omo/ work plans for openspec cleanup and enhanced file panel.
This commit is contained in:
377
packages/ion/src/schema/__tests__/dag-node.test.ts
Normal file
377
packages/ion/src/schema/__tests__/dag-node.test.ts
Normal file
@@ -0,0 +1,377 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
dagNodeSchema,
|
||||
promptNodeSchema,
|
||||
commandNodeSchema,
|
||||
bashNodeSchema,
|
||||
scriptNodeSchema,
|
||||
loopNodeSchema,
|
||||
approvalNodeSchema,
|
||||
cancelNodeSchema,
|
||||
loopNodeConfigSchema,
|
||||
stepRetryConfigSchema,
|
||||
triggerRuleSchema,
|
||||
isBashNode,
|
||||
isLoopNode,
|
||||
isApprovalNode,
|
||||
isCancelNode,
|
||||
isScriptNode,
|
||||
isPromptNode,
|
||||
isCommandNode,
|
||||
effortLevelSchema,
|
||||
thinkingConfigSchema,
|
||||
approvalOnRejectSchema,
|
||||
dagNodeBaseSchema,
|
||||
} from '../index.js';
|
||||
import type {
|
||||
DagNode,
|
||||
PromptNode,
|
||||
CommandNode,
|
||||
BashNode,
|
||||
ScriptNode,
|
||||
LoopNode,
|
||||
ApprovalNode,
|
||||
CancelNode,
|
||||
} from '../index.js';
|
||||
|
||||
const validBase = {
|
||||
id: 'test-node',
|
||||
depends_on: [],
|
||||
};
|
||||
|
||||
describe('dagNodeSchema', () => {
|
||||
it('validates a prompt node', () => {
|
||||
const node = { ...validBase, kind: 'prompt' as const, prompt: 'Do the thing' };
|
||||
const result = dagNodeSchema.safeParse(node);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('validates a command node', () => {
|
||||
const node = { ...validBase, kind: 'command' as const, command: 'echo hello' };
|
||||
const result = dagNodeSchema.safeParse(node);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('validates a bash node', () => {
|
||||
const node = { ...validBase, kind: 'bash' as const, bash: 'echo hello' };
|
||||
const result = dagNodeSchema.safeParse(node);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('validates a script node', () => {
|
||||
const node = { ...validBase, kind: 'script' as const, script: 'print(1)', runtime: 'bun' as const };
|
||||
const result = dagNodeSchema.safeParse(node);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('validates a loop node', () => {
|
||||
const node = {
|
||||
...validBase,
|
||||
kind: 'loop' as const,
|
||||
config: { prompt: 'iterate', until: 'done', max_iterations: 5 },
|
||||
};
|
||||
const result = dagNodeSchema.safeParse(node);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('validates an approval node', () => {
|
||||
const node = { ...validBase, kind: 'approval' as const, message: 'Approve this?' };
|
||||
const result = dagNodeSchema.safeParse(node);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('validates a cancel node', () => {
|
||||
const node = { ...validBase, kind: 'cancel' as const };
|
||||
const result = dagNodeSchema.safeParse(node);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a node with no kind field', () => {
|
||||
const result = dagNodeSchema.safeParse({ id: 'x', prompt: 'y' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a node with an invalid kind', () => {
|
||||
const result = dagNodeSchema.safeParse({ ...validBase, kind: 'unknown' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('node type unique fields', () => {
|
||||
it('prompt node requires prompt or command_file', () => {
|
||||
const result = promptNodeSchema.safeParse({ ...validBase, kind: 'prompt' });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('command node requires command', () => {
|
||||
const result = commandNodeSchema.safeParse({ ...validBase, kind: 'command' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('bash node requires bash', () => {
|
||||
const result = bashNodeSchema.safeParse({ ...validBase, kind: 'bash' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('script node requires script and runtime', () => {
|
||||
const noScript = scriptNodeSchema.safeParse({ ...validBase, kind: 'script', runtime: 'bun' });
|
||||
expect(noScript.success).toBe(false);
|
||||
|
||||
const noRuntime = scriptNodeSchema.safeParse({ ...validBase, kind: 'script', script: 'print(1)' });
|
||||
expect(noRuntime.success).toBe(false);
|
||||
|
||||
const valid = scriptNodeSchema.safeParse({
|
||||
...validBase,
|
||||
kind: 'script',
|
||||
script: 'print(1)',
|
||||
runtime: 'bun',
|
||||
});
|
||||
expect(valid.success).toBe(true);
|
||||
});
|
||||
|
||||
it('loop node requires config', () => {
|
||||
const result = loopNodeSchema.safeParse({ ...validBase, kind: 'loop' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('approval node requires message', () => {
|
||||
const result = approvalNodeSchema.safeParse({ ...validBase, kind: 'approval' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('cancel node does not require reason', () => {
|
||||
const result = cancelNodeSchema.safeParse({ ...validBase, kind: 'cancel' });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loopNodeConfigSchema', () => {
|
||||
it('validates a minimal config', () => {
|
||||
const result = loopNodeConfigSchema.safeParse({
|
||||
prompt: 'iterate',
|
||||
until: 'done',
|
||||
max_iterations: 5,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects empty prompt', () => {
|
||||
const result = loopNodeConfigSchema.safeParse({
|
||||
prompt: '',
|
||||
until: 'done',
|
||||
max_iterations: 5,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects empty until', () => {
|
||||
const result = loopNodeConfigSchema.safeParse({
|
||||
prompt: 'iterate',
|
||||
until: '',
|
||||
max_iterations: 5,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects non-positive max_iterations', () => {
|
||||
const zeroResult = loopNodeConfigSchema.safeParse({
|
||||
prompt: 'iterate',
|
||||
until: 'done',
|
||||
max_iterations: 0,
|
||||
});
|
||||
expect(zeroResult.success).toBe(false);
|
||||
|
||||
const negResult = loopNodeConfigSchema.safeParse({
|
||||
prompt: 'iterate',
|
||||
until: 'done',
|
||||
max_iterations: -1,
|
||||
});
|
||||
expect(negResult.success).toBe(false);
|
||||
});
|
||||
|
||||
it('requires gate_message when interactive is true', () => {
|
||||
const noGate = loopNodeConfigSchema.safeParse({
|
||||
prompt: 'iterate',
|
||||
until: 'done',
|
||||
max_iterations: 5,
|
||||
interactive: true,
|
||||
});
|
||||
expect(noGate.success).toBe(false);
|
||||
|
||||
const withGate = loopNodeConfigSchema.safeParse({
|
||||
prompt: 'iterate',
|
||||
until: 'done',
|
||||
max_iterations: 5,
|
||||
interactive: true,
|
||||
gate_message: 'Approve this iteration?',
|
||||
});
|
||||
expect(withGate.success).toBe(true);
|
||||
});
|
||||
|
||||
it('allows interactive=false without gate_message', () => {
|
||||
const result = loopNodeConfigSchema.safeParse({
|
||||
prompt: 'iterate',
|
||||
until: 'done',
|
||||
max_iterations: 5,
|
||||
interactive: false,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stepRetryConfigSchema', () => {
|
||||
it('validates a minimal retry config', () => {
|
||||
const result = stepRetryConfigSchema.safeParse({ max_attempts: 3 });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects max_attempts below 1', () => {
|
||||
const result = stepRetryConfigSchema.safeParse({ max_attempts: 0 });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects max_attempts above 5', () => {
|
||||
const result = stepRetryConfigSchema.safeParse({ max_attempts: 6 });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts max_attempts at boundaries (1 and 5)', () => {
|
||||
expect(stepRetryConfigSchema.safeParse({ max_attempts: 1 }).success).toBe(true);
|
||||
expect(stepRetryConfigSchema.safeParse({ max_attempts: 5 }).success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects delay_ms below 1000', () => {
|
||||
const result = stepRetryConfigSchema.safeParse({ max_attempts: 2, delay_ms: 500 });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects delay_ms above 60000', () => {
|
||||
const result = stepRetryConfigSchema.safeParse({ max_attempts: 2, delay_ms: 70000 });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts delay_ms at boundaries (1000 and 60000)', () => {
|
||||
expect(stepRetryConfigSchema.safeParse({ max_attempts: 2, delay_ms: 1000 }).success).toBe(true);
|
||||
expect(stepRetryConfigSchema.safeParse({ max_attempts: 2, delay_ms: 60000 }).success).toBe(true);
|
||||
});
|
||||
|
||||
it('defaults on_error to transient', () => {
|
||||
const result = stepRetryConfigSchema.parse({ max_attempts: 2 });
|
||||
expect(result.on_error).toBe('transient');
|
||||
});
|
||||
});
|
||||
|
||||
describe('triggerRuleSchema', () => {
|
||||
it('validates all trigger rules', () => {
|
||||
const rules = ['all_success', 'one_success', 'all_done', 'none_failed_min_one_success'];
|
||||
for (const rule of rules) {
|
||||
expect(triggerRuleSchema.safeParse(rule).success).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects invalid trigger rules', () => {
|
||||
const result = triggerRuleSchema.safeParse('invalid_rule');
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('type guards', () => {
|
||||
const nodes: DagNode[] = [
|
||||
{ ...validBase, kind: 'prompt', prompt: 'test' } as PromptNode,
|
||||
{ ...validBase, kind: 'command', command: 'echo' } as CommandNode,
|
||||
{ ...validBase, kind: 'bash', bash: 'echo' } as BashNode,
|
||||
{ ...validBase, kind: 'script', script: 'print(1)', runtime: 'bun' } as ScriptNode,
|
||||
{ ...validBase, kind: 'loop', config: { prompt: 'x', until: 'y', max_iterations: 3 } } as LoopNode,
|
||||
{ ...validBase, kind: 'approval', message: 'ok?' } as ApprovalNode,
|
||||
{ ...validBase, kind: 'cancel' } as CancelNode,
|
||||
];
|
||||
|
||||
it('isPromptNode identifies prompt nodes', () => {
|
||||
expect(isPromptNode(nodes[0])).toBe(true);
|
||||
expect(isPromptNode(nodes[1])).toBe(false);
|
||||
});
|
||||
|
||||
it('isCommandNode identifies command nodes', () => {
|
||||
expect(isCommandNode(nodes[1])).toBe(true);
|
||||
expect(isCommandNode(nodes[0])).toBe(false);
|
||||
});
|
||||
|
||||
it('isBashNode identifies bash nodes', () => {
|
||||
expect(isBashNode(nodes[2])).toBe(true);
|
||||
expect(isBashNode(nodes[0])).toBe(false);
|
||||
});
|
||||
|
||||
it('isScriptNode identifies script nodes', () => {
|
||||
expect(isScriptNode(nodes[3])).toBe(true);
|
||||
expect(isScriptNode(nodes[0])).toBe(false);
|
||||
});
|
||||
|
||||
it('isLoopNode identifies loop nodes', () => {
|
||||
expect(isLoopNode(nodes[4])).toBe(true);
|
||||
expect(isLoopNode(nodes[0])).toBe(false);
|
||||
});
|
||||
|
||||
it('isApprovalNode identifies approval nodes', () => {
|
||||
expect(isApprovalNode(nodes[5])).toBe(true);
|
||||
expect(isApprovalNode(nodes[0])).toBe(false);
|
||||
});
|
||||
|
||||
it('isCancelNode identifies cancel nodes', () => {
|
||||
expect(isCancelNode(nodes[6])).toBe(true);
|
||||
expect(isCancelNode(nodes[0])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('effortLevelSchema', () => {
|
||||
it('validates all effort levels', () => {
|
||||
expect(effortLevelSchema.safeParse('low').success).toBe(true);
|
||||
expect(effortLevelSchema.safeParse('medium').success).toBe(true);
|
||||
expect(effortLevelSchema.safeParse('high').success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects invalid effort levels', () => {
|
||||
expect(effortLevelSchema.safeParse('extreme').success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('thinkingConfigSchema', () => {
|
||||
it('validates a minimal thinking config', () => {
|
||||
const result = thinkingConfigSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.enabled).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('validates a full thinking config', () => {
|
||||
const result = thinkingConfigSchema.safeParse({ enabled: true, max_tokens: 4096 });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('approvalOnRejectSchema', () => {
|
||||
it('validates all on-reject actions', () => {
|
||||
expect(approvalOnRejectSchema.safeParse('retry').success).toBe(true);
|
||||
expect(approvalOnRejectSchema.safeParse('fail').success).toBe(true);
|
||||
expect(approvalOnRejectSchema.safeParse('skip').success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects invalid on-reject actions', () => {
|
||||
expect(approvalOnRejectSchema.safeParse('restart').success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dagNodeBaseSchema', () => {
|
||||
it('validates a minimal base node', () => {
|
||||
const result = dagNodeBaseSchema.safeParse({
|
||||
id: 'node-1',
|
||||
kind: 'prompt',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('defaults depends_on to empty array', () => {
|
||||
const result = dagNodeBaseSchema.parse({ id: 'node-1', kind: 'prompt' });
|
||||
expect(result.depends_on).toEqual([]);
|
||||
});
|
||||
});
|
||||
273
packages/ion/src/schema/dag-node.ts
Normal file
273
packages/ion/src/schema/dag-node.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import { z } from 'zod';
|
||||
import { stepRetryConfigSchema } from './retry.js';
|
||||
import { loopNodeConfigSchema } from './loop.js';
|
||||
import { triggerRuleSchema } from './trigger-rule.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Effort level
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Effort level for AI model calls. */
|
||||
export const effortLevelSchema = z.enum(['low', 'medium', 'high']);
|
||||
|
||||
export type EffortLevel = z.infer<typeof effortLevelSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Thinking configuration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Configuration for extended thinking / chain-of-thought. */
|
||||
export const thinkingConfigSchema = z.object({
|
||||
/** Whether thinking is enabled. */
|
||||
enabled: z.boolean().default(false),
|
||||
/** Maximum thinking tokens. */
|
||||
max_tokens: z.number().int().positive().optional(),
|
||||
});
|
||||
|
||||
export type ThinkingConfig = z.infer<typeof thinkingConfigSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Approval on-reject action
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** What to do when an approval node is rejected. */
|
||||
export const approvalOnRejectSchema = z.enum(['retry', 'fail', 'skip']);
|
||||
|
||||
export type ApprovalOnReject = z.infer<typeof approvalOnRejectSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Base DAG node
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** The kind of a DAG node determines how it executes. */
|
||||
export const dagNodeKindSchema = z.enum([
|
||||
'prompt',
|
||||
'command',
|
||||
'bash',
|
||||
'script',
|
||||
'approval',
|
||||
'loop',
|
||||
'cancel',
|
||||
]);
|
||||
|
||||
/** Base fields shared by all DAG node types. */
|
||||
export const dagNodeBaseSchema = z.object({
|
||||
id: z.string(),
|
||||
kind: dagNodeKindSchema,
|
||||
name: z.string().optional(),
|
||||
when: z.string().optional(),
|
||||
depends_on: z.array(z.string()).default([]),
|
||||
trigger_rule: triggerRuleSchema.optional(),
|
||||
retry: stepRetryConfigSchema.optional(),
|
||||
env: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
export type DagNodeBase = z.infer<typeof dagNodeBaseSchema>;
|
||||
|
||||
export type DagNodeKind = z.infer<typeof dagNodeKindSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Prompt node — sends a prompt to an AI provider
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const promptNodeSchema = z.object({
|
||||
id: z.string(),
|
||||
kind: z.literal('prompt'),
|
||||
/** Human-readable name for display. */
|
||||
name: z.string().optional(),
|
||||
/** The prompt text (inline). Mutually exclusive with command_file. */
|
||||
prompt: z.string().optional(),
|
||||
/** Path to a command file containing the prompt. */
|
||||
command_file: z.string().optional(),
|
||||
/** Provider id to use (overrides workflow default). */
|
||||
provider: z.string().optional(),
|
||||
/** Model override for the provider. */
|
||||
model: z.string().optional(),
|
||||
/** Structured output format definition. */
|
||||
output_format: z
|
||||
.record(z.unknown())
|
||||
.optional(),
|
||||
/** Condition expression; node runs only when truthy. */
|
||||
when: z.string().optional(),
|
||||
/** Node ids this node depends on. */
|
||||
depends_on: z.array(z.string()).default([]),
|
||||
/** Trigger rule for evaluating dependency states. */
|
||||
trigger_rule: triggerRuleSchema.optional(),
|
||||
/** Retry configuration. */
|
||||
retry: stepRetryConfigSchema.optional(),
|
||||
/** Idle timeout in milliseconds. */
|
||||
idle_timeout_ms: z.number().positive().optional(),
|
||||
/** Environment variable overrides for this node. */
|
||||
env: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Command node — runs a shell command
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const commandNodeSchema = z.object({
|
||||
id: z.string(),
|
||||
kind: z.literal('command'),
|
||||
name: z.string().optional(),
|
||||
/** The command string to execute. */
|
||||
command: z.string(),
|
||||
/** Working directory override. */
|
||||
cwd: z.string().optional(),
|
||||
when: z.string().optional(),
|
||||
depends_on: z.array(z.string()).default([]),
|
||||
trigger_rule: triggerRuleSchema.optional(),
|
||||
retry: stepRetryConfigSchema.optional(),
|
||||
env: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bash node — runs a bash script
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const bashNodeSchema = z.object({
|
||||
id: z.string(),
|
||||
kind: z.literal('bash'),
|
||||
name: z.string().optional(),
|
||||
/** Bash script content to execute. */
|
||||
bash: z.string(),
|
||||
/** Timeout in milliseconds. */
|
||||
timeout_ms: z.number().positive().optional(),
|
||||
when: z.string().optional(),
|
||||
depends_on: z.array(z.string()).default([]),
|
||||
trigger_rule: triggerRuleSchema.optional(),
|
||||
retry: stepRetryConfigSchema.optional(),
|
||||
env: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Script node — runs a script with a specific runtime
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const scriptNodeSchema = z.object({
|
||||
id: z.string(),
|
||||
kind: z.literal('script'),
|
||||
name: z.string().optional(),
|
||||
/** Script content to execute. */
|
||||
script: z.string(),
|
||||
/** Runtime: 'bun' or 'uv'. */
|
||||
runtime: z.enum(['bun', 'uv']),
|
||||
/** Dependencies to install before running. */
|
||||
deps: z.array(z.string()).default([]),
|
||||
/** Timeout in milliseconds. */
|
||||
timeout_ms: z.number().positive().optional(),
|
||||
when: z.string().optional(),
|
||||
depends_on: z.array(z.string()).default([]),
|
||||
trigger_rule: triggerRuleSchema.optional(),
|
||||
retry: stepRetryConfigSchema.optional(),
|
||||
env: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Approval node — pauses for human approval
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const approvalNodeSchema = z.object({
|
||||
id: z.string(),
|
||||
kind: z.literal('approval'),
|
||||
name: z.string().optional(),
|
||||
/** Message shown to the approver. */
|
||||
message: z.string(),
|
||||
/** Prompt to execute if the approval is rejected. */
|
||||
on_reject: z.string().optional(),
|
||||
when: z.string().optional(),
|
||||
depends_on: z.array(z.string()).default([]),
|
||||
trigger_rule: triggerRuleSchema.optional(),
|
||||
env: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Loop node — iterates until a condition is met
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const loopNodeSchema = z.object({
|
||||
id: z.string(),
|
||||
kind: z.literal('loop'),
|
||||
name: z.string().optional(),
|
||||
/** Loop configuration (prompt, until, max_iterations, etc.). */
|
||||
config: loopNodeConfigSchema,
|
||||
/** Provider id to use (overrides workflow default). */
|
||||
provider: z.string().optional(),
|
||||
/** Model override for the provider. */
|
||||
model: z.string().optional(),
|
||||
when: z.string().optional(),
|
||||
depends_on: z.array(z.string()).default([]),
|
||||
trigger_rule: triggerRuleSchema.optional(),
|
||||
retry: stepRetryConfigSchema.optional(),
|
||||
env: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cancel node — cancels the workflow
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const cancelNodeSchema = z.object({
|
||||
id: z.string(),
|
||||
kind: z.literal('cancel'),
|
||||
name: z.string().optional(),
|
||||
/** Reason for cancellation. */
|
||||
reason: z.string().optional(),
|
||||
when: z.string().optional(),
|
||||
depends_on: z.array(z.string()).default([]),
|
||||
trigger_rule: triggerRuleSchema.optional(),
|
||||
env: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Union type — any DAG node
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const dagNodeSchema = z.discriminatedUnion('kind', [
|
||||
promptNodeSchema,
|
||||
commandNodeSchema,
|
||||
bashNodeSchema,
|
||||
scriptNodeSchema,
|
||||
approvalNodeSchema,
|
||||
loopNodeSchema,
|
||||
cancelNodeSchema,
|
||||
]);
|
||||
|
||||
export type DagNode = z.infer<typeof dagNodeSchema>;
|
||||
export type PromptNode = z.infer<typeof promptNodeSchema>;
|
||||
export type CommandNode = z.infer<typeof commandNodeSchema>;
|
||||
export type BashNode = z.infer<typeof bashNodeSchema>;
|
||||
export type ScriptNode = z.infer<typeof scriptNodeSchema>;
|
||||
export type ApprovalNode = z.infer<typeof approvalNodeSchema>;
|
||||
export type LoopNode = z.infer<typeof loopNodeSchema>;
|
||||
export type CancelNode = z.infer<typeof cancelNodeSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Type guards
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function isBashNode(node: DagNode): node is BashNode {
|
||||
return node.kind === 'bash';
|
||||
}
|
||||
|
||||
export function isScriptNode(node: DagNode): node is ScriptNode {
|
||||
return node.kind === 'script';
|
||||
}
|
||||
|
||||
export function isLoopNode(node: DagNode): node is LoopNode {
|
||||
return node.kind === 'loop';
|
||||
}
|
||||
|
||||
export function isApprovalNode(node: DagNode): node is ApprovalNode {
|
||||
return node.kind === 'approval';
|
||||
}
|
||||
|
||||
export function isCancelNode(node: DagNode): node is CancelNode {
|
||||
return node.kind === 'cancel';
|
||||
}
|
||||
|
||||
export function isPromptNode(node: DagNode): node is PromptNode {
|
||||
return node.kind === 'prompt';
|
||||
}
|
||||
|
||||
export function isCommandNode(node: DagNode): node is CommandNode {
|
||||
return node.kind === 'command';
|
||||
}
|
||||
111
packages/ion/src/schema/index.ts
Normal file
111
packages/ion/src/schema/index.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ion Schema Layer — Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// retry.ts
|
||||
export {
|
||||
stepRetryConfigSchema,
|
||||
type StepRetryConfig,
|
||||
} from './retry.js';
|
||||
|
||||
// loop.ts
|
||||
export {
|
||||
loopNodeConfigSchema,
|
||||
type LoopNodeConfig,
|
||||
} from './loop.js';
|
||||
|
||||
// trigger-rule.ts
|
||||
export {
|
||||
triggerRuleSchema,
|
||||
TRIGGER_RULES,
|
||||
DEFAULT_TRIGGER_RULE,
|
||||
type TriggerRule,
|
||||
} from './trigger-rule.js';
|
||||
|
||||
// dag-node.ts
|
||||
export {
|
||||
effortLevelSchema,
|
||||
type EffortLevel,
|
||||
thinkingConfigSchema,
|
||||
type ThinkingConfig,
|
||||
approvalOnRejectSchema,
|
||||
type ApprovalOnReject,
|
||||
dagNodeBaseSchema,
|
||||
type DagNodeBase,
|
||||
commandNodeSchema,
|
||||
promptNodeSchema,
|
||||
bashNodeSchema,
|
||||
scriptNodeSchema,
|
||||
loopNodeSchema,
|
||||
approvalNodeSchema,
|
||||
cancelNodeSchema,
|
||||
type CommandNode,
|
||||
type PromptNode,
|
||||
type BashNode,
|
||||
type ScriptNode,
|
||||
type LoopNode,
|
||||
type ApprovalNode,
|
||||
type CancelNode,
|
||||
dagNodeSchema,
|
||||
type DagNode,
|
||||
isBashNode,
|
||||
isLoopNode,
|
||||
isApprovalNode,
|
||||
isCancelNode,
|
||||
isScriptNode,
|
||||
isPromptNode,
|
||||
isCommandNode,
|
||||
} from './dag-node.js';
|
||||
|
||||
// workflow.ts
|
||||
export {
|
||||
modelReasoningEffortSchema,
|
||||
type ModelReasoningEffort,
|
||||
webSearchModeSchema,
|
||||
type WebSearchMode,
|
||||
workflowRequirementSchema,
|
||||
type WorkflowRequirement,
|
||||
workflowWorktreePolicySchema,
|
||||
type WorkflowWorktreePolicy,
|
||||
sandboxConfigSchema,
|
||||
type SandboxConfig,
|
||||
providerOverridesSchema,
|
||||
type ProviderOverrides,
|
||||
workflowBaseSchema,
|
||||
type WorkflowBase,
|
||||
workflowDefinitionSchema,
|
||||
type WorkflowDefinition,
|
||||
WorkflowSourceSchema,
|
||||
type WorkflowSource,
|
||||
workflowExecutionResultSchema,
|
||||
type WorkflowExecutionResult,
|
||||
workflowWithSourceSchema,
|
||||
type WorkflowWithSource,
|
||||
workflowLoadErrorSchema,
|
||||
type WorkflowLoadError,
|
||||
workflowLoadResultSchema,
|
||||
type WorkflowLoadResult,
|
||||
loadCommandResultSchema,
|
||||
type LoadCommandResult,
|
||||
} from './workflow.js';
|
||||
|
||||
// workflow-run.ts
|
||||
export {
|
||||
WorkflowRunStatusSchema,
|
||||
type WorkflowRunStatus,
|
||||
TERMINAL_WORKFLOW_STATUSES,
|
||||
RESUMABLE_WORKFLOW_STATUSES,
|
||||
NodeStateSchema,
|
||||
type NodeState,
|
||||
ApprovalContextSchema,
|
||||
type ApprovalContext,
|
||||
WorkflowRunSchema,
|
||||
type WorkflowRun,
|
||||
} from './workflow-run.js';
|
||||
|
||||
// node-output.ts
|
||||
export {
|
||||
nodeOutputSchema,
|
||||
type NodeOutput,
|
||||
type NodeExecutionResult,
|
||||
} from './node-output.js';
|
||||
46
packages/ion/src/schema/loop.ts
Normal file
46
packages/ion/src/schema/loop.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Configuration for a loop-type DAG node.
|
||||
*
|
||||
* The loop repeatedly invokes the model with the given prompt until the
|
||||
* `until` condition is satisfied or `max_iterations` is reached.
|
||||
* When `interactive` is true, a human gate must approve each iteration.
|
||||
*/
|
||||
export const loopNodeConfigSchema = z
|
||||
.object({
|
||||
/** The prompt sent to the model on each iteration. */
|
||||
prompt: z.string().min(1, 'loop prompt must not be empty'),
|
||||
|
||||
/** Natural-language condition that must be true for the loop to exit. */
|
||||
until: z.string().min(1, 'loop until condition must not be empty'),
|
||||
|
||||
/** Maximum iterations before the loop is force-stopped. */
|
||||
max_iterations: z
|
||||
.number()
|
||||
.int()
|
||||
.positive('max_iterations must be a positive integer'),
|
||||
|
||||
/** Whether each iteration starts with a fresh context window. */
|
||||
fresh_context: z.boolean().default(false),
|
||||
|
||||
/** Optional bash command whose exit code is used as the until-check. */
|
||||
until_bash: z.string().optional(),
|
||||
|
||||
/** When true, a human must approve each iteration before it proceeds. */
|
||||
interactive: z.boolean().optional(),
|
||||
|
||||
/** Message shown to the human gate when interactive is true. */
|
||||
gate_message: z.string().optional(),
|
||||
})
|
||||
.superRefine((config, ctx) => {
|
||||
if (config.interactive && !config.gate_message) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'gate_message is required when interactive is true',
|
||||
path: ['gate_message'],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type LoopNodeConfig = z.infer<typeof loopNodeConfigSchema>;
|
||||
46
packages/ion/src/schema/node-output.ts
Normal file
46
packages/ion/src/schema/node-output.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Output produced by a single DAG node after execution.
|
||||
*
|
||||
* Captures the result text, structured fields, and execution metadata
|
||||
* so downstream nodes can reference outputs via `$nodeId.output` syntax.
|
||||
*/
|
||||
export const nodeOutputSchema = z.object({
|
||||
/** The node id that produced this output. */
|
||||
nodeId: z.string(),
|
||||
|
||||
/** Current state of the node execution. */
|
||||
state: z.enum(['pending', 'running', 'completed', 'failed', 'skipped']),
|
||||
|
||||
/** The raw text output from the node (alias for text for backward compat). */
|
||||
output: z.string().default(''),
|
||||
|
||||
/** The raw text output from the node. */
|
||||
text: z.string().optional(),
|
||||
|
||||
/** Structured output fields (when output_format is defined). */
|
||||
fields: z.record(z.unknown()).optional(),
|
||||
|
||||
/** Error message if the node failed. */
|
||||
error: z.string().optional(),
|
||||
|
||||
/** Token usage or cost metadata. */
|
||||
costUsd: z.number().optional(),
|
||||
});
|
||||
|
||||
export type NodeOutput = z.infer<typeof nodeOutputSchema>;
|
||||
|
||||
/**
|
||||
* Result of executing a single node within the DAG.
|
||||
*
|
||||
* Internal to the engine — not persisted directly but used to build
|
||||
* the nodeOutputs map that flows between layers.
|
||||
*/
|
||||
export interface NodeExecutionResult {
|
||||
state: 'completed' | 'failed' | 'skipped';
|
||||
output?: string;
|
||||
fields?: Record<string, unknown>;
|
||||
error?: string;
|
||||
costUsd?: number;
|
||||
}
|
||||
31
packages/ion/src/schema/retry.ts
Normal file
31
packages/ion/src/schema/retry.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Retry configuration for a DAG node step.
|
||||
*
|
||||
* Controls how many times a step can be re-attempted on failure,
|
||||
* the delay between attempts, and which classes of errors trigger a retry.
|
||||
*/
|
||||
export const stepRetryConfigSchema = z.object({
|
||||
/** Maximum number of retry attempts (1–5 inclusive). */
|
||||
max_attempts: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1, 'max_attempts must be at least 1')
|
||||
.max(5, 'max_attempts must be at most 5'),
|
||||
|
||||
/** Milliseconds to wait between retry attempts (1000–60000). */
|
||||
delay_ms: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1000, 'delay_ms must be at least 1000')
|
||||
.max(60000, 'delay_ms must be at most 60000')
|
||||
.optional(),
|
||||
|
||||
/** Which errors trigger a retry. Defaults to 'transient'. */
|
||||
on_error: z
|
||||
.enum(['transient', 'all'])
|
||||
.default('transient'),
|
||||
});
|
||||
|
||||
export type StepRetryConfig = z.infer<typeof stepRetryConfigSchema>;
|
||||
26
packages/ion/src/schema/trigger-rule.ts
Normal file
26
packages/ion/src/schema/trigger-rule.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Trigger rule for a DAG node.
|
||||
*
|
||||
* Determines when a node should run based on the completion status of its
|
||||
* dependencies. Mirrors Airflow's trigger rules.
|
||||
*/
|
||||
export const triggerRuleSchema = z.enum([
|
||||
/** All dependencies must have completed successfully. */
|
||||
'all_success',
|
||||
/** At least one dependency must have completed successfully. */
|
||||
'one_success',
|
||||
/** All dependencies must have finished (any status). */
|
||||
'all_done',
|
||||
/** No dependency failed AND at least one succeeded. */
|
||||
'none_failed_min_one_success',
|
||||
]);
|
||||
|
||||
export type TriggerRule = z.infer<typeof triggerRuleSchema>;
|
||||
|
||||
/** All valid trigger rule values. */
|
||||
export const TRIGGER_RULES: TriggerRule[] = triggerRuleSchema.options;
|
||||
|
||||
/** Default trigger rule used when none is specified. */
|
||||
export const DEFAULT_TRIGGER_RULE: TriggerRule = 'all_success';
|
||||
109
packages/ion/src/schema/workflow-run.ts
Normal file
109
packages/ion/src/schema/workflow-run.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { z } from 'zod';
|
||||
import { approvalOnRejectSchema } from './dag-node.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workflow run status
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const WorkflowRunStatusSchema = z.enum([
|
||||
'pending',
|
||||
'running',
|
||||
'paused',
|
||||
'completed',
|
||||
'failed',
|
||||
'cancelled',
|
||||
]);
|
||||
|
||||
export type WorkflowRunStatus = z.infer<typeof WorkflowRunStatusSchema>;
|
||||
|
||||
/** Statuses that indicate the run has finished (no further transitions). */
|
||||
export const TERMINAL_WORKFLOW_STATUSES = WorkflowRunStatusSchema.options.filter(
|
||||
(s): s is 'completed' | 'failed' | 'cancelled' =>
|
||||
s === 'completed' || s === 'failed' || s === 'cancelled',
|
||||
);
|
||||
|
||||
/** Statuses from which a run can be resumed. */
|
||||
export const RESUMABLE_WORKFLOW_STATUSES = WorkflowRunStatusSchema.options.filter(
|
||||
(s): s is 'paused' | 'failed' =>
|
||||
s === 'paused' || s === 'failed',
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Node state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const NodeStateSchema = z.enum([
|
||||
'pending',
|
||||
'running',
|
||||
'completed',
|
||||
'failed',
|
||||
'skipped',
|
||||
]);
|
||||
|
||||
export type NodeState = z.infer<typeof NodeStateSchema>;
|
||||
|
||||
// NOTE: NodeOutput type is in node-output.ts — re-exported via schema/index.ts
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Approval context
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const ApprovalContextSchema = z.object({
|
||||
/** Discriminator for the approval type. */
|
||||
type: z.literal('approval'),
|
||||
|
||||
/** The node that is waiting for approval. */
|
||||
nodeId: z.string().min(1, 'nodeId must not be empty'),
|
||||
|
||||
/** Message shown to the approver. */
|
||||
message: z.string().min(1, 'approval message must not be empty'),
|
||||
|
||||
/** Whether the approver's response should be captured. */
|
||||
capture_response: z.boolean().default(false),
|
||||
|
||||
/** What to do when the approval is rejected. */
|
||||
on_reject: approvalOnRejectSchema.optional(),
|
||||
});
|
||||
|
||||
export type ApprovalContext = z.infer<typeof ApprovalContextSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workflow run
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const WorkflowRunSchema = z.object({
|
||||
/** Unique run identifier. */
|
||||
id: z.string().min(1, 'run id must not be empty'),
|
||||
|
||||
/** Name of the workflow being run. */
|
||||
workflow_name: z.string().min(1, 'workflow_name must not be empty'),
|
||||
|
||||
/** Current status of the run. */
|
||||
status: WorkflowRunStatusSchema,
|
||||
|
||||
/** Conversation this run belongs to. */
|
||||
conversation_id: z.string().min(1, 'conversation_id must not be empty'),
|
||||
|
||||
/** Codebase this run operates on. */
|
||||
codebase_id: z.string().min(1, 'codebase_id must not be empty'),
|
||||
|
||||
/** The user message that triggered this run. */
|
||||
user_message: z.string().optional(),
|
||||
|
||||
/** Working directory path for the run. */
|
||||
working_path: z.string().optional(),
|
||||
|
||||
/** Timestamp when the run was created. */
|
||||
created_at: z.string().datetime(),
|
||||
|
||||
/** Timestamp when the run started executing. */
|
||||
started_at: z.string().datetime().optional(),
|
||||
|
||||
/** Arbitrary metadata attached to the run. */
|
||||
metadata: z.record(z.string(), z.unknown()).default({}),
|
||||
|
||||
/** Parent conversation ID for nested runs. */
|
||||
parent_conversation_id: z.string().optional(),
|
||||
});
|
||||
|
||||
export type WorkflowRun = z.infer<typeof WorkflowRunSchema>;
|
||||
246
packages/ion/src/schema/workflow.ts
Normal file
246
packages/ion/src/schema/workflow.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import { z } from 'zod';
|
||||
import { dagNodeSchema, effortLevelSchema, thinkingConfigSchema } from './dag-node.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Model reasoning effort
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const modelReasoningEffortSchema = z.enum(['low', 'medium', 'high']);
|
||||
|
||||
export type ModelReasoningEffort = z.infer<typeof modelReasoningEffortSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Web search mode
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const webSearchModeSchema = z.enum(['off', 'auto', 'on']);
|
||||
|
||||
export type WebSearchMode = z.infer<typeof webSearchModeSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workflow requirement
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const workflowRequirementSchema = z.object({
|
||||
/** Human-readable name of the requirement. */
|
||||
name: z.string().min(1, 'requirement name must not be empty'),
|
||||
|
||||
/** Short description of what the requirement enforces. */
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export type WorkflowRequirement = z.infer<typeof workflowRequirementSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Worktree policy
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const workflowWorktreePolicySchema = z.object({
|
||||
/** Whether worktree isolation is enabled for this workflow. */
|
||||
enabled: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type WorkflowWorktreePolicy = z.infer<typeof workflowWorktreePolicySchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sandbox config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const sandboxConfigSchema = z.object({
|
||||
/** Whether sandboxing is enabled. */
|
||||
enabled: z.boolean().default(false),
|
||||
|
||||
/** Docker image to use for sandboxing. */
|
||||
image: z.string().optional(),
|
||||
|
||||
/** Memory limit in megabytes. */
|
||||
memory_mb: z.number().int().positive().optional(),
|
||||
|
||||
/** CPU limit in cores. */
|
||||
cpu_cores: z.number().positive().optional(),
|
||||
|
||||
/** Network access inside the sandbox. */
|
||||
network: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export type SandboxConfig = z.infer<typeof sandboxConfigSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Provider overrides
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const providerOverridesSchema = z.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
model: z.string().optional(),
|
||||
base_url: z.string().optional(),
|
||||
api_key_env: z.string().optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
export type ProviderOverrides = z.infer<typeof providerOverridesSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workflow base schema (shared between definition and metadata)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const workflowBaseSchema = z.object({
|
||||
/** Human-readable workflow name. */
|
||||
name: z.string().min(1, 'workflow name must not be empty'),
|
||||
|
||||
/** Short description of what the workflow does. */
|
||||
description: z.string().optional(),
|
||||
|
||||
/** Default provider for all nodes. */
|
||||
provider: z.string().optional(),
|
||||
|
||||
/** Default model for all nodes. */
|
||||
model: z.string().optional(),
|
||||
|
||||
/** Reasoning effort level for the model. */
|
||||
modelReasoningEffort: modelReasoningEffortSchema.optional(),
|
||||
|
||||
/** Web search mode for the model. */
|
||||
webSearchMode: webSearchModeSchema.optional(),
|
||||
|
||||
/** Whether the workflow can pause for human input. */
|
||||
interactive: z.boolean().default(false),
|
||||
|
||||
/** Default effort level for all nodes. */
|
||||
effort: effortLevelSchema.optional(),
|
||||
|
||||
/** Default thinking configuration for all nodes. */
|
||||
thinking: thinkingConfigSchema.optional(),
|
||||
|
||||
/** Fallback model when the primary model is unavailable. */
|
||||
fallbackModel: z.string().optional(),
|
||||
|
||||
/** Tags for categorisation and filtering. */
|
||||
tags: z.array(z.string()).default([]),
|
||||
|
||||
/** Requirements that must be met before the workflow can run. */
|
||||
requires: z.array(workflowRequirementSchema).default([]),
|
||||
|
||||
/** Whether this workflow modifies the git checkout. */
|
||||
mutates_checkout: z.boolean().default(false),
|
||||
|
||||
/** Whether to persist conversation sessions between runs. */
|
||||
persist_sessions: z.boolean().default(false),
|
||||
|
||||
/** Per-provider configuration overrides. */
|
||||
provider_overrides: providerOverridesSchema.optional(),
|
||||
|
||||
/** Sandbox configuration for the workflow. */
|
||||
sandbox: sandboxConfigSchema.optional(),
|
||||
|
||||
/** Worktree policy for the workflow. */
|
||||
worktree: workflowWorktreePolicySchema.optional(),
|
||||
});
|
||||
|
||||
export type WorkflowBase = z.infer<typeof workflowBaseSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Full workflow definition (base + nodes)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const workflowDefinitionSchema = workflowBaseSchema.extend({
|
||||
/** The DAG nodes that make up this workflow. */
|
||||
nodes: z.array(dagNodeSchema),
|
||||
});
|
||||
|
||||
export type WorkflowDefinition = z.infer<typeof workflowDefinitionSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workflow source
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const WorkflowSourceSchema = z.enum(['bundled', 'global', 'project']);
|
||||
|
||||
export type WorkflowSource = z.infer<typeof WorkflowSourceSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workflow execution result
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const workflowExecutionResultSchema = z.discriminatedUnion('status', [
|
||||
z.object({
|
||||
status: z.literal('success'),
|
||||
output: z.string(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
}),
|
||||
z.object({
|
||||
status: z.literal('fail'),
|
||||
error: z.string(),
|
||||
node_id: z.string().optional(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
}),
|
||||
z.object({
|
||||
status: z.literal('paused'),
|
||||
node_id: z.string(),
|
||||
reason: z.string(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type WorkflowExecutionResult = z.infer<typeof workflowExecutionResultSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workflow with source metadata
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const workflowWithSourceSchema = z.object({
|
||||
definition: workflowDefinitionSchema,
|
||||
source: WorkflowSourceSchema,
|
||||
path: z.string().optional(),
|
||||
});
|
||||
|
||||
export type WorkflowWithSource = z.infer<typeof workflowWithSourceSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workflow load error
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const workflowLoadErrorSchema = z.object({
|
||||
message: z.string(),
|
||||
path: z.string().optional(),
|
||||
cause: z.unknown().optional(),
|
||||
});
|
||||
|
||||
export type WorkflowLoadError = z.infer<typeof workflowLoadErrorSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workflow load result (success or error)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const workflowLoadResultSchema = z.union([
|
||||
workflowWithSourceSchema,
|
||||
workflowLoadErrorSchema,
|
||||
]);
|
||||
|
||||
export type WorkflowLoadResult = z.infer<typeof workflowLoadResultSchema>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Load command result
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const loadCommandResultSchema = z.discriminatedUnion('status', [
|
||||
z.object({
|
||||
status: z.literal('success'),
|
||||
definition: workflowDefinitionSchema,
|
||||
source: WorkflowSourceSchema,
|
||||
path: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
status: z.literal('fail'),
|
||||
reason: z.enum([
|
||||
'not_found',
|
||||
'parse_error',
|
||||
'validation_error',
|
||||
'permission_denied',
|
||||
]),
|
||||
message: z.string(),
|
||||
path: z.string().optional(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type LoadCommandResult = z.infer<typeof loadCommandResultSchema>;
|
||||
Reference in New Issue
Block a user