# Bridge V1.2 — Agent-Connected Orchestration One-shot orchestration with a real agent backend. `POST /runs` with `adapter="agent"` executes the full agent pipeline and returns the final result. ## Run Lifecycle ``` POST /runs {"goal": "...", "adapter": "agent"} │ ├─ status: created → state.json written, run_created logged ├─ instruction built → "Task: \n\nPlease complete..." │ instruction_generated logged ├─ policy check → blocked? → status=failed, return ├─ status: running → task_sent logged ├─ agent_adapter.execute(instruction, bridge_run_id=rid) │ └─ subprocess: python agent/main.py --goal --run-id agent- │ └─ reads agent/workspace/runs/agent-/state.json │ response_received logged (mode="real", exit_code, agent_run_id) └─ exit_code==0 → status=done | else → status=failed run_finished logged (agent_final_status, agent_run_id) ``` Status transitions: `created → running → done | failed` Status on return from `POST /runs`: always `done` or `failed`, never `running`. ## Run ID Mapping ``` Bridge run_id : br-flow-01 Agent run_id : agent-br-flow-01 Bridge state : bridge/runs/br-flow-01/state.json Agent state : agent/workspace/runs/agent-br-flow-01/state.json ``` Every bridge run maps to exactly one agent run. Both are traceable by ID. ## Project Structure ``` bridge/ app.py FastAPI entry point (v1.2.0) config.py Env config api/routes.py POST /runs, GET /runs/{id}, GET /health core/ run_manager.py One-shot execute_run() with metadata extraction instruction_builder.py Goal → instruction (deterministic) state_store.py state.json read/write audit.py JSONL append-only logger adapters/ __init__.py Registry: stub, local_echo, openai, cloudcode, agent base.py AdapterResult(response,error,exit_code,mode,metadata) stub_adapter.py Always-available stub local_echo_adapter.py Real subprocess (Python child process) openai_adapter.py Real if OPENAI_API_KEY, stub otherwise cloudcode_adapter.py Stub — transport unavailable agent_adapter.py REAL — subprocess call to agent/main.py policy/guard.py Prompt validation models/schemas.py Pydantic schemas (RunResponse includes agent_* fields) runs// state.json audit.jsonl ``` ## Agent Adapter Design ```python # bridge/adapters/agent_adapter.py _AGENT_MAIN = bridge/../agent/main.py def execute(prompt: str, bridge_run_id: str = None) -> AdapterResult: agent_run_id = f"agent-{bridge_run_id}" proc = subprocess.run( [sys.executable, str(_AGENT_MAIN), "--goal", prompt, "--run-id", agent_run_id], capture_output=True, text=True, timeout=60, shell=False, cwd=str(_AGENT_DIR), ) # Read agent/workspace/runs/{agent_run_id}/state.json # Extract agent_final_status return AdapterResult( response=proc.stdout, error=... if proc.returncode != 0 else None, exit_code=proc.returncode, mode="real", metadata={"agent_run_id": ..., "agent_final_status": ..., "agent_state_path": ...} ) ``` **Goal routing:** The instruction passed as `--goal` contains the original goal as a substring. The agent's deterministic planner uses `keyword in goal.lower()` matching, so all keywords resolve correctly through the wrapper text. ## API ### GET /health ```json { "status": "ok", "adapters": { "stub": "stub", "local_echo": "real", "openai": "stub", "cloudcode": "stub", "agent": "real" } } ``` ### POST /runs ```bash curl -X POST http://localhost:8000/runs \ -H "Content-Type: application/json" \ -d '{"goal": "test flow pwd to file", "adapter": "agent"}' ``` Returns final state (status = done or failed): ```json { "run_id": "br-flow-01", "status": "done", "adapter": "agent", "adapter_mode": "real", "exit_code": 0, "agent_run_id": "agent-br-flow-01", "agent_final_status": "done", "agent_state_path": ".../agent/workspace/runs/agent-br-flow-01/state.json" } ``` ### GET /runs/{id} Returns persisted bridge state. ## Adapter Modes | Adapter | Mode | Execution | |---|---|---| | `stub` | stub | Python string echo | | `local_echo` | **real** | `subprocess.run([python, -c, echo_script])` | | `openai` | real/stub | Real if `OPENAI_API_KEY` set, stub otherwise | | `cloudcode` | stub | Transport unavailable in V1.2 | | `agent` | **real** | `subprocess.run([python, agent/main.py, --goal, ...])` | ## Storage Layout ``` bridge/runs//state.json fields: run_id, goal, status, adapter, adapter_mode, instruction, response, error, exit_code, agent_run_id, agent_final_status, agent_state_path, created_at, updated_at agent/workspace/runs//state.json fields: run_id, goal, plan, history, status, current_step_id, started_at, updated_at ``` ## Audit Events (agent adapter) ``` run_created goal + adapter="agent" instruction_generated instruction_len task_sent adapter="agent", instruction_len response_received mode="real", exit_code, response_len, agent_run_id run_finished status, adapter_mode="real", agent_run_id, agent_final_status ``` ## Controller Loop V1 (Bridge V1.3) ### Architecture ``` POST /controller/runs {"goal": "..."} │ ├─ ControllerState created, controller_audit.jsonl opened ├─ Loop (max 3 iterations): │ ├─ decide(goal, iteration, last_result) │ │ ├─ stub: deterministic rules │ │ └─ real: OpenAI call → JSON parse → fallback to stub │ ├─ if action == "stop": break │ ├─ RunManager.execute_run(goal, adapter="agent") │ │ └─ bridge run → agent subprocess → result │ └─ observe result → update controller state └─ final_status persisted, controller_finished logged ``` ### Loop Stop Conditions 1. `decision.action == "stop"` — decision layer says stop 2. `iteration >= MAX_ITERATIONS (3)` — hard iteration cap 3. `consecutive_failures >= 2` — repeated failure guard ### Stub Decision Rules | Condition | Action | Reason | |---|---|---| | No previous run (iteration 0) | run_agent | First execution | | Last run status=done + agent_done | stop | Success | | Last run status=failed | stop | No controller-level retry | | Iteration approaching max | stop | Safety cap | ### Ollama Decision Rules (V1.3 — default) Ollama is the primary decision backend. If Ollama is unreachable at startup, falls back to stub automatically. **Decision flow:** ``` decide(goal, iteration, last_result, recovery_attempted) │ ├─ DECISION_MODE=stub → _decide_stub() (Ollama unreachable at startup) └─ DECISION_MODE=ollama → ollama_adapter.execute(prompt, temperature=0.1, max_tokens=150) ├─ connection error / empty → ollama-fallback (stub logic used) ├─ bad/truncated JSON → ollama-fallback (stub logic used) ├─ invalid action/goal → ollama-fallback (stub logic used) └─ valid JSON → Decision(mode="ollama", parsed_success=True) ``` **Prompt context sent:** ``` goal, iteration/max_iterations, last_status, last_agent_status, recovery_attempted ``` **Expected JSON output:** ```json {"action": "run_agent"|"stop", "goal": "...", "reason": "..."} ``` **Safety guard (before accepting):** - `action` must be `"run_agent"` or `"stop"` - `goal` must be a string ≤ 500 chars - markdown fences and leading prose stripped automatically **Decision modes in state/audit:** | mode | meaning | |---|---| | `ollama` | Ollama response parsed successfully | | `ollama-fallback` | Ollama called but response invalid — stub used | | `stub` | Ollama unreachable at startup — deterministic rules | **Ollama setup:** ```bash # Install: https://ollama.com — already running on Windows # Default model ollama pull qwen2.5:7b # Optional fallback ollama pull llama3.2:3b ``` **Configuration (env vars):** ```bash export OLLAMA_MODEL=qwen2.5:7b # default export OLLAMA_HOST=... # auto-resolved (0.0.0.0 → http://localhost:11434) ``` **Health endpoint shows current mode:** ```json { "status": "ok", "decision_backend": "ollama", "ollama_model": "qwen2.5:7b", "adapters": {"ollama": "real", ...} } ``` ### Run ID Traceability ``` controller_run_id : ctrl-xxxx └─ bridge run_id : bri-ctrl-xxxx-i0 (iteration 0) └─ agent run_id : agent-bri-ctrl-xxxx-i0 └─ bridge run_id : bri-ctrl-xxxx-i1 (iteration 1, if any) ``` ### Storage ``` bridge/runs/ctrl-xxxx/ controller_state.json — controller run, decisions, bridge results controller_audit.jsonl — controller-level events bridge/runs/bri-ctrl-xxxx-i0/ state.json — bridge run state audit.jsonl — bridge run events agent/workspace/runs/agent-bri-ctrl-xxxx-i0/ state.json — agent run with full step history audit.jsonl — agent step events ``` ### Controller Audit Events ``` controller_started goal, decision_mode, max_iterations decision_made iteration, action, reason, mode, parsed_success, fallback_used bridge_run_started iteration, bridge_run_id, goal bridge_run_finished iteration, status, agent_final_status, exit_code recovery_considered iteration, original_goal, recovery_applied, reason recovery_applied iteration, original_goal, rewritten_goal, rule_reason recovery_skipped iteration, original_goal, reason controller_finished final_status, iterations, linked_runs ``` ### API ```bash # Start controller run POST /controller/runs {"goal": "test retry execution"} → 201 ControllerResponse (final state) # Get controller run GET /controller/runs/{ctrl_id} → 200 ControllerResponse ``` ## V1.2 Limitations (agent/bridge layer) - instruction_builder is a string wrapper — not an LLM planner - agent planner is deterministic keyword-matching — not dynamic - bridge sends full instruction as `--goal`; agent's `goal` field in state.json shows the wrapper text (not the raw goal) - one step per bridge run (single agent invocation) - cloudcode adapter is stub-only - no authentication, no streaming, no retries