mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-08 03:10:26 +00:00
simplify model stylesheet: rename llm_model/llm_provider, add provider inference
Rename `llm_model` → `model` and `llm_provider` → `provider` in stylesheet properties, accessor methods, and all DOT/doc references. Add ProviderInferenceTransform that automatically infers provider from the model catalog, eliminating redundant provider declarations in stylesheets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5538ae4cd7
commit
d37fb9a9b4
62 changed files with 418 additions and 324 deletions
|
|
@ -58,8 +58,8 @@ digraph PlanImplement {
|
|||
graph [
|
||||
goal="Plan, approve, implement, and simplify a change"
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-haiku-4-5; reasoning_effort: low; }
|
||||
.coding { llm_model: claude-sonnet-4-5; reasoning_effort: high; }
|
||||
* { model: claude-haiku-4-5; reasoning_effort: low; }
|
||||
.coding { model: claude-sonnet-4-5; reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
digraph GoldenGate {
|
||||
graph [
|
||||
goal="Create a hyperrealistic interactive 3D Golden Gate Bridge flight experience",
|
||||
model_stylesheet="* { llm_model: gpt-5.4; llm_provider: openai }"
|
||||
model_stylesheet="* { model: gpt-5.4;}"
|
||||
]
|
||||
rankdir=LR
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ digraph ImplementAndSimplify {
|
|||
graph [
|
||||
goal="Implement and simplify",
|
||||
model_stylesheet="
|
||||
* { backend: api; llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
* { backend: api; model: claude-opus-4-6;}
|
||||
"
|
||||
]
|
||||
rankdir=LR
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
digraph PlaywrightDemo {
|
||||
graph [goal="Use Playwright MCP to browse Hacker News and take screenshots", model_stylesheet="* { llm_model: claude-sonnet-4-6; llm_provider: anthropic }"]
|
||||
graph [goal="Use Playwright MCP to browse Hacker News and take screenshots", model_stylesheet="* { model: claude-sonnet-4-6;}"]
|
||||
rankdir=LR
|
||||
|
||||
start [shape=Mdiamond, label="Start"]
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ digraph BuildSolitaire {
|
|||
retry_target="impl_setup",
|
||||
fallback_retry_target="impl_logic",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-sonnet; llm_provider: anthropic; }
|
||||
.hard { llm_model: claude-opus; llm_provider: anthropic; }
|
||||
.verify { llm_model: claude-haiku; llm_provider: anthropic; }
|
||||
* { model: claude-sonnet;}
|
||||
.hard { model: claude-opus; }
|
||||
.verify { model: claude-haiku; }
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ digraph SpecDoDMultiModel {
|
|||
retry_target="triage_merge",
|
||||
default_fidelity="full",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
.opus { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; }
|
||||
.gpt { llm_model: gpt-5.2; llm_provider: openai; reasoning_effort: high; }
|
||||
.codex { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; }
|
||||
.merge { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; }
|
||||
* { model: claude-opus-4-6;}
|
||||
.opus { model: claude-opus-4-6;reasoning_effort: high; }
|
||||
.gpt { model: gpt-5.2; reasoning_effort: high; }
|
||||
.codex { model: gpt-5.2-codex; reasoning_effort: high; }
|
||||
.merge { model: claude-opus-4-6; reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ digraph SpecDoD {
|
|||
default_max_retry="3",
|
||||
retry_target="triage",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
* { model: claude-opus-4-6;}
|
||||
.audit { reasoning_effort: high; }
|
||||
.fix { llm_model: claude-opus-4-6; reasoning_effort: high; }
|
||||
.fix { model: claude-opus-4-6; reasoning_effort: high; }
|
||||
#final_audit { reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
digraph VNCDemo {
|
||||
graph [
|
||||
goal="Browse the web with Playwright while user watches via VNC",
|
||||
model_stylesheet="* { llm_model: claude-sonnet-4-6; llm_provider: anthropic }"
|
||||
model_stylesheet="* { model: claude-sonnet-4-6;}"
|
||||
]
|
||||
rankdir=LR
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
digraph WebGameDemo {
|
||||
graph [
|
||||
goal="Verify develop-web-game skill setup with asset files",
|
||||
model_stylesheet="* { llm_model: claude-sonnet-4-6; llm_provider: anthropic }"
|
||||
model_stylesheet="* { model: claude-sonnet-4-6;}"
|
||||
]
|
||||
rankdir=LR
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ The CLI is selected automatically based on the node's provider:
|
|||
Set the CLI backend on a node with `backend="cli"` or via a [model stylesheet](/workflows/stylesheets):
|
||||
|
||||
```dot
|
||||
implement [label="Implement", backend="cli", llm_provider="anthropic"]
|
||||
implement [label="Implement", backend="cli"]
|
||||
```
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -56,9 +56,9 @@ Assign models to workflow nodes using [model stylesheets](/workflows/stylesheets
|
|||
digraph Example {
|
||||
graph [
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-haiku-4-5; }
|
||||
.coding { llm_model: claude-sonnet-4-5; reasoning_effort: high; }
|
||||
#review { llm_model: gemini-3.1-pro-preview; }
|
||||
* { model: claude-haiku-4-5; }
|
||||
.coding { model: claude-sonnet-4-5; reasoning_effort: high; }
|
||||
#review { model: gemini-3.1-pro-preview; }
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -47,11 +47,11 @@ GitHub to Railway validated by code review only — no live deployment execution
|
|||
retry_target="plan_fanout",
|
||||
fallback_retry_target="plan_fanout",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
.hard { llm_model: gpt-5.3-codex; llm_provider: openai; }
|
||||
.verify { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
.branch-a { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
.branch-b { llm_model: gemini-3-flash-preview; llm_provider: gemini; }
|
||||
* { model: claude-opus-4-6; }
|
||||
.hard { model: gpt-5.3-codex; }
|
||||
.verify { model: claude-opus-4-6; }
|
||||
.branch-a { model: claude-opus-4-6; }
|
||||
.branch-b { model: gemini-3-flash-preview;}
|
||||
"
|
||||
]
|
||||
|
||||
|
|
@ -693,11 +693,11 @@ check_toolchain -> postmortem [condition="outcome=fail && context.failure_
|
|||
The stylesheet assigns models based on task difficulty:
|
||||
|
||||
```
|
||||
* { llm_model: claude-opus-4-6; } // Default: spec expansion, debate, postmortem
|
||||
.hard { llm_model: gpt-5.3-codex; } // Implementation: optimized for code generation
|
||||
.verify { llm_model: claude-opus-4-6; } // Fidelity verification: careful analysis
|
||||
.branch-a { llm_model: claude-opus-4-6; } // Plan A, Review A: Anthropic perspective
|
||||
.branch-b { llm_model: gemini-3-flash-preview; } // Plan B, Review B: Google perspective
|
||||
* { model: claude-opus-4-6; } // Default: spec expansion, debate, postmortem
|
||||
.hard { model: gpt-5.3-codex; } // Implementation: optimized for code generation
|
||||
.verify { model: claude-opus-4-6; } // Fidelity verification: careful analysis
|
||||
.branch-a { model: claude-opus-4-6; } // Plan A, Review A: Anthropic perspective
|
||||
.branch-b { model: gemini-3-flash-preview; } // Plan B, Review B: Google perspective
|
||||
```
|
||||
|
||||
The `.branch-b` class uses a different provider for both planning and review. This ensures the second opinion is genuinely independent — not just a second run of the same model. The `.hard` class routes implementation to OpenAI's Codex, which is optimized for high-throughput code generation.
|
||||
|
|
|
|||
|
|
@ -29,9 +29,9 @@ digraph SpecDoD {
|
|||
default_max_retry="3",
|
||||
retry_target="triage",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
* { model: claude-opus-4-6;}
|
||||
.audit { reasoning_effort: high; }
|
||||
.fix { llm_model: claude-opus-4-6; reasoning_effort: high; }
|
||||
.fix { model: claude-opus-4-6; reasoning_effort: high; }
|
||||
#final_audit { reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
|
|
@ -286,11 +286,11 @@ digraph SpecDoDMultiModel {
|
|||
retry_target="triage_merge",
|
||||
default_fidelity="full",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
.opus { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; }
|
||||
.gpt { llm_model: gpt-5.2; llm_provider: openai; reasoning_effort: high; }
|
||||
.codex { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; }
|
||||
.merge { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; }
|
||||
* { model: claude-opus-4-6;}
|
||||
.opus { model: claude-opus-4-6;reasoning_effort: high; }
|
||||
.gpt { model: gpt-5.2; reasoning_effort: high; }
|
||||
.codex { model: gpt-5.2-codex; reasoning_effort: high; }
|
||||
.merge { model: claude-opus-4-6; reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ digraph NLSpecConformance {
|
|||
graph [
|
||||
goal="Implement a conformant system from a natural language specification",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-haiku-4-5; llm_provider: anthropic; }
|
||||
.impl { llm_model: claude-sonnet-4-5; reasoning_effort: high; }
|
||||
* { model: claude-haiku-4-5;}
|
||||
.impl { model: claude-sonnet-4-5; reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
rankdir=LR
|
||||
|
|
@ -135,8 +135,8 @@ The `model_stylesheet` assigns a cheaper model as the default and routes impleme
|
|||
|
||||
```dot
|
||||
graph [model_stylesheet="
|
||||
* { llm_model: claude-haiku-4-5; llm_provider: anthropic; }
|
||||
.impl { llm_model: claude-sonnet-4-5; reasoning_effort: high; }
|
||||
* { model: claude-haiku-4-5;}
|
||||
.impl { model: claude-sonnet-4-5; reasoning_effort: high; }
|
||||
"]
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ digraph SemanticPort {
|
|||
rankdir=LR,
|
||||
default_max_retry=3,
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-sonnet-4-5; llm_provider: anthropic; }
|
||||
.hard { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
.analyze { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; }
|
||||
* { model: claude-sonnet-4-5;}
|
||||
.hard { model: claude-opus-4-6; }
|
||||
.analyze { model: gemini-3.1-pro-preview;}
|
||||
"
|
||||
]
|
||||
|
||||
|
|
@ -170,7 +170,7 @@ The `analyze` node is the decision point. It examines each upstream commit for *
|
|||
This distinction is critical — routing a different model (Gemini) to the analysis node via the `.analyze` class brings a fresh perspective to the port/skip decision:
|
||||
|
||||
```
|
||||
.analyze { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; }
|
||||
.analyze { model: gemini-3.1-pro-preview;}
|
||||
```
|
||||
|
||||
### The fix loop
|
||||
|
|
@ -200,9 +200,9 @@ When the agent decides a commit is irrelevant, it updates the ledger, commits th
|
|||
The stylesheet assigns three tiers of models:
|
||||
|
||||
```
|
||||
* { llm_model: claude-sonnet-4-5; } // Default: plan, finalize
|
||||
.hard { llm_model: claude-opus-4-6; } // Implementation, fixing
|
||||
.analyze { llm_model: gemini-3.1-pro-preview; } // Analysis: fresh eyes
|
||||
* { model: claude-sonnet-4-5; } // Default: plan, finalize
|
||||
.hard { model: claude-opus-4-6; } // Implementation, fixing
|
||||
.analyze { model: gemini-3.1-pro-preview; } // Analysis: fresh eyes
|
||||
```
|
||||
|
||||
- **Sonnet** handles routine tasks: fetching commits, finalizing plans, updating the ledger
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ digraph BuildSolitaire {
|
|||
retry_target="impl_setup",
|
||||
fallback_retry_target="impl_logic",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-sonnet; llm_provider: anthropic; }
|
||||
.hard { llm_model: claude-opus; llm_provider: anthropic; }
|
||||
.verify { llm_model: claude-haiku; llm_provider: anthropic; }
|
||||
* { model: claude-sonnet;}
|
||||
.hard { model: claude-opus; }
|
||||
.verify { model: claude-haiku; }
|
||||
"
|
||||
]
|
||||
|
||||
|
|
@ -218,9 +218,9 @@ If a node fails and has no local retry target, Fabro jumps back to `impl_setup`
|
|||
The stylesheet assigns models by role:
|
||||
|
||||
```
|
||||
* { llm_model: claude-sonnet-4-5; } // Default: spec, setup, integration
|
||||
.hard { llm_model: claude-opus-4-6; } // Hard work: game logic, UI, review
|
||||
.verify { llm_model: claude-haiku-4-5; } // Verification: fast, cheap checks
|
||||
* { model: claude-sonnet-4-5; } // Default: spec, setup, integration
|
||||
.hard { model: claude-opus-4-6; } // Hard work: game logic, UI, review
|
||||
.verify { model: claude-haiku-4-5; } // Verification: fast, cheap checks
|
||||
```
|
||||
|
||||
- **Sonnet** handles routine phases: expanding the spec, setting up the project, wiring integration
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ Set workflow-level configuration:
|
|||
|
||||
```dot
|
||||
// Block syntax
|
||||
graph [goal="Build a feature", model_stylesheet="* { llm_model: claude-haiku-4-5; }"]
|
||||
graph [goal="Build a feature", model_stylesheet="* { model: claude-haiku-4-5; }"]
|
||||
|
||||
// Declaration syntax
|
||||
rankdir=LR
|
||||
|
|
@ -199,8 +199,8 @@ Start nodes can also be identified by ID (`start` or `Start`). Exit nodes can be
|
|||
| `max_tokens` | Integer | Maximum output tokens |
|
||||
| `fidelity` | String | How much prior context is passed: `compact`, `full`, `summary:high`, `summary:medium`, `summary:low`, `truncate` |
|
||||
| `thread_id` | String | Groups nodes into a shared conversation thread |
|
||||
| `llm_model` | String | Explicit model ID (overrides stylesheet) |
|
||||
| `llm_provider` | String | Explicit provider name (overrides stylesheet) |
|
||||
| `model` | String | Explicit model ID (overrides stylesheet) |
|
||||
| `provider` | String | Explicit provider name (overrides stylesheet) |
|
||||
| `project_memory` | Boolean | When `true` (default), prompt nodes discover and include project docs (`AGENTS.md`, `CLAUDE.md`, etc.) as a system prompt. Set to `false` to disable. |
|
||||
| `backend` | String | Agent execution backend. `api` (default): Fabro calls the LLM API directly and runs its own tool loop. `cli`: Fabro delegates to an external CLI tool (`claude`, `codex`, or `gemini` based on provider). See [Agents — Backends](/core-concepts/agents#backends). |
|
||||
|
||||
|
|
@ -350,9 +350,9 @@ digraph ImplementFeature {
|
|||
graph [
|
||||
goal="Implement a feature with tests and code review",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-haiku-4-5; llm_provider: anthropic; reasoning_effort: low; }
|
||||
.coding { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; }
|
||||
#review { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; }
|
||||
* { model: claude-haiku-4-5;reasoning_effort: low; }
|
||||
.coding { model: claude-sonnet-4-5;reasoning_effort: high; }
|
||||
#review { model: claude-sonnet-4-5;reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
rankdir=LR
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ digraph Ensemble {
|
|||
graph [
|
||||
goal="Get independent opinions from multiple providers, then synthesize",
|
||||
model_stylesheet="
|
||||
#opus { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
#gemini { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; }
|
||||
#codex { llm_model: gpt-5.3-codex; llm_provider: openai; }
|
||||
#mercury { llm_model: mercury-2; llm_provider: inception; }
|
||||
#synth { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; }
|
||||
#opus { model: claude-opus-4-6; }
|
||||
#gemini { model: gemini-3.1-pro-preview;}
|
||||
#codex { model: gpt-5.3-codex; }
|
||||
#mercury { model: mercury-2; provider: inception; }
|
||||
#synth { model: claude-opus-4-6; reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
rankdir=LR
|
||||
|
|
@ -68,10 +68,10 @@ The workflow has three phases:
|
|||
The `fork` node spawns four parallel branches, each assigned to a different provider via the stylesheet:
|
||||
|
||||
```
|
||||
#opus { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
#gemini { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; }
|
||||
#codex { llm_model: gpt-5.3-codex; llm_provider: openai; }
|
||||
#mercury { llm_model: mercury-2; llm_provider: inception; }
|
||||
#opus { model: claude-opus-4-6; }
|
||||
#gemini { model: gemini-3.1-pro-preview;}
|
||||
#codex { model: gpt-5.3-codex; }
|
||||
#mercury { model: mercury-2; provider: inception; }
|
||||
```
|
||||
|
||||
Each branch receives the same prompt but runs on a completely different model. The branches execute concurrently and have no knowledge of each other's responses.
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ digraph MultiModel {
|
|||
graph [
|
||||
goal="Build and review a utility function using multiple models",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-haiku-4-5; llm_provider: anthropic; reasoning_effort: low; }
|
||||
.coding { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; }
|
||||
#review { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; }
|
||||
* { model: claude-haiku-4-5;reasoning_effort: low; }
|
||||
.coding { model: claude-sonnet-4-5;reasoning_effort: high; }
|
||||
#review { model: claude-sonnet-4-5;reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
rankdir=LR
|
||||
|
|
@ -44,9 +44,9 @@ fabro run files-internal/demo/08-multi-model.dot
|
|||
The `model_stylesheet` graph attribute contains CSS-like rules that assign models to nodes:
|
||||
|
||||
```
|
||||
* { llm_model: claude-haiku-4-5; llm_provider: anthropic; reasoning_effort: low; }
|
||||
.coding { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; }
|
||||
#review { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; }
|
||||
* { model: claude-haiku-4-5;reasoning_effort: low; }
|
||||
.coding { model: claude-sonnet-4-5;reasoning_effort: high; }
|
||||
#review { model: claude-sonnet-4-5;reasoning_effort: high; }
|
||||
```
|
||||
|
||||
### Selectors
|
||||
|
|
@ -85,8 +85,8 @@ Stylesheets support four properties:
|
|||
|
||||
| Property | Description |
|
||||
|---|---|
|
||||
| `llm_model` | Model ID or alias (e.g. `claude-sonnet-4-5`, `opus`, `gemini-pro`) |
|
||||
| `llm_provider` | Provider name (`anthropic`, `openai`, `gemini`, etc.) |
|
||||
| `model` | Model ID or alias (e.g. `claude-sonnet-4-5`, `opus`, `gemini-pro`) |
|
||||
| `provider` | Provider name (`anthropic`, `openai`, `gemini`, etc.) |
|
||||
| `reasoning_effort` | `low`, `medium`, or `high` |
|
||||
| `backend` | `api` (default) or `cli` |
|
||||
|
||||
|
|
@ -105,7 +105,7 @@ Model routing lets you optimize cost and latency without changing the workflow s
|
|||
A model set directly on a node attribute always beats the stylesheet:
|
||||
|
||||
```dot
|
||||
implement [label="Implement", class="coding", llm_model="claude-opus-4-6"]
|
||||
implement [label="Implement", class="coding", model="claude-opus-4-6"]
|
||||
```
|
||||
|
||||
This node uses Opus regardless of what `.coding` says.
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ digraph Example {
|
|||
graph [
|
||||
goal="Build and review a utility function",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-haiku-4-5; llm_provider: anthropic; }
|
||||
.coding { llm_model: claude-sonnet-4-5; reasoning_effort: high; }
|
||||
#review { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; }
|
||||
* { model: claude-haiku-4-5;}
|
||||
.coding { model: claude-sonnet-4-5; reasoning_effort: high; }
|
||||
#review { model: gemini-3.1-pro-preview;}
|
||||
"
|
||||
]
|
||||
|
||||
|
|
@ -64,8 +64,8 @@ Stylesheets support four properties:
|
|||
|
||||
| Property | Description | Example |
|
||||
|---|---|---|
|
||||
| `llm_model` | Model ID or alias | `claude-sonnet-4-5`, `opus`, `gemini-pro` |
|
||||
| `llm_provider` | Provider name | `anthropic`, `openai`, `gemini` |
|
||||
| `model` | Model ID or alias | `claude-sonnet-4-5`, `opus`, `gemini-pro` |
|
||||
| `provider` | Provider name | `anthropic`, `openai`, `gemini` |
|
||||
| `reasoning_effort` | Reasoning effort level | `low`, `medium`, `high` |
|
||||
| `backend` | Agent execution backend — `api` (default) runs Fabro's own tool loop, `cli` delegates to an external CLI tool. See [Backends](/core-concepts/agents#backends). | `cli`, `api` |
|
||||
|
||||
|
|
@ -82,9 +82,9 @@ When multiple rules match the same node, the rule with the **highest specificity
|
|||
For example:
|
||||
|
||||
```
|
||||
* { llm_model: claude-haiku-4-5; }
|
||||
.coding { llm_model: claude-sonnet-4-5; }
|
||||
#review { llm_model: gpt-5.2; }
|
||||
* { model: claude-haiku-4-5; }
|
||||
.coding { model: claude-sonnet-4-5; }
|
||||
#review { model: gpt-5.2; }
|
||||
```
|
||||
|
||||
A node with `id="review"` and `class="coding"` gets `gpt-5.2` because `#id` (specificity 3) beats `.class` (specificity 2).
|
||||
|
|
@ -96,10 +96,10 @@ If two rules have the same specificity, the **last one** in the stylesheet wins.
|
|||
A model set directly on a node attribute always takes precedence over stylesheets, regardless of specificity:
|
||||
|
||||
```dot
|
||||
implement [label="Implement", class="coding", llm_model="claude-opus-4-6"]
|
||||
implement [label="Implement", class="coding", model="claude-opus-4-6"]
|
||||
```
|
||||
|
||||
Even if `.coding` sets `llm_model: claude-sonnet-4-5`, this node uses Opus because the explicit attribute wins.
|
||||
Even if `.coding` sets `model: claude-sonnet-4-5`, this node uses Opus because the explicit attribute wins.
|
||||
|
||||
## Syntax reference
|
||||
|
||||
|
|
@ -118,12 +118,12 @@ selector { property: value; property: value; }
|
|||
### Full example
|
||||
|
||||
```
|
||||
* { llm_model: claude-haiku-4-5; llm_provider: anthropic; reasoning_effort: low; }
|
||||
* { model: claude-haiku-4-5;reasoning_effort: low; }
|
||||
box { reasoning_effort: high; }
|
||||
tab { reasoning_effort: low; }
|
||||
.coding { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; }
|
||||
.review { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; }
|
||||
#final_check { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; }
|
||||
.coding { model: claude-sonnet-4-5;reasoning_effort: high; }
|
||||
.review { model: gemini-3.1-pro-preview;}
|
||||
#final_check { model: claude-opus-4-6;reasoning_effort: high; }
|
||||
```
|
||||
|
||||
This stylesheet:
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ digraph MultiModel {
|
|||
graph [
|
||||
goal="Build and review a utility function using multiple models",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-haiku-4-5; llm_provider: anthropic; reasoning_effort: low; }
|
||||
.coding { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; }
|
||||
#review { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; }
|
||||
* { model: claude-haiku-4-5;reasoning_effort: low; }
|
||||
.coding { model: claude-sonnet-4-5;reasoning_effort: high; }
|
||||
#review { model: claude-sonnet-4-5;reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
rankdir=LR
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ digraph Ensemble {
|
|||
graph [
|
||||
goal="Get independent opinions from multiple providers, then synthesize",
|
||||
model_stylesheet="
|
||||
#opus { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
#gemini { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; }
|
||||
#codex { llm_model: gpt-5.3-codex; llm_provider: openai; }
|
||||
#mercury { llm_model: mercury-2; llm_provider: inception; }
|
||||
#synth { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; }
|
||||
#opus { model: claude-opus-4-6; }
|
||||
#gemini { model: gemini-3.1-pro-preview;}
|
||||
#codex { model: gpt-5.3-codex; }
|
||||
#mercury { model: mercury-2; provider: inception; }
|
||||
#synth { model: claude-opus-4-6; reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
rankdir=LR
|
||||
|
|
|
|||
|
|
@ -124,9 +124,9 @@ digraph Styled {
|
|||
graph [
|
||||
goal="Build feature",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-sonnet-4-5; llm_provider: anthropic; }
|
||||
.code { llm_model: claude-opus-4-6; }
|
||||
#critical_review { llm_model: gpt-5.2; llm_provider: openai; }
|
||||
* { model: claude-sonnet-4-5;}
|
||||
.code { model: claude-opus-4-6; }
|
||||
#critical_review { model: gpt-5.2;}
|
||||
"
|
||||
]
|
||||
// ...
|
||||
|
|
|
|||
|
|
@ -242,9 +242,9 @@ impl CodergenBackend for AgentApiBackend {
|
|||
.await
|
||||
.map_err(|e| FabroError::handler(format!("Failed to create LLM client: {e}")))?;
|
||||
|
||||
let model = node.llm_model().unwrap_or(&self.model);
|
||||
let model = node.model().unwrap_or(&self.model);
|
||||
let provider = node
|
||||
.llm_provider()
|
||||
.provider()
|
||||
.map(String::from)
|
||||
.or_else(|| Some(self.provider.as_str().to_string()));
|
||||
|
||||
|
|
@ -281,7 +281,7 @@ impl CodergenBackend for AgentApiBackend {
|
|||
|
||||
// Build per-request fallback chain: if the node overrides the provider,
|
||||
// no failover is available; otherwise use the backend's.
|
||||
let fallback_chain: &[FallbackTarget] = if node.llm_provider().is_some() {
|
||||
let fallback_chain: &[FallbackTarget] = if node.provider().is_some() {
|
||||
&[]
|
||||
} else {
|
||||
&self.fallback_chain
|
||||
|
|
|
|||
|
|
@ -463,9 +463,9 @@ impl CodergenBackend for AgentCliBackend {
|
|||
.map_err(|e| FabroError::handler(format!("Failed to write prompt file: {e}")))?;
|
||||
|
||||
// 3. Build CLI command
|
||||
let model = node.llm_model().unwrap_or(&self.model);
|
||||
let model = node.model().unwrap_or(&self.model);
|
||||
let provider = node
|
||||
.llm_provider()
|
||||
.provider()
|
||||
.and_then(|s| s.parse::<Provider>().ok())
|
||||
.unwrap_or(self.provider);
|
||||
|
||||
|
|
@ -733,7 +733,7 @@ impl BackendRouter {
|
|||
}
|
||||
|
||||
// CLI-only model on the node
|
||||
if let Some(model) = node.llm_model() {
|
||||
if let Some(model) = node.model() {
|
||||
if is_cli_only_model(model) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1164,7 +1164,7 @@ mod tests {
|
|||
fn router_uses_api_for_non_cli_model() {
|
||||
let mut node = Node::new("test");
|
||||
node.attrs.insert(
|
||||
"llm_model".to_string(),
|
||||
"model".to_string(),
|
||||
AttrValue::String("claude-opus-4-6".to_string()),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -2094,8 +2094,8 @@ async fn run_preflight(
|
|||
if !crate::graph::types::is_llm_handler_type(node.handler_type()) {
|
||||
continue;
|
||||
}
|
||||
let node_model = node.llm_model().unwrap_or(&model);
|
||||
let node_provider = node.llm_provider().unwrap_or(default_provider);
|
||||
let node_model = node.model().unwrap_or(&model);
|
||||
let node_provider = node.provider().unwrap_or(default_provider);
|
||||
|
||||
// Resolve through catalog to get canonical model ID and provider
|
||||
let (resolved_model, resolved_provider) =
|
||||
|
|
@ -2106,7 +2106,7 @@ async fn run_preflight(
|
|||
};
|
||||
|
||||
// Use node-level provider override if explicitly set, otherwise catalog provider
|
||||
let final_provider = if node.llm_provider().is_some() {
|
||||
let final_provider = if node.provider().is_some() {
|
||||
node_provider.to_string()
|
||||
} else {
|
||||
resolved_provider
|
||||
|
|
|
|||
|
|
@ -191,13 +191,13 @@ impl Node {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn llm_model(&self) -> Option<&str> {
|
||||
self.str_attr("llm_model")
|
||||
pub fn model(&self) -> Option<&str> {
|
||||
self.str_attr("model")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn llm_provider(&self) -> Option<&str> {
|
||||
self.str_attr("llm_provider")
|
||||
pub fn provider(&self) -> Option<&str> {
|
||||
self.str_attr("provider")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
|
@ -526,8 +526,8 @@ mod tests {
|
|||
assert_eq!(node.thread_id(), None);
|
||||
assert_eq!(node.class(), None);
|
||||
assert_eq!(node.timeout(), None);
|
||||
assert_eq!(node.llm_model(), None);
|
||||
assert_eq!(node.llm_provider(), None);
|
||||
assert_eq!(node.model(), None);
|
||||
assert_eq!(node.provider(), None);
|
||||
assert_eq!(node.reasoning_effort(), "high");
|
||||
assert!(!node.auto_status());
|
||||
assert!(!node.allow_partial());
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ impl Handler for PromptHandler {
|
|||
let system_prompt = if node.project_memory() {
|
||||
let working_dir = services.sandbox.working_directory();
|
||||
let provider = node
|
||||
.llm_provider()
|
||||
.provider()
|
||||
.and_then(|s| s.parse::<Provider>().ok())
|
||||
.unwrap_or(Provider::Anthropic);
|
||||
let docs = fabro_agent::discover_project_docs(
|
||||
|
|
|
|||
|
|
@ -177,8 +177,7 @@ fn parse_declarations(remaining: &mut &str) -> Result<Vec<Declaration>, FabroErr
|
|||
}
|
||||
|
||||
/// Recognized stylesheet properties.
|
||||
const STYLESHEET_PROPERTIES: &[&str] =
|
||||
&["llm_model", "llm_provider", "reasoning_effort", "backend"];
|
||||
const STYLESHEET_PROPERTIES: &[&str] = &["model", "provider", "reasoning_effort", "backend"];
|
||||
|
||||
/// Apply a stylesheet to a graph. Rules are applied by specificity order;
|
||||
/// higher specificity wins. Explicit node attributes are never overridden.
|
||||
|
|
@ -246,26 +245,24 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn parse_universal_rule() {
|
||||
let ss = parse_stylesheet("* { llm_model: claude-sonnet-4-5; llm_provider: anthropic; }")
|
||||
.unwrap();
|
||||
let ss = parse_stylesheet("* { model: claude-sonnet-4-5; provider: anthropic; }").unwrap();
|
||||
assert_eq!(ss.rules.len(), 1);
|
||||
assert_eq!(ss.rules[0].selector, Selector::Universal);
|
||||
assert_eq!(ss.rules[0].declarations.len(), 2);
|
||||
assert_eq!(ss.rules[0].declarations[0].property, "llm_model");
|
||||
assert_eq!(ss.rules[0].declarations[0].property, "model");
|
||||
assert_eq!(ss.rules[0].declarations[0].value, "claude-sonnet-4-5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_class_rule() {
|
||||
let ss = parse_stylesheet(".code { llm_model: claude-opus-4-6; }").unwrap();
|
||||
let ss = parse_stylesheet(".code { model: claude-opus-4-6; }").unwrap();
|
||||
assert_eq!(ss.rules[0].selector, Selector::Class("code".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_id_rule() {
|
||||
let ss =
|
||||
parse_stylesheet("#critical_review { llm_model: gpt-5.2; reasoning_effort: high; }")
|
||||
.unwrap();
|
||||
let ss = parse_stylesheet("#critical_review { model: gpt-5.2; reasoning_effort: high; }")
|
||||
.unwrap();
|
||||
assert_eq!(ss.rules[0].selector, Selector::Id("critical_review".into()));
|
||||
assert_eq!(ss.rules[0].declarations.len(), 2);
|
||||
}
|
||||
|
|
@ -273,9 +270,9 @@ mod tests {
|
|||
#[test]
|
||||
fn parse_multiple_rules() {
|
||||
let input = r"
|
||||
* { llm_model: claude-sonnet-4-5; llm_provider: anthropic; }
|
||||
.code { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
#critical_review { llm_model: gpt-5.2; llm_provider: openai; reasoning_effort: high; }
|
||||
* { model: claude-sonnet-4-5; provider: anthropic; }
|
||||
.code { model: claude-opus-4-6; provider: anthropic; }
|
||||
#critical_review { model: gpt-5.2; provider: openai; reasoning_effort: high; }
|
||||
";
|
||||
let ss = parse_stylesheet(input).unwrap();
|
||||
assert_eq!(ss.rules.len(), 3);
|
||||
|
|
@ -283,37 +280,37 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn parse_error_missing_brace() {
|
||||
let result = parse_stylesheet("* llm_model: test; }");
|
||||
let result = parse_stylesheet("* model: test; }");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_error_missing_selector() {
|
||||
let result = parse_stylesheet("{ llm_model: test; }");
|
||||
let result = parse_stylesheet("{ model: test; }");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_universal_to_all_nodes() {
|
||||
let ss = parse_stylesheet("* { llm_model: sonnet; }").unwrap();
|
||||
let ss = parse_stylesheet("* { model: sonnet; }").unwrap();
|
||||
let mut graph = Graph::new("test");
|
||||
graph.nodes.insert("a".into(), Node::new("a"));
|
||||
graph.nodes.insert("b".into(), Node::new("b"));
|
||||
apply_stylesheet(&ss, &mut graph);
|
||||
|
||||
assert_eq!(
|
||||
graph.nodes["a"].attrs.get("llm_model"),
|
||||
graph.nodes["a"].attrs.get("model"),
|
||||
Some(&AttrValue::String("sonnet".into()))
|
||||
);
|
||||
assert_eq!(
|
||||
graph.nodes["b"].attrs.get("llm_model"),
|
||||
graph.nodes["b"].attrs.get("model"),
|
||||
Some(&AttrValue::String("sonnet".into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_class_overrides_universal() {
|
||||
let ss = parse_stylesheet("* { llm_model: sonnet; } .code { llm_model: opus; }").unwrap();
|
||||
let ss = parse_stylesheet("* { model: sonnet; } .code { model: opus; }").unwrap();
|
||||
let mut graph = Graph::new("test");
|
||||
|
||||
let mut code_node = Node::new("impl");
|
||||
|
|
@ -326,19 +323,18 @@ mod tests {
|
|||
apply_stylesheet(&ss, &mut graph);
|
||||
|
||||
assert_eq!(
|
||||
graph.nodes["impl"].attrs.get("llm_model"),
|
||||
graph.nodes["impl"].attrs.get("model"),
|
||||
Some(&AttrValue::String("opus".into()))
|
||||
);
|
||||
assert_eq!(
|
||||
graph.nodes["plan"].attrs.get("llm_model"),
|
||||
graph.nodes["plan"].attrs.get("model"),
|
||||
Some(&AttrValue::String("sonnet".into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_id_overrides_class() {
|
||||
let ss =
|
||||
parse_stylesheet(".code { llm_model: opus; } #special { llm_model: gpt; }").unwrap();
|
||||
let ss = parse_stylesheet(".code { model: opus; } #special { model: gpt; }").unwrap();
|
||||
let mut graph = Graph::new("test");
|
||||
|
||||
let mut node = Node::new("special");
|
||||
|
|
@ -348,25 +344,25 @@ mod tests {
|
|||
apply_stylesheet(&ss, &mut graph);
|
||||
|
||||
assert_eq!(
|
||||
graph.nodes["special"].attrs.get("llm_model"),
|
||||
graph.nodes["special"].attrs.get("model"),
|
||||
Some(&AttrValue::String("gpt".into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_attrs_not_overridden() {
|
||||
let ss = parse_stylesheet("* { llm_model: sonnet; }").unwrap();
|
||||
let ss = parse_stylesheet("* { model: sonnet; }").unwrap();
|
||||
let mut graph = Graph::new("test");
|
||||
|
||||
let mut node = Node::new("a");
|
||||
node.attrs
|
||||
.insert("llm_model".into(), AttrValue::String("explicit".into()));
|
||||
.insert("model".into(), AttrValue::String("explicit".into()));
|
||||
graph.nodes.insert("a".into(), node);
|
||||
|
||||
apply_stylesheet(&ss, &mut graph);
|
||||
|
||||
assert_eq!(
|
||||
graph.nodes["a"].attrs.get("llm_model"),
|
||||
graph.nodes["a"].attrs.get("model"),
|
||||
Some(&AttrValue::String("explicit".into()))
|
||||
);
|
||||
}
|
||||
|
|
@ -382,9 +378,9 @@ mod tests {
|
|||
#[test]
|
||||
fn spec_section_86_example() {
|
||||
let input = r"
|
||||
* { llm_model: claude-sonnet-4-5; llm_provider: anthropic; }
|
||||
.code { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
#critical_review { llm_model: gpt-5.2; llm_provider: openai; reasoning_effort: high; }
|
||||
* { model: claude-sonnet-4-5; provider: anthropic; }
|
||||
.code { model: claude-opus-4-6; provider: anthropic; }
|
||||
#critical_review { model: gpt-5.2; provider: openai; reasoning_effort: high; }
|
||||
";
|
||||
let ss = parse_stylesheet(input).unwrap();
|
||||
let mut graph = Graph::new("test");
|
||||
|
|
@ -404,21 +400,21 @@ mod tests {
|
|||
apply_stylesheet(&ss, &mut graph);
|
||||
|
||||
assert_eq!(
|
||||
graph.nodes["plan"].attrs.get("llm_model"),
|
||||
graph.nodes["plan"].attrs.get("model"),
|
||||
Some(&AttrValue::String("claude-sonnet-4-5".into()))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
graph.nodes["implement"].attrs.get("llm_model"),
|
||||
graph.nodes["implement"].attrs.get("model"),
|
||||
Some(&AttrValue::String("claude-opus-4-6".into()))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
graph.nodes["critical_review"].attrs.get("llm_model"),
|
||||
graph.nodes["critical_review"].attrs.get("model"),
|
||||
Some(&AttrValue::String("gpt-5.2".into()))
|
||||
);
|
||||
assert_eq!(
|
||||
graph.nodes["critical_review"].attrs.get("llm_provider"),
|
||||
graph.nodes["critical_review"].attrs.get("provider"),
|
||||
Some(&AttrValue::String("openai".into()))
|
||||
);
|
||||
assert_eq!(
|
||||
|
|
@ -429,7 +425,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn parse_shape_selector() {
|
||||
let ss = parse_stylesheet("box { llm_model: opus; }").unwrap();
|
||||
let ss = parse_stylesheet("box { model: opus; }").unwrap();
|
||||
assert_eq!(ss.rules.len(), 1);
|
||||
assert_eq!(ss.rules[0].selector, Selector::Shape("box".into()));
|
||||
assert_eq!(ss.rules[0].declarations[0].value, "opus");
|
||||
|
|
@ -437,7 +433,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn apply_shape_selector_to_matching_nodes() {
|
||||
let ss = parse_stylesheet("box { llm_model: opus; }").unwrap();
|
||||
let ss = parse_stylesheet("box { model: opus; }").unwrap();
|
||||
let mut graph = Graph::new("test");
|
||||
|
||||
// Default shape is "box"
|
||||
|
|
@ -453,42 +449,42 @@ mod tests {
|
|||
apply_stylesheet(&ss, &mut graph);
|
||||
|
||||
assert_eq!(
|
||||
graph.nodes["a"].attrs.get("llm_model"),
|
||||
graph.nodes["a"].attrs.get("model"),
|
||||
Some(&AttrValue::String("opus".into()))
|
||||
);
|
||||
// Mdiamond node should NOT get the box rule
|
||||
assert_eq!(graph.nodes["b"].attrs.get("llm_model"), None);
|
||||
assert_eq!(graph.nodes["b"].attrs.get("model"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_overrides_universal_specificity() {
|
||||
let ss = parse_stylesheet("* { llm_model: sonnet; } box { llm_model: opus; }").unwrap();
|
||||
let ss = parse_stylesheet("* { model: sonnet; } box { model: opus; }").unwrap();
|
||||
let mut graph = Graph::new("test");
|
||||
graph.nodes.insert("a".into(), Node::new("a")); // default shape = box
|
||||
apply_stylesheet(&ss, &mut graph);
|
||||
assert_eq!(
|
||||
graph.nodes["a"].attrs.get("llm_model"),
|
||||
graph.nodes["a"].attrs.get("model"),
|
||||
Some(&AttrValue::String("opus".into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_overrides_shape_specificity() {
|
||||
let ss = parse_stylesheet("box { llm_model: opus; } .fast { llm_model: flash; }").unwrap();
|
||||
let ss = parse_stylesheet("box { model: opus; } .fast { model: flash; }").unwrap();
|
||||
let mut graph = Graph::new("test");
|
||||
let mut node = Node::new("a");
|
||||
node.classes.push("fast".into());
|
||||
graph.nodes.insert("a".into(), node);
|
||||
apply_stylesheet(&ss, &mut graph);
|
||||
assert_eq!(
|
||||
graph.nodes["a"].attrs.get("llm_model"),
|
||||
graph.nodes["a"].attrs.get("model"),
|
||||
Some(&AttrValue::String("flash".into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_overrides_universal_specificity() {
|
||||
let ss = parse_stylesheet("* { llm_model: sonnet; } .special { llm_model: gpt; }").unwrap();
|
||||
let ss = parse_stylesheet("* { model: sonnet; } .special { model: gpt; }").unwrap();
|
||||
let mut graph = Graph::new("test");
|
||||
|
||||
let mut node_a = Node::new("a");
|
||||
|
|
@ -502,12 +498,12 @@ mod tests {
|
|||
|
||||
// .special (specificity 1) overrides * (specificity 0)
|
||||
assert_eq!(
|
||||
graph.nodes["a"].attrs.get("llm_model"),
|
||||
graph.nodes["a"].attrs.get("model"),
|
||||
Some(&AttrValue::String("gpt".into()))
|
||||
);
|
||||
// No class, gets universal
|
||||
assert_eq!(
|
||||
graph.nodes["b"].attrs.get("llm_model"),
|
||||
graph.nodes["b"].attrs.get("model"),
|
||||
Some(&AttrValue::String("sonnet".into()))
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,6 +112,29 @@ impl Transform for StylesheetApplicationTransform {
|
|||
}
|
||||
}
|
||||
|
||||
/// For nodes with `model` but no `provider`, infer the provider from the model catalog.
|
||||
pub struct ProviderInferenceTransform;
|
||||
|
||||
impl Transform for ProviderInferenceTransform {
|
||||
fn apply(&self, graph: &mut Graph) {
|
||||
for node in graph.nodes.values_mut() {
|
||||
let model = node
|
||||
.attrs
|
||||
.get("model")
|
||||
.and_then(AttrValue::as_str)
|
||||
.map(String::from);
|
||||
if let Some(model) = model {
|
||||
if !node.attrs.contains_key("provider") {
|
||||
if let Some(info) = fabro_llm::catalog::get_model_info(&model) {
|
||||
node.attrs
|
||||
.insert("provider".to_string(), AttrValue::String(info.provider));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a potential `@path` file reference.
|
||||
///
|
||||
/// If `value` starts with `@` and the referenced file exists locally, the file
|
||||
|
|
@ -479,7 +502,7 @@ mod tests {
|
|||
);
|
||||
primary.attrs.insert(
|
||||
"model_stylesheet".to_string(),
|
||||
AttrValue::String("* { llm_model: sonnet; }".to_string()),
|
||||
AttrValue::String("* { model: sonnet; }".to_string()),
|
||||
);
|
||||
|
||||
let mut secondary = Graph::new("sub");
|
||||
|
|
@ -493,7 +516,7 @@ mod tests {
|
|||
transform.apply(&mut primary);
|
||||
|
||||
assert_eq!(primary.goal(), "Build feature");
|
||||
assert_eq!(primary.model_stylesheet(), "* { llm_model: sonnet; }");
|
||||
assert_eq!(primary.model_stylesheet(), "* { model: sonnet; }");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -592,6 +615,82 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ProviderInferenceTransform tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn provider_inference_sets_provider_from_catalog() {
|
||||
let mut graph = Graph::new("test");
|
||||
let mut node = Node::new("a");
|
||||
node.attrs.insert(
|
||||
"model".to_string(),
|
||||
AttrValue::String("claude-sonnet-4-5".to_string()),
|
||||
);
|
||||
graph.nodes.insert("a".to_string(), node);
|
||||
|
||||
ProviderInferenceTransform.apply(&mut graph);
|
||||
|
||||
assert_eq!(
|
||||
graph.nodes["a"]
|
||||
.attrs
|
||||
.get("provider")
|
||||
.and_then(AttrValue::as_str),
|
||||
Some("anthropic")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_inference_does_not_override_explicit_provider() {
|
||||
let mut graph = Graph::new("test");
|
||||
let mut node = Node::new("a");
|
||||
node.attrs.insert(
|
||||
"model".to_string(),
|
||||
AttrValue::String("claude-sonnet-4-5".to_string()),
|
||||
);
|
||||
node.attrs.insert(
|
||||
"provider".to_string(),
|
||||
AttrValue::String("custom".to_string()),
|
||||
);
|
||||
graph.nodes.insert("a".to_string(), node);
|
||||
|
||||
ProviderInferenceTransform.apply(&mut graph);
|
||||
|
||||
assert_eq!(
|
||||
graph.nodes["a"]
|
||||
.attrs
|
||||
.get("provider")
|
||||
.and_then(AttrValue::as_str),
|
||||
Some("custom")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_inference_unknown_model_leaves_no_provider() {
|
||||
let mut graph = Graph::new("test");
|
||||
let mut node = Node::new("a");
|
||||
node.attrs.insert(
|
||||
"model".to_string(),
|
||||
AttrValue::String("unknown-model-xyz".to_string()),
|
||||
);
|
||||
graph.nodes.insert("a".to_string(), node);
|
||||
|
||||
ProviderInferenceTransform.apply(&mut graph);
|
||||
|
||||
assert_eq!(graph.nodes["a"].attrs.get("provider"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_inference_no_model_no_change() {
|
||||
let mut graph = Graph::new("test");
|
||||
let node = Node::new("a");
|
||||
graph.nodes.insert("a".to_string(), node);
|
||||
|
||||
ProviderInferenceTransform.apply(&mut graph);
|
||||
|
||||
assert_eq!(graph.nodes["a"].attrs.get("provider"), None);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// resolve_file_ref tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -929,7 +929,7 @@ impl LintRule for StylesheetModelKnownRule {
|
|||
let label = Self::selector_label(&rule.selector);
|
||||
for decl in &rule.declarations {
|
||||
match decl.property.as_str() {
|
||||
"llm_model" => {
|
||||
"model" => {
|
||||
if fabro_llm::catalog::get_model_info(&decl.value).is_none() {
|
||||
diagnostics.push(Diagnostic {
|
||||
rule: self.name().to_string(),
|
||||
|
|
@ -944,7 +944,7 @@ impl LintRule for StylesheetModelKnownRule {
|
|||
});
|
||||
}
|
||||
}
|
||||
"llm_provider" => {
|
||||
"provider" => {
|
||||
if fabro_llm::Provider::from_str(&decl.value).is_err() {
|
||||
let valid: Vec<&str> = fabro_llm::Provider::ALL
|
||||
.iter()
|
||||
|
|
@ -1432,7 +1432,7 @@ mod tests {
|
|||
let mut g = minimal_graph();
|
||||
g.attrs.insert(
|
||||
"model_stylesheet".to_string(),
|
||||
AttrValue::String("* { llm_model: foo;".to_string()),
|
||||
AttrValue::String("* { model: foo;".to_string()),
|
||||
);
|
||||
let rule = StylesheetSyntaxRule;
|
||||
let d = rule.apply(&g);
|
||||
|
|
@ -1445,7 +1445,7 @@ mod tests {
|
|||
let mut g = minimal_graph();
|
||||
g.attrs.insert(
|
||||
"model_stylesheet".to_string(),
|
||||
AttrValue::String("* { llm_model: foo; }".to_string()),
|
||||
AttrValue::String("* { model: foo; }".to_string()),
|
||||
);
|
||||
let rule = StylesheetSyntaxRule;
|
||||
let d = rule.apply(&g);
|
||||
|
|
@ -2228,8 +2228,7 @@ mod tests {
|
|||
g.attrs.insert(
|
||||
"model_stylesheet".to_string(),
|
||||
AttrValue::String(
|
||||
"* { llm_model: gpt-4; } .fast { llm_model: gpt-3.5; reasoning_effort: low; }"
|
||||
.to_string(),
|
||||
"* { model: gpt-4; } .fast { model: gpt-3.5; reasoning_effort: low; }".to_string(),
|
||||
),
|
||||
);
|
||||
let rule = StylesheetSyntaxRule;
|
||||
|
|
@ -2836,9 +2835,7 @@ mod tests {
|
|||
let mut g = minimal_graph();
|
||||
g.attrs.insert(
|
||||
"model_stylesheet".to_string(),
|
||||
AttrValue::String(
|
||||
"* { llm_model: claude-sonnet-4-5; llm_provider: anthropic; }".to_string(),
|
||||
),
|
||||
AttrValue::String("* { model: claude-sonnet-4-5; provider: anthropic; }".to_string()),
|
||||
);
|
||||
let rule = StylesheetModelKnownRule;
|
||||
let d = rule.apply(&g);
|
||||
|
|
@ -2850,7 +2847,7 @@ mod tests {
|
|||
let mut g = minimal_graph();
|
||||
g.attrs.insert(
|
||||
"model_stylesheet".to_string(),
|
||||
AttrValue::String("#opus { llm_model: claude-opus-4-5; }".to_string()),
|
||||
AttrValue::String("#opus { model: claude-opus-4-5; }".to_string()),
|
||||
);
|
||||
let rule = StylesheetModelKnownRule;
|
||||
let d = rule.apply(&g);
|
||||
|
|
@ -2865,7 +2862,7 @@ mod tests {
|
|||
let mut g = minimal_graph();
|
||||
g.attrs.insert(
|
||||
"model_stylesheet".to_string(),
|
||||
AttrValue::String("* { llm_provider: google; }".to_string()),
|
||||
AttrValue::String("* { provider: google; }".to_string()),
|
||||
);
|
||||
let rule = StylesheetModelKnownRule;
|
||||
let d = rule.apply(&g);
|
||||
|
|
@ -2879,7 +2876,7 @@ mod tests {
|
|||
let mut g = minimal_graph();
|
||||
g.attrs.insert(
|
||||
"model_stylesheet".to_string(),
|
||||
AttrValue::String("* { llm_model: opus; }".to_string()),
|
||||
AttrValue::String("* { model: opus; }".to_string()),
|
||||
);
|
||||
let rule = StylesheetModelKnownRule;
|
||||
let d = rule.apply(&g);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ use std::path::Path;
|
|||
use crate::error::FabroError;
|
||||
use crate::graph::Graph;
|
||||
use crate::transform::{
|
||||
FileInliningTransform, StylesheetApplicationTransform, Transform, VariableExpansionTransform,
|
||||
FileInliningTransform, ProviderInferenceTransform, StylesheetApplicationTransform, Transform,
|
||||
VariableExpansionTransform,
|
||||
};
|
||||
use crate::validation::{self, Diagnostic};
|
||||
|
||||
|
|
@ -60,6 +61,7 @@ impl WorkflowBuilder {
|
|||
// Built-in transforms (PreambleTransform moved to engine execution time)
|
||||
VariableExpansionTransform.apply(&mut graph);
|
||||
StylesheetApplicationTransform.apply(&mut graph);
|
||||
ProviderInferenceTransform.apply(&mut graph);
|
||||
|
||||
// File inlining when base_dir is provided
|
||||
if let Some(dir) = base_dir {
|
||||
|
|
@ -148,7 +150,7 @@ mod tests {
|
|||
#[test]
|
||||
fn prepare_from_source_applies_stylesheet() {
|
||||
let dot = r#"digraph Test {
|
||||
graph [goal="Test", model_stylesheet="* { llm_model: sonnet; }"]
|
||||
graph [goal="Test", model_stylesheet="* { model: sonnet; }"]
|
||||
start [shape=Mdiamond]
|
||||
work [label="Work"]
|
||||
exit [shape=Msquare]
|
||||
|
|
@ -156,7 +158,7 @@ mod tests {
|
|||
}"#;
|
||||
let graph = prepare_from_source(dot).unwrap();
|
||||
assert_eq!(
|
||||
graph.nodes["work"].attrs.get("llm_model"),
|
||||
graph.nodes["work"].attrs.get("model"),
|
||||
Some(&AttrValue::String("sonnet".into()))
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ fn parse_attractor_batch_clean() {
|
|||
|
||||
#[test]
|
||||
fn parse_attractor_batch_has_errors() {
|
||||
// This file is intentionally missing llm_provider on the work node.
|
||||
// This file is intentionally missing provider on the work node.
|
||||
// It should still parse successfully — validation is separate from parsing.
|
||||
let graph = parse_attractor_dot("batch_has_errors.dot").unwrap();
|
||||
assert_eq!(graph.nodes.len(), 3);
|
||||
|
|
|
|||
|
|
@ -809,9 +809,9 @@ fn variable_expansion_replaces_goal_in_prompts() {
|
|||
#[test]
|
||||
fn stylesheet_application_by_specificity() {
|
||||
let stylesheet_text = r"
|
||||
* { llm_model: claude-sonnet-4-5; llm_provider: anthropic; }
|
||||
.code { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
#critical_review { llm_model: gpt-5.2; llm_provider: openai; reasoning_effort: high; }
|
||||
* { model: claude-sonnet-4-5; provider: anthropic; }
|
||||
.code { model: claude-opus-4-6; provider: anthropic; }
|
||||
#critical_review { model: gpt-5.2; provider: openai; reasoning_effort: high; }
|
||||
";
|
||||
|
||||
let mut graph = Graph::new("test");
|
||||
|
|
@ -834,10 +834,10 @@ fn stylesheet_application_by_specificity() {
|
|||
critical.classes.push("code".to_string());
|
||||
graph.nodes.insert("critical_review".to_string(), critical);
|
||||
|
||||
// explicit node: has explicit llm_model, should NOT be overridden
|
||||
// explicit node: has explicit model, should NOT be overridden
|
||||
let mut explicit = Node::new("explicit_node");
|
||||
explicit.attrs.insert(
|
||||
"llm_model".to_string(),
|
||||
"model".to_string(),
|
||||
AttrValue::String("my-custom-model".to_string()),
|
||||
);
|
||||
graph.nodes.insert("explicit_node".to_string(), explicit);
|
||||
|
|
@ -847,31 +847,31 @@ fn stylesheet_application_by_specificity() {
|
|||
|
||||
// plan: universal -> claude-sonnet-4-5
|
||||
assert_eq!(
|
||||
graph.nodes["plan"].attrs.get("llm_model"),
|
||||
graph.nodes["plan"].attrs.get("model"),
|
||||
Some(&AttrValue::String("claude-sonnet-4-5".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
graph.nodes["plan"].attrs.get("llm_provider"),
|
||||
graph.nodes["plan"].attrs.get("provider"),
|
||||
Some(&AttrValue::String("anthropic".to_string()))
|
||||
);
|
||||
|
||||
// implement: .code -> claude-opus-4-6
|
||||
assert_eq!(
|
||||
graph.nodes["implement"].attrs.get("llm_model"),
|
||||
graph.nodes["implement"].attrs.get("model"),
|
||||
Some(&AttrValue::String("claude-opus-4-6".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
graph.nodes["implement"].attrs.get("llm_provider"),
|
||||
graph.nodes["implement"].attrs.get("provider"),
|
||||
Some(&AttrValue::String("anthropic".to_string()))
|
||||
);
|
||||
|
||||
// critical_review: #critical_review -> gpt-5.2 (id overrides class)
|
||||
assert_eq!(
|
||||
graph.nodes["critical_review"].attrs.get("llm_model"),
|
||||
graph.nodes["critical_review"].attrs.get("model"),
|
||||
Some(&AttrValue::String("gpt-5.2".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
graph.nodes["critical_review"].attrs.get("llm_provider"),
|
||||
graph.nodes["critical_review"].attrs.get("provider"),
|
||||
Some(&AttrValue::String("openai".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
|
|
@ -881,7 +881,7 @@ fn stylesheet_application_by_specificity() {
|
|||
|
||||
// explicit_node: explicit attr NOT overridden by universal
|
||||
assert_eq!(
|
||||
graph.nodes["explicit_node"].attrs.get("llm_model"),
|
||||
graph.nodes["explicit_node"].attrs.get("model"),
|
||||
Some(&AttrValue::String("my-custom-model".to_string()))
|
||||
);
|
||||
}
|
||||
|
|
@ -891,7 +891,7 @@ fn stylesheet_application_via_parsed_graph() {
|
|||
let input = r#"digraph StyleTest {
|
||||
graph [
|
||||
goal="Test stylesheet",
|
||||
model_stylesheet="* { llm_model: sonnet; }"
|
||||
model_stylesheet="* { model: sonnet; }"
|
||||
]
|
||||
start [shape=Mdiamond]
|
||||
exit [shape=Msquare]
|
||||
|
|
@ -905,24 +905,24 @@ fn stylesheet_application_via_parsed_graph() {
|
|||
let transform = StylesheetApplicationTransform;
|
||||
transform.apply(&mut graph);
|
||||
|
||||
// All nodes without explicit llm_model should get "sonnet"
|
||||
// All nodes without explicit model should get "sonnet"
|
||||
assert_eq!(
|
||||
graph.nodes["work"].attrs.get("llm_model"),
|
||||
graph.nodes["work"].attrs.get("model"),
|
||||
Some(&AttrValue::String("sonnet".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
graph.nodes["start"].attrs.get("llm_model"),
|
||||
graph.nodes["start"].attrs.get("model"),
|
||||
Some(&AttrValue::String("sonnet".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
graph.nodes["exit"].attrs.get("llm_model"),
|
||||
graph.nodes["exit"].attrs.get("model"),
|
||||
Some(&AttrValue::String("sonnet".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylesheet_parse_and_apply_directly() {
|
||||
let stylesheet_text = "* { llm_model: base; } .fast { llm_model: turbo; }";
|
||||
let stylesheet_text = "* { model: base; } .fast { model: turbo; }";
|
||||
let stylesheet = parse_stylesheet(stylesheet_text).expect("stylesheet parse should succeed");
|
||||
assert_eq!(stylesheet.rules.len(), 2);
|
||||
|
||||
|
|
@ -937,11 +937,11 @@ fn stylesheet_parse_and_apply_directly() {
|
|||
apply_stylesheet(&stylesheet, &mut graph);
|
||||
|
||||
assert_eq!(
|
||||
graph.nodes["a"].attrs.get("llm_model"),
|
||||
graph.nodes["a"].attrs.get("model"),
|
||||
Some(&AttrValue::String("base".to_string()))
|
||||
);
|
||||
assert_eq!(
|
||||
graph.nodes["b"].attrs.get("llm_model"),
|
||||
graph.nodes["b"].attrs.get("model"),
|
||||
Some(&AttrValue::String("turbo".to_string()))
|
||||
);
|
||||
}
|
||||
|
|
@ -3341,7 +3341,7 @@ async fn stylesheet_applies_model_override() {
|
|||
let input = r#"digraph StylesheetTest {
|
||||
graph [
|
||||
goal="Test stylesheet",
|
||||
model_stylesheet="* { llm_model: custom-model; }"
|
||||
model_stylesheet="* { model: custom-model; }"
|
||||
]
|
||||
start [shape=Mdiamond]
|
||||
exit [shape=Msquare]
|
||||
|
|
@ -3351,7 +3351,7 @@ async fn stylesheet_applies_model_override() {
|
|||
let mut graph = parse(input).expect("parse");
|
||||
validate_or_raise(&graph, &[]).expect("validate");
|
||||
StylesheetApplicationTransform.apply(&mut graph);
|
||||
assert_eq!(graph.nodes["work"].llm_model(), Some("custom-model"));
|
||||
assert_eq!(graph.nodes["work"].model(), Some("custom-model"));
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let engine = WorkflowRunEngine::new(
|
||||
|
|
@ -3453,7 +3453,7 @@ async fn integration_smoke_plan_implement_review_done() {
|
|||
let dot = r#"digraph SmokeIntegration {
|
||||
graph [
|
||||
goal="Build the feature",
|
||||
model_stylesheet="* { llm_model: test-model; }"
|
||||
model_stylesheet="* { model: test-model; }"
|
||||
]
|
||||
rankdir=LR
|
||||
start [shape=Mdiamond]
|
||||
|
|
@ -3484,7 +3484,7 @@ async fn integration_smoke_plan_implement_review_done() {
|
|||
graph.nodes["plan"].prompt().unwrap(),
|
||||
"Plan: Build the feature"
|
||||
);
|
||||
assert_eq!(graph.nodes["plan"].llm_model(), Some("test-model"));
|
||||
assert_eq!(graph.nodes["plan"].model(), Some("test-model"));
|
||||
|
||||
// Run pipeline
|
||||
let interviewer = Arc::new(AutoApproveInterviewer);
|
||||
|
|
@ -6530,7 +6530,7 @@ mod real_llm {
|
|||
),
|
||||
);
|
||||
classify.attrs.insert(
|
||||
"llm_model".to_string(),
|
||||
"model".to_string(),
|
||||
AttrValue::String("claude-haiku-4-5".to_string()),
|
||||
);
|
||||
graph.nodes.insert("classify".to_string(), classify);
|
||||
|
|
@ -9910,7 +9910,7 @@ async fn cli_backend_run_uses_node_model_override() {
|
|||
|
||||
let mut node = Node::new("step");
|
||||
node.attrs.insert(
|
||||
"llm_model".to_string(),
|
||||
"model".to_string(),
|
||||
AttrValue::String("claude-sonnet-4-5".to_string()),
|
||||
);
|
||||
|
||||
|
|
@ -9956,11 +9956,11 @@ async fn cli_backend_run_uses_node_provider_override() {
|
|||
|
||||
let mut node = Node::new("step");
|
||||
node.attrs.insert(
|
||||
"llm_provider".to_string(),
|
||||
"provider".to_string(),
|
||||
AttrValue::String("openai".to_string()),
|
||||
);
|
||||
node.attrs.insert(
|
||||
"llm_model".to_string(),
|
||||
"model".to_string(),
|
||||
AttrValue::String("gpt-5.3-codex".to_string()),
|
||||
);
|
||||
|
||||
|
|
@ -10133,7 +10133,7 @@ async fn backend_router_delegates_to_cli_for_backend_attr() {
|
|||
node.attrs
|
||||
.insert("backend".to_string(), AttrValue::String("cli".to_string()));
|
||||
node.attrs.insert(
|
||||
"llm_provider".to_string(),
|
||||
"provider".to_string(),
|
||||
AttrValue::String("openai".to_string()),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -81,9 +81,9 @@ Use `model_stylesheet` for model assignment rather than per-node attributes:
|
|||
|
||||
```dot
|
||||
graph [model_stylesheet="
|
||||
* { llm_model: claude-sonnet-4-6; llm_provider: anthropic; }
|
||||
.coding { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
.review { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; }
|
||||
* { model: claude-sonnet-4-6;}
|
||||
.coding { model: claude-opus-4-6;}
|
||||
.review { model: gemini-3.1-pro-preview;}
|
||||
"]
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ Comments: `//` line and `/* */` block.
|
|||
|
||||
Multi-turn LLM with tools (shell, read_file, write_file, grep, glob, web_search, web_fetch, edit_file).
|
||||
|
||||
Key attributes: `prompt`, `reasoning_effort` (low/medium/high), `max_tokens`, `fidelity`, `thread_id`, `timeout`, `backend` (api or cli), `llm_model`, `llm_provider`, `project_memory`.
|
||||
Key attributes: `prompt`, `reasoning_effort` (low/medium/high), `max_tokens`, `fidelity`, `thread_id`, `timeout`, `backend` (api or cli), `model`, `provider`, `project_memory`.
|
||||
|
||||
### Prompt Nodes (tab)
|
||||
|
||||
|
|
@ -156,17 +156,17 @@ CSS-like syntax for assigning models to nodes:
|
|||
|
||||
```dot
|
||||
graph [model_stylesheet="
|
||||
* { llm_model: claude-haiku-4-5; llm_provider: anthropic; }
|
||||
.coding { llm_model: claude-sonnet-4-6; llm_provider: anthropic; }
|
||||
#review { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; }
|
||||
* { model: claude-haiku-4-5;}
|
||||
.coding { model: claude-sonnet-4-6;}
|
||||
#review { model: gemini-3.1-pro-preview;}
|
||||
"]
|
||||
```
|
||||
|
||||
Selectors by specificity (low to high): `*` (universal, 0), shape name (1), `.class` (2), `#nodeid` (3). Higher specificity wins. Same specificity: last rule wins. Explicit node attributes override stylesheets.
|
||||
|
||||
Properties: `llm_model`, `llm_provider`, `reasoning_effort`, `backend`.
|
||||
Properties: `model`, `provider`, `reasoning_effort`, `backend`.
|
||||
|
||||
**Critical:** Use semicolons between properties (e.g. `llm_model: foo; llm_provider: bar;`).
|
||||
**Critical:** Use semicolons between properties (e.g. `model: foo; provider: bar;`).
|
||||
|
||||
## Variables
|
||||
|
||||
|
|
|
|||
|
|
@ -151,9 +151,9 @@ digraph MultiModel {
|
|||
graph [
|
||||
goal="Build and review a utility function using multiple models",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-haiku-4-5; llm_provider: anthropic; reasoning_effort: low; }
|
||||
.coding { llm_model: claude-sonnet-4-6; llm_provider: anthropic; reasoning_effort: high; }
|
||||
#review { llm_model: claude-sonnet-4-6; llm_provider: anthropic; reasoning_effort: high; }
|
||||
* { model: claude-haiku-4-5;reasoning_effort: low; }
|
||||
.coding { model: claude-sonnet-4-6;reasoning_effort: high; }
|
||||
#review { model: claude-sonnet-4-6;reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
rankdir=LR
|
||||
|
|
@ -179,10 +179,10 @@ digraph Ensemble {
|
|||
graph [
|
||||
goal="Get independent opinions from multiple providers, then synthesize",
|
||||
model_stylesheet="
|
||||
#opus { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
#gemini { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; }
|
||||
#codex { llm_model: gpt-5.3-codex; llm_provider: openai; }
|
||||
#synth { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; }
|
||||
#opus { model: claude-opus-4-6; }
|
||||
#gemini { model: gemini-3.1-pro-preview;}
|
||||
#codex { model: gpt-5.3-codex; }
|
||||
#synth { model: claude-opus-4-6; reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
rankdir=LR
|
||||
|
|
@ -219,7 +219,7 @@ digraph ImplementAndSimplify {
|
|||
graph [
|
||||
goal="Implement and simplify",
|
||||
model_stylesheet="
|
||||
* { backend: api; llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
* { backend: api; model: claude-opus-4-6;}
|
||||
"
|
||||
]
|
||||
rankdir=LR
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
digraph G {
|
||||
start [shape=Mdiamond]
|
||||
exit [shape=Msquare]
|
||||
work [shape=box, llm_provider=openai, llm_model=gpt-5.2, prompt="Do the work."]
|
||||
work [shape=box, model=gpt-5.2, prompt="Do the work."]
|
||||
start -> work -> exit
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
digraph G {
|
||||
start [shape=Mdiamond]
|
||||
exit [shape=Msquare]
|
||||
work [shape=box, llm_model=gpt-5.2, prompt="Do the work."]
|
||||
work [shape=box, model=gpt-5.2, prompt="Do the work."]
|
||||
start -> work -> exit
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
digraph G {
|
||||
start [shape=Mdiamond]
|
||||
exit [shape=Msquare]
|
||||
work [shape=box, llm_provider=openai, llm_model=gpt-5.2]
|
||||
work [shape=box, model=gpt-5.2]
|
||||
start -> work -> exit
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,118 +12,118 @@ digraph Workflow {
|
|||
|
||||
Start [
|
||||
node_type="start", label="Start", shape="Mdiamond", style="rounded,filled",
|
||||
is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex",
|
||||
is_codergen="true", model="gpt-5.2-codex",
|
||||
max_agent_turns="4", timeout="300"
|
||||
];
|
||||
|
||||
CheckDoD [
|
||||
node_type="stack.steer", label="Check DoD", shape="box", style="rounded,filled",
|
||||
is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex",
|
||||
is_codergen="true", model="gpt-5.2-codex",
|
||||
max_agent_turns="2", timeout="120",
|
||||
llm_prompt="Check if definition of done provided.\n\nTASK: $task\nDOD: $definition_of_done\n\nWrite status.json with outcome=needs_dod if DOD is empty or just a placeholder, else outcome=has_dod if a real DOD was provided."
|
||||
];
|
||||
|
||||
DefineDoD_Gemini [
|
||||
node_type="stack.observe", label="Define DoD (Gemini)", shape="box", style="rounded,filled",
|
||||
is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex",
|
||||
is_codergen="true", model="gpt-5.2-codex",
|
||||
max_agent_turns="6", timeout="300",
|
||||
llm_prompt="Propose definition of done.\n\nTASK: $task\n\nWrite to .ai/dod_gemini.md"
|
||||
];
|
||||
|
||||
DefineDoD_GPT [
|
||||
node_type="stack.observe", label="Define DoD (GPT)", shape="box", style="rounded,filled",
|
||||
is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex",
|
||||
is_codergen="true", model="gpt-5.2-codex",
|
||||
max_agent_turns="6", timeout="300", reasoning_effort="high",
|
||||
llm_prompt="Propose definition of done.\n\nTASK: $task\n\nWrite to .ai/dod_gpt.md"
|
||||
];
|
||||
|
||||
DefineDoD_Opus [
|
||||
node_type="stack.observe", label="Define DoD (Opus)", shape="box", style="rounded,filled",
|
||||
is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex",
|
||||
is_codergen="true", model="gpt-5.2-codex",
|
||||
max_agent_turns="6", timeout="300",
|
||||
llm_prompt="Propose definition of done.\n\nTASK: $task\n\nWrite to .ai/dod_opus.md"
|
||||
];
|
||||
|
||||
ConsolidateDoD [
|
||||
node_type="stack.observe", label="Consolidate DoD", shape="box", style="rounded,filled",
|
||||
is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex",
|
||||
is_codergen="true", model="gpt-5.2-codex",
|
||||
max_agent_turns="8", timeout="420",
|
||||
llm_prompt="Synthesize three DoD proposals.\n\nTASK: $task\n\nRead .ai/dod_*.md, write consensus to .ai/definition_of_done.md"
|
||||
];
|
||||
|
||||
PlanGemini [
|
||||
node_type="stack.observe", label="Plan (Gemini)", shape="box", style="rounded,filled",
|
||||
is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex",
|
||||
is_codergen="true", model="gpt-5.2-codex",
|
||||
max_agent_turns="8", timeout="420",
|
||||
llm_prompt="Create implementation plan.\n\nTASK: $task\nDOD: .ai/definition_of_done.md or $definition_of_done\n\nWrite to .ai/plan_gemini.md"
|
||||
];
|
||||
|
||||
PlanGPT [
|
||||
node_type="stack.observe", label="Plan (GPT)", shape="box", style="rounded,filled",
|
||||
is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex",
|
||||
is_codergen="true", model="gpt-5.2-codex",
|
||||
max_agent_turns="8", timeout="420", reasoning_effort="high",
|
||||
llm_prompt="Create implementation plan.\n\nTASK: $task\nDOD: .ai/definition_of_done.md or $definition_of_done\n\nWrite to .ai/plan_gpt.md"
|
||||
];
|
||||
|
||||
PlanOpus [
|
||||
node_type="stack.observe", label="Plan (Opus)", shape="box", style="rounded,filled",
|
||||
is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex",
|
||||
is_codergen="true", model="gpt-5.2-codex",
|
||||
max_agent_turns="8", timeout="420",
|
||||
llm_prompt="Create implementation plan.\n\nTASK: $task\nDOD: .ai/definition_of_done.md or $definition_of_done\n\nWrite to .ai/plan_opus.md"
|
||||
];
|
||||
|
||||
DebateConsolidate [
|
||||
node_type="stack.observe", label="Debate & Consolidate", shape="box", style="rounded,filled",
|
||||
is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex",
|
||||
is_codergen="true", model="gpt-5.2-codex",
|
||||
max_agent_turns="10", timeout="600",
|
||||
llm_prompt="Synthesize three plans.\n\nTASK: $task\n\nRead .ai/plan_*.md, write final to .ai/plan_final.md"
|
||||
];
|
||||
|
||||
Implement [
|
||||
node_type="stack.observe", label="Implement (Opus)", shape="box", style="rounded,filled",
|
||||
is_codergen="true", allow_partial="true", llm_provider="openai", llm_model="gpt-5.2-codex",
|
||||
is_codergen="true", allow_partial="true", model="gpt-5.2-codex",
|
||||
max_agent_turns="25", timeout="1200",
|
||||
llm_prompt="Execute plan.\n\nTASK: $task\n\nFollow .ai/plan_final.md. Log to .ai/implementation_log.md"
|
||||
];
|
||||
|
||||
ReviewGemini [
|
||||
node_type="stack.observe", label="Review (Gemini)", shape="box", style="rounded,filled",
|
||||
is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex",
|
||||
is_codergen="true", model="gpt-5.2-codex",
|
||||
max_agent_turns="6", timeout="300",
|
||||
llm_prompt="Review implementation.\n\nTASK: $task\n\nWrite to .ai/review_gemini.md with PASS/FAIL"
|
||||
];
|
||||
|
||||
ReviewGPT [
|
||||
node_type="stack.observe", label="Review (GPT)", shape="box", style="rounded,filled",
|
||||
is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex",
|
||||
is_codergen="true", model="gpt-5.2-codex",
|
||||
max_agent_turns="6", timeout="300", reasoning_effort="high",
|
||||
llm_prompt="Review implementation.\n\nTASK: $task\n\nWrite to .ai/review_gpt.md with PASS/FAIL"
|
||||
];
|
||||
|
||||
ReviewOpus [
|
||||
node_type="stack.observe", label="Review (Opus)", shape="box", style="rounded,filled",
|
||||
is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex",
|
||||
is_codergen="true", model="gpt-5.2-codex",
|
||||
max_agent_turns="6", timeout="300",
|
||||
llm_prompt="Review implementation.\n\nTASK: $task\n\nWrite to .ai/review_opus.md with PASS/FAIL"
|
||||
];
|
||||
|
||||
ReviewConsensus [
|
||||
node_type="stack.steer", label="Review Consensus", shape="box", style="rounded,filled",
|
||||
is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex",
|
||||
is_codergen="true", model="gpt-5.2-codex",
|
||||
max_agent_turns="6", timeout="300",
|
||||
llm_prompt="Reach consensus.\n\nRead .ai/review_*.md\nWrite to .ai/review_consensus.md\n\noutcome=yes if PASS, outcome=retry if FAIL. Set preferred_next_label to \"\" (empty) or to \"yes\"/\"retry\"; do not use \"No\"."
|
||||
];
|
||||
|
||||
Postmortem [
|
||||
node_type="stack.observe", label="Postmortem", shape="box", style="rounded,filled",
|
||||
is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex",
|
||||
is_codergen="true", model="gpt-5.2-codex",
|
||||
max_agent_turns="8", timeout="420",
|
||||
llm_prompt="Failure postmortem.\n\nTASK: $task\n\nWrite to .ai/postmortem_NN.md"
|
||||
];
|
||||
|
||||
Exit [
|
||||
node_type="exit", label="Exit", shape="Msquare", style="rounded,filled",
|
||||
is_codergen="true", llm_provider="openai", llm_model="gpt-5.2-codex",
|
||||
is_codergen="true", model="gpt-5.2-codex",
|
||||
max_agent_turns="4", timeout="120"
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ digraph dttf {
|
|||
default_max_retry=3,
|
||||
retry_target="impl_setup",
|
||||
model_stylesheet="
|
||||
* { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: medium; }
|
||||
.hard { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; }
|
||||
.verify { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: medium; }
|
||||
.review { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; }
|
||||
* { model: gpt-5.2-codex;reasoning_effort: medium; }
|
||||
.hard { model: gpt-5.2-codex;reasoning_effort: high; }
|
||||
.verify { model: gpt-5.2-codex;reasoning_effort: medium; }
|
||||
.review { model: gpt-5.2-codex;reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ digraph linkcheck {
|
|||
default_max_retry=3,
|
||||
retry_target="impl_setup",
|
||||
model_stylesheet="
|
||||
* { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: medium; }
|
||||
.hard { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; }
|
||||
.verify { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: medium; }
|
||||
.review { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; }
|
||||
* { model: gpt-5.2-codex;reasoning_effort: medium; }
|
||||
.hard { model: gpt-5.2-codex;reasoning_effort: high; }
|
||||
.verify { model: gpt-5.2-codex;reasoning_effort: medium; }
|
||||
.review { model: gpt-5.2-codex;reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ digraph solitaire {
|
|||
default_max_retry=3,
|
||||
retry_target="impl_setup",
|
||||
model_stylesheet="
|
||||
* { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: medium; }
|
||||
.hard { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; }
|
||||
.verify { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: medium; }
|
||||
.review { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; }
|
||||
* { model: gpt-5.2-codex;reasoning_effort: medium; }
|
||||
.hard { model: gpt-5.2-codex;reasoning_effort: high; }
|
||||
.verify { model: gpt-5.2-codex;reasoning_effort: medium; }
|
||||
.review { model: gpt-5.2-codex;reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ digraph dttf {
|
|||
retry_target="impl_setup",
|
||||
fallback_retry_target="impl_loader",
|
||||
model_stylesheet="
|
||||
* { llm_model: gemini-3-flash-preview; llm_provider: google; reasoning_effort: medium; }
|
||||
.hard { llm_model: gemini-3-flash-preview; llm_provider: google; reasoning_effort: high; }
|
||||
.verify { llm_model: gemini-3-flash-preview; llm_provider: google; reasoning_effort: medium; }
|
||||
.review { llm_model: gemini-3-flash-preview; llm_provider: google; reasoning_effort: high; }
|
||||
* { model: gemini-3-flash-preview; provider: google; reasoning_effort: medium; }
|
||||
.hard { model: gemini-3-flash-preview; provider: google; reasoning_effort: high; }
|
||||
.verify { model: gemini-3-flash-preview; provider: google; reasoning_effort: medium; }
|
||||
.review { model: gemini-3-flash-preview; provider: google; reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ digraph linkcheck {
|
|||
retry_target="impl_setup",
|
||||
fallback_retry_target="impl_crawler",
|
||||
model_stylesheet="
|
||||
* { llm_model: gemini-3-flash-preview; llm_provider: google; reasoning_effort: medium; }
|
||||
.hard { llm_model: gemini-3-flash-preview; llm_provider: google; reasoning_effort: high; }
|
||||
.verify { llm_model: gemini-3-flash-preview; llm_provider: google; reasoning_effort: medium; }
|
||||
.review { llm_model: gemini-3-flash-preview; llm_provider: google; reasoning_effort: high; }
|
||||
* { model: gemini-3-flash-preview; provider: google; reasoning_effort: medium; }
|
||||
.hard { model: gemini-3-flash-preview; provider: google; reasoning_effort: high; }
|
||||
.verify { model: gemini-3-flash-preview; provider: google; reasoning_effort: medium; }
|
||||
.review { model: gemini-3-flash-preview; provider: google; reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ digraph solitaire {
|
|||
retry_target="impl_setup",
|
||||
fallback_retry_target="impl_game_logic",
|
||||
model_stylesheet="
|
||||
* { llm_model: gemini-3-flash-preview; llm_provider: google; }
|
||||
* { model: gemini-3-flash-preview; provider: google; }
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ digraph reference_template {
|
|||
fallback_retry_target="debate_consolidate",
|
||||
provenance_version="1",
|
||||
model_stylesheet="
|
||||
* { llm_model: DEFAULT_MODEL; llm_provider: DEFAULT_PROVIDER; }
|
||||
.hard { llm_model: HARD_MODEL; llm_provider: HARD_PROVIDER; }
|
||||
.verify { llm_model: VERIFY_MODEL; llm_provider: VERIFY_PROVIDER; }
|
||||
.branch-a { llm_model: BRANCH_A_MODEL; llm_provider: BRANCH_A_PROVIDER; }
|
||||
.branch-b { llm_model: BRANCH_B_MODEL; llm_provider: BRANCH_B_PROVIDER; }
|
||||
.branch-c { llm_model: BRANCH_C_MODEL; llm_provider: BRANCH_C_PROVIDER; }
|
||||
* { model: DEFAULT_MODEL; provider: DEFAULT_PROVIDER; }
|
||||
.hard { model: HARD_MODEL; provider: HARD_PROVIDER; }
|
||||
.verify { model: VERIFY_MODEL; provider: VERIFY_PROVIDER; }
|
||||
.branch-a { model: BRANCH_A_MODEL; provider: BRANCH_A_PROVIDER; }
|
||||
.branch-b { model: BRANCH_B_MODEL; provider: BRANCH_B_PROVIDER; }
|
||||
.branch-c { model: BRANCH_C_MODEL; provider: BRANCH_C_PROVIDER; }
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
digraph Workflow {
|
||||
graph [ label="Semantic Port Tracking Loop", goal="We want to intelligently track and port semantic changes from the upstream openai-agents-python repository to our Go implementation.\n\nWe want to fetch the latest commits from inspiration/openai-agents-python, analyze each new commit for semantic changes (not just syntax), and intelligently coalesce/merge those changes into our Go codebase while respecting Go idioms and our existing architecture.\n\nWe want to track the disposition of each upstream commit in semport/ledger.tsv with three states: 'new' (unprocessed), 'implemented' (changes made), or 'acknowledged' (reviewed but no changes needed).\n\nWe want to make sure we are surgical in this monorepo and follow pre-existing coding conventions and standards.", rankdir="LR", context_fidelity_default="truncate", context_thread_default="semport-tracking", default_max_retry="4" ];
|
||||
|
||||
FinalizeAndUpdateLedger [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="6) Finalize & update ledger", llm_model="gpt-5.2-codex", llm_prompt="**Finalize implementation and update ledger in one step.**\\n\\n1. Synthesize the port plan from .ai/semport_plan_sonnet.md and implementation results into .ai/semport_implementation_summary.md. List which upstream commits were processed, what changes were made (with file:line references), and the disposition ('implemented').\\n\\n2. Update the ledger using:\\n```\\npython3 semport/ledger.py update <shortsha> implemented\\npython3 semport/ledger.py sort\\n```\\n\\n3. Verify with `python3 semport/ledger.py stats` to see progress.\\n\\n4. **Commit all changes** (implementation + ledger update) with a clear message:\\n ```\\n git add -A\\n git commit -m \"semport: implement <shortsha> - <brief description of what was ported>\"\\n ```\\n Example: `git commit -m \"semport: implement a776d80 - nest handoff history by default\"`\\n\\nKeep our goal $goal in mind. Then loop back to process the next commit.", llm_provider="openai", margin="0.1,0.08", max_agent_turns="8", node_type="stack.observe", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"];
|
||||
FinalizeAndUpdateLedger [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="6) Finalize & update ledger", model="gpt-5.2-codex", llm_prompt="**Finalize implementation and update ledger in one step.**\\n\\n1. Synthesize the port plan from .ai/semport_plan_sonnet.md and implementation results into .ai/semport_implementation_summary.md. List which upstream commits were processed, what changes were made (with file:line references), and the disposition ('implemented').\\n\\n2. Update the ledger using:\\n```\\npython3 semport/ledger.py update <shortsha> implemented\\npython3 semport/ledger.py sort\\n```\\n\\n3. Verify with `python3 semport/ledger.py stats` to see progress.\\n\\n4. **Commit all changes** (implementation + ledger update) with a clear message:\\n ```\\n git add -A\\n git commit -m \"semport: implement <shortsha> - <brief description of what was ported>\"\\n ```\\n Example: `git commit -m \"semport: implement a776d80 - nest handoff history by default\"`\\n\\nKeep our goal $goal in mind. Then loop back to process the next commit.", margin="0.1,0.08", max_agent_turns="8", node_type="stack.observe", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"];
|
||||
|
||||
TestValidate [allow_partial="true", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="5) Test/Validate changes", llm_model="gpt-5.2-codex", llm_prompt="Keeping our goal in mind: $goal. From repo root, validate that all ported changes work correctly. Run relevant tests (go test ./...), ensure compilation succeeds, and verify the ported functionality matches the upstream semantic intent (not necessarily syntax). Write validation results to .ai/semport_validation_report_NN.md. Use outcome=yes if all tests pass and changes are semantically correct; otherwise use outcome=retry with concrete failure details.", llm_provider="openai", margin="0.1,0.08", max_agent_turns="8", node_type="stack.steer", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1800"];
|
||||
TestValidate [allow_partial="true", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="5) Test/Validate changes", model="gpt-5.2-codex", llm_prompt="Keeping our goal in mind: $goal. From repo root, validate that all ported changes work correctly. Run relevant tests (go test ./...), ensure compilation succeeds, and verify the ported functionality matches the upstream semantic intent (not necessarily syntax). Write validation results to .ai/semport_validation_report_NN.md. Use outcome=yes if all tests pass and changes are semantically correct; otherwise use outcome=retry with concrete failure details.", margin="0.1,0.08", max_agent_turns="8", node_type="stack.steer", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1800"];
|
||||
|
||||
AnalyzeFailureSonnet [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="6a) Analyze failure (sonnet)", llm_model="gpt-5.2-codex", llm_prompt="When tests or validation fail, inspect .ai/semport_validation_report_*.md, logs, diffs, and error messages. Write .ai/semport_failure_sonnet.md summarizing root causes, impacted files (with line references), and what needs to be fixed. Clearly note where failure artifacts are located. Keep our goal $goal in mind and be subjective.", llm_provider="openai", margin="0.1,0.08", max_agent_turns="8", node_type="stack.observe", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"];
|
||||
AnalyzeFailureSonnet [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="6a) Analyze failure (sonnet)", model="gpt-5.2-codex", llm_prompt="When tests or validation fail, inspect .ai/semport_validation_report_*.md, logs, diffs, and error messages. Write .ai/semport_failure_sonnet.md summarizing root causes, impacted files (with line references), and what needs to be fixed. Clearly note where failure artifacts are located. Keep our goal $goal in mind and be subjective.", margin="0.1,0.08", max_agent_turns="8", node_type="stack.observe", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"];
|
||||
|
||||
FetchUpstreamSonnet [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="1) Fetch upstream & identify next commit (sonnet)", llm_model="gpt-5.2-codex", llm_prompt="Our goal is: $goal\\n\\n---\\n\\n**CRITICAL: Use semport/ledger.py for all ledger operations to ensure proper chronological ordering. Ledger entries MUST always use short git hashes (7 characters, e.g. from `git rev-parse --short`).**\\n\\n1. Run `python3 semport/ledger.py earliest` to get the chronologically earliest commit with disposition='new'\\n2. If a commit is found, write ONLY that single commit (shortsha, iso8601, and full commit message from git show) to .ai/semport_new_commits.md and use outcome=process\\n3. If NO 'new' commits exist:\\n a. Ensure inspiration/openai-agents-python exists (clone if missing)\\n b. Run git fetch && git pull in that directory\\n c. Use git log to find commits newer than the latest in ledger.tsv, capturing a short hash for each commit (e.g. `git log --format='%h %cI' ...`)\\n d. Add new commits using `python3 semport/ledger.py add <shortsha> <timestamp>`\\n e. Run `python3 semport/ledger.py sort` to maintain chronological order\\n f. Then run `python3 semport/ledger.py earliest` to get the first new commit\\n g. If a new commit is found after fetching, write it to .ai/semport_new_commits.md and use outcome=process\\n h. If still no new commits after fetching, write a completion report to .ai/semport_completion.md and use outcome=done\\n\\n**IMPORTANT**: You MUST end with exactly one of these outcomes:\\n- outcome=process (when there is a commit to process)\\n- outcome=done (when fully caught up with no new commits)", llm_provider="openai", margin="0.1,0.08", max_agent_turns="8", node_type="stack.steer", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"];
|
||||
FetchUpstreamSonnet [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="1) Fetch upstream & identify next commit (sonnet)", model="gpt-5.2-codex", llm_prompt="Our goal is: $goal\\n\\n---\\n\\n**CRITICAL: Use semport/ledger.py for all ledger operations to ensure proper chronological ordering. Ledger entries MUST always use short git hashes (7 characters, e.g. from `git rev-parse --short`).**\\n\\n1. Run `python3 semport/ledger.py earliest` to get the chronologically earliest commit with disposition='new'\\n2. If a commit is found, write ONLY that single commit (shortsha, iso8601, and full commit message from git show) to .ai/semport_new_commits.md and use outcome=process\\n3. If NO 'new' commits exist:\\n a. Ensure inspiration/openai-agents-python exists (clone if missing)\\n b. Run git fetch && git pull in that directory\\n c. Use git log to find commits newer than the latest in ledger.tsv, capturing a short hash for each commit (e.g. `git log --format='%h %cI' ...`)\\n d. Add new commits using `python3 semport/ledger.py add <shortsha> <timestamp>`\\n e. Run `python3 semport/ledger.py sort` to maintain chronological order\\n f. Then run `python3 semport/ledger.py earliest` to get the first new commit\\n g. If a new commit is found after fetching, write it to .ai/semport_new_commits.md and use outcome=process\\n h. If still no new commits after fetching, write a completion report to .ai/semport_completion.md and use outcome=done\\n\\n**IMPORTANT**: You MUST end with exactly one of these outcomes:\\n- outcome=process (when there is a commit to process)\\n- outcome=done (when fully caught up with no new commits)", margin="0.1,0.08", max_agent_turns="8", node_type="stack.steer", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"];
|
||||
|
||||
ImplementPort [allow_partial="true", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="4) Implement port (gpt-5.1)", llm_model="gpt-5.2-codex", llm_prompt="Follow the port plan in .ai/semport_plan_finalized.md. For each upstream commit, port the semantic changes to the Go codebase. Focus on semantic equivalence, not literal translation. Use Go idioms, respect existing architecture, and reference specific files/line ranges. Log all changes and commands to .ai/semport_impl.log.", llm_provider="openai", margin="0.1,0.08", max_agent_turns="8", node_type="stack.observe", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="2400"];
|
||||
ImplementPort [allow_partial="true", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="4) Implement port (gpt-5.1)", model="gpt-5.2-codex", llm_prompt="Follow the port plan in .ai/semport_plan_finalized.md. For each upstream commit, port the semantic changes to the Go codebase. Focus on semantic equivalence, not literal translation. Use Go idioms, respect existing architecture, and reference specific files/line ranges. Log all changes and commands to .ai/semport_impl.log.", margin="0.1,0.08", max_agent_turns="8", node_type="stack.observe", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="2400"];
|
||||
|
||||
FinalizePlanGPT [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="3) Finalize port plan (gpt-5.1)", llm_model="gpt-5.2-codex", llm_prompt="Keeping our goal $goal in mind. Perform a final editorial pass over .ai/semport_plan_sonnet.md and write .ai/semport_plan_finalized.md. Ensure each port task has concrete file:line references, clear acceptance criteria, and is directly executable. Remove vague language and ensure the plan is actionable.", llm_provider="openai", margin="0.1,0.08", max_agent_turns="8", node_type="stack.observe", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"];
|
||||
FinalizePlanGPT [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="3) Finalize port plan (gpt-5.1)", model="gpt-5.2-codex", llm_prompt="Keeping our goal $goal in mind. Perform a final editorial pass over .ai/semport_plan_sonnet.md and write .ai/semport_plan_finalized.md. Ensure each port task has concrete file:line references, clear acceptance criteria, and is directly executable. Remove vague language and ensure the plan is actionable.", margin="0.1,0.08", max_agent_turns="8", node_type="stack.observe", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"];
|
||||
|
||||
Exit [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="Exit", llm_model="gpt-5.2-codex", llm_provider="openai", margin="0.1,0.08", max_agent_turns="8", node_type="exit", penwidth="1.2", reasoning_effort="high", shape="doublecircle", style="rounded,filled", timeout="1200"];
|
||||
Exit [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="Exit", model="gpt-5.2-codex", margin="0.1,0.08", max_agent_turns="8", node_type="exit", penwidth="1.2", reasoning_effort="high", shape="doublecircle", style="rounded,filled", timeout="1200"];
|
||||
|
||||
Start [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="Start", llm_model="gpt-5.2-codex", llm_provider="openai", margin="0.1,0.08", max_agent_turns="8", node_type="start", penwidth="1.2", reasoning_effort="high", shape="circle", style="rounded,filled", timeout="1200"];
|
||||
Start [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="Start", model="gpt-5.2-codex", margin="0.1,0.08", max_agent_turns="8", node_type="start", penwidth="1.2", reasoning_effort="high", shape="circle", style="rounded,filled", timeout="1200"];
|
||||
|
||||
AnalyzePlanSonnet [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="2) Analyze & plan port (sonnet)", llm_model="gpt-5.2-codex", llm_prompt="Keeping our goal $goal in mind. Read .ai/semport_new_commits.md which contains a SINGLE commit to process. Examine this one commit in inspiration/openai-agents-python using git show. Analyze the semantic changes (what functionality changed, not just syntax). Decide if this change is relevant to our Go implementation or if it's Python-specific/docs-only/not-applicable.\\n\\nWrite .ai/semport_plan_sonnet.md with sections: Commit Being Processed (shortsha and summary), Semantic Analysis (what changed functionally), DECISION (port or acknowledge with clear reasoning), Port Plan (if porting: concrete tasks with file:line references for Go code), and Disposition Recommendation.\\n\\n**If decision is to ACKNOWLEDGE (skip):**\\n1. Update the ledger: `python3 semport/ledger.py update <shortsha> acknowledged && python3 semport/ledger.py sort`\\n2. Verify with `python3 semport/ledger.py stats`\\n3. **Commit the ledger change** with a clear message summarizing why this commit was acknowledged:\\n ```\\n git add semport/ledger.tsv\\n git commit -m \"semport: acknowledge <shortsha> - <brief reason>\"\\n ```\\n Example: `git commit -m \"semport: acknowledge e3fe4f4 - docs typo fix, no Go changes needed\"`\\n4. Use outcome=skip to loop back for next commit\\n\\n**If decision is to PORT:**\\nUse outcome=port to proceed to implementation.", llm_provider="openai", margin="0.1,0.08", max_agent_turns="8", node_type="stack.steer", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"];
|
||||
AnalyzePlanSonnet [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="2) Analyze & plan port (sonnet)", model="gpt-5.2-codex", llm_prompt="Keeping our goal $goal in mind. Read .ai/semport_new_commits.md which contains a SINGLE commit to process. Examine this one commit in inspiration/openai-agents-python using git show. Analyze the semantic changes (what functionality changed, not just syntax). Decide if this change is relevant to our Go implementation or if it's Python-specific/docs-only/not-applicable.\\n\\nWrite .ai/semport_plan_sonnet.md with sections: Commit Being Processed (shortsha and summary), Semantic Analysis (what changed functionally), DECISION (port or acknowledge with clear reasoning), Port Plan (if porting: concrete tasks with file:line references for Go code), and Disposition Recommendation.\\n\\n**If decision is to ACKNOWLEDGE (skip):**\\n1. Update the ledger: `python3 semport/ledger.py update <shortsha> acknowledged && python3 semport/ledger.py sort`\\n2. Verify with `python3 semport/ledger.py stats`\\n3. **Commit the ledger change** with a clear message summarizing why this commit was acknowledged:\\n ```\\n git add semport/ledger.tsv\\n git commit -m \"semport: acknowledge <shortsha> - <brief reason>\"\\n ```\\n Example: `git commit -m \"semport: acknowledge e3fe4f4 - docs typo fix, no Go changes needed\"`\\n4. Use outcome=skip to loop back for next commit\\n\\n**If decision is to PORT:**\\nUse outcome=port to proceed to implementation.", margin="0.1,0.08", max_agent_turns="8", node_type="stack.steer", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"];
|
||||
|
||||
FinalizeAndUpdateLedger -> FetchUpstreamSonnet [loop_restart="true"];
|
||||
Start -> FetchUpstreamSonnet;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ digraph Simple {
|
|||
goal="Run tests and report",
|
||||
rankdir=LR,
|
||||
model_stylesheet="
|
||||
* { llm_model: gpt-5.2-codex; llm_provider: openai; }
|
||||
* { model: gpt-5.2-codex;}
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ digraph solitaire {
|
|||
retry_target="impl_setup",
|
||||
fallback_retry_target="impl_game_logic",
|
||||
model_stylesheet="
|
||||
* { llm_model: gpt-5.3-codex-spark; llm_provider: openai; }
|
||||
* { model: gpt-5.3-codex-spark;}
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
digraph Example {
|
||||
graph [
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-haiku-4-5; }
|
||||
.coding { llm_model: claude-sonnet-4-5; reasoning_effort: high; }
|
||||
#review { llm_model: gemini-3.1-pro-preview; }
|
||||
* { model: claude-haiku-4-5; }
|
||||
.coding { model: claude-sonnet-4-5; reasoning_effort: high; }
|
||||
#review { model: gemini-3.1-pro-preview; }
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -24,11 +24,11 @@ GitHub to Railway validated by code review only — no live deployment execution
|
|||
retry_target="plan_fanout",
|
||||
fallback_retry_target="plan_fanout",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
.hard { llm_model: gpt-5.3-codex; llm_provider: openai; }
|
||||
.verify { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
.branch-a { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
.branch-b { llm_model: gemini-3-flash-preview; llm_provider: gemini; }
|
||||
* { model: claude-opus-4-6; }
|
||||
.hard { model: gpt-5.3-codex; }
|
||||
.verify { model: claude-opus-4-6; }
|
||||
.branch-a { model: claude-opus-4-6; }
|
||||
.branch-b { model: gemini-3-flash-preview;}
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ digraph SpecDoDMultiModel {
|
|||
retry_target="triage_merge",
|
||||
default_fidelity="full",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
.opus { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; }
|
||||
.gpt { llm_model: gpt-5.2; llm_provider: openai; reasoning_effort: high; }
|
||||
.codex { llm_model: gpt-5.2-codex; llm_provider: openai; reasoning_effort: high; }
|
||||
.merge { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; }
|
||||
* { model: claude-opus-4-6;}
|
||||
.opus { model: claude-opus-4-6;reasoning_effort: high; }
|
||||
.gpt { model: gpt-5.2; reasoning_effort: high; }
|
||||
.codex { model: gpt-5.2-codex; reasoning_effort: high; }
|
||||
.merge { model: claude-opus-4-6; reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ digraph SpecDoD {
|
|||
default_max_retry="3",
|
||||
retry_target="triage",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
* { model: claude-opus-4-6;}
|
||||
.audit { reasoning_effort: high; }
|
||||
.fix { llm_model: claude-opus-4-6; reasoning_effort: high; }
|
||||
.fix { model: claude-opus-4-6; reasoning_effort: high; }
|
||||
#final_audit { reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ digraph NLSpecConformance {
|
|||
graph [
|
||||
goal="Implement a conformant system from a natural language specification",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-haiku-4-5; llm_provider: anthropic; }
|
||||
.impl { llm_model: claude-sonnet-4-5; reasoning_effort: high; }
|
||||
* { model: claude-haiku-4-5;}
|
||||
.impl { model: claude-sonnet-4-5; reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
rankdir=LR
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ digraph SemanticPort {
|
|||
rankdir=LR,
|
||||
default_max_retry=3,
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-sonnet-4-5; llm_provider: anthropic; }
|
||||
.hard { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
.analyze { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; }
|
||||
* { model: claude-sonnet-4-5;}
|
||||
.hard { model: claude-opus-4-6; }
|
||||
.analyze { model: gemini-3.1-pro-preview;}
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ digraph ImplementFeature {
|
|||
graph [
|
||||
goal="Implement a feature with tests and code review",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-haiku-4-5; llm_provider: anthropic; reasoning_effort: low; }
|
||||
.coding { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; }
|
||||
#review { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; }
|
||||
* { model: claude-haiku-4-5;reasoning_effort: low; }
|
||||
.coding { model: claude-sonnet-4-5;reasoning_effort: high; }
|
||||
#review { model: claude-sonnet-4-5;reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
rankdir=LR
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ digraph Ensemble {
|
|||
graph [
|
||||
goal="Get independent opinions from multiple providers, then synthesize",
|
||||
model_stylesheet="
|
||||
#opus { llm_model: claude-opus-4-6; llm_provider: anthropic; }
|
||||
#gemini { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; }
|
||||
#codex { llm_model: gpt-5.3-codex; llm_provider: openai; }
|
||||
#mercury { llm_model: mercury-2; llm_provider: inception; }
|
||||
#synth { llm_model: claude-opus-4-6; llm_provider: anthropic; reasoning_effort: high; }
|
||||
#opus { model: claude-opus-4-6; }
|
||||
#gemini { model: gemini-3.1-pro-preview;}
|
||||
#codex { model: gpt-5.3-codex; }
|
||||
#mercury { model: mercury-2; provider: inception; }
|
||||
#synth { model: claude-opus-4-6; reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
rankdir=LR
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ digraph MultiModel {
|
|||
graph [
|
||||
goal="Build and review a utility function using multiple models",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-haiku-4-5; llm_provider: anthropic; reasoning_effort: low; }
|
||||
.coding { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; }
|
||||
#review { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; }
|
||||
* { model: claude-haiku-4-5;reasoning_effort: low; }
|
||||
.coding { model: claude-sonnet-4-5;reasoning_effort: high; }
|
||||
#review { model: claude-sonnet-4-5;reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
rankdir=LR
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ digraph Example {
|
|||
graph [
|
||||
goal="Build and review a utility function",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-haiku-4-5; llm_provider: anthropic; }
|
||||
.coding { llm_model: claude-sonnet-4-5; reasoning_effort: high; }
|
||||
#review { llm_model: gemini-3.1-pro-preview; llm_provider: gemini; }
|
||||
* { model: claude-haiku-4-5;}
|
||||
.coding { model: claude-sonnet-4-5; reasoning_effort: high; }
|
||||
#review { model: gemini-3.1-pro-preview;}
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ digraph Styled {
|
|||
graph [
|
||||
goal="Build a styled pipeline",
|
||||
model_stylesheet="
|
||||
* { llm_model: claude-sonnet-4-5; llm_provider: anthropic; }
|
||||
.code { llm_model: claude-opus-4-6; }
|
||||
#critical_review { llm_model: gpt-5.2; llm_provider: openai; reasoning_effort: high; }
|
||||
* { model: claude-sonnet-4-5;}
|
||||
.code { model: claude-opus-4-6; }
|
||||
#critical_review { model: gpt-5.2;reasoning_effort: high; }
|
||||
"
|
||||
]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue