Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
602302e
docs(landlord): design spec for dynamic-workflow runtime
DizzyMii May 31, 2026
12f3ce1
docs(landlord): implementation plan for dynamic-workflow runtime
DizzyMii May 31, 2026
ecd227e
feat(landlord): workflow errors and shared types
DizzyMii May 31, 2026
a19c705
feat(landlord): concurrency semaphore and agent cap
DizzyMii May 31, 2026
1db2fa0
feat(landlord): workflow budget token-target tracker
DizzyMii May 31, 2026
3cf03af
feat(landlord): workflow event emitter
DizzyMii May 31, 2026
47f800a
feat(landlord): journal store and call hashing for resume
DizzyMii May 31, 2026
9473964
feat(landlord): agent-type and workflow registries
DizzyMii May 31, 2026
eb3f61a
feat(landlord): workdir and git-worktree isolation backends
DizzyMii May 31, 2026
ff743d6
feat(landlord): structured-output tool with ajv validation
DizzyMii May 31, 2026
70eebbf
feat(landlord): agent() hook with schema, isolation, journaling
DizzyMii May 31, 2026
a2770f0
feat(landlord): workflow context (parallel/pipeline/phase/log)
DizzyMii May 31, 2026
3af142b
feat(landlord): meta literal parser and determinism sandbox
DizzyMii May 31, 2026
3d50e09
feat(landlord): script compiler and typed defineWorkflow
DizzyMii May 31, 2026
daa3666
fix(landlord): block this-based workflow sandbox escape
DizzyMii May 31, 2026
be7a33a
feat(landlord): workflow run engine with resume and nesting
DizzyMii May 31, 2026
8f432c7
fix(landlord): re-journal replayed entries for multi-hop resume
DizzyMii May 31, 2026
630be77
feat(landlord): workflowTool, orchestratorAgent, and tool guide
DizzyMii May 31, 2026
708d0cb
style(landlord): clear biome lint errors in workflow modules
DizzyMii May 31, 2026
a7345ff
feat(landlord): rebuild orchestrate on runtime; export workflow surface
DizzyMii May 31, 2026
3813912
docs(landlord): dynamic-workflow runtime docs, example, and changeset
DizzyMii May 31, 2026
63bb0fd
docs(landlord): fix two workflow doc samples to use the real API
DizzyMii May 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/landlord-dynamic-workflows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"landlord": minor
---

Add a dynamic-workflow runtime: author workflows as typed functions (`defineWorkflow`) or model-written JS scripts (`runWorkflowScript`) that orchestrate subagents via `agent`/`parallel`/`pipeline`/`phase`/`log`/`args`/`budget`/`workflow` hooks, with structured-output schemas, concurrency/agent caps, resume/journaling, a determinism sandbox, an agent-type registry, isolation backends, and a model-facing `workflowTool`. `orchestrate()` is now built on this runtime (API unchanged).
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ if (out.ok) console.log(out.value.message.content); // "579"
| `@flint/adapter-anthropic` | Anthropic Messages API — prompt-cache aware |
| `@flint/adapter-openai-compat` | Any OpenAI-compatible endpoint |
| `@flint/graph` | State-machine agent workflows |
| `@flint/landlord` | Multi-agent orchestration: dynamic workflow runtime (ultracode-style script orchestration) and auto-decompose `orchestrate()` |

## Flint vs LangChain

