Commit e09516f
authored
refactor: agent state machine, deterministic result passing, overflow-to-file (#57)
* refactor: deterministic agent result passing + completion notification injection
Three architectural changes to the multi-agent communication system:
1. Deterministic result passing (no fallback):
- agent/completed notification now carries reason + result fields
- Hub agent_io_loop extracts result from agent/completed only
- Removed last_stream/completion_output fallback in agent_io_loop
- AgentOutput.result flows: runtime → server → IPC → Hub (single path)
2. Completion notification injection (push model):
- MessageSource::System variant for Hub-generated notifications
- Hub deliver_to_parent() sends Envelope to parent's completion_tx
- completion_bridge forwards from Hub channel to parent via IPC
- session_forward handles agent/message notifications for injection
- System messages are ephemeral (not persisted to session)
3. Remove max_turns limit:
- TerminateReason::MaxTurns, AgentEventPayload::MaxTurnsReached removed
- Settings.max_turns, AgentConfig.max_turns fields removed
- LOOPAL_MAX_TURNS env var handling removed
- Agent loop runs without artificial turn limits
Agent tool updated: foreground (blocking) is default, multiple foreground
spawns in one turn execute in parallel via JoinSet. Background mode
reserved for when parent has independent work to do.
* refactor: remove AttemptCompletion tool — agent output is streaming text
AttemptCompletion was a flawed abstraction that:
1. Forced LLMs to re-summarize content already output as streaming text
2. Used the compressed summary as agent output, losing information
3. Conflated "I'm done" signal with content delivery
After removal, agent completion works naturally:
- Agent stops when LLM generates no tool calls (existing path)
- Agent output = last assistant streaming text (TurnOutput.output)
- No special tool or is_completion flag needed
Removed across the entire stack:
- AttemptCompletionTool definition and registration
- ToolResult::completion() constructor and is_completion field
- ContentBlock::ToolResult is_completion field
- AgentEventPayload::ToolResult is_completion field
- Runtime completion detection in turn_exec/tools/finalize_tool_results
- Session tool_result_handler completion promotion logic
- Protocol projection completion promotion
- Agent bridge completion_result tracking
- Headless mode is_completion detection
- Sub-agent prompt "call AttemptCompletion" instructions
- All related test infrastructure (FakeCompletionTool, scenarios, etc.)
* refactor: explicit agent state machine — Runtime drives AgentStatus
Runtime layer now holds AgentStatus as source of truth and drives
state transitions via events. Replaces the `initial_prompt` heuristic.
State machine: Starting → Running → WaitingForInput → Running → ... → Finished
Key changes:
- AgentLoopRunner holds `status: AgentStatus` field
- `transition()` method updates status and emits corresponding event
- `run_loop()` unified: all agents (interactive + task) run the same loop
- Removed `initial_prompt = !store.is_empty()` hack
- `wait_for_input()` no longer emits AwaitingInput (run_loop controls it)
- AwaitingInput emitted for ALL agents after turn completion (unified idle signal)
- Task agents exit when input channel closes (channel lifecycle = agent lifecycle)
- IntegrationHarness.close_input() for tests that need prompt-driven exit
* refactor: complete agent state machine — all transitions through transition()
Every AgentStatus change now goes through transition() or transition_error(),
ensuring Runtime is the deterministic SSOT for agent state.
State transition table:
- Starting → Running: transition(Running) + emit(Started) [once at startup]
- Running → WaitingForInput: transition(WaitingForInput) → emit(AwaitingInput)
- WaitingForInput → Running: transition(Running) [implicit — Stream/ToolCall signal activity]
- Running → Error: transition_error(msg) → emit(Error { message })
- Interrupted: status = WaitingForInput + emit(Interrupted)
- * → Finished: transition(Finished) → emit(Finished)
Removed: direct `self.status = AgentStatus::Running` assignment,
standalone `self.emit(Error/Finished)` calls that bypassed status update.
Exception: FinishedGuard (panic Drop) uses try_emit without status update —
acceptable since self is inaccessible during stack unwinding.
* feat: overflow-to-file for large outputs + agent edge case fixes
Large tool/agent outputs are now saved to overflow files instead of being
silently truncated. The LLM receives a preview + file path and can use
the Read tool to access full content on demand.
Overflow-to-file integration:
- tool-api: handle_overflow() + save_to_overflow_file() core abstraction
- tool_pipeline: uses handle_overflow instead of truncate + manual save
- backend/shell: bash stdout/stderr overflow to file
- backend/grep: full match results saved when exceeding limit
- backend/glob: full file list saved when exceeding limit
- backend/net: HTTP body saved when exceeding fetch size limit
- agent-hub/completion: large agent results overflow before parent delivery
Overflow files stored in {tmp}/loopal/overflow/{label}_{timestamp}.txt
Agent edge case fixes:
- Background spawn: 1-hour timeout on worktree cleanup wait
- GrepSearchResult, GlobSearchResult, FetchResult: overflow_path field added
* fix: close input channel for task agents so they exit after prompt
Prompt-driven (task) sub-agents were hanging forever in wait_for_input()
because SharedSession held the only input_tx sender clone, preventing
the channel from closing.
Fix: for task sessions (has_initial_prompt=true), use a dummy sender in
SharedSession instead of the real input_tx. The real sender is dropped
immediately, and the placeholder clone is dropped on replace_session().
With zero connected senders, recv_input() returns None → agent exits
after processing the pre-loaded prompt.
* Revert "fix: close input channel for task agents so they exit after prompt"
This reverts commit ea9d992.
* feat: LifecycleMode — Task agents exit on idle, Interactive agents wait
Task agents (sub-agents, headless) now exit when idle with no pending
input, instead of relying on channel closure. This is an agent-internal
decision, not an external hack.
- LifecycleMode enum: Task (exit on idle) vs Interactive (wait for input)
- AgentConfig.lifecycle field, set by agent_setup based on prompt presence
- run_loop idle phase: Task drains pending input, exits if empty
- drain_pending_input() non-blocking check for queued messages
- HarnessBuilder defaults to Task; build_spawned() forces Interactive
- Reverted channel-closure hack (ea9d992)
* fix: implement HubFrontend.drain_pending + yield before Task exit
Two fixes for Task agent idle behavior:
1. HubFrontend now implements drain_pending() — non-blocking try_recv
from input_rx. Previously returned empty (default trait impl),
causing Task agents to miss queued IPC messages (sub-agent completions).
2. Task agent yields before drain check — gives in-flight IPC messages
time to arrive before deciding to exit.1 parent 58c58c7 commit e09516f
147 files changed
Lines changed: 1670 additions & 1700 deletions
File tree
- crates
- loopal-acp/src
- adapter
- translate
- loopal-agent-hub
- src
- agent_registry
- tests
- suite
- loopal-agent-server
- src
- tests/suite
- loopal-agent
- src
- tools
- collaboration
- tests
- suite
- loopal-backend/src
- search
- loopal-config
- src
- tests/suite
- loopal-context/tests/suite
- loopal-error
- src
- tests/suite
- loopal-ipc/tests/suite
- loopal-kernel/tests/suite
- loopal-mcp/src
- loopal-memory
- agent-prompts
- tests/suite
- loopal-message
- src
- tests/suite
- loopal-prompt-system/prompts/agents
- loopal-protocol
- src
- tests/suite
- loopal-provider/tests/suite
- loopal-runtime
- src
- agent_loop
- tests
- agent_loop
- suite
- loopal-sandbox/src
- loopal-session
- src
- tests/suite
- loopal-test-support/src
- loopal-tool-api/src
- loopal-tui/tests
- suite
- src/bootstrap
- tests
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
99 | | - | |
| 99 | + | |
100 | 100 | | |
101 | 101 | | |
102 | 102 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
101 | 101 | | |
102 | 102 | | |
103 | 103 | | |
104 | | - | |
| 104 | + | |
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
137 | 137 | | |
138 | 138 | | |
139 | 139 | | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | 140 | | |
144 | 141 | | |
145 | 142 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
62 | | - | |
63 | | - | |
64 | | - | |
65 | 62 | | |
66 | 63 | | |
67 | 64 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
99 | | - | |
100 | 99 | | |
101 | 100 | | |
102 | 101 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
21 | | - | |
| 21 | + | |
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
29 | | - | |
30 | | - | |
| 29 | + | |
31 | 30 | | |
32 | 31 | | |
33 | 32 | | |
34 | 33 | | |
35 | 34 | | |
36 | | - | |
37 | | - | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
38 | 41 | | |
39 | 42 | | |
40 | 43 | | |
41 | | - | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
53 | | - | |
54 | | - | |
55 | 44 | | |
56 | 45 | | |
57 | 46 | | |
| |||
101 | 90 | | |
102 | 91 | | |
103 | 92 | | |
104 | | - | |
105 | | - | |
106 | | - | |
107 | | - | |
108 | | - | |
109 | | - | |
| 93 | + | |
110 | 94 | | |
111 | 95 | | |
112 | 96 | | |
| |||
162 | 146 | | |
163 | 147 | | |
164 | 148 | | |
165 | | - | |
166 | | - | |
167 | | - | |
168 | | - | |
169 | | - | |
170 | | - | |
171 | | - | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
172 | 162 | | |
173 | 163 | | |
174 | 164 | | |
| |||
185 | 175 | | |
186 | 176 | | |
187 | 177 | | |
188 | | - | |
189 | | - | |
190 | | - | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
191 | 189 | | |
192 | 190 | | |
193 | 191 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
5 | | - | |
| 4 | + | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
12 | | - | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
13 | 20 | | |
14 | 21 | | |
15 | 22 | | |
16 | 23 | | |
17 | 24 | | |
18 | 25 | | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
19 | 29 | | |
20 | 30 | | |
21 | 31 | | |
| |||
28 | 38 | | |
29 | 39 | | |
30 | 40 | | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
31 | 72 | | |
32 | 73 | | |
33 | 74 | | |
| |||
87 | 128 | | |
88 | 129 | | |
89 | 130 | | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
| 49 | + | |
49 | 50 | | |
50 | 51 | | |
51 | 52 | | |
52 | 53 | | |
53 | 54 | | |
54 | | - | |
| 55 | + | |
55 | 56 | | |
56 | 57 | | |
57 | 58 | | |
| |||
60 | 61 | | |
61 | 62 | | |
62 | 63 | | |
| 64 | + | |
63 | 65 | | |
64 | 66 | | |
65 | 67 | | |
| |||
74 | 76 | | |
75 | 77 | | |
76 | 78 | | |
| 79 | + | |
77 | 80 | | |
78 | 81 | | |
79 | 82 | | |
| |||
0 commit comments