Files
Anton 052591281d Initial clean version - Agent Maxim
- Complete agent system with planning and execution
- Bridge API with FastAPI server
- WhatsApp-style mobile dashboard
- Enhanced web search with query improvement
- Security guards and policy checks
- Comprehensive documentation
2026-04-09 18:07:43 +03:00

325 lines
10 KiB
Markdown

# 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: <goal>\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 <instruction> --run-id agent-<rid>
│ └─ reads agent/workspace/runs/agent-<rid>/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/<run_id>/
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/<run_id>/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/<agent_run_id>/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