-
Notifications
You must be signed in to change notification settings - Fork 80
M6: onboarding, app bundle scripts, shortcuts #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: claude/proxy-monitor-m5-agent-sidecar
Are you sure you want to change the base?
Changes from all commits
0a8ecbb
dc5b446
76cd39e
3398539
66f8e60
5aac5fe
f72c1b1
43f06dc
a4f2ef4
4148846
049a7f6
1d94726
866bd4e
45cadd0
d208f89
54ff351
cdbce82
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| import os | ||
| import time | ||
| import uuid | ||
| from pathlib import Path | ||
| from typing import Any | ||
|
|
||
|
|
||
| class DebugLog: | ||
| """Append-only JSONL of every WS in/out + task lifecycle event, | ||
| with monotonic timestamps and per-turn elapsed timing. Lives at | ||
| `~/.reverse-api/logs/agent-<uuid>.jsonl` per sidecar run.""" | ||
|
|
||
| def __init__(self, log_dir: Path | None = None) -> None: | ||
| if log_dir is None: | ||
| log_dir = Path.home() / ".reverse-api" / "logs" | ||
| log_dir.mkdir(parents=True, exist_ok=True) | ||
| self.path = log_dir / f"agent-{uuid.uuid4().hex[:8]}.jsonl" | ||
| self._sidecar_start = time.monotonic() | ||
| self._turn_starts: dict[str, float] = {} | ||
| try: | ||
| self.path.write_text("") | ||
| except OSError: | ||
| pass | ||
|
|
||
| def _append(self, payload: dict[str, Any]) -> None: | ||
| line = json.dumps(payload, default=str) | ||
| try: | ||
| with self.path.open("a") as fh: | ||
| fh.write(line + "\n") | ||
| except OSError: | ||
| pass | ||
|
|
||
| def _now(self, chat_id: str | None = None) -> dict[str, float]: | ||
| sidecar_ms = (time.monotonic() - self._sidecar_start) * 1000 | ||
| out: dict[str, float] = {"tSidecarMs": round(sidecar_ms, 2)} | ||
| if chat_id and chat_id in self._turn_starts: | ||
| out["tTurnMs"] = round((time.monotonic() - self._turn_starts[chat_id]) * 1000, 2) | ||
| return out | ||
|
|
||
| def turn_started(self, chat_id: str, model: str, num_flows: int) -> None: | ||
| self._turn_starts[chat_id] = time.monotonic() | ||
| self._append({ | ||
| "ts": time.time(), | ||
| "kind": "turn_started", | ||
| "chatId": chat_id, | ||
| "model": model, | ||
| "numFlows": num_flows, | ||
| **self._now(chat_id), | ||
| }) | ||
|
|
||
| def turn_finished(self, chat_id: str, outcome: str) -> None: | ||
| self._append({ | ||
| "ts": time.time(), | ||
| "kind": "turn_finished", | ||
| "chatId": chat_id, | ||
| "outcome": outcome, | ||
| **self._now(chat_id), | ||
| }) | ||
| self._turn_starts.pop(chat_id, None) | ||
|
|
||
| def ws_in(self, msg_type: str, chat_id: str | None) -> None: | ||
| self._append({ | ||
| "ts": time.time(), | ||
| "kind": "ws_in", | ||
| "msgType": msg_type, | ||
| "chatId": chat_id, | ||
| **self._now(chat_id), | ||
| }) | ||
|
|
||
| def event_out(self, event_type: str, chat_id: str | None, extra: dict[str, Any] | None = None) -> None: | ||
| payload: dict[str, Any] = { | ||
| "ts": time.time(), | ||
| "kind": "event_out", | ||
| "eventType": event_type, | ||
| "chatId": chat_id, | ||
| **self._now(chat_id), | ||
| } | ||
| if extra: | ||
| payload.update(extra) | ||
| self._append(payload) | ||
|
|
||
| def note(self, message: str, chat_id: str | None = None, **fields: Any) -> None: | ||
| self._append({ | ||
| "ts": time.time(), | ||
| "kind": "note", | ||
| "message": message, | ||
| "chatId": chat_id, | ||
| **self._now(chat_id), | ||
| **fields, | ||
| }) | ||
|
|
||
|
|
||
| _singleton: DebugLog | None = None | ||
|
|
||
|
|
||
| def get_log() -> DebugLog: | ||
| global _singleton | ||
| if _singleton is None: | ||
| _singleton = DebugLog() | ||
| return _singleton |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,11 +5,17 @@ | |
| from enum import Enum | ||
| from typing import Any | ||
|
|
||
| from reverse_api.utils import OUTPUT_LANGUAGE_EXTENSIONS | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The agent sidecar can fail to start before handling any requests because this new import eagerly loads Prompt for AI agents |
||
|
|
||
| class TargetLanguage(str, Enum): | ||
| PYTHON = "python" | ||
| TYPESCRIPT = "typescript" | ||
| GO = "go" | ||
| # Derive the accepted output languages from reverse-api's single source of | ||
| # truth so the agent panel never drifts behind the CLI (it used to accept only | ||
| # python/typescript/go while the CLI had grown to nine). Building the enum from | ||
| # the registry keeps the `.value` interface the rest of the sidecar relies on. | ||
| TargetLanguage = Enum( | ||
| "TargetLanguage", | ||
| {lang.upper(): lang for lang in OUTPUT_LANGUAGE_EXTENSIONS}, | ||
| type=str, | ||
| ) | ||
|
|
||
|
|
||
| class ProtocolError(Exception): | ||
|
|
@@ -82,10 +88,8 @@ class ChatRequest: | |
| target: TargetLanguage | ||
| flows: list[FlowSummary] = field(default_factory=list) | ||
| history: list[dict[str, str]] = field(default_factory=list) | ||
| # The Claude Agent SDK session id from a previous turn. When present we | ||
| # pass it as ClaudeAgentOptions.resume so the SDK can re-attach to its | ||
| # own persisted state instead of replaying our local history. | ||
| claude_session_id: str | None = None | ||
| model: str | None = None | ||
|
|
||
| @classmethod | ||
| def from_payload(cls, payload: dict[str, Any]) -> "ChatRequest": | ||
|
|
@@ -103,13 +107,16 @@ def from_payload(cls, payload: dict[str, Any]) -> "ChatRequest": | |
| ] | ||
| raw_sid = payload.get("claudeSessionId") or payload.get("claude_session_id") | ||
| claude_session_id = str(raw_sid) if isinstance(raw_sid, str) and raw_sid else None | ||
| raw_model = payload.get("model") | ||
| model = str(raw_model).strip() if isinstance(raw_model, str) and raw_model.strip() else None | ||
| return cls( | ||
| id=str(payload.get("id", "")), | ||
| user_message=str(payload.get("message", "")), | ||
| target=target, | ||
| flows=flows, | ||
| history=history, | ||
| claude_session_id=claude_session_id, | ||
| model=model, | ||
| ) | ||
|
|
||
|
|
||
|
|
@@ -158,9 +165,41 @@ def complete(cls, chat_id: str, workdir: str, files: list[str]) -> "AgentEvent": | |
| payload={"id": chat_id, "workdir": workdir, "files": files}, | ||
| ) | ||
|
|
||
| @classmethod | ||
| def usage( | ||
| cls, | ||
| chat_id: str, | ||
| model: str | None, | ||
| input_tokens: int, | ||
| output_tokens: int, | ||
| cache_creation_input_tokens: int, | ||
| cache_read_input_tokens: int, | ||
| total_cost_usd: float | None, | ||
| duration_ms: int, | ||
| num_turns: int, | ||
| ) -> "AgentEvent": | ||
| return cls( | ||
| type="usage", | ||
| payload={ | ||
| "id": chat_id, | ||
| "model": model, | ||
| "inputTokens": input_tokens, | ||
| "outputTokens": output_tokens, | ||
| "cacheCreationInputTokens": cache_creation_input_tokens, | ||
| "cacheReadInputTokens": cache_read_input_tokens, | ||
| "totalCostUsd": total_cost_usd, | ||
| "durationMs": duration_ms, | ||
| "numTurns": num_turns, | ||
| }, | ||
| ) | ||
|
|
||
| @classmethod | ||
| def error(cls, chat_id: str | None, message: str) -> "AgentEvent": | ||
| payload: dict[str, Any] = {"message": message} | ||
| if chat_id is not None: | ||
| payload["id"] = chat_id | ||
| return cls(type="error", payload=payload) | ||
|
|
||
| @classmethod | ||
| def cancelled(cls, chat_id: str) -> "AgentEvent": | ||
| return cls(type="cancelled", payload={"id": chat_id}) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Java clients targeting JDK 11 can fail to compile when the model follows the allowed
recordsoption, since records require JDK 16+. Keeping the JDK 11 target would be consistent if this guidance required Gson instead, or the minimum should be raised to JDK 16.Prompt for AI agents