diff --git a/web/AGENTS.md b/web/AGENTS.md new file mode 100644 index 0000000..643577d --- /dev/null +++ b/web/AGENTS.md @@ -0,0 +1,9 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/web/CLAUDE.md b/web/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/web/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/web/app/flows/flow-examples.ts b/web/app/flows/flow-examples.ts index 0009d32..2085ae8 100644 --- a/web/app/flows/flow-examples.ts +++ b/web/app/flows/flow-examples.ts @@ -5,7 +5,7 @@ export const flowExamples = [ "title": "Turn a ticket into a pull request.", "description": "Plan the change, write the code, and run the tests before opening a PR.", "filename": "software-factory.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n repo: string;\n ticket: string;\n};\n\nexport default flow(\n \"software-factory\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"planner\", {\n task: `${input.ticket} Write a plan to plan.md.`,\n }).gate((r) => r.artifacts.includes(\"plan.md\"));\n\n await f.agent(\"implementer\", {\n task: \"Read plan.md. Implement it on branch flow/fix. \" +\n \"Write the PR description to summary.md.\",\n }).gate((r) => r.artifacts.includes(\"summary.md\"));\n\n // The tests run outside the agent.\n // The agent cannot lie about the exit code.\n await f.run(\"git checkout flow/fix && npm test\");\n\n await f.agent(\"reviewer\", {\n task: \"Review the diff against main. \" +\n \"Write review.passed only if ready for a PR.\",\n }).gate((r) => r.artifacts.includes(\"review.passed\"));\n\n // Deterministic step, not an agent decision.\n await f.github.createPullRequest({\n repo: input.repo,\n head: \"flow/fix\",\n base: \"main\",\n bodyPath: \"summary.md\",\n });\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n repo: string;\n ticket: string;\n};\n\nexport default flow(\n \"software-factory\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"planner\", {\n task: `${input.ticket} Write a plan to plan.md.`,\n }).gate({ type: \"subprocess_gate\", command: `test -s plan.md` });\n\n await f.agent(\"implementer\", {\n task: \"Read plan.md. Implement it on branch flow/fix. \" +\n \"Write the PR description to summary.md.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s summary.md` });\n\n // The tests run outside the agent.\n // The agent cannot lie about the exit code.\n await f.run(\"git checkout flow/fix && npm test\");\n\n await f.agent(\"reviewer\", {\n task: \"Review the diff against main. \" +\n \"Write review.passed only if ready for a PR.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s review.passed` });\n\n // Deterministic step, not an agent decision.\n const [owner, repo] = input.repo.split(\"/\");\n await f.github.createPullRequest({\n owner, repo,\n title: input.ticket,\n head: \"flow/fix\",\n base: \"main\",\n body: await f.run(\"cat summary.md\"),\n });\n f.done(\"success\");\n },\n);" }, { "id": "pr-review", @@ -13,7 +13,7 @@ export const flowExamples = [ "title": "Give every PR a second opinion.", "description": "Review a diff for security, correctness, and performance in parallel. Then reconcile the findings.", "filename": "pr-review.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\nexport default flow(\n \"pr-review\",\n { budget: \"$5/run\" },\n async (f) => {\n const diff = await f.run(\"git diff main...HEAD\");\n const lenses = [\"security\", \"correctness\", \"performance\"];\n\n await Promise.all(lenses.map((lens) =>\n f.agent(`${lens}-reviewer`, {\n task: `Review this diff for ${lens}: ${diff}. ` +\n `Write findings to review/${lens}.md.`,\n workspace: \"review/: readwrite\",\n }).gate((r) =>\n r.artifacts.includes(`review/${lens}.md`)\n )\n ));\n\n // Resolve disagreements between reviewers.\n await f.agent(\"reconciler\", {\n task: \"Read the three reviews in review/. \" +\n \"Resolve disagreements, flag unresolved issues, \" +\n \"and write review/consensus.md.\",\n workspace: \"review/: readwrite\",\n }).gate((r) =>\n r.artifacts.includes(\"review/consensus.md\")\n );\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\nexport default flow(\n \"pr-review\",\n { budget: \"$5/run\" },\n async (f) => {\n const diff = await f.run(\"git diff main...HEAD\");\n const lenses = [\"security\", \"correctness\", \"performance\"];\n\n await Promise.all(lenses.map((lens) =>\n f.agent(`${lens}-reviewer`, {\n task: `Review this diff for ${lens}: ${diff}. ` +\n `Write findings to review/${lens}.md.`,\n workspace: \"review/: readwrite\",\n }).gate({ type: \"subprocess_gate\", command: `test -s review/${lens}.md` })\n ));\n\n // Resolve disagreements between reviewers.\n await f.agent(\"reconciler\", {\n task: \"Read the three reviews in review/. \" +\n \"Resolve disagreements, flag unresolved issues, \" +\n \"and write review/consensus.md.\",\n workspace: \"review/: readwrite\",\n }).gate({ type: \"subprocess_gate\", command: `test -s review/consensus.md` });\n f.done(\"success\");\n },\n);" }, { "id": "support-triage", @@ -21,7 +21,7 @@ export const flowExamples = [ "title": "Draft the reply. Let a human decide.", "description": "Draft a response to a support thread, get approval, and reply in Slack.", "filename": "support-triage.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n message: string;\n channel: string;\n threadTs: string;\n approver: string;\n};\n\nexport default flow(\n \"support-triage\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"triage\", {\n task: `Classify this request: ${input.message}. ` +\n \"Write the category and urgency to triage.md.\",\n }).gate((r) => r.artifacts.includes(\"triage.md\"));\n\n await f.agent(\"writer\", {\n task: `Read triage.md and draft a reply to: ` +\n `${input.message}. Write only the reply to reply.md.`,\n }).gate((r) => r.artifacts.includes(\"reply.md\"));\n\n const reply = await f.run(\"cat reply.md\");\n const approved = await f.human(\n `Send this reply?\\n\\n${reply}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"canceled\");\n\n // Send only after a human approves.\n await f.slack.reply(input.channel, input.threadTs, reply);\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n message: string;\n channel: string;\n threadTs: string;\n approver: string;\n};\n\nexport default flow(\n \"support-triage\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"triage\", {\n task: `Classify this request: ${input.message}. ` +\n \"Write the category and urgency to triage.md.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s triage.md` });\n\n await f.agent(\"writer\", {\n task: `Read triage.md and draft a reply to: ` +\n `${input.message}. Write only the reply to reply.md.`,\n }).gate({ type: \"subprocess_gate\", command: `test -s reply.md` });\n\n const reply = await f.run(\"cat reply.md\");\n // f.human is declared but not yet executed by the SDK (flows#400) \u2014 this\n // shows the intended approval gate, not a runnable one, until it lands.\n const approved = await f.human(\n `Send this reply?\\n\\n${reply}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n\n // Send only after a human approves.\n await f.slack.reply(input.channel, input.threadTs, reply);\n f.done(\"success\");\n },\n);" }, { "id": "content-pipeline", @@ -29,7 +29,7 @@ export const flowExamples = [ "title": "Put a fact-check between draft and publish.", "description": "Research, draft, and check a post before a human approves sharing it in Slack.", "filename": "content-pipeline.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n topic: string;\n channel: string;\n approver: string;\n};\n\nexport default flow(\n \"content-pipeline\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"researcher\", {\n task: `Research ${input.topic}. Cite your sources. ` +\n \"Write research.md.\",\n }).gate((r) => r.artifacts.includes(\"research.md\"));\n\n await f.agent(\"writer\", {\n task: \"Use research.md to write post.md. \" +\n \"Keep the source links with each claim.\",\n }).gate((r) => r.artifacts.includes(\"post.md\"));\n\n await f.agent(\"fact-checker\", {\n task: \"Check post.md against its sources. \" +\n \"Write checked.passed only if all claims hold up.\",\n }).gate((r) => r.artifacts.includes(\"checked.passed\"));\n\n const post = await f.run(\"cat post.md\");\n const approved = await f.human(\n `Publish this post?\\n\\n${post}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"canceled\");\n\n await f.slack.post(input.channel, post);\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n topic: string;\n channel: string;\n approver: string;\n};\n\nexport default flow(\n \"content-pipeline\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"researcher\", {\n task: `Research ${input.topic}. Cite your sources. ` +\n \"Write research.md.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s research.md` });\n\n await f.agent(\"writer\", {\n task: \"Use research.md to write post.md. \" +\n \"Keep the source links with each claim.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s post.md` });\n\n await f.agent(\"fact-checker\", {\n task: \"Check post.md against its sources. \" +\n \"Write checked.passed only if all claims hold up.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s checked.passed` });\n\n const post = await f.run(\"cat post.md\");\n // f.human is declared but not yet executed by the SDK (flows#400) \u2014 this\n // shows the intended approval gate, not a runnable one, until it lands.\n const approved = await f.human(\n `Publish this post?\\n\\n${post}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n\n await f.slack.post(input.channel, post);\n f.done(\"success\");\n },\n);" }, { "id": "repo-migration", @@ -37,7 +37,7 @@ export const flowExamples = [ "title": "Make the same change across repositories.", "description": "Apply a migration in each local checkout, test it, and open a PR.", "filename": "repo-migration.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n repos: { path: string; slug: string }[];\n instructions: string;\n};\n\nexport default flow(\n \"repo-migration\",\n { budget: \"$5/run\" },\n async (f, input) => {\n for (const repo of input.repos) {\n // Quote the local checkout path for shell commands.\n const dir = \"'\" + repo.path.replaceAll(\"'\", \"'\\\"'\\\"'\") + \"'\";\n\n await f.agent(`migrate-${repo.slug}`, {\n cwd: repo.path,\n task: `${input.instructions} ` +\n \"Use branch flow/migration and commit the change. \" +\n \"Write migration.md with a summary.\",\n }).gate((r) => r.artifacts.includes(\"migration.md\"));\n\n await f.run(`cd ${dir} && npm test`);\n\n await f.agent(`review-${repo.slug}`, {\n cwd: repo.path,\n task: \"Review this migration against main. \" +\n \"Write review.passed only if it is ready.\",\n }).gate((r) => r.artifacts.includes(\"review.passed\"));\n\n // Each checkout opens its own pull request.\n await f.run(\n `cd ${dir} && gh pr create --base main ` +\n '--head flow/migration --title \"Apply migration\" ' +\n '--body-file migration.md'\n );\n }\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n repos: { path: string; slug: string }[];\n instructions: string;\n};\n\nexport default flow(\n \"repo-migration\",\n { budget: \"$5/run\" },\n async (f, input) => {\n for (const repo of input.repos) {\n // Quote the local checkout path for shell commands.\n const dir = \"'\" + repo.path.replaceAll(\"'\", \"'\\\"'\\\"'\") + \"'\";\n\n await f.agent(`migrate-${repo.slug}`, {\n cwd: repo.path,\n task: `${input.instructions} ` +\n \"Use branch flow/migration and commit the change. \" +\n \"Write migration.md with a summary.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s migration.md` });\n\n await f.run(`cd ${dir} && npm test`);\n\n await f.agent(`review-${repo.slug}`, {\n cwd: repo.path,\n task: \"Review this migration against main. \" +\n \"Write review.passed only if it is ready.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s review.passed` });\n\n // Each checkout opens its own pull request.\n await f.run(\n `cd ${dir} && gh pr create --base main ` +\n '--head flow/migration --title \"Apply migration\" ' +\n '--body-file migration.md'\n );\n }\n f.done(\"success\");\n },\n);" }, { "id": "voicemail-follow-up", @@ -45,7 +45,7 @@ export const flowExamples = [ "title": "Turn a voicemail into a callback brief.", "description": "Use a transcript to prepare the callback, then send the approved brief to a teammate.", "filename": "voicemail-follow-up.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n transcript: string;\n approver: string;\n callbackOwner: string;\n};\n\nexport default flow(\n \"voicemail-follow-up\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"triage\", {\n task: `Read this voicemail: ${input.transcript}. ` +\n \"Identify urgency and the caller's request. \" +\n \"Write triage.md.\",\n }).gate((r) => r.artifacts.includes(\"triage.md\"));\n\n await f.agent(\"callback-writer\", {\n task: \"Read triage.md. Prepare a callback brief \" +\n \"with the questions we need to answer. \" +\n \"Write callback.md.\",\n }).gate((r) => r.artifacts.includes(\"callback.md\"));\n\n const brief = await f.run(\"cat callback.md\");\n const approved = await f.human(\n `Ready for a callback?\\n\\n${brief}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"canceled\");\n\n await f.slack.dm(input.callbackOwner, brief);\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n transcript: string;\n approver: string;\n callbackOwner: string;\n};\n\nexport default flow(\n \"voicemail-follow-up\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"triage\", {\n task: `Read this voicemail: ${input.transcript}. ` +\n \"Identify urgency and the caller's request. \" +\n \"Write triage.md.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s triage.md` });\n\n await f.agent(\"callback-writer\", {\n task: \"Read triage.md. Prepare a callback brief \" +\n \"with the questions we need to answer. \" +\n \"Write callback.md.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s callback.md` });\n\n const brief = await f.run(\"cat callback.md\");\n // f.human is declared but not yet executed by the SDK (flows#400) \u2014 this\n // shows the intended approval gate, not a runnable one, until it lands.\n const approved = await f.human(\n `Ready for a callback?\\n\\n${brief}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n\n await f.slack.dm(input.callbackOwner, brief);\n f.done(\"success\");\n },\n);" }, { "id": "research-report", @@ -53,7 +53,7 @@ export const flowExamples = [ "title": "Bring independent research into one report.", "description": "Give researchers different angles, combine their findings, and ask another agent to check the citations.", "filename": "research-report.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n question: string;\n};\n\nexport default flow(\n \"research-report\",\n { budget: \"$5/run\" },\n async (f, input) => {\n const angles = [\"technical\", \"commercial\", \"risks\"];\n\n await Promise.all(angles.map((angle) =>\n f.agent(`research-${angle}`, {\n task: `Research ${input.question} from a ${angle} ` +\n `angle. Cite sources in research/${angle}.md.`,\n workspace: \"research/: readwrite\",\n }).gate((r) =>\n r.artifacts.includes(`research/${angle}.md`)\n )\n ));\n\n await f.agent(\"editor\", {\n task: \"Read research/*.md. Combine the findings, \" +\n \"keep disagreements visible, and cite sources. \" +\n \"Write report.md.\",\n }).gate((r) => r.artifacts.includes(\"report.md\"));\n\n await f.agent(\"citation-reviewer\", {\n task: \"Open every source in report.md and check \" +\n \"the claims it supports. Write citations.passed \" +\n \"only when every citation checks out.\",\n }).gate((r) => r.artifacts.includes(\"citations.passed\"));\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n question: string;\n};\n\nexport default flow(\n \"research-report\",\n { budget: \"$5/run\" },\n async (f, input) => {\n const angles = [\"technical\", \"commercial\", \"risks\"];\n\n await Promise.all(angles.map((angle) =>\n f.agent(`research-${angle}`, {\n task: `Research ${input.question} from a ${angle} ` +\n `angle. Cite sources in research/${angle}.md.`,\n workspace: \"research/: readwrite\",\n }).gate({ type: \"subprocess_gate\", command: `test -s research/${angle}.md` })\n ));\n\n await f.agent(\"editor\", {\n task: \"Read research/*.md. Combine the findings, \" +\n \"keep disagreements visible, and cite sources. \" +\n \"Write report.md.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s report.md` });\n\n await f.agent(\"citation-reviewer\", {\n task: \"Open every source in report.md and check \" +\n \"the claims it supports. Write citations.passed \" +\n \"only when every citation checks out.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s citations.passed` });\n f.done(\"success\");\n },\n);" }, { "id": "redacted-summary", @@ -61,7 +61,7 @@ export const flowExamples = [ "title": "Choose what the summarizer gets to see.", "description": "Select the fields needed for the task before putting a record into the agent prompt.", "filename": "redacted-summary.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n record: { category: string; status: string; email: string };\n approver: string;\n};\n\nexport default flow(\n \"redacted-summary\",\n { budget: \"$5/run\" },\n async (f, input) => {\n // Select fields in code before building the prompt.\n // The email address is excluded from this input.\n const selected = {\n category: input.record.category,\n status: input.record.status,\n };\n\n await f.agent(\"summarizer\", {\n task: `Summarize this record: ` +\n `${JSON.stringify(selected)}. Write summary.md.`,\n }).gate((r) => r.artifacts.includes(\"summary.md\"));\n\n const summary = await f.run(\"cat summary.md\");\n const approved = await f.human(\n `Review this summary before sharing:\\n\\n${summary}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"canceled\");\n\n f.done(\"success\");\n },\n);" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n record: { category: string; status: string; email: string };\n approver: string;\n};\n\nexport default flow(\n \"redacted-summary\",\n { budget: \"$5/run\" },\n async (f, input) => {\n // Select fields in code before building the prompt.\n // The email address is excluded from this input.\n const selected = {\n category: input.record.category,\n status: input.record.status,\n };\n\n await f.agent(\"summarizer\", {\n task: `Summarize this record: ` +\n `${JSON.stringify(selected)}. Write summary.md.`,\n }).gate({ type: \"subprocess_gate\", command: `test -s summary.md` });\n\n const summary = await f.run(\"cat summary.md\");\n // f.human is declared but not yet executed by the SDK (flows#400) \u2014 this\n // shows the intended approval gate, not a runnable one, until it lands.\n const approved = await f.human(\n `Review this summary before sharing:\\n\\n${summary}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n\n f.done(\"success\");\n },\n);" }, { "id": "ci-repair", @@ -77,7 +77,7 @@ export const flowExamples = [ "title": "Send each issue to the right specialist.", "description": "Route bugs to an implementer and documentation requests to a writer. Ask a human when the category is uncertain.", "filename": "issue-routing.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = { issue: string; approver: string };\n\nexport default flow(\"issue-routing\", async (f, input) => {\n const classification = await f.agent(\"router\", {\n cli: \"claude\",\n task: `Classify this issue: ${input.issue}. ` +\n \"Return only bugfix, docs, or uncertain as your summary. \" +\n \"Choose uncertain if the request is ambiguous.\",\n });\n let route = classification.summary.trim().toLowerCase();\n\n // Unknown output never silently selects a specialist.\n if (route !== \"bugfix\" && route !== \"docs\") {\n const approved = await f.human(\n `Needs manual triage: ${input.issue}\\n` +\n \"Route this to the bugfix specialist?\",\n { to: input.approver },\n );\n if (!approved) return f.done(\"canceled\");\n route = \"bugfix\";\n }\n\n if (route === \"bugfix\") {\n await f.agent(\"implementer\", {\n cli: \"codex\",\n task: `Fix this issue and add regression coverage: ${input.issue}`,\n });\n await f.run(\"npm test\");\n } else {\n await f.agent(\"docs-writer\", {\n cli: \"claude\",\n task: `Update the documentation for: ${input.issue}. ` +\n \"Check examples against the implementation.\",\n });\n }\n f.done(\"success\");\n});" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = { issue: string; approver: string };\n\nexport default flow(\"issue-routing\", async (f, input) => {\n const classification = await f.agent(\"router\", {\n cli: \"claude\",\n task: `Classify this issue: ${input.issue}. ` +\n \"Return only bugfix, docs, or uncertain as your summary. \" +\n \"Choose uncertain if the request is ambiguous.\",\n });\n let route = classification.summary.trim().toLowerCase();\n\n // Unknown output never silently selects a specialist.\n if (route !== \"bugfix\" && route !== \"docs\") {\n // f.human is declared but not yet executed by the SDK (flows#400) \u2014 this\n // shows the intended approval gate, not a runnable one, until it lands.\n const approved = await f.human(\n `Needs manual triage: ${input.issue}\\n` +\n \"Route this to the bugfix specialist?\",\n { to: input.approver },\n );\n if (!approved) return f.done(\"declined\");\n route = \"bugfix\";\n }\n\n if (route === \"bugfix\") {\n await f.agent(\"implementer\", {\n cli: \"codex\",\n task: `Fix this issue and add regression coverage: ${input.issue}`,\n });\n await f.run(\"npm test\");\n } else {\n await f.agent(\"docs-writer\", {\n cli: \"claude\",\n task: `Update the documentation for: ${input.issue}. ` +\n \"Check examples against the implementation.\",\n });\n }\n f.done(\"success\");\n});" }, { "id": "competing-implementations", @@ -93,6 +93,6 @@ export const flowExamples = [ "title": "Turn competing theories into an evidence-backed diagnosis.", "description": "Investigate logs, recent changes, and dependencies in parallel. Challenge each theory and keep unresolved questions visible in the report.", "filename": "root-cause-investigation.flow.ts", - "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = { incident: string };\n\n// Supply sanitized logs and a local reproduction environment.\nexport default flow(\"root-cause-investigation\", async (f, input) => {\n const angles = [\"logs\", \"recent-changes\", \"dependencies\"];\n await f.run(\"mkdir -p investigation\");\n\n const hypotheses = await Promise.all(angles.map((angle) =>\n f.agent(`investigate-${angle}`, {\n task: `Investigate this incident through ${angle}: ` +\n `${input.incident}. Use the local evidence. ` +\n \"Do not change application code. Return a hypothesis, \" +\n \"supporting evidence, and a way to disprove it.\",\n })\n ));\n\n await Promise.all(hypotheses.map((hypothesis, i) =>\n f.agent(`verify-${angles[i]}`, {\n cli: \"codex\",\n task: `Try to disprove this theory: ${hypothesis.summary}. ` +\n \"Run focused checks in the local reproduction environment. \" +\n \"Do not change application code. Record commands, actual \" +\n \"results, and whether the theory is supported, rejected, \" +\n `or unresolved in investigation/${angles[i]}.md.`,\n }).gate((r) =>\n r.artifacts.includes(`investigation/${angles[i]}.md`)\n )\n ));\n\n await f.agent(\"incident-editor\", {\n cli: \"claude\",\n task: \"Read investigation/*.md. Write diagnosis.md with \" +\n \"the strongest supported explanation and its evidence. \" +\n \"Include rejected theories and unresolved questions. \" +\n \"If no cause is supported, say so and propose the next check.\",\n }).gate((r) => r.artifacts.includes(\"diagnosis.md\"));\n // Success means the investigation report is ready.\n f.done(\"success\");\n});" + "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = { incident: string };\n\n// Supply sanitized logs and a local reproduction environment.\nexport default flow(\"root-cause-investigation\", async (f, input) => {\n const angles = [\"logs\", \"recent-changes\", \"dependencies\"];\n await f.run(\"mkdir -p investigation\");\n\n const hypotheses = await Promise.all(angles.map((angle) =>\n f.agent(`investigate-${angle}`, {\n task: `Investigate this incident through ${angle}: ` +\n `${input.incident}. Use the local evidence. ` +\n \"Do not change application code. Return a hypothesis, \" +\n \"supporting evidence, and a way to disprove it.\",\n })\n ));\n\n await Promise.all(hypotheses.map((hypothesis, i) =>\n f.agent(`verify-${angles[i]}`, {\n cli: \"codex\",\n task: `Try to disprove this theory: ${hypothesis.summary}. ` +\n \"Run focused checks in the local reproduction environment. \" +\n \"Do not change application code. Record commands, actual \" +\n \"results, and whether the theory is supported, rejected, \" +\n `or unresolved in investigation/${angles[i]}.md.`,\n }).gate({ type: \"subprocess_gate\", command: `test -s investigation/${angles[i]}.md` })\n ));\n\n await f.agent(\"incident-editor\", {\n cli: \"claude\",\n task: \"Read investigation/*.md. Write diagnosis.md with \" +\n \"the strongest supported explanation and its evidence. \" +\n \"Include rejected theories and unresolved questions. \" +\n \"If no cause is supported, say so and propose the next check.\",\n }).gate({ type: \"subprocess_gate\", command: `test -s diagnosis.md` });\n // Success means the investigation report is ready.\n f.done(\"success\");\n});" } ]; diff --git a/web/content/docs/file/review-bot.mdx b/web/content/docs/file/review-bot.mdx index 266c96d..cbeeb96 100644 --- a/web/content/docs/file/review-bot.mdx +++ b/web/content/docs/file/review-bot.mdx @@ -6,7 +6,7 @@ description: 'End-to-end: connect GitHub with one command, add the rest of your A PR review bot is the shape Relayfile fits best: several agents, several providers, one shared state. This guide runs the whole flow end to end — from an empty machine to a bot whose orchestrator and specialists all read the same PR, coordinate through files, and post the finished review back to GitHub without any of them holding a provider token. - Every command, path, and payload below was run against a live workspace on `relayfile` **0.10.53**. The review in step 7 was posted by writing the file this guide tells you to write — [`agentrelay.com#59` review 5112118049](https://github.com/AgentWorkforce/agentrelay.com/pull/59#pullrequestreview-5112118049). Where a path or flag is version-dependent, it says so. + Every command, path, and payload below was run against a live workspace on `relayfile` **0.10.53**. The review in step 7 was posted by writing the file this guide tells you to write — `agentrelay.com#59` review 5112118049. Where a path or flag is version-dependent, it says so. ## What you're building @@ -62,7 +62,7 @@ relayfile setup \ `--no-open` prints the login and connect URLs instead of launching a browser — always pass it when an agent (or CI) is driving the command, since a headless browser launch burns the OAuth state. -The local mirror it leaves behind is for *you*: somewhere to `ls`, `cat` and `grep` the tree and see exactly what your agents will see. It is not how a deployed bot reads the workspace — cloud sandboxes mount per run in [step 5](#5-give-every-sandbox-the-same-workspace), and short-lived functions skip the mount entirely. `--local-dir` is required here only because `--skip-mount` still prompts for a directory ([relayfile#461](https://github.com/AgentWorkforce/relayfile/pull/461)). +The local mirror it leaves behind is for *you*: somewhere to `ls`, `cat` and `grep` the tree and see exactly what your agents will see. It is not how a deployed bot reads the workspace — cloud sandboxes mount per run in [step 5](#5-give-every-sandbox-the-same-workspace), and short-lived functions skip the mount entirely. `--local-dir` is required here only because `--skip-mount` still prompts for a directory (relayfile#461). Both URLs are short-lived. A Cloud device code expires in minutes and the Nango connect URL has its own TTL, so complete them while the command is still waiting. If it exits first, re-run the same line — a re-run reuses the workspace and only opens a new connect flow when the provider isn't connected yet. diff --git a/web/content/docs/relayflows/build.mdx b/web/content/docs/relayflows/build.mdx index 1356799..fc7cfdd 100644 --- a/web/content/docs/relayflows/build.mdx +++ b/web/content/docs/relayflows/build.mdx @@ -21,7 +21,7 @@ The rest of this page is what the skill encodes — worth reading so you can tel ## Two ways to author the same thing -**YAML** describes a fixed set of steps and their dependencies as data, so `flows check` or a CI gate can read and validate it without running anything. **TypeScript** calls the same primitives imperatively, as ordinary code. Both compile down to the same journal. +**TypeScript** calls the primitives imperatively, as ordinary code. **YAML** describes the same fixed set of steps and their dependencies as data, so `flows check` or a CI gate can read and validate it without running anything. Both compile down to the same journal; every sample on these pages is shown in TypeScript, with the YAML form behind the language switch. ```typescript TypeScript @@ -68,69 +68,126 @@ steps: Reach for YAML when you want the whole flow readable at a glance and checkable in CI. Reach for TypeScript when a step's next move depends on what a previous one returned: an `f.human` approval, ordinary `if`/`for` logic, an `f.dispatch` to a child flow. -Both `edit` steps above name their own `cli` and `model` directly, the same way in either language ([flows#310](https://github.com/AgentWorkforce/flows/issues/310)). Neither is required in TypeScript: omit them and the step falls back to the flow's `cli`, then the nearest `flows.json`'s project-wide default, the same resolution [Introduction](/docs/relayflows/introduction#a-whole-flow) covers. +Both `edit` steps above name their own `cli` and `model` directly, the same way in either language (flows#310). Neither is required in TypeScript: omit them and the step falls back to the flow's `cli`, then the nearest `flows.json`'s project-wide default, the same resolution [Introduction](/docs/relayflows/introduction#a-whole-flow) covers. - `recoveryMode`, `permissions`, `surfaces`, and `budget`, the richer - fields covered further down, are YAML/JSON fields only today. - TypeScript's `Ctx.agent` doesn't have them. If a step needs them, - author it in YAML and either run it directly or have a TypeScript - flow reach it with `f.dispatch`. + `recoveryMode`, `permissions`, and `surfaces`, the richer step fields + covered further down, are YAML/JSON fields only today. TypeScript's + `f.agent` takes `{ task, workspace?, cli?, model? }`, but under + `--local-agent` any `workspace` value is refused — not just an annotated + one. Verified: `workspace: 'repo: readonly'` and a bare `workspace: 'repo'` + both fail identically with `unsupported_workspace_permission: The local + agent worker accepts stream-only steps. Remove workspace or attach a + worker that holds its revision pins.` The local worker holds no revision + pins at all — omit `workspace` entirely for a step you run with + `--local-agent`; it's a real field only against a worker that supports it + (Cloud's). `budget` is available in both: the TypeScript form is the + flow header, `flow('name', { budget: '$5/run' }, async (f) => …)`. ## The context a flow body gets ```ts interface Ctx { - run(command: string): Step; + run(command: string, options?: { timeout?: string | number }): Step; // lease: default 30s, max 15m llm(strings: TemplateStringsArray, ...values: unknown[]): Step; + llm(prompt: string, options: { output: JsonSchema; cli?: string; model?: string }): Step; agent(name: string, options: { task: string; workspace?: string; cli?: string; model?: string }): Step<{ summary: string; artifacts: string[] }>; human(question: string, options: { to: string }): Promise; dispatch(flow: string, input: unknown): Promise; - done(reason: 'success' | 'step_failed' | 'canceled' | 'budget_exceeded'): void; + done(reason: 'success' | 'step_failed' | 'needs_human' | 'declined'): void; + slack: SlackHelper; github: GithubHelper; /* …every generated helper */ + memory: MemoryHelper; + mcp: Record Step>>; } ``` -This is the whole kernel-level vocabulary a step body speaks: `run`, `llm`, `agent` for work, `human`, `dispatch`, `done` for control. `human` parks the run on a durable await instead of a blocking call — it's a wait the journal can survive a restart across. `dispatch` hands work to a named child flow and returns its typed result. +This is the whole kernel-level vocabulary a step body speaks: `run`, `llm`, `agent` for work, `human`, `dispatch`, `done` for control. + + + `human` and `dispatch` are declared and typecheck, but neither runs in + 2.0.16 — verified: a flow that reaches `f.human` fails with + `unsupported_verb: the initial authored executor does not lower f.human`, + and the same for `f.dispatch`. Both are being implemented on flows' + `feat/f-human` branch and land in the release after 2.0.17. Once wired, + `human` is meant to park the run on a durable await instead of a blocking + call — a wait the journal can survive a restart across — and `dispatch` is + meant to hand work to a named child flow and return its typed result. + Until then, the shipped human gate is `f.done('needs_human')`: the run + parks (exit `3`), a person acts, and `flows resume ` continues it. + + +`done` takes one of four authored verdicts. `success` completes the run; `step_failed` says the flow's own checks did not pass (the adversarial review found problems, the tests went red) and exits `1`; `needs_human` parks the run (exit `3`); `declined` records a deliberate decision not to act on the input — a ticket that turned out not to be work — and exits `0` with a `DECLINED` diagnostic. `canceled` and `budget_exceeded` are in the `FlowCompletionReason` type but refused at runtime with `unsupported_completion`: they are kernel facts, recorded when the kernel cancels a run or exhausts its budget, not verdicts a body can declare. + +`f.run` returns the command's output; a step's `.summary` on `f.agent` is the CLI's final text. `artifacts` is always empty in 2.0.16 — populating it from what the agent actually wrote lands in the next release (flows#449). ## Verification Every step's exit code is checked automatically. On top of that: +- **`exit_code`** — a deterministic step's process exit code (always checked; declaring it is only meaningful on `deterministic` steps). - **`output_contains`** — an opt-in string match against the step's output. Fast to write, good for a smoke check. -- **`json_schema`** on an `llm` step — the model's reply must parse and validate against the schema before anything downstream sees it. +- **`json_schema`** on an `llm` step — the model's reply must parse and validate against the schema before anything downstream sees it. A schema of `{}` or `true` accepts anything and is flagged `vacuous_gate`. +- **Named gates** — `references_input`, `regex_match`, `word_count_bounds`, and `subprocess_gate` (run a shell command against the output; exit `0` passes). Each lowers to a deterministic gate step in the same journal, so a resume replays the recorded verdict instead of re-judging. + +In TypeScript the same named gates attach postfix: `f.agent('review', {…}).gate({ type: 'subprocess_gate', command: 'test -s review.md' })`. A callback gate, `.gate((r) => r.artifacts.includes('review.md'))`, is refused in 2.0.16 (`unsupported_gate`: a closure can't be journaled); the next release runs it after the step and journals the verdict (flows#449). A step that fails its check is recorded as `verification_failed`, with exactly which check failed. The agent insisting it went fine doesn't override that. ## Permissions and recovery -An `agent` step declares what it's allowed to touch — `fileGlobs`, `accessPreset` — and what happens if it crashes mid-edit: - -```yaml -- id: edit - type: agent - dependsOn: [greet] - instruction: 'Produce the hello artifact through the deterministic test stub.' - recoveryMode: reset - maxIterations: 2 - surfaces: - workspace: - - surface: repo - streams: - - stream: agent-notes - permissions: - fileGlobs: ['artifacts/**'] - accessPreset: readwrite - verification: - type: output_contains - value: agent-ok +An `agent` step declares what it's allowed to touch — `fileGlobs`, `accessPreset` — and what happens if it crashes mid-edit. These are YAML step fields with no TypeScript equivalent today; a TypeScript body leaves recovery to the kernel's default (`reset`), and will reach a YAML step through `f.dispatch` once that verb ships (see the Note above — it doesn't execute yet in 2.0.16). Author the step in YAML now if you need `permissions`/`recoveryMode` before then: + + +```typescript TypeScript +import { flow } from '@relayflows/surface'; + +export default flow('hello-agent', async (f) => { + await f.run('printf hello'); + + // No recoveryMode/permissions here: a crashed attempt resets to the + // pinned revision (the default), and the workspace is the daemon's cwd. + await f.agent('edit', { + task: 'Produce the hello artifact and print agent-ok when it exists.', + cli: 'claude', + }).gate({ type: 'regex_match', pattern: 'agent-ok' }); + + f.done('success'); +}); ``` +```yaml YAML +version: '0.1.0' +name: hello-agent +steps: + - id: greet + type: deterministic + command: 'printf hello' + + - id: edit + type: agent + dependsOn: [greet] + instruction: 'Produce the hello artifact through the deterministic test stub.' + recoveryMode: reset + maxIterations: 2 + surfaces: + workspace: + - surface: repo + streams: + - stream: agent-notes + permissions: + fileGlobs: ['artifacts/**'] + accessPreset: readwrite + verification: + type: output_contains + value: agent-ok +``` + - **`reset`** (the default) — the next attempt starts fresh from the pinned workspace revision, with no half-finished edit left behind. - **`inspect`** — the next attempt starts inside the dirty workspace, with the failed attempt's trajectory tail injected as context, and decides whether to continue or redo. - **`manual`** — parks the run as `needs_human` with a diff of the pinned revision against whatever's actually there. -`maxIterations` caps how many attempts a step gets before one of those three outcomes has to happen. A run-level `budget` (`maxTokensIn`, `maxTokensOut`, `maxDollars`) caps the whole flow the same way, declared once and enforced by the kernel instead of tracked by hand. +`maxIterations` caps how many attempts a step gets before one of those three outcomes has to happen. A run-level `budget` caps the whole flow the same way, declared once and enforced by the kernel instead of tracked by hand. Write it as `"$5/run"` or `"$20/day"` (dollars, priced from a frozen per-model table), or as `{ tokens?, dollars?, wallclock? }` — `{ dollars: 5, wallclock: "45m" }` bounds both. Tokens and wallclock apply to every step; dollars apply to steps whose model has a frozen price. A Codex step, which picks its own model, is reported as `budget_unmetered` under a dollar budget and runs; it counts toward tokens and wallclock but cannot cross the dollar limit. Crossing a limit lets the running step finish and refuses the next one with `budget_exceeded`. ## Next diff --git a/web/content/docs/relayflows/cli.mdx b/web/content/docs/relayflows/cli.mdx index 9d9a90e..0115d27 100644 --- a/web/content/docs/relayflows/cli.mdx +++ b/web/content/docs/relayflows/cli.mdx @@ -1,50 +1,81 @@ --- title: 'CLI' -description: 'Check a spec, run it, resume it, watch it — the flows command surface.' +description: 'Check a flow, run it locally or in Cloud, resume it, deploy it as a listener — the flows command surface.' --- +The full surface of `relayflows` 2.0.16, as `flows --help` prints it: + ```text -flows check [--json] -flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir ] -flows run --cloud [--json] [--wait] +flows check [--watch] [--json] +flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir ] [--local-agent] [--reuse-from ] flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir ] [--local-agent] --input -flows resume [--json] [--no-spawn] [--no-observer-link] [--data-dir ] +flows resume [--allow-human-influenced] [--json] [--no-spawn] [--no-observer-link] [--data-dir ] [--local-agent] +flows replay [--allow-human-influenced] [--json] [--data-dir ] [--at ] + +flows run --cloud [--json] [--wait] [--sync-code] +flows run --cloud [--json] [--wait] [--sync-code] --input +flows sync [--json] [--dir ] +flows deploy --repo --on [:key=value,...] [--on ...] --approver [--agents claude[,codex]] [--name ] [--draft] [--json] +flows deployments [--json] +flows undeploy [--json] + +flows build [--out ] +flows build --verify +flows deploy @sha256: --to +flows run @sha256: [--bucket ] [--data-dir ] [--json] +flows add + +flows serve-webhook --data-dir --port

[--allow [,]] flows tick start --schedule-id --interval-ms [--epoch-ms ] [--max-catch-up ] [--poll-interval-ms ] [--data-dir

] flows hn-monitor start [--data-dir ] [--poll-interval-ms ] flows observer [--data-dir ] ``` +Every verb refuses an unknown or duplicated flag with `REFUSED [invalid_invocation]` and the usage above, before anything runs. + ## Check ```bash flows check my-flow.flow.yaml +flows check my-flow.flow.ts +flows check --watch my-flow.flow.ts # re-check on every save ``` -Validates the spec: schema, step graph, verification blocks, and whether every declared CLI actually exists and is authenticated. Nothing runs and nothing is spawned. This is the same preflight a run does before its first step, exposed on its own so a broken spec fails in CI instead of at minute 27 of a real run. +Validates the flow: schema, step graph, verification blocks, helper and trigger declarations, and whether every declared CLI actually exists and is authenticated (a real `claude -p --model …` / `codex exec …` probe, not a version check — that probe is a real subprocess, the one thing `check` does spawn). No flow runs, no daemon starts, no worker attaches — `check` never opens the daemon socket, and refuses `--data-dir` for that reason. It prints one `GATE` line per verification and one `RESOLVED` line per `llm`/`agent` step naming the CLI and model and where they came from (step, named agent, flow, or `flows.json`). + +An authored `.flow.ts` is checked too: its declared surface — agents, helpers, `tools`, triggers, the `use:` graph — is preflighted; its control flow is not, so a check pass says the declarations are sound, not that the body is. The same preflight runs again at the top of `flows run`. -`check` takes a declarative `flow.yaml` or `spec.json` — it validates data, so an authored `.flow.ts` file isn't a valid argument here. A TypeScript flow gets the same preflight automatically, run inline at the top of `flows run`. +If a `flows.json` is in scope and declares `models`, every model a step names must be on that list (`model_unknown` otherwise); without a `models` list the model is only probed for access. ## Run ```bash flows run my-flow.flow.ts --local-agent --input '{}' # TypeScript flow, local CLI flows run workflow.yaml # YAML/spec.json flow -flows run --cloud --wait workflow.yaml # dispatch to the hosted engine, block for the result +flows run --cloud --wait workflow.yaml # hosted engine, block for the result ``` -`--local-agent` runs each declared CLI (`claude`, `codex`, …) using whatever login it already has on your machine — no separate credentials to configure. It's what makes an authored TypeScript flow's `agent` steps runnable at all. +An authored `.flow.ts` requires `--input`: an existing JSON file, otherwise inline JSON (up to 1 MiB), handed to the body as its second argument. `flows run --cloud --input ...` used to fail immediately with `{"code":"http_error","message":"Cloud request failed with HTTP 400."}` against any authored flow, on any released `2.0.16` or earlier CLI — a badly-reported Surface version mismatch between Cloud and the CLI (flows#461). Fixed in `2.0.17`: verified for real, it now submits successfully and returns a run ID. Update if you're still on `2.0.16` or earlier and see that error. + +`--local-agent` attaches the SDK's agent and LLM workers to the daemon, running each declared CLI (`claude`, `codex`, or a custom wrapper) with whatever login it already has on your machine. Without it, the first `llm` or `agent` step parks the run (exit `3`) until a worker is attached. Only stream-only agent steps run this way; a step that declares a `workspace` surface needs a worker holding its revision pins. + +By default `run` spawns a daemon for the data directory if one isn't already up. `--no-spawn` (or `FLOWS_NO_SPAWN=1` for a whole environment) asserts a daemon is already present instead — the lever CI uses to fail loudly on a missing daemon rather than silently start one. `--data-dir ` points at the journal's storage directory; it defaults to `.relayflowd` in the current directory. `--reuse-from ` (YAML only) reuses completed steps of an earlier run instead of re-executing them; the report says how many were reused. -By default `run` spawns a daemon for the run if one isn't already up. `--no-spawn` (or `FLOWS_NO_SPAWN=1` for a whole environment) asserts a daemon is already present instead — the lever CI uses to fail loudly on a missing daemon rather than silently start one. +Deterministic steps run in the **daemon's** working directory and environment — the directory you first ran `flows run` from — not in the flow file's directory. A `./script` in a step command is resolved and probed there. -`--data-dir ` points at the journal's storage directory; it defaults to `.relayflowd` in the current project. +## Cloud -## Resume +`flows run --cloud` submits the flow to hosted infrastructure; the receipt prints `ACCEPTED ` and, with `--wait`, the validated completion. `--sync-code` uploads the invoking directory first so the hosted run executes inside your working tree, and `flows sync ` brings the run's file changes back as a patch applied to your checkout, uncommitted. This one-shot form works for both YAML/`spec.json` and authored TypeScript flows on `2.0.17`+ — see **Run**, above, for the version-mismatch bug that broke authored TypeScript on earlier releases. `flows deploy --repo … --on …` turns a flow into a Cloud listener that launches one run per matching ticket; `flows deployments` lists them and `flows undeploy` removes one. See [Cloud](/docs/relayflows/cloud) for the details and credentials. + +## Resume and replay ```bash flows resume +flows resume --local-agent # an authored run started with --local-agent needs it again +flows replay --at # read-only reconstruction up to a step ``` -Picks a run back up from its journal. Completed steps aren't re-executed; only the work that never finished, or never got a recorded outcome, runs again. +`resume` picks a run back up from its journal. Completed steps aren't re-executed; only the work that never finished, or never got a recorded outcome, runs again. A `run_not_found` target exits `2`; any other failure exits `1`, because the journal may already have changed. `replay` walks the journal without executing anything. Both take `--allow-human-influenced` to proceed past a run whose journal records a human intervention. ## Observer links @@ -52,17 +83,42 @@ Picks a run back up from its journal. Completed steps aren't re-executed; only t flows observer ``` -Mints a read-only link for watching a run's activity in real time. Minting is best-effort — a failed mint never fails the run — so `--no-observer-link` is there for anyone who wants to skip it outright (CI, for instance). +Mints a read-only link for watching a run's activity in real time, using the workspace key from `RELAYCAST_WORKSPACE_KEY` or your `agent-relay cloud login`. `run` and `resume` mint one automatically and print it after the `RUN` line; minting is best-effort and never fails the run. `--no-observer-link` or `FLOWS_NO_OBSERVER=1` skips it (CI, for instance). -## Triggers +## Triggers and event sources ```bash +flows serve-webhook --data-dir .relayflowd --port 8787 --allow slack,github flows tick start --schedule-id daily --interval-ms 86400000 spec.json flows hn-monitor start spec.json ``` -`tick` is a durable schedule. Each interval gets a unique ID, so a restart can't fire the same interval twice, and `--max-catch-up` bounds how many missed intervals get replayed if the process was down. `hn-monitor` is a narrower, named trigger built the same way. Both report whether they're still alive, instead of quietly going dark for weeks without anyone noticing. +`serve-webhook` receives provider events (`POST /providers/slack`, `POST /providers/github`, or a generic `POST /`) and writes them to the daemon's inbox for a bound trigger spec. `tick` is a durable local schedule: each interval gets a unique ID, so a restart can't fire the same interval twice, and `--max-catch-up` bounds how many missed intervals get replayed if the process was down. `hn-monitor` is a narrower, named source built the same way. All of them are processes that must stay running; hosted schedules for v2 flows are not available yet. + +## Bundles + +```bash +flows build my-flow.flow.ts --out dist +flows deploy my-flow@sha256: --to file:///srv/flows +flows run my-flow@sha256: --bucket file:///srv/flows +``` + +`build` seals a flow into a content-addressed bundle (canonical spec, compiled TypeScript with pinned dependencies, preflight declaration); the digest form of `deploy` copies it into a file bucket, and `run @sha256:…` executes it from there with no checkout. Only `file://` buckets are supported today. + +**Verified for real, and this doesn't work the way the example above implies.** `flows build` on an ordinary authored `.flow.ts` — including a minimal, header-less one, and every flow shown elsewhere on this site — is refused: + +``` +REFUSED [bundle_invalid] TypeScript build requires Bun: ... +error: authored flow() requires an exported spec declaration for build-time preflight; +the body is not executed during build +``` + +`build` wants a separate exported `spec` declaration for build-time preflight, distinct from the `flow(name, header?, body)` default export every other example on this site uses; a flow with a non-empty header is refused outright (`unsupported authored flow header`). What exact shape `build` expects instead isn't documented anywhere in this site's docs or in `docs/SURFACE.md`. Until that's resolved, `flows build`/the digest-bundle path is unverified for a normal authored flow — treat this section as aspirational for TypeScript flows, not a working command to copy. ## `--json` -`check`, `run`, and `resume` accept `--json` for structured output instead of the human-readable progress line — the shape a CI step or another program should read, not the terminal renderer. `tick start`, `hn-monitor start`, and `observer` don't take it. +`check`, `run`, `resume`, `replay`, `build`, `sync`, `deploy` (listener form), `deployments`, and `undeploy` accept `--json` for a single machine-readable object on stdout instead of the human-readable lines — the shape a CI step or another program should read. `tick start`, `hn-monitor start`, `serve-webhook`, and `observer` don't take it. + +## Exit codes + +`0` completed with `success` (a deliberate `done("declined")` also exits `0`, with a `DECLINED` diagnostic) · `1` failed with a declared reason, or the outcome is unknown · `2` refused before any journal write · `3` parked. [Reliability](/docs/relayflows/reliability) has the contract. diff --git a/web/content/docs/relayflows/cloud.mdx b/web/content/docs/relayflows/cloud.mdx index eeedf4f..6e5fd14 100644 --- a/web/content/docs/relayflows/cloud.mdx +++ b/web/content/docs/relayflows/cloud.mdx @@ -1,57 +1,98 @@ --- title: 'Cloud' -description: 'Run a flow on hosted infrastructure instead of your own machine — the same verification, the same journal.' +description: 'Run a flow on hosted infrastructure, sync your working tree up and the changes back, or deploy it as a listener — the same verification, the same journal.' --- -A flow doesn't need your laptop up to run. `flows run --cloud` submits the exact same spec to hosted infrastructure. The Rust runtime still executes and verifies every step; only where it runs is different. +A flow doesn't need your laptop up to run. `flows run --cloud` submits the same flow to hosted infrastructure; the Rust runtime still executes and verifies every step, only where it runs is different. `flows deploy` goes one step further and leaves the flow listening for tickets. ## Run it ```bash flows run --cloud examples/ship-feature.flow.yaml flows run --cloud --wait --json examples/ship-feature.flow.yaml +flows run --cloud --wait ship-feature.flow.ts --input '{"ticket":"ENG-42"}' ``` -Without `--wait`, exit `0` means the run was **accepted**. You get a run ID back and the run continues on its own; it hasn't completed yet. With `--wait`, exit `0` means Cloud reported the run `completed` with a validated `success` reason. A failed or cancelled run, or an observation failure, exits `1`. +Without `--wait`, exit `0` means the run was **accepted**. You get a run ID back and the run continues on its own; it hasn't completed yet — verified for real against a YAML flow: `{"ok":true,"runId":"...","status":"pending",...}`, exit `0`. With `--wait`, exit `0` means Cloud reported the run `completed` with a validated `success` reason; a failed or cancelled run, or an observation failure, exits `1` — also verified: a `--wait` run that came back with an inconsistent terminal record (`invalid_response: Cloud terminal record lacks a valid, consistent run completionReason`) exited `1`, matching the documented observation-failure case. -## From the SDK +An authored `.flow.ts` takes `--input` exactly as a local run does (an existing JSON file, otherwise inline JSON) and travels as one self-contained source: `use:` dependencies and sibling imports are refused before any HTTP call, since the hosted runner loads the flow from the request, not from a checkout. This path used to fail immediately with a bare `{"ok":false,"code":"http_error","message":"Cloud request failed with HTTP 400."}` against any real authored flow — root cause was Cloud pinning an older `@relayflows/surface` than the CLI authored against, badly reported (flows#461). Fixed in `2.0.17`: verified for real, the third command above now submits successfully and returns a run ID. Update to `2.0.17`+ if you still see that error. -```ts -import { runInCloud, waitForCloudFlowRun } from '@relayflows/sdk'; +## Bring your working tree -const accepted = await runInCloud( - { path: './flow.yaml' }, - { token: process.env.FLOWS_CLOUD_TOKEN } -); -console.log(accepted.runId); // accepted, not completed +```bash +cd my-repo +flows run --cloud --sync-code --wait review.flow.ts --input '{"pr": 7}' +flows sync # apply the run's changes to this checkout +``` -const finished = await waitForCloudFlowRun(accepted.runId); -console.log(finished.status, finished.completionReason); +Fixed in `2.0.17`, same as above — re-verified with `--sync-code` specifically: this exact command, run against a real git checkout with a `flows check`-passing `review.flow.ts`, now submits successfully and returns a run ID instead of the `http_error: HTTP 400` earlier releases gave. `flows sync ` itself (and the `git apply`/`patch_conflict` mechanics below) is still unverified in this pass — the run didn't reach completion inside this check's time budget, only submission was confirmed. + +`--sync-code` uploads the invoking directory before submission, so every `f.run` and `f.agent` in the hosted run executes inside your tree. In a Git checkout the upload is exactly `git ls-files --cached --others --exclude-standard`: `.gitignore` governs, untracked files ride along, `.git` and `node_modules` never do, executable bits survive. A checkout whose `git` fails for any other reason is refused rather than uploaded without its ignore rules. The limit is 256 MiB uncompressed. + +`flows sync ` fetches the diff the run left behind and applies it with `git apply` after a `--check` pass — a conflict leaves your tree untouched (`patch_conflict`, exit `2`). It lands **uncommitted**, with every touched path listed, so you review it with `git diff` before keeping any of it. + +## Deploy it as a listener + +```bash +flows deploy software-factory.flow.ts \ + --repo acme/api \ + --on linear:team=ENG \ + --approver you +flows deployments +flows undeploy ``` -`FLOWS_CLOUD_TOKEN` needs a Cloud API token; the `flows` CLI never logs in for you. Provision one from the [Cloud dashboard](https://cloud.agentrelay.com/dashboard/settings), the same place you'd create a deployment token or a Relayfile agent key: +`flows deploy ` is the command-line form of the [Flows onboarding](/flows) deploy step. Cloud stores the source and creates a listener whose watch rules match the chosen ticket sources; there is no webhook to register — your workspace's GitHub App installation or Slack, Linear, Jira, or Shortcut connection is the ingress. Each matching ticket launches one run of the stored source, cloned from `--repo`'s default branch onto a fresh `relayflow/-` branch, with `{ approver, issue, event }` as the flow's input. The flow must therefore be the default body, `flow(name, header, async (f, input) => …)`, reading `input.issue`. + +`--on [:key=value,…]` takes `github` (`repository`, `labels`, `contains`), `slack` (`channel`, `contains`), `linear` (`team`, `contains`), `jira` (`project`, `contains`) or `shortcut` (`workspace`, `contains`), each at most once; a GitHub source without `repository` is scoped to `--repo`. `--agents` names the coding-agent harnesses the flow uses (default `claude`); activation checks their credentials are connected and refuses with `flow_model_not_connected` otherwise. `--draft` saves without activating. Refusals are named — `flow_repository_not_connected`, `flow_name_taken`, … — rather than reported as a bare status. + + + Today a GitHub listener wakes on `issues.opened` and `issues.labeled`. + Pull-request events (`--on github:events=pull_request`) and hosted + schedules for v2 flows are in progress and not in 2.0.16; the local + `flows tick` schedule still works in the meantime. + + +## Credentials + +Every hosted verb resolves its credential the same way: the SDK's `token` option, then `FLOWS_CLOUD_TOKEN`, then the `agent-relay cloud login` store (`~/.agentworkforce/relay/cloud-auth.json`). Once you've run `agent-relay cloud login`, no environment variable is needed; the login's API URL is also the default base, so a login against one deployment never sends its token to another, and an expired login is refused with the re-login remedy instead of sent. + +For CI, or a host with no browser, provision a token from the [Cloud dashboard](https://cloud.agentrelay.com/dashboard/settings): 1. Open **Settings → Workspace API tokens**. 2. Under **Purpose**, pick **Flows Cloud token**. 3. Name it, set an expiry, and create it. -4. Copy the one-time `cld_at_...` value — it's shown once — and export it, replacing the placeholder below with what you copied: +4. Copy the one-time `cld_at_...` value — it's shown once — and export it: ```bash export FLOWS_CLOUD_TOKEN="cld_at_paste-your-copied-token-here" ``` -This mints a token scoped to exactly `workflow:invoke:read`, `workflow:invoke:write`, `workflow:runs:read`, and `workflow:logs:read` — nothing else — which covers both submitting and polling a run on this page. It's a workspace-level credential: anyone with dashboard access to the workspace can create or revoke one, no CLI or special access needed. +That token is scoped to `workflow:invoke:read`, `workflow:invoke:write`, `workflow:runs:read`, and `workflow:logs:read`, which covers `run --cloud`, `--sync-code`, and `sync`. Deploying, listing, and removing listeners need the interactive `cli:auth` credential the login produces; with a deployment token the CLI says so (`session_required`) rather than failing opaquely. `FLOWS_CLOUD_URL` points at a different Cloud deployment if you're not using the default. + +## From the SDK + +```ts +import { runInCloud, waitForCloudFlowRun } from '@relayflows/sdk'; -Prefer a CLI instead (headless/SSH host, no browser to click through)? `agent-relay cloud login` (`--device` for headless) plus `agent-relay cloud session --json --reveal-token` gets you a token too, though that one carries a broader `cli:auth` scope rather than the four scopes above — Cloud's workflow-run endpoints accept either. +const accepted = await runInCloud( + { path: './flow.yaml' }, + { token: process.env.FLOWS_CLOUD_TOKEN } +); +console.log(accepted.runId); // accepted, not completed + +const finished = await waitForCloudFlowRun(accepted.runId); +console.log(finished.status, 'completionReason' in finished ? finished.completionReason : undefined); +``` -`FLOWS_CLOUD_URL` points at a different Cloud deployment if you're not using the default. +`runInCloud` also takes `input` for an authored flow and `syncCode: { root }` to upload a tree; `deployToCloud`, `listCloudDeployments`, `undeployFromCloud`, `downloadCloudPatch`, and `applyCloudPatch` back the corresponding verbs. ## What's different about a cloud run -- **Only declarative flows.** `--cloud` accepts `flow.yaml` / `spec.json`, not an authored `.flow.ts` file — the SDK refuses those before making an HTTP call rather than uploading code that can't run there. -- **Accepted isn't completed.** An interruption after submission but before the acceptance receipt reports `admission_unknown` — the server may already have started a non-idempotent run. Don't resubmit blindly; check the run ID you already have first. -- **One-hour execution ceiling.** Cloud's current executor has a one-hour deadline per run, independent of any local timeout you'd otherwise configure. -- **You get the completion reason, not the step-by-step journal.** It's validated against the same closed vocabulary as a local run, but this API doesn't expose per-step detail yet. +- **Accepted isn't completed.** An interruption during the submission request itself reports `admission_unknown` — the server may already have started a non-idempotent run; check the run ID before resubmitting. An interruption earlier, while preparing or uploading a synced tree, reports `submission_aborted`: nothing was admitted and rerunning is safe. +- **One-hour execution ceiling.** Cloud's executor has a one-hour deadline per run, independent of any local timeout you'd otherwise configure. +- **You get the completion reason, not the step-by-step journal.** It's validated against the same closed vocabulary as a local run, but this API doesn't expose per-step output. +- **Pinned runtime.** Cloud runs a pinned build of the flows runtime, promoted separately from the npm release, so a brand-new CLI feature can be published before the hosted runtime that honours it inside the sandbox. ## Next diff --git a/web/content/docs/relayflows/cookbook.mdx b/web/content/docs/relayflows/cookbook.mdx new file mode 100644 index 0000000..27fa56f --- /dev/null +++ b/web/content/docs/relayflows/cookbook.mdx @@ -0,0 +1,70 @@ +--- +title: 'Cookbook' +description: 'Copy-paste flows for the automations people actually build first — verified end to end, not just typechecked.' +--- + +**We cook. Everybody eats.** [`AgentWorkforce/flows-cookbook`](https://github.com/AgentWorkforce/flows-cookbook) is a growing collection of single-file recipes — copy one, point it at your repo, run it locally or deploy it on our infrastructure at [agentrelay.com/flows](https://agentrelay.com/flows). Every recipe was run for real before it was published: see its README for the run link and exactly what was exercised. + +```bash +npm install -g relayflows +npm install --save-dev @relayflows/surface +``` + +## Getting started + + + + One step, one gate — confirms your setup works before you build on it. + + + Generate a message with an LLM, post it to Slack. + + + +cloud-gates: [![Deploy Flow](https://agentrelay.com/deploy-flow_small.svg)](https://agentrelay.com/cloud/flows/deploy?flow=https%3A%2F%2Fgithub.com%2FAgentWorkforce%2Fflows-cookbook%2Fblob%2Fmain%2Fcloud-gates%2Fcloud-gates.flow.yaml) +prospect-demo: [![Deploy Flow](https://agentrelay.com/deploy-flow_small.svg)](https://agentrelay.com/cloud/flows/deploy?flow=https%3A%2F%2Fgithub.com%2FAgentWorkforce%2Fflows-cookbook%2Fblob%2Fmain%2Fprospect-demo%2Fdemo.flow.ts) + +## Automations + + + + Triage every open issue on a schedule — stale vs. needs-attention, not just "quiet for N days" — and post a Slack digest. + + + Same shape, for pull requests: ready-to-merge, needs-review, needs-author, or abandoned. + + + +stale-issues: [![Deploy Flow](https://agentrelay.com/deploy-flow_small.svg)](https://agentrelay.com/cloud/flows/deploy?flow=https%3A%2F%2Fgithub.com%2FAgentWorkforce%2Fflows-cookbook%2Fblob%2Fmain%2Fstale-issues%2Fstale-issues.flow.ts) +stale-prs: [![Deploy Flow](https://agentrelay.com/deploy-flow_small.svg)](https://agentrelay.com/cloud/flows/deploy?flow=https%3A%2F%2Fgithub.com%2FAgentWorkforce%2Fflows-cookbook%2Fblob%2Fmain%2Fstale-prs%2Fstale-prs.flow.ts) + +## Review + + + + Reads a PR, fixes what's mechanical, and only says "ready for a human" when the agent, the tests, and GitHub all agree. + + + +[![Deploy Flow](https://agentrelay.com/deploy-flow_small.svg)](https://agentrelay.com/cloud/flows/deploy?flow=https%3A%2F%2Fgithub.com%2FAgentWorkforce%2Fflows-cookbook%2Fblob%2Fmain%2Fpr-reviewer%2Fpr-reviewer.flow.ts) + +## Software Factory + +An issue becomes a pull request: implementation agent → deterministic tests, run outside the agent → adversarial review agent that must write an explicit verdict → PR for a human. + +```bash +flows deploy software-factory/software-factory.flow.ts \ + --repo acme/api --on linear:team=ENG --approver you +``` + +[![Deploy Flow](https://agentrelay.com/deploy-flow_small.svg)](https://agentrelay.com/cloud/flows/deploy?flow=https%3A%2F%2Fgithub.com%2FAgentWorkforce%2Fflows-cookbook%2Fblob%2Fmain%2Fsoftware-factory%2Fsoftware-factory.flow.ts&on=linear%3Ateam%3DENG) + +`--on` also takes `github:labels=agent`, `jira:project=OPS`, `shortcut:workspace=…` or `slack:channel=#eng`. `flows deployments` lists what's listening; `flows undeploy ` stops it. + +## Coming soon + +Four more recipes are real but currently blocked on upstream work in [flows](https://github.com/AgentWorkforce/flows) — the cookbook's README tracks each one against its blocking issue instead of quietly leaving it out: a two-sandbox dependency upgrade bot, a three-lens PR review pipeline, a research-then-fact-checked social post pipeline, and a multi-model research fan-out. + + + Browse every recipe, its verification evidence, and what's still blocked. + diff --git a/web/content/docs/relayflows/introduction.mdx b/web/content/docs/relayflows/introduction.mdx index dab46ec..dc95e59 100644 --- a/web/content/docs/relayflows/introduction.mdx +++ b/web/content/docs/relayflows/introduction.mdx @@ -1,12 +1,18 @@ --- title: 'Flows' -description: 'A deterministic script over agentic primitives: steps you can inspect, verify, and resume.' +description: 'Stop babysitting agents. Script them — steps you can inspect, verify, and resume.' --- -Flows turns a coding-agent task into steps you can inspect and verify. A flow combines shell commands, model calls, and coding agents with a journal that records what each step did and why it completed. +**Stop babysitting agents. Script them.** + +Define complex sequences of tasks for agents instead of hoping they follow the rules in your prompt. A flow combines shell commands, model calls, and coding agents with a journal that records what each step did and why it completed — predictable, auditable, and dependable. Every effect is journaled before it's treated as real. If a step's journal write fails, the step fails. There's no silent fallback and no "it probably worked." Flows makes that trade everywhere: less magic, more evidence. + + Don't want to install anything yet? Try Flows at agentrelay.com/cloud first — no local setup. + + ## The ladder Every flow is built from the same small set of rungs, and you only climb as high as the task needs: @@ -14,7 +20,7 @@ Every flow is built from the same small set of rungs, and you only climb as high 1. **`run`** — a shell command. No model involved. 2. **`llm`** — a bare model call. Prompt in, verified output out, no workspace, no tool use. 3. **`agent`** — a harnessed coding agent in a workspace. Returns an artifact, not just text. -4. **`human` / `dispatch` / `done`** — durable approval, handing work to a child flow, and a typed finish. +4. **`human` / `dispatch` / `done`** — durable approval, handing work to a child flow, and a typed finish. `human` and `dispatch` are declared in the surface and pass `flows check`, but the 2.0.16 runtime refuses them (`unsupported_verb`); they ship in the release after 2.0.17. Today the shipped human gate is `f.done('needs_human')`, which parks the run for a person to resume. ## Simple Example Flow @@ -52,15 +58,31 @@ steps: ``` -`f.run` (a `deterministic` step in YAML) executes a shell command and returns its output. `f.agent` (an `agent` step) hands a task to a coding agent and returns a summary rather than a raw transcript. `f.done` finishes the run with one reason from a closed set: `success`, `step_failed`, `canceled`, or `budget_exceeded`. There's no fifth option to guess about. +`f.run` (a `deterministic` step in YAML) executes a shell command and returns its output. `f.agent` (an `agent` step) hands a task to a coding agent and returns a summary rather than a raw transcript. `f.done` finishes the run with one verdict from a closed set: `success`, `step_failed`, `needs_human`, or `declined`. There's no fifth option to guess about — `canceled` and `budget_exceeded` exist in the journal vocabulary, but only the kernel records them. -Both steps above name their own `cli` and `model` directly — `Ctx.agent`'s options are `{ task, workspace?, cli?, model? }`, matching the YAML step's fields ([flows#310](https://github.com/AgentWorkforce/flows/issues/310)). Neither is required: omit `cli` and a step falls back to its flow's `cli`, then the nearest `flows.json`'s project-wide default; omit `model` and it just runs whatever model its resolved CLI defaults to, since there's no flow or project default for that. Without a `cli` at step, flow, or project level, `flows run` refuses before anything is journaled — exit `2`, `REFUSED [invalid_spec]`. +Both steps above name their own `cli` and `model` directly — `f.agent`'s options are `{ task, workspace?, cli?, model? }`, matching the YAML step's fields (flows#310). Neither is required: omit `cli` and a step falls back to its flow's `cli`, then the nearest `flows.json`'s project-wide default; omit `model` and Claude runs its adapter default (`claude-opus-5`) while Codex picks its own. Without a `cli` at step, flow, or project level, `flows check` and `flows run` refuse before anything is journaled — exit `2`, `REFUSED [cli_unresolved]`. If that `flows.json` also lists `models`, every model a step names must be on the list. ## Verification An agent rarely fails by crashing. It fails by returning something plausible and wrong, which a plain retry-on-error never catches. So a step's completion is decided by a check the kernel runs against the real output, not by the agent's own account of what happened. -```yaml + +```typescript TypeScript +import { flow } from '@relayflows/surface'; + +export default flow('hello-agent', async (f) => { + await f.run('printf hello') + .gate({ type: 'regex_match', pattern: '^hello' }); + + await f.agent('edit', { + task: 'Produce the hello artifact and print agent-ok when it exists.', + cli: 'claude', + }).gate({ type: 'regex_match', pattern: 'agent-ok' }); + + f.done('success'); +}); +``` +```yaml YAML version: '0.1.0' name: hello-agent steps: @@ -84,10 +106,11 @@ steps: type: output_contains value: agent-ok ``` + -The exit code is always checked. On top of that, `output_contains` adds an opt-in string match, and an `llm` step can require its output to match a `json_schema` instead. A step that fails verification is recorded as `verification_failed` in the journal — it doesn't get to report success on its own say. +The exit code is always checked. On top of that, `output_contains` (YAML) or a postfix `.gate({ … })` (TypeScript) adds an opt-in check against the real output, and an `llm` step can require its output to match a `json_schema` instead. A step that fails verification is recorded as `verification_failed` in the journal — it doesn't get to report success on its own say. -`recoveryMode: reset` governs what happens if this step dies mid-edit: the next attempt starts over from the pinned workspace revision instead of picking up whatever half-finished state got left behind. `permissions` limits what that attempt is allowed to touch while it runs. +`recoveryMode: reset` governs what happens if this step dies mid-edit: the next attempt starts over from the pinned workspace revision instead of picking up whatever half-finished state got left behind. `permissions` limits what that attempt is allowed to touch while it runs. Both are declared per step in YAML today; see [Build a flow](/docs/relayflows/build#permissions-and-recovery). ## Resumable by construction @@ -122,6 +145,10 @@ This is a real run, captured directly from the terminal. Every step carries a `c + + Don't start from a blank file — the cookbook has verified, copy-paste recipes for the automations people build first. + + See how far the ladder goes: named agents, cloud dispatch, memory, and integrations. diff --git a/web/content/docs/relayflows/memory-and-integrations.mdx b/web/content/docs/relayflows/memory-and-integrations.mdx index 53c3d32..43f207a 100644 --- a/web/content/docs/relayflows/memory-and-integrations.mdx +++ b/web/content/docs/relayflows/memory-and-integrations.mdx @@ -10,15 +10,17 @@ A flow doesn't carry its own database or its own Slack client. Two existing syst A step declares what it needs from prior runs, and whatever comes back is charged to that step's own budget, itemized in its own journal entry. There's no shared pool it draws from: ```ts -export default flow('triage', async (f) => { - const priorArt = await f.memory.recall('similar bugs in the billing module'); - const why = await f.memory.why('fix the billing race condition'); +import { flow } from '@relayflows/surface'; + +export default flow('triage', { memory: { script: true } }, async (f) => { + const priorArt = await f.memory.recall('similar bugs in the billing module'); // HistoryEntry[] + const why = await f.memory.why('fix the billing race condition'); // TrajectoryEntry[] const fix = await f.agent('fixer', { - task: `Fix the race condition.\n\nRelevant history:\n${priorArt}`, + task: `Fix the race condition.\n\nRelevant history:\n${priorArt.map((e) => e.prompt).join('\n')}`, }); - await f.memory.learn(`Root cause: ${fix.summary}`); + // learn() is declared but refuses in 2.0.16, pending the journal-backed write. f.done('success'); }); ``` @@ -28,10 +30,15 @@ Under the hood this is [Relayloop](/docs/loop)'s `ai-hist pack` — the same sea What this feature is judged on is retrieval quality, not the plumbing: an agent visibly avoiding a mistake a prior run already made, citation - included. Today's shipped provider is a fixed stub while real `ai-hist - pack` retrieval lands. The `memory.injected` journal entry and its - per-step budget accounting are real regardless of which provider answers - the query. + included. In 2.0.16, `recall` and `why` read the local `ai-hist` SQLite + database (`AI_HIST_DB` overrides the default path) with no journal step; + `learn` and `memory: { agent: true }` refuse until the journal-backed + write lands. The `memory: { script: true }` header makes the reachability + check happen — but verified for real, that check runs at `flows run` time, + not at `flows check`: a flow with an unreachable database passes `flows + check` cleanly and only fails when actually run, with `memory_unreachable: + Script memory requires the ai-hist Node SDK and a readable SQLite database + at AI_HIST_DB (or defaultDbPath()). Run ai-hist sync first.` ## Integrations @@ -39,7 +46,9 @@ Under the hood this is [Relayloop](/docs/loop)'s `ai-hist pack` — the same sea Reaching Slack, GitHub, or Linear from a flow doesn't mean writing a provider SDK call and hoping the token scope is right. [Relayfile](/docs/file) mounts each provider as a directory a flow reads and writes: ```ts -export default flow('release-note', async (f) => { +import { flow } from '@relayflows/surface'; + +export default flow('release-note', { tools: { slack: true } }, async (f) => { const diff = await f.run('git diff main'); const note = await f.llm`One-line release note for: ${diff}`; @@ -48,7 +57,7 @@ export default flow('release-note', async (f) => { }); ``` -`f.slack`, `f.github`, `f.linear`, and every other generated helper compile to a write against Relayfile's [adapter catalog](/docs/file/adapters-and-providers) — the same 50 providers Relayfile ships today, each turned into a flow-native verb. The receipt a helper returns *is* the journaled effect record, so if a step retries and posts the same Slack message twice, the second write gets deduped instead of double-sending. +`f.slack`, `f.github`, `f.linear`, and every other generated helper compile to a write against Relayfile's [adapter catalog](/docs/file/adapters-and-providers) — the same 50 providers Relayfile ships today, each turned into a flow-native verb. The receipt a helper returns *is* the journaled effect record, so if a step retries and posts the same Slack message twice, the second write gets deduped instead of double-sending. A helper needs a Relayfile mount for its provider (Cloud provides one for connected providers); locally, `flows check` refuses `helper_slack.mount_required` until one exists, and `RELAYFLOWS_SLACK_MOCK=1` records the effect instead of sending it. ## Next diff --git a/web/content/docs/relayflows/multi-agent.mdx b/web/content/docs/relayflows/multi-agent.mdx index 63dc8e7..9fcb2f6 100644 --- a/web/content/docs/relayflows/multi-agent.mdx +++ b/web/content/docs/relayflows/multi-agent.mdx @@ -7,11 +7,39 @@ A flow isn't locked to one CLI or one model. Name as many agents as the task nee ## Named agents -```yaml + +```typescript TypeScript +import { flow } from '@relayflows/surface'; + +// Each call names its own cli and model; a plain object keeps them in one place. +const planner = { cli: 'claude', model: 'claude-opus-5' }; +const implementer = { cli: 'codex', model: 'gpt-5.6-codex' }; +const reviewer = { cli: 'claude', model: 'claude-sonnet-4-6' }; + +export default flow('ship-feature', async (f) => { + const plan = await f.agent('plan', { + ...planner, + task: 'Plan the implementation for: add OAuth2 support', + }); + + await f.agent('implement', { + ...implementer, + task: `Implement this plan:\n${plan.summary}`, + }); + + await f.agent('review', { + ...reviewer, + task: 'Review the diff for correctness and security. End with APPROVED or BLOCKED.', + }).gate({ type: 'regex_match', pattern: 'APPROVED' }); + + f.done('success'); +}); +``` +```yaml YAML version: '0.1.0' name: ship-feature agents: - planner: { cli: claude, model: claude-opus-4-6 } + planner: { cli: claude, model: claude-opus-5 } implementer: { cli: codex, model: gpt-5.6-codex } reviewer: { cli: claude, model: claude-sonnet-4-6 } @@ -36,22 +64,21 @@ steps: type: output_contains value: 'APPROVED' ``` + -Each step's `agent:` selector resolves to that named `{ cli, model }` pair at compile time, from an explicit declaration. `flows check` also flags a named agent nobody ever selects, and one that a step overrides without using, so a stale declaration doesn't quietly rot in the spec. +In TypeScript every `f.agent` call names its own `cli` and `model` (flows#310); the `name` argument labels the step in the journal. In YAML the `agents:` map declares each `{ cli, model }` pair once and a step's `agent:` selector resolves to it at compile time, and `flows check` flags a named agent nobody selects, or one a step overrides without using, so a stale declaration doesn't quietly rot in the spec. - The named-agent **map** (`agents:` plus a step's `agent:` selector, - reused across steps by name) is YAML/JSON authoring only today. In - TypeScript, `f.agent(name, options)` takes `{ task, workspace?, cli?, - model? }` ([flows#310](https://github.com/AgentWorkforce/flows/issues/310)) - — so a call can set its own `cli`/`model` directly, but the `name` - argument still just labels the call in the journal; there's no map to - select a declared pair from, or to reuse across multiple calls, the - way YAML's `agent:` selector does ([flows#300](https://github.com/AgentWorkforce/flows/issues/300)). + The reusable named-agent **map** is YAML/JSON authoring only today. + TypeScript has no `agents:` header to select a declared pair from + (flows#300); an + ordinary object spread, as above, is the idiom until it does. ## Asking a human, then handing off +`Ctx` declares both verbs, and this typechecks and passes `flows check` — but neither runs yet in 2.0.16. Treat the shape below as the intended design, not something to ship on today: + ```ts import { flow } from '@relayflows/surface'; @@ -61,14 +88,25 @@ export default flow('ship-feature', async (f) => { }); const ok = await f.human(`Ship this?\n${plan.summary}`, { to: 'khaliq' }); - if (!ok) return f.done('canceled'); + if (!ok) return f.done('declined'); // a decision not to act, not a kernel cancellation const pr = await f.dispatch('garden/implement', plan); f.done('success'); }); ``` -`f.human` parks the run on a durable wait — nothing sits there blocking a thread, and the wait survives a restart exactly like a crash mid-step does. `f.dispatch` hands the plan to a named child flow and returns its typed result, so one large flow decomposes into several smaller ones instead of a script that tries to do everything. +Once wired, `f.human` is meant to park the run on a durable wait — nothing sits there blocking a thread, and the wait would survive a restart exactly like a crash mid-step does. A "no" ends the run with `declined`, the verdict for choosing not to proceed; `canceled` is reserved for the kernel. `f.dispatch` is meant to hand the plan to a named child flow and return its typed result, so one large flow decomposes into several smaller ones instead of a script that tries to do everything. + + + Verified directly: `flows run` on a flow that reaches `f.human` fails with + `unsupported_verb: the initial authored executor does not lower f.human`, + and the same for `f.dispatch`. Both are being implemented on flows' + `feat/f-human` branch and ship in the release after 2.0.17. Until then, do + durable approval in YAML instead (`recoveryMode: manual` parks a step as + `needs_human` with a diff for a person to resolve), or use the shipped + human gate directly: `f.done('needs_human')` parks the run (exit `3`) for + a person to act on, then `flows resume ` continues it. + ## Next diff --git a/web/content/docs/relayflows/quickstart.mdx b/web/content/docs/relayflows/quickstart.mdx index 3fc315d..31c8388 100644 --- a/web/content/docs/relayflows/quickstart.mdx +++ b/web/content/docs/relayflows/quickstart.mdx @@ -5,6 +5,8 @@ description: 'From zero to a verified, resumable run: write a flow, run it, and This walks you from a fresh install to a real run in a few commands, using nothing but the published `relayflows` CLI. No scaffolder, no project template. +Just want to see it work first, with nothing to install? Skip straight to [agentrelay.com/cloud](https://agentrelay.com/cloud) and run a flow in the browser — come back here when you're ready to author your own. + ## 1. Install @@ -50,7 +52,7 @@ steps: ``` -YAML is canonical — the compiler can check it without running anything. TypeScript is the power tool: the same primitives, called imperatively, with ordinary `if`/`for` control flow around each `await`. +TypeScript is the default way to write a flow: the same primitives, called imperatively, with ordinary `if`/`for` control flow around each `await`. The YAML form (pick it from the language switch) is the canonical data the compiler checks without running anything — the shape a CI gate or a generator emits. ## 3. Run it @@ -59,11 +61,13 @@ YAML is canonical — the compiler can check it without running anything. TypeSc npx flows run hello.flow.ts --input '{}' ``` ```bash YAML -npx flows check hello.flow.yaml # preflight only — nothing runs +npx flows check hello.flow.yaml # preflight only — no flow, daemon, or worker starts npx flows run hello.flow.yaml ``` +`flows check` works on the TypeScript flow too — `npx flows check hello.flow.ts` — and `flows run` repeats the same preflight before its first step. + This is a real, captured run: @@ -77,7 +81,7 @@ RUN 01M26HYJNS6A4K5Q64FH6D0SG9 completed (2 steps) completionReason: success ``` -Every step's outcome is in the journal now, keyed by that run ID. `flows check` only takes a `flow.yaml` or `spec.json` — a TypeScript flow gets the same preflight for free, as the first thing `flows run` does. +Every step's outcome is in the journal now, keyed by that run ID — the `01M26JC2VPAGFVTCVWFT3GSCXQ` in the `RUN ... completed` line above. That's the ID everything below refers back to. ## 4. If it gets interrupted @@ -87,6 +91,8 @@ Every step's outcome is in the journal now, keyed by that run ID. `flows check` flows resume 01M26JC2VPAGFVTCVWFT3GSCXQ ``` +That's the same run ID `flows run` printed in step 3 — copy it from your own terminal's `RUN ...` line (or, on a failure, from the `Inspect: flows replay ...` line the CLI prints). If you didn't capture it, there's no `flows list`-style lookup today: the journal for each run lives at `/runs/.sqlite3` (default data dir is `.relayflowd` in the directory you ran `flows` from), so `ls .relayflowd/runs/` recovers it by filename. + `resume` starts a fresh daemon if none is up, reads the journal from disk, and continues from the first step that never recorded a `completionReason` — the step already marked done doesn't run again. ## Next diff --git a/web/content/docs/relayflows/reliability.mdx b/web/content/docs/relayflows/reliability.mdx index a1e53f1..d146978 100644 --- a/web/content/docs/relayflows/reliability.mdx +++ b/web/content/docs/relayflows/reliability.mdx @@ -8,9 +8,9 @@ This page covers what makes a run's completion trustworthy: not an agent's own r ## A run's exit code is a contract ```text -0 completionReason: success +0 completionReason: success (a deliberate done("declined") also exits 0, with a DECLINED diagnostic) 1 a declared completionReason failure, or an unknown outcome from a transport/runtime/protocol error -2 refused before any journal write: invalid input, failed preflight, unreachable daemon +2 refused before any journal write: invalid input, failed preflight, unreachable daemon, a run_not_found resume target 3 parked — waiting on an llm/agent worker or a needs_human recovery wait ``` @@ -27,7 +27,7 @@ Step: success · verification_failed · retries_exhausted · lease_expired · Run: success · step_failed · canceled · budget_exceeded ``` -Handle every value in that list and there's no case left over to surprise you later. A step that fails verification is recorded as `verification_failed`, with exactly which check failed. The agent reporting that it went fine doesn't change the outcome. +An authored TypeScript body declares its verdict with `f.done`: `success`, `step_failed`, `needs_human` (parks), or `declined` (a deliberate decision not to act; the kernel records `success` and the CLI adds a `DECLINED` diagnostic). `canceled` and `budget_exceeded` are the kernel's to record, never a body's. Handle every value in those lists and there's no case left over to surprise you later. A step that fails verification is recorded as `verification_failed`, with exactly which check failed. The agent reporting that it went fine doesn't change the outcome. ## Crashing mid-step diff --git a/web/lib/flow-onboarding.ts b/web/lib/flow-onboarding.ts index af5eebb..a1abd5e 100644 --- a/web/lib/flow-onboarding.ts +++ b/web/lib/flow-onboarding.ts @@ -97,9 +97,11 @@ export function cloudConnectionsHref(draft: FactoryDraft, handoffId: string, jou } export function factoryCodeSections(draft: FactoryDraft, target: 'cloud' | 'local' = 'cloud') { - // A wall-clock budget for every target: a dollar budget makes the Relayflow - // runtime refuse any agent step without a frozen-priced model (Codex has no - // default model), which stops the run before the first Codex step. + // A wall-clock budget for every target. Since relayflows 2.0.13 a dollar + // budget no longer refuses a model-less Codex step (AgentWorkforce/flows#421); + // such a step runs unmetered, so a dollar cap cannot bound it. Wall-clock is + // enforced on every step regardless of pricing, which is why it stays the + // default here; `{ dollars, wallclock }` together is also valid. const budget = '{ wallclock: "1h" }'; if (!draft.sources.length) return [{ id: 'empty', code: `import { flow } from "@relayflows/surface"; diff --git a/web/lib/product-docs-nav.ts b/web/lib/product-docs-nav.ts index 554ba66..41d2095 100644 --- a/web/lib/product-docs-nav.ts +++ b/web/lib/product-docs-nav.ts @@ -241,6 +241,10 @@ export const relayflowsSection: ProductDocSection = { { title: 'Reliability', slug: 'reliability' }, ], }, + { + title: 'Cookbook', + items: [{ title: 'Recipes', slug: 'cookbook' }], + }, ], }; diff --git a/web/public/deploy-flow_small.svg b/web/public/deploy-flow_small.svg new file mode 100644 index 0000000..b8ec8a4 --- /dev/null +++ b/web/public/deploy-flow_small.svg @@ -0,0 +1,23 @@ + + Deploy Flow on Agent Relay + + + + + + + + + + + + + + + Deploy Flow + + + + + +