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:
2026-06-07 22:16:45 +00:00
parent 33bf509c3f
commit b1e4e5fd2a
63 changed files with 14025 additions and 0 deletions

View 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([]);
});
});

View 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';
}

View 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';

View 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>;

View 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;
}

View 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 (15 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 (100060000). */
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>;

View 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';

View 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>;

View 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>;