monkeyscode 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
monkeyscode/daemon.py ADDED
@@ -0,0 +1,201 @@
1
+ """
2
+ MonkeysCode SDK — Daemon Client.
3
+
4
+ WebSocket JSON-RPC client for communicating with the MonkeysCode daemon process.
5
+ Used when connecting to a running daemon instance (e.g., from an IDE extension
6
+ or a local web UI) rather than making direct HTTP calls.
7
+
8
+ Usage::
9
+
10
+ from monkeyscode.daemon import DaemonClient
11
+
12
+ client = DaemonClient("ws://localhost:9120")
13
+ await client.connect()
14
+
15
+ async for event in client.run("fix-123", prompt="Fix all errors"):
16
+ print(event)
17
+
18
+ await client.disconnect()
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import asyncio
24
+ import json
25
+ import time
26
+ from collections.abc import AsyncGenerator
27
+ from dataclasses import dataclass, field
28
+ from typing import Any
29
+
30
+ import httpx
31
+
32
+ # ── Types ────────────────────────────────────────────────────────────
33
+
34
+
35
+ @dataclass
36
+ class DaemonClientOptions:
37
+ """Options for connecting to the daemon."""
38
+
39
+ url: str = "ws://localhost:9120"
40
+ timeout: float = 30.0
41
+ auto_reconnect: bool = True
42
+ max_reconnect_attempts: int = 5
43
+
44
+
45
+ @dataclass
46
+ class DaemonRunParams:
47
+ """Parameters for starting an agent run via the daemon."""
48
+
49
+ prompt: str
50
+ workspace: str | None = None
51
+ mode: str = "fast" # "fast" | "plan" | "thinking"
52
+ model: str | None = None
53
+ max_tokens: int | None = None
54
+
55
+
56
+ @dataclass
57
+ class DaemonEvent:
58
+ """An event received from the daemon."""
59
+
60
+ kind: str
61
+ data: dict[str, Any] = field(default_factory=dict)
62
+ timestamp: int = 0
63
+
64
+
65
+ # ── Client ───────────────────────────────────────────────────────────
66
+
67
+
68
+ class DaemonClient:
69
+ """
70
+ WebSocket JSON-RPC client for the MonkeysCode daemon.
71
+
72
+ Provides request/response communication and streaming event delivery.
73
+ """
74
+
75
+ def __init__(self, url: str = "ws://localhost:9120", **kwargs: Any) -> None:
76
+ self._url = url
77
+ self._opts = DaemonClientOptions(url=url, **kwargs)
78
+ self._ws: Any = None
79
+ self._connected = False
80
+ self._request_id = 0
81
+ self._pending: dict[int, asyncio.Future[Any]] = {}
82
+
83
+ @property
84
+ def connected(self) -> bool:
85
+ return self._connected
86
+
87
+ @property
88
+ def url(self) -> str:
89
+ return self._url
90
+
91
+ async def connect(self) -> None:
92
+ """Connect to the daemon WebSocket."""
93
+ try:
94
+ import websockets # type: ignore[import-untyped]
95
+ self._ws = await websockets.connect(self._url)
96
+ self._connected = True
97
+ except ImportError:
98
+ # Fallback: use HTTP long-polling via REST API
99
+ self._connected = True
100
+
101
+ async def disconnect(self) -> None:
102
+ """Disconnect from the daemon."""
103
+ if self._ws is not None:
104
+ await self._ws.close()
105
+ self._connected = False
106
+ self._ws = None
107
+
108
+ async def request(self, method: str, params: dict[str, Any] | None = None) -> Any:
109
+ """Send a JSON-RPC request and wait for a response."""
110
+ self._request_id += 1
111
+ req_id = self._request_id
112
+
113
+ msg = {
114
+ "jsonrpc": "2.0",
115
+ "id": req_id,
116
+ "method": method,
117
+ "params": params or {},
118
+ }
119
+
120
+ if self._ws is not None:
121
+ await self._ws.send(json.dumps(msg))
122
+ response = await self._ws.recv()
123
+ data = json.loads(response)
124
+ if "error" in data:
125
+ raise RuntimeError(f"RPC error: {data['error']}")
126
+ return data.get("result")
127
+ else:
128
+ # HTTP fallback
129
+ async with httpx.AsyncClient() as client:
130
+ http_url = self._url.replace("ws://", "http://").replace("wss://", "https://")
131
+ resp = await client.post(f"{http_url}/rpc", json=msg)
132
+ resp.raise_for_status()
133
+ data = resp.json()
134
+ if "error" in data:
135
+ raise RuntimeError(f"RPC error: {data['error']}")
136
+ return data.get("result")
137
+
138
+ async def run(
139
+ self,
140
+ run_id: str,
141
+ *,
142
+ prompt: str,
143
+ workspace: str | None = None,
144
+ mode: str = "fast",
145
+ model: str | None = None,
146
+ ) -> AsyncGenerator[DaemonEvent, None]:
147
+ """Start a run and stream events."""
148
+ params: dict[str, Any] = {
149
+ "runId": run_id,
150
+ "prompt": prompt,
151
+ }
152
+ if workspace:
153
+ params["workspace"] = workspace
154
+ if model:
155
+ params["model"] = model
156
+ params["mode"] = mode
157
+
158
+ # Start the run
159
+ await self.request("agent/run", params)
160
+
161
+ # Stream events
162
+ if self._ws is not None:
163
+ while True:
164
+ try:
165
+ raw = await asyncio.wait_for(self._ws.recv(), timeout=self._opts.timeout)
166
+ data = json.loads(raw)
167
+
168
+ # Skip RPC responses
169
+ if "jsonrpc" in data and "id" in data:
170
+ continue
171
+
172
+ event = DaemonEvent(
173
+ kind=data.get("type", data.get("kind", "unknown")),
174
+ data=data,
175
+ timestamp=data.get("timestamp", int(time.time() * 1000)),
176
+ )
177
+ yield event
178
+
179
+ if event.kind in ("complete", "error", "done"):
180
+ break
181
+ except asyncio.TimeoutError:
182
+ break
183
+
184
+ async def cancel(self, run_id: str) -> None:
185
+ """Cancel a running agent."""
186
+ await self.request("agent/cancel", {"runId": run_id})
187
+
188
+ async def status(self, run_id: str) -> dict[str, Any]:
189
+ """Get the status of a run."""
190
+ return await self.request("agent/status", {"runId": run_id})
191
+
192
+ async def list_runs(self) -> list[dict[str, Any]]:
193
+ """List active runs."""
194
+ return await self.request("agent/list")
195
+
196
+ async def __aenter__(self) -> DaemonClient:
197
+ await self.connect()
198
+ return self
199
+
200
+ async def __aexit__(self, *args: Any) -> None:
201
+ await self.disconnect()
monkeyscode/events.py ADDED
@@ -0,0 +1,323 @@
1
+ """
2
+ MonkeysCode SDK — Event Types.
3
+
4
+ Pydantic models for all events emitted by the agent during a run.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Literal
10
+
11
+ from pydantic import BaseModel, Field
12
+
13
+ # ── Token Usage ──────────────────────────────────────────────────────
14
+
15
+
16
+ class TokenUsage(BaseModel):
17
+ """Token usage breakdown."""
18
+
19
+ input: int = 0
20
+ output: int = 0
21
+ thinking: int = 0
22
+ total: int = 0
23
+
24
+
25
+ # ── File Change ──────────────────────────────────────────────────────
26
+
27
+
28
+ class FileChange(BaseModel):
29
+ """A file changed by the agent."""
30
+
31
+ path: str
32
+ action: Literal["created", "modified", "deleted"] = "modified"
33
+ diff: str | None = None
34
+ lines_added: int = 0
35
+ lines_removed: int = 0
36
+
37
+
38
+ # ── Tool Call Record ─────────────────────────────────────────────────
39
+
40
+
41
+ class ToolCallRecord(BaseModel):
42
+ """A recorded tool call."""
43
+
44
+ tool: str
45
+ args: dict[str, Any] = Field(default_factory=dict)
46
+ duration: int = 0
47
+ result: str | None = None
48
+
49
+
50
+ # ── Verification ─────────────────────────────────────────────────────
51
+
52
+
53
+ class VerificationCheck(BaseModel):
54
+ """A single verification check result."""
55
+
56
+ passed: bool
57
+ output: str = ""
58
+
59
+
60
+ class VerificationResult(BaseModel):
61
+ """Verification results from lint, typecheck, and tests."""
62
+
63
+ lint: VerificationCheck | None = None
64
+ typecheck: VerificationCheck | None = None
65
+ tests: VerificationCheck | None = None
66
+
67
+
68
+ # ── Plan Step ────────────────────────────────────────────────────────
69
+
70
+
71
+ class PlanStep(BaseModel):
72
+ """A step in a plan."""
73
+
74
+ description: str
75
+ status: Literal["pending", "running", "done", "failed"] = "pending"
76
+ tool: str | None = None
77
+
78
+
79
+ # ── Events ───────────────────────────────────────────────────────────
80
+
81
+
82
+ class StartEvent(BaseModel):
83
+ """Emitted when a run starts."""
84
+
85
+ type: Literal["start"] = "start"
86
+ timestamp: int
87
+ session_id: str
88
+ model: str
89
+
90
+
91
+ class ThinkingEvent(BaseModel):
92
+ """Emitted when the agent is thinking."""
93
+
94
+ type: Literal["thinking"] = "thinking"
95
+ timestamp: int
96
+ content: str | None = None
97
+
98
+
99
+ class TextEvent(BaseModel):
100
+ """Emitted for streaming text content."""
101
+
102
+ type: Literal["text"] = "text"
103
+ timestamp: int
104
+ content: str
105
+
106
+
107
+ class ToolCallEvent(BaseModel):
108
+ """Emitted when the agent calls a tool."""
109
+
110
+ type: Literal["tool_call"] = "tool_call"
111
+ timestamp: int
112
+ tool: str
113
+ args: dict[str, Any] = Field(default_factory=dict)
114
+ summary: str = ""
115
+
116
+
117
+ class ToolResultEvent(BaseModel):
118
+ """Emitted when a tool call completes."""
119
+
120
+ type: Literal["tool_result"] = "tool_result"
121
+ timestamp: int
122
+ tool: str
123
+ result: str = ""
124
+ success: bool = True
125
+ duration_ms: int = 0
126
+
127
+
128
+ class FileEditEvent(BaseModel):
129
+ """Emitted when the agent edits a file."""
130
+
131
+ type: Literal["file_edit"] = "file_edit"
132
+ timestamp: int
133
+ path: str
134
+ diff: str = ""
135
+ lines_added: int = 0
136
+ lines_removed: int = 0
137
+
138
+
139
+ class FileReadEvent(BaseModel):
140
+ """Emitted when the agent reads a file."""
141
+
142
+ type: Literal["file_read"] = "file_read"
143
+ timestamp: int
144
+ path: str
145
+ lines: int = 0
146
+
147
+
148
+ class TerminalCommandEvent(BaseModel):
149
+ """Emitted when the agent runs a command."""
150
+
151
+ type: Literal["terminal_command"] = "terminal_command"
152
+ timestamp: int
153
+ command: str
154
+ exit_code: int = 0
155
+ output: str = ""
156
+
157
+
158
+ class SearchResultEvent(BaseModel):
159
+ """Emitted for search results."""
160
+
161
+ type: Literal["search_result"] = "search_result"
162
+ timestamp: int
163
+ query: str
164
+ matches: list[dict[str, Any]] = Field(default_factory=list)
165
+
166
+
167
+ # ── Daemon Events ────────────────────────────────────────────────────
168
+
169
+
170
+ class PlanReadyEvent(BaseModel):
171
+ """Emitted when the agent produces a plan."""
172
+
173
+ type: Literal["plan_ready"] = "plan_ready"
174
+ timestamp: int
175
+ steps: list[PlanStep] = Field(default_factory=list)
176
+ summary: str = ""
177
+
178
+
179
+ class CostUpdateEvent(BaseModel):
180
+ """Emitted for mid-run cost updates."""
181
+
182
+ type: Literal["cost_update"] = "cost_update"
183
+ timestamp: int
184
+ tokens_in: int = 0
185
+ tokens_out: int = 0
186
+ tokens_thinking: int = 0
187
+ cost: float = 0.0
188
+
189
+
190
+ class AgentSpawnedEvent(BaseModel):
191
+ """Emitted when a sub-agent is spawned."""
192
+
193
+ type: Literal["agent_spawned"] = "agent_spawned"
194
+ timestamp: int
195
+ agent_id: str
196
+ task: str
197
+
198
+
199
+ class AgentCompleteEvent(BaseModel):
200
+ """Emitted when a sub-agent completes."""
201
+
202
+ type: Literal["agent_complete"] = "agent_complete"
203
+ timestamp: int
204
+ agent_id: str
205
+ success: bool = True
206
+ summary: str = ""
207
+
208
+
209
+ class FanOutProgressEvent(BaseModel):
210
+ """Emitted for parallel progress updates."""
211
+
212
+ type: Literal["fan_out_progress"] = "fan_out_progress"
213
+ timestamp: int
214
+ total: int = 0
215
+ completed: int = 0
216
+ running: int = 0
217
+
218
+
219
+ class VerificationEvent(BaseModel):
220
+ """Emitted when verification runs."""
221
+
222
+ type: Literal["verification"] = "verification"
223
+ timestamp: int
224
+ command: str
225
+ passed: bool = False
226
+ output: str = ""
227
+
228
+
229
+ class SuggestionEvent(BaseModel):
230
+ """Emitted with follow-up suggestions."""
231
+
232
+ type: Literal["suggestion"] = "suggestion"
233
+ timestamp: int
234
+ suggestions: list[str] = Field(default_factory=list)
235
+
236
+
237
+ # ── Terminal Events ──────────────────────────────────────────────────
238
+
239
+
240
+ class ErrorEvent(BaseModel):
241
+ """Emitted on error."""
242
+
243
+ type: Literal["error"] = "error"
244
+ timestamp: int
245
+ code: str = "UNKNOWN"
246
+ message: str = ""
247
+
248
+
249
+ class CompleteEvent(BaseModel):
250
+ """Emitted when a run completes."""
251
+
252
+ type: Literal["complete"] = "complete"
253
+ timestamp: int
254
+ summary: str = ""
255
+ files_changed: list[FileChange] = Field(default_factory=list)
256
+ tokens: TokenUsage = Field(default_factory=TokenUsage)
257
+ cost: float = 0.0
258
+ duration_ms: int = 0
259
+
260
+
261
+ # ── Union Type ───────────────────────────────────────────────────────
262
+
263
+ AgentEvent = (
264
+ StartEvent
265
+ | ThinkingEvent
266
+ | TextEvent
267
+ | ToolCallEvent
268
+ | ToolResultEvent
269
+ | FileEditEvent
270
+ | FileReadEvent
271
+ | TerminalCommandEvent
272
+ | SearchResultEvent
273
+ | PlanReadyEvent
274
+ | CostUpdateEvent
275
+ | AgentSpawnedEvent
276
+ | AgentCompleteEvent
277
+ | FanOutProgressEvent
278
+ | VerificationEvent
279
+ | SuggestionEvent
280
+ | ErrorEvent
281
+ | CompleteEvent
282
+ )
283
+
284
+
285
+ # ── Result Types ─────────────────────────────────────────────────────
286
+
287
+
288
+ class AgentResult(BaseModel):
289
+ """Result from a completed agent run."""
290
+
291
+ success: bool = True
292
+ status: Literal[
293
+ "completed", "failed", "partial",
294
+ "timeout", "interrupted", "cancelled",
295
+ ] = "completed"
296
+ summary: str = ""
297
+ files_changed: list[FileChange] = Field(default_factory=list)
298
+ tool_calls: list[ToolCallRecord] = Field(default_factory=list)
299
+ tokens: TokenUsage = Field(default_factory=TokenUsage)
300
+ cost: float = 0.0
301
+ duration_ms: int = 0
302
+ session_id: str = ""
303
+ exit_code: int = 0
304
+ model: str = "auto"
305
+ verification: VerificationResult | None = None
306
+ error: str | None = None
307
+
308
+
309
+ class GoalResult(AgentResult):
310
+ """Result from a goal-mode run."""
311
+
312
+ goal_met: bool = False
313
+ iterations: int = 0
314
+ iteration_summaries: list[str] = Field(default_factory=list)
315
+
316
+
317
+ class GoalOptions(BaseModel):
318
+ """Options for goal-mode runs."""
319
+
320
+ max_iterations: int = 10
321
+ timeout: int = 300_000
322
+ max_cost: float | None = None
323
+ verify_command: str | None = None