diff --git a/run.json b/run.json index 4bd62caca..fe716e107 100644 --- a/run.json +++ b/run.json @@ -359,7 +359,7 @@ "kind": "running" }, "status_updated_at": "2026-07-08T15:07:06.493388133Z", - "last_event_at": "2026-07-08T15:07:42.318808758Z", + "last_event_at": "2026-07-08T15:07:46.323790616Z", "pending_control": null, "checkpoints": [ { @@ -400,19 +400,89 @@ "diff": {} }, { - "seq": 0, + "seq": 44, "checkpoint": { - "timestamp": "2026-07-08T15:07:42.343955440Z", + "timestamp": "2026-07-08T15:07:46.323516967Z", "current_node": "fork", "completed_nodes": [ "start", "fork" ], "node_retries": {}, + "context_values": { + "outcome": "succeeded", + "internal.thread_id": "start", + "thread.start.current_node": "fork", + "internal.retry_count.fork": 0, + "internal.work_dir": "/home/daytona/workspace/fabro", + "current_node": "fork", + "failure_class": "", + "failure_signature": "", + "graph.model_stylesheet": "* { model: claude-sonnet-4-5; }", + "graph.rankdir": "LR", + "parallel.results": [ + { + "id": "a", + "status": "succeeded", + "head_sha": "379d13d4f7218e30c9ec925d730f40194c63616b" + }, + { + "id": "b", + "status": "succeeded", + "head_sha": "8ee19a3b6be176881b7063c1340d26477c997742" + } + ], + "internal.run_id": "01KX148ZAMMJRAADHK1HBF7PC3", + "internal.retry_count.start": 0, + "parallel.branch_count": 2, + "internal.node_visit_count": 1, + "graph.goal": "Repro: does the synth/merge node see branch outputs?", + "internal.fidelity": "compact" + }, + "node_outcomes": { + "start": { + "status": "succeeded", + "usage": null + }, + "fork": { + "status": "succeeded", + "jump_to_node": "merge", + "notes": "Parallel node dispatched 2 branches (2 succeeded, 0 failed)", + "usage": null + } + }, + "next_node_id": "merge", + "git_commit_sha": "9b85837e7249b74aaedc28a45a1ecbf402d4d63f", + "node_visits": { + "start": 1, + "fork": 1 + } + }, + "diff": { + "summary": { + "files_changed": 0, + "additions": 0, + "deletions": 0 + } + } + }, + { + "seq": 0, + "checkpoint": { + "timestamp": "2026-07-08T15:07:46.363877667Z", + "current_node": "merge", + "completed_nodes": [ + "start", + "fork", + "merge" + ], + "node_retries": {}, "context_values": { "internal.run_id": "01KX148ZAMMJRAADHK1HBF7PC3", "failure_signature": "", - "internal.thread_id": "start", + "internal.thread_id": "fork", + "thread.fork.current_node": "merge", + "internal.retry_count.merge": 0, "graph.model_stylesheet": "* { model: claude-sonnet-4-5; }", "parallel.results": [ { @@ -435,24 +505,44 @@ "graph.goal": "Repro: does the synth/merge node see branch outputs?", "internal.retry_count.fork": 0, "graph.rankdir": "LR", + "parallel.fan_in.best_id": "a", "parallel.branch_count": 2, + "parallel.fan_in.best_outcome": "succeeded", + "parallel.fan_in.best_head_sha": "379d13d4f7218e30c9ec925d730f40194c63616b", "outcome": "succeeded", - "current_node": "fork" + "current_node": "merge" }, "node_outcomes": { - "start": { + "merge": { "status": "succeeded", - "usage": null + "context_updates": { + "parallel.fan_in.best_id": "a", + "parallel.fan_in.best_outcome": "succeeded", + "parallel.fan_in.best_head_sha": "379d13d4f7218e30c9ec925d730f40194c63616b" + }, + "notes": "Selected best candidate: a", + "usage": null, + "timing": { + "wall_time_ms": 0, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + } }, "fork": { "status": "succeeded", "jump_to_node": "merge", "notes": "Parallel node dispatched 2 branches (2 succeeded, 0 failed)", "usage": null + }, + "start": { + "status": "succeeded", + "usage": null } }, - "next_node_id": "merge", + "next_node_id": "synth", "node_visits": { + "merge": 1, "start": 1, "fork": 1 } @@ -520,11 +610,39 @@ }, "state": "succeeded" }, + "merge@1": { + "first_event_seq": 47, + "prompt": null, + "response": null, + "completion": null, + "provider_used": null, + "diff": null, + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "output": null, + "started_at": "2026-07-08T15:07:46.323790616Z", + "handler": "parallel.fan_in", + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "total_tokens": 0, + "reasoning_tokens": 0, + "cache_read_tokens": 0, + "cache_write_tokens": 0 + }, + "state": "running" + }, "fork@1": { "first_event_seq": 21, "prompt": null, "response": null, - "completion": null, + "completion": { + "outcome": "succeeded", + "notes": "Parallel node dispatched 2 branches (2 succeeded, 0 failed)", + "failure_reason": null, + "timestamp": "2026-07-08T15:07:42.343874651Z" + }, "provider_used": { "mode": "prompt", "provider": "anthropic", @@ -533,10 +651,27 @@ "diff": null, "script_invocation": null, "script_timing": null, - "parallel_results": null, + "parallel_results": [ + { + "id": "a", + "status": "succeeded", + "head_sha": "379d13d4f7218e30c9ec925d730f40194c63616b" + }, + { + "id": "b", + "status": "succeeded", + "head_sha": "8ee19a3b6be176881b7063c1340d26477c997742" + } + ], "output": null, "started_at": "2026-07-08T15:07:08.314410727Z", "handler": "parallel", + "timing": { + "wall_time_ms": 34029, + "inference_time_ms": 0, + "tool_time_ms": 0, + "active_time_ms": 0 + }, "usage": { "input_tokens": 49, "output_tokens": 2021, @@ -550,7 +685,7 @@ "provider": "anthropic", "model_id": "claude-sonnet-4-5" }, - "state": "running" + "state": "succeeded" } } } \ No newline at end of file diff --git a/stages/002-fork@1/parallel_results.json b/stages/002-fork@1/parallel_results.json new file mode 100644 index 000000000..9c1826c8b --- /dev/null +++ b/stages/002-fork@1/parallel_results.json @@ -0,0 +1,12 @@ +[ + { + "id": "a", + "status": "succeeded", + "head_sha": "379d13d4f7218e30c9ec925d730f40194c63616b" + }, + { + "id": "b", + "status": "succeeded", + "head_sha": "8ee19a3b6be176881b7063c1340d26477c997742" + } +] \ No newline at end of file diff --git a/stages/002-fork@1/response.md b/stages/002-fork@1/response.md deleted file mode 100644 index 8349733c1..000000000 --- a/stages/002-fork@1/response.md +++ /dev/null @@ -1,228 +0,0 @@ -I need to understand what you're asking about and create a test to reproduce the behavior. - -Let me search for information about synth/merge nodes and branch outputs in the codebase: - -```bash -rg "synth.*merge|merge.*branch" --type rust -A 5 -``` - -```bash -rg "BRANCH.*MARKER" --type rust -``` - -Let me also look at the workflow engine's handling of branches and merge nodes: - -```bash -rg "fn.*merge|MergeNode|SynthNode" lib/crates/fabro-workflow/src/ -A 10 -``` - -```bash -rg "branch.*output|output.*branch" lib/crates/fabro-workflow/src/ -A 5 -``` - -Let me check the stage types and how they handle outputs: - -```bash -cat lib/crates/fabro-workflow/src/stage.rs | head -200 -``` - -```bash -rg "Stage::Synth|Stage::Merge" lib/crates/fabro-workflow/src/ -B 5 -A 10 -``` - -Now let me look for existing tests that demonstrate branch/merge behavior: - -```bash -rg "test.*branch.*merge|test.*merge.*branch" --type rust -l -``` - -```bash -find . -name "*.toml" -path "*/workflows/*" | xargs grep -l "stage = \"merge\"" -``` - -Let me create a minimal test workflow to reproduce the behavior: - -```bash -mkdir -p /tmp/test-merge-branch -cd /tmp/test-merge-branch -``` - -```bash -cat > workflow.dot << 'EOF' -digraph { - start [stage="prompt", prompt="start node", model="gpt-4o-mini"]; - - branch_a [stage="prompt", prompt="Output: BRANCH_A_MARKER_7f3a: apples are red.", model="gpt-4o-mini"]; - branch_b [stage="prompt", prompt="Output: BRANCH_B_MARKER: bananas are yellow.", model="gpt-4o-mini"]; - - merge [stage="synth", prompt="List all the MARKER lines you see from previous outputs.", model="gpt-4o-mini"]; - - start -> branch_a; - start -> branch_b; - branch_a -> merge; - branch_b -> merge; -} -EOF -``` - -```bash -cat > workflow.toml << 'EOF' -name = "test-merge-branch" -origin = "local" -[graph] -path = "workflow.dot" -EOF -``` - -Now let me run this workflow: - -```bash -cd /tmp/test-merge-branch && fabro run --path . 2>&1 | tee output.txt -``` - -Let me check what the merge node actually received: - -```bash -grep -A 20 "merge" output.txt | head -40 -``` - -Let me look at how the workflow engine passes context to synth nodes: - -```bash -rg "Stage::Synth" lib/crates/fabro-workflow/src/engine.rs -B 10 -A 30 -``` - -```bash -cat lib/crates/fabro-workflow/src/engine.rs | grep -A 50 "fn execute_stage" -``` - -Let me check the context building for stages: - -```bash -rg "build.*context|context.*build" lib/crates/fabro-workflow/src/ -A 10 | head -100 -``` - -```bash -rg "parent.*output|predecessor.*output" lib/crates/fabro-workflow/src/ -A 10 -``` - -Let me look at the actual output from the test run more carefully: - -```bash -cat output.txt | grep -i "marker\|merge\|branch" -``` - -Based on the codebase structure, let me create a more targeted test. Let me look at existing workflow tests: - -```bash -ls -la lib/crates/fabro-workflow/tests/ -``` - -```bash -cat lib/crates/fabro-workflow/tests/it/scenario_tests.rs | head -100 -``` - -Let me check if there's a simpler way to test this by looking at how outputs are tracked: - -```bash -rg "outputs:|outputs\[" lib/crates/fabro-workflow/src/ -B 3 -A 3 -``` - -Let me examine the OutputTracker: - -```bash -rg "OutputTracker|struct Output" lib/crates/fabro-workflow/src/ -A 20 -``` - -Now let me trace through how a synth node gets its inputs: - -```bash -rg "synth.*execute|execute.*synth" lib/crates/fabro-workflow/src/ -i -B 5 -A 20 -``` - -Let me create a proper test in the workflow test suite: - -```bash -cat > /tmp/test_branch_merge.rs << 'EOF' -use fabro_test::e2e_test; -use fabro_workflow::operations; -use std::path::PathBuf; - -#[e2e_test(twin)] -async fn test_synth_sees_branch_outputs() -> anyhow::Result<()> { - let temp_dir = tempfile::tempdir()?; - let workflow_dir = temp_dir.path().join("workflow"); - std::fs::create_dir(&workflow_dir)?; - - // Create workflow with parallel branches feeding into a synth merge - let dot = r#" -digraph { - start [stage="prompt", prompt="say: starting", model="gpt-4o-mini"]; - - branch_a [stage="prompt", prompt="Output exactly: BRANCH_A_MARKER_7f3a: apples are red.", model="gpt-4o-mini"]; - branch_b [stage="prompt", prompt="Output exactly: BRANCH_B_MARKER: bananas are yellow.", model="gpt-4o-mini"]; - - merge [stage="synth", prompt="List all MARKER lines from predecessor outputs.", model="gpt-4o-mini"]; - - start -> branch_a; - start -> branch_b; - branch_a -> merge; - branch_b -> merge; -} -"#; - - std::fs::write(workflow_dir.join("workflow.dot"), dot)?; - - let toml = r#" -name = "test-merge-branch" -origin = "local" -[graph] -path = "workflow.dot" -"#; - std::fs::write(workflow_dir.join("workflow.toml"), toml)?; - - let (base_url, api_key) = fabro_test::e2e_openai!(); - let run_spec = operations::RunSpec { - manifest_path: Some(workflow_dir.join("workflow.toml")), - openai_api_key: Some(api_key), - openai_base_url: Some(base_url), - ..Default::default() - }; - - let outcome = operations::run(run_spec).await?; - - // Get the merge node output - let merge_output = outcome.outputs.iter() - .find(|o| o.stage == "merge") - .ok_or_else(|| anyhow::anyhow!("merge stage not found"))?; - - println!("Merge output: {}", merge_output.content); - - // The synth node should see both branch outputs - assert!(merge_output.content.contains("BRANCH_A_MARKER_7f3a"), - "merge output should contain branch A marker"); - assert!(merge_output.content.contains("BRANCH_B_MARKER"), - "merge output should contain branch B marker"); - - Ok(()) -} -EOF -``` - -Actually, let me just run a simpler direct test: - -```bash -cd /tmp/test-merge-branch && fabro run --path . --format json > result.json 2>&1 -``` - -```bash -cat result.json | jq '.outputs[] | select(.stage == "merge") | .content' -``` - -Let me check the actual workflow execution more carefully by looking at progress events: - -```bash -cd /tmp/test-merge-branch && fabro run --path . 2>&1 | grep -E "stage|output|MARKER" | head -50 -``` - -BRANCH_A_MARKER_7f3a: apples are red. \ No newline at end of file diff --git a/stages/002-fork@1/status.json b/stages/002-fork@1/status.json new file mode 100644 index 000000000..bb1a9131d --- /dev/null +++ b/stages/002-fork@1/status.json @@ -0,0 +1,6 @@ +{ + "outcome": "succeeded", + "notes": "Parallel node dispatched 2 branches (2 succeeded, 0 failed)", + "failure_reason": null, + "timestamp": "2026-07-08T15:07:42.343874651Z" +} \ No newline at end of file