Skip to content

Commit 46a9d6f

Browse files
committed
fix mcp loading and spill large tool output
1 parent 6b1edfe commit 46a9d6f

19 files changed

Lines changed: 423 additions & 82 deletions

AGENTS.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,21 @@ A CEO-class orchestrator. It owns the outcome end-to-end: from understanding the
1313
| Tool | Purpose |
1414
|------|---------|
1515
| `dispatch_agent` | Spawn an agent in a Kaku pane |
16-
| `read_pane_output` | Check agent progress (last 150 lines) |
16+
| `submit_plan` | Validate and register the task graph before dispatch |
17+
| `wait_for_agent_message` | Primary monitoring tool for batched progress/done events |
18+
| `read_pane_output` | Inspect a pane on demand when mailbox monitoring is insufficient |
1719
| `send_text_to_pane` | Send instructions or corrections |
1820
| `list_managed_panes` | Get pane topology and states |
1921
| `mark_task_done` | Mark a sub-task complete |
20-
| `wait` | Pause 5-120s between monitoring rounds |
22+
| `permanent_error` | Mark a sub-task permanently failed |
2123
| `record_phase_anchor` | Persist phase summary for session recovery |
2224
| `report_completion` | Final report with completion % |
2325

2426
### Constraints
2527

26-
- NO direct file access. No Read/Write/Edit/Bash/Glob/Grep tools.
27-
- Works exclusively through dispatched agents.
28-
- Must call `wait` between monitoring rounds (30-60s).
28+
- No direct file mutation. Lightweight read/search helpers may exist for context, but implementation work must go through dispatched agents.
29+
- Works primarily through dispatched agents.
30+
- Prefer `wait_for_agent_message` for monitoring; use `read_pane_output` only for explicit inspection or stalled agents.
2931
- Must tell agents to commit their work when done.
3032
- After verification passes, must ensure the finished work is committed and landed on `main` before reporting completion.
3133
- When asking another LLM/agent for repo analysis or design feedback, ensure it can read/search the relevant repo context or provide the needed files explicitly. Do not ask for blind analysis.
@@ -44,7 +46,7 @@ A CEO-class orchestrator. It owns the outcome end-to-end: from understanding the
4446
The system prompt in `prompts/lead_agent.md` should:
4547
1. **Set the identity** — a decisive, proactive CEO who delivers results.
4648
2. **Define the workflow** — align, plan, dispatch, monitor, finish.
47-
3. **Enforce good habits**wait between checks, write specific agent prompts, verify and commit.
49+
3. **Enforce good habits**submit a plan first, write specific agent prompts, monitor via mailbox, verify and commit.
4850
4. **Stay concise** — every sentence should change behavior. Remove anything that doesn't.
4951

5052
### Sub-agents

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## 0.9.55
4+
5+
- **Fix**: Large MCP / lead-agent tool outputs now spill to `.openmax/tool-outputs/` and return a short pointer + preview instead of flooding the model context
6+
- **Fix**: `submit_plan` no longer blocks inline on long monitoring loops; it dispatches and returns immediately so MCP calls stop appearing hung
7+
- **Fix**: Interactive agent ready-wait no longer sits on a 30s floor; pane startup now exits early on dead panes and uses a short bounded timeout
8+
- **Fix**: Exited panes are no longer treated as success by default; auto-detect now requires a report or committed branch changes, otherwise the task is marked permanent error
9+
- **Fix**: Auto-detected successful exits now run the same mark/merge path as mailbox `done`, so they no longer bypass merge
10+
- **Docs**: Align lead-agent prompt and `AGENTS.md` with the actual tool contract (`submit_plan`, `wait_for_agent_message`, `permanent_error`, auto verify/report behavior)
11+
312
## 0.9.54
413

514
- **Fix**: Interactive agents (claude-code, codex, opencode) that exit without calling `report_done` are now auto-detected and marked DONE — previously they stayed in RUNNING state, causing the monitoring loop to spin until timeout with no notification

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "openmax"
7-
version = "0.9.54"
7+
version = "0.9.55"
88
description = "Extreme parallel acceleration — one command dispatches multiple AI agents with domain-aware task decomposition"
99
readme = "README.md"
1010
license = "MIT"