Expand Down Expand Up @@ -303,7 +304,7 @@ Full documentation at **[dizzymii.github.io/Flint](https://dizzymii.github.io/Fl
- [Features](https://dizzymii.github.io/Flint/features/budget) — budget, compress, memory, RAG, recipes, safety, graph
- [Adapters](https://dizzymii.github.io/Flint/adapters/anthropic) — Anthropic, OpenAI-compatible, custom
- [Examples](https://dizzymii.github.io/Flint/examples/basic-call) — basic call, tools, agent, streaming, RAG, multi-agent, memory, graph
- [Landlord](https://dizzymii.github.io/Flint/landlord/) — `@flint/landlord` multi-agent orchestration
- [Landlord](https://dizzymii.github.io/Flint/landlord/) — `@flint/landlord` dynamic workflow runtime and multi-agent orchestration
- [Reference](https://dizzymii.github.io/Flint/reference/errors) — error types catalog

## Contributing
Expand Down
7 changes: 7 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export default defineConfig({
{ text: 'Tool Approval', link: '/examples/tool-approval' },
{ text: 'Memory Agent', link: '/examples/memory-agent' },
{ text: 'Graph Workflow', link: '/examples/graph-workflow' },
{ text: 'Dynamic Workflow', link: '/examples/dynamic-workflow' },
],
},
],
Expand All @@ -108,6 +109,12 @@ export default defineConfig({
text: 'Landlord',
items: [
{ text: 'Overview', link: '/landlord/' },
{ text: 'Workflows', link: '/landlord/workflow' },
{ text: 'Hooks', link: '/landlord/hooks' },
{ text: 'Resume', link: '/landlord/resume' },
{ text: 'Agent Types', link: '/landlord/agent-types' },
{ text: 'Isolation', link: '/landlord/isolation' },
{ text: 'Workflow Tool', link: '/landlord/workflow-tool' },
{ text: 'Contracts', link: '/landlord/contract' },
{ text: 'decompose()', link: '/landlord/decompose' },
{ text: 'orchestrate()', link: '/landlord/orchestrate' },
Expand Down
319 changes: 319 additions & 0 deletions docs/examples/dynamic-workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
# Dynamic Workflow: Review and Verify Pipeline

This example implements a two-phase security review pipeline using the `@flint/landlord` workflow runtime. It shows the same workflow written two ways: as a string script (for model-authored workflows) and as a typed `defineWorkflow` (for production code).

## What this demonstrates

- `runWorkflowScript` — executing a model-authored JS string
- `defineWorkflow` + `runWorkflow` — the typed authoring path
- `parallel` for a barrier gather, `pipeline` for per-item multi-stage processing
- `schema` for structured output per agent
- `onEvent` progress logging
- `fileJournalStore` for crash-safe resume

## Setup

```ts
import {
defineWorkflow,
fileJournalStore,
runWorkflow,
runWorkflowScript,
} from '@flint/landlord';
import { anthropicAdapter } from '@flint/adapter-anthropic';
import { join } from 'node:path';

const adapter = anthropicAdapter({ apiKey: process.env.ANTHROPIC_API_KEY! });
const journal = fileJournalStore(join(process.cwd(), '.workflow-journal'));

function onEvent(e: import('@flint/landlord').WorkflowEvent) {
switch (e.type) {
case 'phase_started':
console.log(`\n=== ${e.title} ===`);
break;
case 'agent_started':
console.log(` → ${e.label} [${e.agentType}] (${e.model})`);
break;
case 'agent_complete':
console.log(` ✓ ${e.label} (${e.tokens} tokens)`);
break;
case 'agent_error':
console.error(` ✗ ${e.label}: ${e.error}`);
break;
case 'workflow_complete':
console.log('\n[workflow complete]');
break;
}
}
```

## Version 1: string script

The same logic as a model-authored JS string. This is the format the model writes when using `workflowTool`.

```ts
const source = `
export const meta = {
name: 'security-review',
description: 'Scan files for issues, then verify each finding independently',
phases: [
{ title: 'Scan', detail: 'Parallel scan per file' },
{ title: 'Verify', detail: 'Independent verification per finding' }
]
}

const files = args

// Phase 1: scan all files in parallel (barrier — we need all findings before verifying)
phase('Scan')
const FINDING_SCHEMA = {
type: 'object',
properties: {
file: { type: 'string' },
issues: { type: 'array', items: { type: 'string' } },
severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] }
},
required: ['file', 'issues', 'severity']
}

const rawFindings = await parallel(
files.map(f => () => agent('Scan ' + f + ' for security vulnerabilities', {
label: 'scan:' + f,
agentType: 'code-reviewer',
schema: FINDING_SCHEMA
}))
)

const findings = rawFindings.filter(Boolean)
log('Found ' + findings.length + ' scan results')

if (findings.length === 0) {
return { findings: [], verified: [] }
}

// Phase 2: verify each finding independently, no barrier needed between items
phase('Verify')
const VERIFY_SCHEMA = {
type: 'object',
properties: {
confirmed: { type: 'boolean' },
reason: { type: 'string' },
severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] }
},
required: ['confirmed', 'reason', 'severity']
}

const verified = await pipeline(
findings,
(finding) => agent(
'You are an independent security reviewer. Verify this finding — is it a real vulnerability, ' +
'or a false positive? Be skeptical. Finding: ' + JSON.stringify(finding),
{
label: 'verify:' + finding.file,
agentType: 'code-reviewer',
schema: VERIFY_SCHEMA
}
)
)

const confirmed = verified.filter(v => v?.confirmed)
log('Confirmed ' + confirmed.length + ' of ' + findings.length + ' findings')

return {
findings,
verified,
confirmed
}
`;

const files = ['src/auth.ts', 'src/api.ts', 'src/db.ts'];

const result1 = await runWorkflowScript(source, {
adapter,
models: { default: 'claude-opus-4-7' },
args: files,
journal,
runId: 'review-001',
onEvent,
});

if (result1.ok) {
const { findings, confirmed } = result1.value.result as {
findings: unknown[];
confirmed: unknown[];
};
console.log(`\nTotal findings: ${findings.length}`);
console.log(`Confirmed vulnerabilities: ${confirmed.length}`);
console.log('runId:', result1.value.runId);
}
```

## Version 2: typed workflow

The identical logic using `defineWorkflow` — fully type-checked, no eval.

```ts
type Finding = {
file: string;
issues: string[];
severity: 'low' | 'medium' | 'high' | 'critical';
};

type Verification = {
confirmed: boolean;
reason: string;
severity: 'low' | 'medium' | 'high' | 'critical';
};

const FINDING_SCHEMA = {
type: 'object',
properties: {
file: { type: 'string' },
issues: { type: 'array', items: { type: 'string' } },
severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
},
required: ['file', 'issues', 'severity'],
} as const;

const VERIFY_SCHEMA = {
type: 'object',
properties: {
confirmed: { type: 'boolean' },
reason: { type: 'string' },
severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
},
required: ['confirmed', 'reason', 'severity'],
} as const;

const reviewWorkflow = defineWorkflow({
meta: {
name: 'security-review',
description: 'Scan files for issues, then verify each finding independently',
phases: [
{ title: 'Scan', detail: 'Parallel scan per file' },
{ title: 'Verify', detail: 'Independent verification per finding' },
],
},

run: async (wf) => {
const files = wf.args as string[];

// Phase 1: scan all files in parallel — barrier because we want all findings before verifying
wf.phase('Scan');
const rawFindings = await wf.parallel(
files.map((f) => () =>
wf.agent(`Scan ${f} for security vulnerabilities`, {
label: `scan:${f}`,
agentType: 'code-reviewer',
schema: FINDING_SCHEMA,
}),
),
);

const findings = rawFindings.filter((f): f is Finding => f !== null);
wf.log(`Found ${findings.length} scan results`);

if (findings.length === 0) {
return { findings: [], verified: [], confirmed: [] };
}

// Phase 2: verify each finding — pipeline because each item is independent
wf.phase('Verify');
const verified = await wf.pipeline(
findings,
(finding) =>
wf.agent(
`You are an independent security reviewer. Verify this finding — is it a real ` +
`vulnerability, or a false positive? Be skeptical. Finding: ${JSON.stringify(finding)}`,
{
label: `verify:${(finding as Finding).file}`,
agentType: 'code-reviewer',
schema: VERIFY_SCHEMA,
},
),
);

const confirmed = (verified as Array<Verification | null>).filter(
(v): v is Verification => v?.confirmed === true,
);
wf.log(`Confirmed ${confirmed.length} of ${findings.length} findings`);

return { findings, verified, confirmed };
},
});

const result2 = await runWorkflow(reviewWorkflow, {
adapter,
models: { default: 'claude-opus-4-7' },
args: ['src/auth.ts', 'src/api.ts', 'src/db.ts'],
journal,
runId: 'review-002',
onEvent,
});

if (result2.ok) {
const output = result2.value.result as {
findings: Finding[];
confirmed: Verification[];
};
console.log(`\nTotal findings: ${output.findings.length}`);
console.log(`Confirmed vulnerabilities: ${output.confirmed.length}`);

for (const v of output.confirmed) {
console.log(` [${v.severity}] ${v.reason}`);
}
}
```

## Resume after a crash

If the run crashes halfway (e.g. network error during the Verify phase), you can resume without re-running the Scan phase:

```ts
const resumed = await runWorkflow(reviewWorkflow, {
adapter,
models: { default: 'claude-opus-4-7' },
args: ['src/auth.ts', 'src/api.ts', 'src/db.ts'],
journal,
runId: 'review-003',
resumeFromRunId: 'review-002', // replay unchanged prefix from this run
onEvent,
});
```

The Scan agents whose calls are journaled will be replayed instantly. The first Verify agent that didn't complete will re-run live, and all subsequent agents will run live too.

## Expected output

```
=== Scan ===
→ scan:src/auth.ts [code-reviewer] (claude-opus-4-7)
→ scan:src/api.ts [code-reviewer] (claude-opus-4-7)
→ scan:src/db.ts [code-reviewer] (claude-opus-4-7)
✓ scan:src/auth.ts (1240 tokens)
✓ scan:src/api.ts (980 tokens)
✓ scan:src/db.ts (1105 tokens)

=== Verify ===
→ verify:src/auth.ts [code-reviewer] (claude-opus-4-7)
✓ verify:src/auth.ts (850 tokens)
→ verify:src/api.ts [code-reviewer] (claude-opus-4-7)
✓ verify:src/api.ts (720 tokens)
→ verify:src/db.ts [code-reviewer] (claude-opus-4-7)
✓ verify:src/db.ts (910 tokens)

[workflow complete]

Total findings: 3
Confirmed vulnerabilities: 2
[high] SQL query in db.ts line 42 uses string concatenation — SQL injection risk
[medium] auth.ts token expiry not validated on refresh path
```

## See also

- [Workflow Runtime](/landlord/workflow) — `RuntimeConfig`, `WorkflowEvent`
- [Hooks reference](/landlord/hooks) — `parallel` vs `pipeline`, `schema` for structured output
- [Resume and journaling](/landlord/resume) — how the journal replay works
- [Agent Types](/landlord/agent-types) — `code-reviewer` and other built-in presets
- [Multi-Agent with Landlord](/examples/multi-agent) — the `orchestrate()` equivalent
Loading
Loading