V0 finally out

This commit is contained in:
Renn F
2025-12-14 22:05:34 +01:00
parent 348795c50b
commit bc9cd7575d
17 changed files with 1248 additions and 240 deletions
+5 -10
View File
@@ -39,11 +39,6 @@ _toon = ToonAdapter()
# =============================================================================
def _get_api_url() -> str:
"""Get the RoboCo API base URL."""
return f"http://{settings.host}:{settings.port}/api/v1"
def _format_error_response(
error_code: str,
message: str,
@@ -67,7 +62,7 @@ async def _post_journal_entry(
"""Post to a journal endpoint. Returns (data, error)."""
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{_get_api_url()}/journals/me/{endpoint}",
f"{settings.internal_api_url}/journals/me/{endpoint}",
json=payload,
headers={"X-Agent-Id": agent_id},
)
@@ -224,7 +219,7 @@ async def _handle_search(query: str, top_k: int, agent_id: str) -> dict[str, Any
async with httpx.AsyncClient() as client:
payload = {"query": query, "top_k": min(top_k, 20)}
resp = await client.post(
f"{_get_api_url()}/journals/me/search",
f"{settings.internal_api_url}/journals/me/search",
json=payload,
headers={"X-Agent-Id": agent_id},
)
@@ -253,11 +248,11 @@ async def _handle_stats(agent_id: str) -> dict[str, Any]:
"""Handle journal stats retrieval."""
async with httpx.AsyncClient() as client:
stats_resp = await client.get(
f"{_get_api_url()}/journals/me/stats",
f"{settings.internal_api_url}/journals/me/stats",
headers={"X-Agent-Id": agent_id},
)
growth_resp = await client.get(
f"{_get_api_url()}/journals/me/growth",
f"{settings.internal_api_url}/journals/me/growth",
headers={"X-Agent-Id": agent_id},
)
@@ -302,7 +297,7 @@ async def _handle_recent(
params["task_id"] = task_id
resp = await client.get(
f"{_get_api_url()}/journals/me/entries",
f"{settings.internal_api_url}/journals/me/entries",
params=params,
headers={"X-Agent-Id": agent_id},
)
+9 -12
View File
@@ -52,11 +52,6 @@ def _check_channel_access(agent_id: str, channel_slug: str, action: str) -> bool
return bool(action == "read" and agent_id in channel.get("silent", []))
def _get_api_url() -> str:
"""Get the RoboCo API base URL."""
return f"http://{settings.host}:{settings.port}/api/v1"
def _format_error_response(
error_code: str,
message: str,
@@ -122,13 +117,15 @@ async def _get_or_create_session(
channel_id: str,
) -> str | dict[str, Any]:
"""Get or create session for channel. Returns session_id or error dict."""
session_resp = await client.get(f"{_get_api_url()}/channels/{channel_id}/session")
session_resp = await client.get(
f"{settings.internal_api_url}/channels/{channel_id}/session"
)
if session_resp.status_code == status.HTTP_200_OK:
return str(session_resp.json()["id"])
create_resp = await client.post(
f"{_get_api_url()}/sessions",
f"{settings.internal_api_url}/sessions",
json={"channel_id": channel_id},
)
if create_resp.status_code in [status.HTTP_200_OK, status.HTTP_201_CREATED]:
@@ -184,7 +181,7 @@ async def _handle_channel_history(
async with httpx.AsyncClient() as client:
channels_resp = await client.get(
f"{_get_api_url()}/channels",
f"{settings.internal_api_url}/channels",
params={"slug": channel_slug},
)
@@ -200,7 +197,7 @@ async def _handle_channel_history(
channel_id = channels[0]["id"]
messages_resp = await client.get(
f"{_get_api_url()}/channels/{channel_id}/messages",
f"{settings.internal_api_url}/channels/{channel_id}/messages",
params={"after": since.isoformat(), "limit": limit},
)
@@ -230,7 +227,7 @@ async def _handle_message_send(
async with httpx.AsyncClient() as client:
channels_resp = await client.get(
f"{_get_api_url()}/channels",
f"{settings.internal_api_url}/channels",
params={"slug": data.channel_slug},
)
@@ -258,7 +255,7 @@ async def _handle_message_send(
}
send_resp = await client.post(
f"{_get_api_url()}/messages",
f"{settings.internal_api_url}/messages",
json=message_data,
headers={"X-Agent-Id": agent_id},
)
@@ -279,7 +276,7 @@ async def _handle_message_send(
async def _handle_message_get(message_id: str) -> dict[str, Any]:
"""Handle message retrieval."""
async with httpx.AsyncClient() as client:
resp = await client.get(f"{_get_api_url()}/messages/{message_id}")
resp = await client.get(f"{settings.internal_api_url}/messages/{message_id}")
if resp.status_code == status.HTTP_404_NOT_FOUND:
return _format_error_response(
+4 -9
View File
@@ -60,11 +60,6 @@ def _can_send_notification(sender_id: str, recipient_id: str) -> tuple[bool, str
return False, f"You cannot send notifications to {recipient_id}"
def _get_api_url() -> str:
"""Get the RoboCo API base URL."""
return f"http://{settings.host}:{settings.port}/api/v1"
def _format_error_response(
error_code: str,
message: str,
@@ -100,7 +95,7 @@ async def _handle_list(
}
resp = await client.get(
f"{_get_api_url()}/notifications",
f"{settings.internal_api_url}/notifications",
params=params,
headers={"X-Agent-Id": agent_id},
)
@@ -137,7 +132,7 @@ async def _handle_get(agent_id: str, notification_id: str) -> dict[str, Any]:
"""Handle getting a specific notification."""
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{_get_api_url()}/notifications/{notification_id}",
f"{settings.internal_api_url}/notifications/{notification_id}",
headers={"X-Agent-Id": agent_id},
)
@@ -168,7 +163,7 @@ async def _handle_ack(agent_id: str, notification_id: str) -> dict[str, Any]:
"""Handle acknowledging a notification."""
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{_get_api_url()}/notifications/{notification_id}/ack",
f"{settings.internal_api_url}/notifications/{notification_id}/ack",
headers={"X-Agent-Id": agent_id},
)
@@ -249,7 +244,7 @@ async def _handle_send(agent_id: str, data: SendNotificationInput) -> dict[str,
}
resp = await client.post(
f"{_get_api_url()}/notifications",
f"{settings.internal_api_url}/notifications",
json=payload,
headers={"X-Agent-Id": agent_id},
)
+84 -42
View File
@@ -35,16 +35,6 @@ _toon = ToonAdapter()
# NOTE: For task lifecycle validation, use enforcement.task_lifecycle.VALID_TRANSITIONS
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
def _get_api_url() -> str:
"""Get the RoboCo API base URL."""
return f"http://{settings.host}:{settings.port}/api/v1"
def _format_task_response(
task: dict[str, Any],
next_step: str,
@@ -158,7 +148,7 @@ async def _handle_task_scan(team: str | None, agent_id: str) -> dict[str, Any]:
async with httpx.AsyncClient() as client:
# Get paused tasks for this agent
paused_resp = await client.get(
f"{_get_api_url()}/tasks",
f"{settings.internal_api_url}/tasks",
params={"assigned_to": agent_id, "status": "paused"},
)
paused_tasks = (
@@ -167,7 +157,7 @@ async def _handle_task_scan(team: str | None, agent_id: str) -> dict[str, Any]:
# Get assigned tasks (claimed, in_progress)
assigned_resp = await client.get(
f"{_get_api_url()}/tasks",
f"{settings.internal_api_url}/tasks",
params={"assigned_to": agent_id},
)
assigned_data = (
@@ -187,7 +177,7 @@ async def _handle_task_scan(team: str | None, agent_id: str) -> dict[str, Any]:
if team:
params["team"] = team
available_resp = await client.get(
f"{_get_api_url()}/tasks",
f"{settings.internal_api_url}/tasks",
params=params,
)
available_tasks = (
@@ -229,7 +219,7 @@ async def _handle_task_scan(team: str | None, agent_id: str) -> dict[str, Any]:
async def _handle_task_get(task_id: str) -> dict[str, Any]:
"""Handle getting task details."""
async with httpx.AsyncClient() as client:
resp = await client.get(f"{_get_api_url()}/tasks/{task_id}")
resp = await client.get(f"{settings.internal_api_url}/tasks/{task_id}")
if resp.status_code == status.HTTP_404_NOT_FOUND:
return _format_error_response(
@@ -285,7 +275,7 @@ def _validate_task_claimable(task: dict) -> dict[str, Any] | None:
async def _get_project_context(project_id: str) -> dict[str, Any] | None:
"""Fetch project context if available."""
async with httpx.AsyncClient() as client:
resp = await client.get(f"{_get_api_url()}/projects/{project_id}")
resp = await client.get(f"{settings.internal_api_url}/projects/{project_id}")
if resp.status_code == status.HTTP_200_OK:
result: dict[str, Any] = resp.json()
return result
@@ -296,7 +286,7 @@ async def _handle_task_claim(task_id: str, agent_id: str) -> dict[str, Any]:
"""Handle task claiming."""
async with httpx.AsyncClient() as client:
active_resp = await client.get(
f"{_get_api_url()}/tasks",
f"{settings.internal_api_url}/tasks",
params={"assigned_to": agent_id},
)
if active_resp.status_code == status.HTTP_200_OK:
@@ -306,7 +296,7 @@ async def _handle_task_claim(task_id: str, agent_id: str) -> dict[str, Any]:
if error := _check_paused_tasks(active_tasks):
return error
task_resp = await client.get(f"{_get_api_url()}/tasks/{task_id}")
task_resp = await client.get(f"{settings.internal_api_url}/tasks/{task_id}")
if task_resp.status_code == status.HTTP_404_NOT_FOUND:
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
@@ -315,7 +305,7 @@ async def _handle_task_claim(task_id: str, agent_id: str) -> dict[str, Any]:
return error
claim_resp = await client.post(
f"{_get_api_url()}/tasks/{task_id}/claim",
f"{settings.internal_api_url}/tasks/{task_id}/claim",
json={"agent_id": agent_id},
)
if claim_resp.status_code != status.HTTP_200_OK:
@@ -392,7 +382,7 @@ async def _handle_task_plan(
) -> dict[str, Any]:
"""Handle task planning."""
async with httpx.AsyncClient() as client:
task_resp = await client.get(f"{_get_api_url()}/tasks/{task_id}")
task_resp = await client.get(f"{settings.internal_api_url}/tasks/{task_id}")
if task_resp.status_code == status.HTTP_404_NOT_FOUND:
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
@@ -404,7 +394,7 @@ async def _handle_task_plan(
plan_data = _build_plan_data(plan_params)
update_resp = await client.patch(
f"{_get_api_url()}/tasks/{task_id}",
f"{settings.internal_api_url}/tasks/{task_id}",
json={"plan": plan_data},
)
if update_resp.status_code != status.HTTP_200_OK:
@@ -470,7 +460,7 @@ def _validate_task_start(task: dict[str, Any], agent_id: str) -> dict[str, Any]
async def _handle_task_start(task_id: str, agent_id: str) -> dict[str, Any]:
"""Handle task start."""
async with httpx.AsyncClient() as client:
task_resp = await client.get(f"{_get_api_url()}/tasks/{task_id}")
task_resp = await client.get(f"{settings.internal_api_url}/tasks/{task_id}")
if task_resp.status_code == status.HTTP_404_NOT_FOUND:
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
@@ -480,7 +470,9 @@ async def _handle_task_start(task_id: str, agent_id: str) -> dict[str, Any]:
return validation_error
# Start the task
start_resp = await client.post(f"{_get_api_url()}/tasks/{task_id}/start")
start_resp = await client.post(
f"{settings.internal_api_url}/tasks/{task_id}/start"
)
if start_resp.status_code != status.HTTP_200_OK:
return _format_error_response(
@@ -509,7 +501,7 @@ async def _handle_task_progress(
) -> dict[str, Any]:
"""Handle task progress update."""
async with httpx.AsyncClient() as client:
task_resp = await client.get(f"{_get_api_url()}/tasks/{task_id}")
task_resp = await client.get(f"{settings.internal_api_url}/tasks/{task_id}")
if task_resp.status_code == status.HTTP_404_NOT_FOUND:
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
@@ -528,7 +520,7 @@ async def _handle_task_progress(
# Add progress update
progress_resp = await client.post(
f"{_get_api_url()}/tasks/{task_id}/progress",
f"{settings.internal_api_url}/tasks/{task_id}/progress",
json={
"agent_id": agent_id,
"message": message,
@@ -566,7 +558,7 @@ async def _handle_task_block(
)
async with httpx.AsyncClient() as client:
task_resp = await client.get(f"{_get_api_url()}/tasks/{task_id}")
task_resp = await client.get(f"{settings.internal_api_url}/tasks/{task_id}")
if task_resp.status_code == status.HTTP_404_NOT_FOUND:
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
@@ -585,7 +577,7 @@ async def _handle_task_block(
# Block the task
block_resp = await client.post(
f"{_get_api_url()}/tasks/{task_id}/block",
f"{settings.internal_api_url}/tasks/{task_id}/block",
json={
"reason": reason,
"blocker_type": blocker_type,
@@ -614,7 +606,7 @@ async def _handle_task_block(
async def _handle_task_unblock(task_id: str, agent_id: str) -> dict[str, Any]:
"""Handle task unblocking."""
async with httpx.AsyncClient() as client:
task_resp = await client.get(f"{_get_api_url()}/tasks/{task_id}")
task_resp = await client.get(f"{settings.internal_api_url}/tasks/{task_id}")
if task_resp.status_code == status.HTTP_404_NOT_FOUND:
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
@@ -631,7 +623,9 @@ async def _handle_task_unblock(task_id: str, agent_id: str) -> dict[str, Any]:
"Task is not blocked",
)
unblock_resp = await client.post(f"{_get_api_url()}/tasks/{task_id}/unblock")
unblock_resp = await client.post(
f"{settings.internal_api_url}/tasks/{task_id}/unblock"
)
if unblock_resp.status_code != status.HTTP_200_OK:
return _format_error_response("UNBLOCK_FAILED", "Failed to unblock task")
@@ -654,7 +648,7 @@ async def _handle_task_pause(
) -> dict[str, Any]:
"""Handle task pausing."""
async with httpx.AsyncClient() as client:
task_resp = await client.get(f"{_get_api_url()}/tasks/{task_id}")
task_resp = await client.get(f"{settings.internal_api_url}/tasks/{task_id}")
if task_resp.status_code == status.HTTP_404_NOT_FOUND:
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
@@ -673,7 +667,7 @@ async def _handle_task_pause(
# Add checkpoint
await client.post(
f"{_get_api_url()}/tasks/{task_id}/checkpoint",
f"{settings.internal_api_url}/tasks/{task_id}/checkpoint",
json={
"agent_id": agent_id,
"state_summary": checkpoint_summary,
@@ -683,7 +677,9 @@ async def _handle_task_pause(
)
# Pause the task
pause_resp = await client.post(f"{_get_api_url()}/tasks/{task_id}/pause")
pause_resp = await client.post(
f"{settings.internal_api_url}/tasks/{task_id}/pause"
)
if pause_resp.status_code != status.HTTP_200_OK:
return _format_error_response("PAUSE_FAILED", "Failed to pause task")
@@ -705,7 +701,7 @@ async def _handle_task_submit_verification(
) -> dict[str, Any]:
"""Handle task verification submission."""
async with httpx.AsyncClient() as client:
task_resp = await client.get(f"{_get_api_url()}/tasks/{task_id}")
task_resp = await client.get(f"{settings.internal_api_url}/tasks/{task_id}")
if task_resp.status_code == status.HTTP_404_NOT_FOUND:
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
@@ -731,7 +727,9 @@ async def _handle_task_submit_verification(
"before verification.",
)
verify_resp = await client.post(f"{_get_api_url()}/tasks/{task_id}/verify")
verify_resp = await client.post(
f"{settings.internal_api_url}/tasks/{task_id}/verify"
)
if verify_resp.status_code != status.HTTP_200_OK:
return _format_error_response(
@@ -768,7 +766,7 @@ async def _handle_task_submit_qa(
)
async with httpx.AsyncClient() as client:
task_resp = await client.get(f"{_get_api_url()}/tasks/{task_id}")
task_resp = await client.get(f"{settings.internal_api_url}/tasks/{task_id}")
if task_resp.status_code == status.HTTP_404_NOT_FOUND:
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
@@ -787,7 +785,7 @@ async def _handle_task_submit_qa(
# Update with notes
await client.patch(
f"{_get_api_url()}/tasks/{task_id}",
f"{settings.internal_api_url}/tasks/{task_id}",
json={
"dev_notes": dev_notes,
"documenter_handoff": handoff_summary,
@@ -795,7 +793,9 @@ async def _handle_task_submit_qa(
)
# Submit for QA
qa_resp = await client.post(f"{_get_api_url()}/tasks/{task_id}/submit-qa")
qa_resp = await client.post(
f"{settings.internal_api_url}/tasks/{task_id}/submit-qa"
)
if qa_resp.status_code != status.HTTP_200_OK:
return _format_error_response("SUBMIT_FAILED", "Failed to submit for QA")
@@ -825,7 +825,7 @@ async def _handle_task_qa_pass(
)
async with httpx.AsyncClient() as client:
task_resp = await client.get(f"{_get_api_url()}/tasks/{task_id}")
task_resp = await client.get(f"{settings.internal_api_url}/tasks/{task_id}")
if task_resp.status_code == status.HTTP_404_NOT_FOUND:
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
@@ -845,7 +845,7 @@ async def _handle_task_qa_pass(
)
pass_resp = await client.post(
f"{_get_api_url()}/tasks/{task_id}/pass-qa",
f"{settings.internal_api_url}/tasks/{task_id}/pass-qa",
json={"notes": qa_notes},
)
@@ -882,7 +882,7 @@ async def _handle_task_qa_fail(
)
async with httpx.AsyncClient() as client:
task_resp = await client.get(f"{_get_api_url()}/tasks/{task_id}")
task_resp = await client.get(f"{settings.internal_api_url}/tasks/{task_id}")
if task_resp.status_code == status.HTTP_404_NOT_FOUND:
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
@@ -897,7 +897,7 @@ async def _handle_task_qa_fail(
full_notes = f"{qa_notes}\n\nIssues:\n" + "\n".join(f"- {i}" for i in issues)
fail_resp = await client.post(
f"{_get_api_url()}/tasks/{task_id}/fail-qa",
f"{settings.internal_api_url}/tasks/{task_id}/fail-qa",
json={"notes": full_notes},
)
@@ -918,7 +918,7 @@ async def _handle_task_qa_fail(
async def _handle_task_complete(task_id: str) -> dict[str, Any]:
"""Handle task completion."""
async with httpx.AsyncClient() as client:
task_resp = await client.get(f"{_get_api_url()}/tasks/{task_id}")
task_resp = await client.get(f"{settings.internal_api_url}/tasks/{task_id}")
if task_resp.status_code == status.HTTP_404_NOT_FOUND:
return _format_error_response("NOT_FOUND", f"Task {task_id} not found")
@@ -930,7 +930,9 @@ async def _handle_task_complete(task_id: str) -> dict[str, Any]:
"Task must be awaiting documentation to complete",
)
complete_resp = await client.post(f"{_get_api_url()}/tasks/{task_id}/complete")
complete_resp = await client.post(
f"{settings.internal_api_url}/tasks/{task_id}/complete"
)
if complete_resp.status_code != status.HTTP_200_OK:
return _format_error_response("COMPLETE_FAILED", "Failed to complete task")
@@ -944,6 +946,32 @@ async def _handle_task_complete(task_id: str) -> dict[str, Any]:
)
async def _handle_agent_idle(agent_id: str) -> dict[str, Any]:
"""Handle agent going idle (no work available)."""
async with httpx.AsyncClient() as client:
# Signal to orchestrator that this agent is idle
resp = await client.post(
f"{settings.internal_api_url}/orchestrator/agents/{agent_id}/mark-waiting",
params={"waiting_for": "task_assignment"},
)
if resp.status_code == status.HTTP_204_NO_CONTENT:
return {
"status": "idle",
"message": (
"You are now in WAITING state. Your container will terminate "
"to save resources. You will be respawned when work is available."
),
"action": "EXIT_GRACEFULLY",
}
return _format_error_response(
"IDLE_FAILED",
"Failed to signal idle state to orchestrator",
{"status_code": resp.status_code},
)
# =============================================================================
# MCP SERVER FACTORY
# =============================================================================
@@ -1262,6 +1290,20 @@ def create_task_mcp_server(agent_id: str) -> FastMCP:
"""
return await _handle_task_complete(task_id)
@mcp.tool()
async def roboco_agent_idle() -> dict[str, Any]:
"""
Signal that you have no work and should go idle.
Call this when roboco_task_scan returns no tasks.
Your container will be terminated to save resources.
You will be automatically respawned when new work is available.
Returns:
Confirmation of idle state
"""
return await _handle_agent_idle(agent_id)
return mcp