feat(api): structured run events via /v1/runs SSE endpoint
Add POST /v1/runs to start async agent runs and GET /v1/runs/{run_id}/events
for SSE streaming of typed lifecycle events (tool.started, tool.completed,
message.delta, reasoning.available, run.completed, run.failed).
Changes the internal tool_progress_callback signature from positional
(tool_name, preview, args) to event-type-first
(event_type, tool_name, preview, args, **kwargs). Existing consumers
filter on event_type and remain backward-compatible.
Adds concurrency limit (_MAX_CONCURRENT_RUNS=10) and orphaned run sweep.
Fixes logic inversion in cli.py _on_tool_progress where the original PR
would have displayed internal tools instead of non-internal ones.
Co-authored-by: Mibayy <mibayy@users.noreply.github.com>
This commit is contained in:
@@ -52,7 +52,7 @@ class TestToolProgressCallback:
|
||||
future.result.return_value = None
|
||||
mock_rcts.return_value = future
|
||||
|
||||
cb("terminal", "$ ls -la", {"command": "ls -la"})
|
||||
cb("tool.started", "terminal", "$ ls -la", {"command": "ls -la"})
|
||||
|
||||
# Should have tracked the tool call ID
|
||||
assert "terminal" in tool_call_ids
|
||||
@@ -75,7 +75,7 @@ class TestToolProgressCallback:
|
||||
future.result.return_value = None
|
||||
mock_rcts.return_value = future
|
||||
|
||||
cb("read_file", "Reading /etc/hosts", '{"path": "/etc/hosts"}')
|
||||
cb("tool.started", "read_file", "Reading /etc/hosts", '{"path": "/etc/hosts"}')
|
||||
|
||||
assert "read_file" in tool_call_ids
|
||||
|
||||
@@ -91,7 +91,7 @@ class TestToolProgressCallback:
|
||||
future.result.return_value = None
|
||||
mock_rcts.return_value = future
|
||||
|
||||
cb("terminal", "$ echo hi", None)
|
||||
cb("tool.started", "terminal", "$ echo hi", None)
|
||||
|
||||
assert "terminal" in tool_call_ids
|
||||
|
||||
@@ -108,8 +108,8 @@ class TestToolProgressCallback:
|
||||
future.result.return_value = None
|
||||
mock_rcts.return_value = future
|
||||
|
||||
progress_cb("terminal", "$ ls", {"command": "ls"})
|
||||
progress_cb("terminal", "$ pwd", {"command": "pwd"})
|
||||
progress_cb("tool.started", "terminal", "$ ls", {"command": "ls"})
|
||||
progress_cb("tool.started", "terminal", "$ pwd", {"command": "pwd"})
|
||||
assert len(tool_call_ids["terminal"]) == 2
|
||||
|
||||
step_cb(1, [{"name": "terminal", "result": "ok-1"}])
|
||||
|
||||
@@ -130,7 +130,7 @@ class TestMcpRegistrationE2E:
|
||||
# 1) Agent fires tool_progress_callback (ToolCallStart)
|
||||
if agent.tool_progress_callback:
|
||||
agent.tool_progress_callback(
|
||||
"terminal", "$ echo hello", {"command": "echo hello"}
|
||||
"tool.started", "terminal", "$ echo hello", {"command": "echo hello"}
|
||||
)
|
||||
|
||||
# 2) Agent fires step_callback with tool results (ToolCallUpdate)
|
||||
@@ -197,8 +197,8 @@ class TestMcpRegistrationE2E:
|
||||
agent = state.agent
|
||||
# Fire two tool calls
|
||||
if agent.tool_progress_callback:
|
||||
agent.tool_progress_callback("read_file", "read: /etc/hosts", {"path": "/etc/hosts"})
|
||||
agent.tool_progress_callback("web_search", "web search: test", {"query": "test"})
|
||||
agent.tool_progress_callback("tool.started", "read_file", "read: /etc/hosts", {"path": "/etc/hosts"})
|
||||
agent.tool_progress_callback("tool.started", "web_search", "web search: test", {"query": "test"})
|
||||
|
||||
if agent.step_callback:
|
||||
agent.step_callback(1, [
|
||||
|
||||
@@ -96,7 +96,7 @@ class TestBuildChildProgressCallback:
|
||||
cb = _build_child_progress_callback(0, parent)
|
||||
assert cb is not None
|
||||
|
||||
cb("web_search", "quantum computing")
|
||||
cb("tool.started", "web_search", "quantum computing", {})
|
||||
output = buf.getvalue()
|
||||
assert "web_search" in output
|
||||
assert "quantum computing" in output
|
||||
@@ -131,11 +131,11 @@ class TestBuildChildProgressCallback:
|
||||
|
||||
# Send 4 tool calls — shouldn't flush yet (BATCH_SIZE = 5)
|
||||
for i in range(4):
|
||||
cb(f"tool_{i}", f"arg_{i}")
|
||||
cb("tool.started", f"tool_{i}", f"arg_{i}", {})
|
||||
parent_cb.assert_not_called()
|
||||
|
||||
# 5th call should trigger flush
|
||||
cb("tool_4", "arg_4")
|
||||
cb("tool.started", "tool_4", "arg_4", {})
|
||||
parent_cb.assert_called_once()
|
||||
call_args = parent_cb.call_args
|
||||
assert "tool_0" in call_args[0][1]
|
||||
@@ -207,7 +207,7 @@ class TestBuildChildProgressCallback:
|
||||
parent.tool_progress_callback = None
|
||||
|
||||
cb = _build_child_progress_callback(0, parent, task_count=1)
|
||||
cb("web_search", "test")
|
||||
cb("tool.started", "web_search", "test", {})
|
||||
|
||||
output = buf.getvalue()
|
||||
assert "[" not in output
|
||||
@@ -330,9 +330,9 @@ class TestBatchFlush:
|
||||
cb = _build_child_progress_callback(0, parent)
|
||||
|
||||
# Send 3 tools (below batch size of 5)
|
||||
cb("web_search", "query1")
|
||||
cb("read_file", "file.txt")
|
||||
cb("write_file", "out.txt")
|
||||
cb("tool.started", "web_search", "query1", {})
|
||||
cb("tool.started", "read_file", "file.txt", {})
|
||||
cb("tool.started", "write_file", "out.txt", {})
|
||||
parent_cb.assert_not_called()
|
||||
|
||||
# Flush should send the remaining 3
|
||||
@@ -365,7 +365,7 @@ class TestBatchFlush:
|
||||
parent.tool_progress_callback = None
|
||||
|
||||
cb = _build_child_progress_callback(0, parent)
|
||||
cb("web_search", "test")
|
||||
cb("tool.started", "web_search", "test", {})
|
||||
cb._flush() # Should not crash
|
||||
|
||||
|
||||
|
||||
@@ -60,9 +60,9 @@ class FakeAgent:
|
||||
self.tools = []
|
||||
|
||||
def run_conversation(self, message, conversation_history=None, task_id=None):
|
||||
self.tool_progress_callback("terminal", "pwd")
|
||||
self.tool_progress_callback("tool.started", "terminal", "pwd", {})
|
||||
time.sleep(0.35)
|
||||
self.tool_progress_callback("browser_navigate", "https://example.com")
|
||||
self.tool_progress_callback("tool.started", "browser_navigate", "https://example.com", {})
|
||||
time.sleep(0.35)
|
||||
return {
|
||||
"final_response": "done",
|
||||
|
||||
Reference in New Issue
Block a user