src/openmax/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""openMax - Multi AI Agent orchestration hub."""
22

3-
__version__ = "0.9.54"
3+
__version__ = "0.9.55"

src/openmax/lead_agent/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
dispatch_agent,
88
mark_task_done,
99
merge_agent_branch,
10+
permanent_error,
1011
read_file_tool,
1112
read_pane_output,
1213
record_phase_anchor,
@@ -36,6 +37,7 @@
3637
"dispatch_agent",
3738
"mark_task_done",
3839
"merge_agent_branch",
40+
"permanent_error",
3941
"read_file_tool",
4042
"read_pane_output",
4143
"record_phase_anchor",

src/openmax/lead_agent/core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ def _task_status_from_value(value: str) -> TaskStatus:
148148
return TaskStatus.RUNNING
149149
if normalized == TaskStatus.DONE.value:
150150
return TaskStatus.DONE
151+
if normalized == TaskStatus.PERMANENT_ERROR.value:
152+
return TaskStatus.PERMANENT_ERROR
151153
if normalized == TaskStatus.ERROR.value:
152154
return TaskStatus.ERROR
153155
return TaskStatus.PENDING

src/openmax/lead_agent/formatting.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"ask_user": "intervention",
2222
"merge_agent_branch": "system",
2323
"mark_task_done": "system",
24+
"permanent_error": "system",
2425
"record_phase_anchor": "system",
2526
"transition_phase": "system",
2627
"check_conflicts": "system",
@@ -77,6 +78,7 @@ def _coerce_tool_int(value: Any) -> int | None:
7778
"read_pane_output": ("Checking pane {0}", ["pane_id"]),
7879
"send_text_to_pane": ("Sending to pane {0}", ["pane_id"]),
7980
"mark_task_done": ("Marking {0} done", ["task_name"]),
81+
"permanent_error": ("Marking {0} failed", ["task_name"]),
8082
"merge_agent_branch": ("Merging branch for {0}", ["task_name"]),
8183
"run_command": ("Running: {0}", ["command"]),
8284
"run_verification": ("Verifying: {0}", ["check_type"]),

src/openmax/lead_agent/prompts/lead_agent.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ Good: "The login endpoint in src/api/auth.py returns 500 when email contains '+'
150150

151151
| `type` | Action |
152152
|-------------|--------|
153-
| `done` | **Auto-handled**: `mark_task_done` + `merge_agent_branch` + `lint verification` run automatically. Check `auto_merged` and `auto_verified` in response. Only intervene if auto-merge failed or verification failed. |
153+
| `done` | **Auto-handled**: `mark_task_done` + `merge_agent_branch` run automatically. Final verification runs when all tasks finish. Check `auto_merged` and `auto_verified` in response. |
154154
| `question` | Decide. Call `send_text_to_pane` with your answer. |
155155
| `blocked` | Send guidance via `send_text_to_pane`. If unresolvable, `permanent_error`. |
156156
| `progress` | No action needed unless pct=100. |
@@ -191,7 +191,7 @@ Include `check_checkpoints` in every monitoring round. For each pending item:
191191

192192
Every dispatch prompt includes "Run tests and commit your changes when done." Agents self-verify.
193193

194-
**Skip `run_verification`** when all agents reported `done` via mailbox AND were auto-merged AND auto-verified successfully (check `auto_verified.status == "pass"` in response). Go straight to `report_completion`.
194+
**Skip `run_verification`** when `wait_for_agent_message` already returned `auto_verified.status == "pass"` or `auto_verified.status == "skipped"`.
195195

196196
Only run verification manually if:
197197
- An agent exited without a `done` message
@@ -200,9 +200,9 @@ Only run verification manually if:
200200

201201
### Finish
202202

203-
0. **Mark done + merge + verify**: All auto-handled on `done` signal. Check `auto_merged` and `auto_verified` in `wait_for_agent_message` response.
204-
1. **When `all_done: true`**: Call `report_completion` immediately in the SAME response. Do not call `wait_for_agent_message` again. One tool call, done.
205-
2. Only intervene manually if auto-merge had conflicts or `auto_verified.status != "pass"`.
203+
0. **Mark done + merge**: Auto-handled on `done` signal. Check `auto_merged` in `wait_for_agent_message` response.
204+
1. **When `all_done: true`**: `wait_for_agent_message` already runs final verification and completion reporting. Read the response, summarize briefly, and stop.
205+
2. Only intervene manually if auto-merge had conflicts or `auto_verified.status == "fail"`.
206206

207207
### Phase Transitions
208208

@@ -222,11 +222,11 @@ Simple tasks: skip to `implement` → `verify` only. Each transition requires a
222222
| Blackboard exists | `read_shared_context` before dispatching dependent agents. |
223223
| Checkpoint file detected | `check_checkpoints` → decide → `resolve_checkpoint`. |
224224
| Need deeper context mid-task | Dispatch research agent to investigate and report. |
225-
| Agent exited successfully | `mark_task_done``merge_agent_branch` immediately. |
225+
| Agent exited successfully | `mark_task_done``merge_agent_branch` immediately, or rely on auto-detect path if mailbox message never arrived. |
226226
| Agent exited with rate limit (`rate_limited: true`) | **Stop and `ask_user`**: "Rate limited — press Enter when ready to retry." Do NOT auto-retry. One rate limit likely means all agents are limited. After user confirms, re-dispatch. No retry cap for rate limits. |
227-
| Agent exited with error (not rate limit) | `permanent_error(task_name)`. |
228-
| Agent exited unexpectedly | retry_count <2: re-dispatch. ≥2: `permanent_error`. |
229-
| All agents done | `run_verification` for lint + test immediately. |
227+
| Agent exited with error (not rate limit) | `permanent_error(task_name, reason)`. |
228+
| Agent exited unexpectedly | retry_count <2: re-dispatch. ≥2: `permanent_error(task_name, reason)`. |
229+
| All agents done | Read `wait_for_agent_message` response first. It already triggers final verify/report. |
230230

231231
## 4. Employees
232232

src/openmax/lead_agent/tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from openmax.lead_agent.tools._planning import (
3232
check_checkpoints,
3333
mark_task_done,
34+
permanent_error,
3435
resolve_checkpoint,
3536
submit_plan,
3637
transition_phase,
@@ -50,6 +51,7 @@
5051
list_employees_tool,
5152
mark_task_done,
5253
merge_agent_branch,
54+
permanent_error,
5355
list_managed_panes,
5456
read_file_tool,
5557
read_pane_output,

src/openmax/lead_agent/tools/_helpers.py

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from openmax.pane_backend import PaneBackendError
2020
from openmax.pane_manager import PaneManager
2121
from openmax.session_runtime import anchor_payload
22-
from openmax.task_file import read_report, read_shared_context, report_path
22+
from openmax.task_file import read_report, read_shared_context, report_path, write_tool_output
2323

2424
logger = logging.getLogger(__name__)
2525

@@ -122,13 +122,100 @@ def _record_phase_anchor(phase: str, summary: str, completion_pct: int | None =
122122
_update_session_phase(normalized_phase)
123123

124124

125-
_TOOL_RESPONSE_MAX_CHARS = 500_000
125+
_TOOL_RESPONSE_MAX_CHARS = 100_000
126+
_TOOL_OUTPUT_SPILL_THRESHOLD = 12_000
127+
_TOOL_OUTPUT_PREVIEW_CHARS = 240
128+
129+
130+
def _runtime_cwd_or_fallback() -> str:
131+
try:
132+
return _runtime().cwd
133+
except RuntimeError:
134+
return str(Path.cwd())
135+
136+
137+
def _compact_preview(text: str, limit: int = _TOOL_OUTPUT_PREVIEW_CHARS) -> str:
138+
preview = " ".join(text.split())
139+
if len(preview) <= limit:
140+
return preview
141+
return preview[: limit - 3].rstrip() + "..."
142+
143+
144+
def _spill_large_text(
145+
text: str,
146+
*,
147+
prefix: str,
148+
ext: str = ".txt",
149+
label: str = "output",
150+
) -> dict[str, Any]:
151+
cwd = _runtime_cwd_or_fallback()
152+
path = write_tool_output(cwd, prefix, text, ext=ext)
153+
try:
154+
rel_path = str(path.relative_to(cwd))
155+
except ValueError:
156+
rel_path = str(path)
157+
return {
158+
"spilled": True,
159+
"label": label,
160+
"path": rel_path,
161+
"chars": len(text),
162+
"preview": _compact_preview(text),
163+
}
164+
165+
166+
def _spill_large_values(value: Any, *, prefix: str) -> Any:
167+
if isinstance(value, str):
168+
if len(value) <= _TOOL_OUTPUT_SPILL_THRESHOLD:
169+
return value
170+
return _spill_large_text(value, prefix=prefix, label="text")
171+
if isinstance(value, dict):
172+
result: dict[str, Any] = {}
173+
for key, child in value.items():
174+
result[key] = _spill_large_values(child, prefix=f"{prefix}-{key}")
175+
return result
176+
if isinstance(value, list):
177+
return [
178+
_spill_large_values(child, prefix=f"{prefix}-{idx}") for idx, child in enumerate(value)
179+
]
180+
return value
181+
182+
183+
def _finalize_payload(payload: Any, max_chars: int) -> str:
184+
if isinstance(payload, (dict, list)):
185+
text = json.dumps(payload, ensure_ascii=False)
186+
else:
187+
text = str(payload)
188+
if len(text) <= max_chars:
189+
return text
190+
191+
ext = ".json" if isinstance(payload, (dict, list)) else ".txt"
192+
spilled = _spill_large_text(
193+
text,
194+
prefix="tool-response",
195+
ext=ext,
196+
label="payload",
197+
)
198+
compact = {
199+
"spilled": True,
200+
"reason": "response_too_large",
201+
"path": spilled["path"],
202+
"chars": spilled["chars"],
203+
"preview": spilled["preview"],
204+
}
205+
return json.dumps(compact, ensure_ascii=False)
126206

127207

128208
def _tool_response(data: Any, max_chars: int = _TOOL_RESPONSE_MAX_CHARS) -> dict[str, Any]:
129-
text = json.dumps(data, ensure_ascii=False) if isinstance(data, (dict, list)) else str(data)
130-
if len(text) > max_chars:
131-
text = text[:max_chars] + "\n...[truncated]"
209+
if isinstance(data, str) and len(data) > _TOOL_OUTPUT_SPILL_THRESHOLD:
210+
spill = _spill_large_text(data, prefix="tool-response", label="text")
211+
text = (
212+
f"large output written to {spill['path']} "
213+
f"({spill['chars']} chars). preview: {spill['preview']}"
214+
)
215+
elif isinstance(data, (dict, list)):
216+
text = _finalize_payload(_spill_large_values(data, prefix="tool-response"), max_chars)
217+
else:
218+
text = _finalize_payload(data, max_chars)
132219
return {"content": [{"type": "text", "text": text}]}
133220

134221

@@ -274,6 +361,8 @@ async def _wait_for_pane_ready(
274361
text = ""
275362
if any(pat in text for pat in ready_patterns):
276363
return True
364+
if not pane_mgr.is_pane_alive(pane_id):
365+
return False
277366
lines = [ln for ln in text.strip().splitlines() if ln.strip()]
278367
if len(lines) >= 3 and text == prev_text:
279368
stable_count += 1
@@ -539,11 +628,12 @@ async def _wait_and_send_prompt(
539628

540629
ready = True
541630
if cmd_spec.ready_patterns:
631+
ready_timeout = max(min(cmd_spec.ready_delay_seconds * 4, 12.0), 5.0)
542632
ready = await _wait_for_pane_ready(
543633
runtime.pane_mgr,
544634
pane.pane_id,
545635
cmd_spec.ready_patterns,
546-
timeout=max(cmd_spec.ready_delay_seconds * 4, 30.0),
636+
timeout=ready_timeout,
547637
)
548638
if not ready:
549639
console.print(

0 commit comments

Comments
 (0)