opencode-agent 0.1.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.
@@ -0,0 +1,26 @@
1
+ from .agent import Agent
2
+ from .client import OpenCodeClient
3
+ from .config import OpenCodeConfig
4
+ from .exceptions import (
5
+ MCPReadinessError,
6
+ OpenCodeError,
7
+ OpenCodeHTTPError,
8
+ SessionNotFoundError,
9
+ TaskTimeoutError,
10
+ ToolNotFoundError,
11
+ )
12
+ from .orchestrator import MultiAgentRunner, SingleAgentRunner
13
+
14
+ __all__ = [
15
+ "Agent",
16
+ "MCPReadinessError",
17
+ "MultiAgentRunner",
18
+ "OpenCodeClient",
19
+ "OpenCodeConfig",
20
+ "OpenCodeError",
21
+ "OpenCodeHTTPError",
22
+ "SessionNotFoundError",
23
+ "SingleAgentRunner",
24
+ "TaskTimeoutError",
25
+ "ToolNotFoundError",
26
+ ]
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, is_dataclass
4
+ from typing import Any
5
+
6
+ from .client import OpenCodeClient
7
+ from .config import OpenCodeConfig
8
+ from .managers import MCPManager, SessionManager, ToolManager
9
+ from .models import ExecutionMetadata, ExecutionResult, StreamEvent
10
+ from .orchestrator import EventStreamer, SingleAgentRunner
11
+
12
+
13
+ class Agent:
14
+ def __init__(
15
+ self,
16
+ name: str = "OpenCodeAgent",
17
+ system_prompt: str | None = None,
18
+ *,
19
+ agent: str | None = None,
20
+ model: str | dict[str, Any] | None = None,
21
+ tools: list[str] | None = None,
22
+ mcp_servers: list[str] | None = None,
23
+ project_path: str | None = None,
24
+ base_url: str | None = None,
25
+ streaming: bool | None = None,
26
+ verbose: bool | None = None,
27
+ ):
28
+ config = OpenCodeConfig.from_env()
29
+ if project_path is not None:
30
+ config.project_path = project_path
31
+ if base_url is not None:
32
+ config.base_url = base_url
33
+ if streaming is not None:
34
+ config.streaming = streaming
35
+ if verbose is not None:
36
+ config.verbose = verbose
37
+ if model is not None:
38
+ config.default_model = model
39
+ if agent is not None:
40
+ config.default_agent = agent
41
+ if tools is not None:
42
+ config.default_tools = tools
43
+
44
+ self.name = name
45
+ self.system_prompt = system_prompt
46
+ self.default_mcp_servers = mcp_servers or []
47
+ self.config = config
48
+ self.client = OpenCodeClient(config=config)
49
+ self.session_manager = SessionManager(self.client)
50
+ self.tool_manager = ToolManager(self.client)
51
+ self.mcp_manager = MCPManager(self.client)
52
+ self.runner = SingleAgentRunner(
53
+ client=self.client,
54
+ config=self.config,
55
+ session_manager=self.session_manager,
56
+ tool_manager=self.tool_manager,
57
+ mcp_manager=self.mcp_manager,
58
+ )
59
+ self.event_streamer = EventStreamer(self.client)
60
+ self.session_id: str | None = None
61
+
62
+ def close(self) -> None:
63
+ self.client.close()
64
+
65
+ def set_model(self, model: str | dict[str, Any] | None) -> None:
66
+ self.config.default_model = model
67
+
68
+ def list_models(self, include_details: bool = False) -> list[Any]:
69
+ return self.client.list_available_models(include_details=include_details)
70
+
71
+ def run(
72
+ self,
73
+ user_prompt: str,
74
+ *,
75
+ agent: str | None = None,
76
+ model: str | dict[str, Any] | None = None,
77
+ tools: list[str] | None = None,
78
+ mcp_servers: list[str] | None = None,
79
+ session_id: str | None = None,
80
+ return_result: bool = False,
81
+ ) -> str | ExecutionResult:
82
+ result = self.runner.run(
83
+ user_prompt,
84
+ agent=agent,
85
+ model=model,
86
+ tools=tools,
87
+ mcp_servers=mcp_servers or self.default_mcp_servers or None,
88
+ session_id=session_id or self.session_id,
89
+ title=self.name,
90
+ system=self.system_prompt,
91
+ )
92
+ self.session_id = result.metadata.session_id
93
+ return result if return_result else result.output
94
+
95
+ def run_async(
96
+ self,
97
+ user_prompt: str,
98
+ *,
99
+ agent: str | None = None,
100
+ model: str | dict[str, Any] | None = None,
101
+ tools: list[str] | None = None,
102
+ session_id: str | None = None,
103
+ ) -> ExecutionMetadata:
104
+ metadata = self.runner.run_async(
105
+ user_prompt,
106
+ agent=agent,
107
+ model=model,
108
+ tools=tools,
109
+ session_id=session_id or self.session_id,
110
+ title=self.name,
111
+ )
112
+ self.session_id = metadata.session_id
113
+ return metadata
114
+
115
+ def run_shell(
116
+ self,
117
+ command: str,
118
+ *,
119
+ agent: str | None = None,
120
+ model: str | dict[str, Any] | None = None,
121
+ return_result: bool = False,
122
+ ) -> str | ExecutionResult:
123
+ if not self.session_id:
124
+ self.session_id = self.client.create_session(title=self.name).id
125
+ result = self.runner.run_shell(command, session_id=self.session_id, agent=agent, model=model)
126
+ return result if return_result else result.output
127
+
128
+ def stream(self, *, limit: int = 10) -> list[StreamEvent]:
129
+ return self.event_streamer.stream(limit=limit)
130
+
131
+ @staticmethod
132
+ def to_dict(value: Any) -> Any:
133
+ if isinstance(value, list):
134
+ return [Agent.to_dict(item) for item in value]
135
+ if is_dataclass(value):
136
+ return asdict(value)
137
+ return value
opencode_agent/cli.py ADDED
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from dataclasses import asdict, is_dataclass
5
+ import json
6
+ from typing import Any
7
+
8
+ from .client import OpenCodeClient
9
+ from .config import OpenCodeConfig
10
+ from .logging import configure_logging
11
+ from .managers import MCPManager, SessionManager, ToolManager
12
+ from .orchestrator import EventStreamer, MultiAgentRunner, SingleAgentRunner
13
+
14
+
15
+ def _build_runtime(config: OpenCodeConfig) -> dict[str, Any]:
16
+ client = OpenCodeClient(config)
17
+ session_manager = SessionManager(client)
18
+ tool_manager = ToolManager(client)
19
+ mcp_manager = MCPManager(client)
20
+ return {
21
+ "client": client,
22
+ "single": SingleAgentRunner(client=client, config=config, session_manager=session_manager, tool_manager=tool_manager, mcp_manager=mcp_manager),
23
+ "multi": MultiAgentRunner(client=client, config=config, session_manager=session_manager, tool_manager=tool_manager, mcp_manager=mcp_manager),
24
+ "session_manager": session_manager,
25
+ "tool_manager": tool_manager,
26
+ "mcp_manager": mcp_manager,
27
+ "event_streamer": EventStreamer(client),
28
+ }
29
+
30
+
31
+ def _print_json(payload: Any) -> None:
32
+ def _default(obj: Any) -> Any:
33
+ if is_dataclass(obj):
34
+ return asdict(obj)
35
+ return str(obj)
36
+
37
+ print(json.dumps(payload, indent=2, default=_default))
38
+
39
+
40
+ def build_parser() -> argparse.ArgumentParser:
41
+ parser = argparse.ArgumentParser(prog="opencode-orchestrator")
42
+ parser.add_argument("--base-url", help="Override OpenCode base URL")
43
+ parser.add_argument("--project-path", help="Override OpenCode project path")
44
+ parser.add_argument("--verbose", action="store_true", help="Enable verbose logging")
45
+ subparsers = parser.add_subparsers(dest="command", required=True)
46
+
47
+ run_parser = subparsers.add_parser("run", help="Run a single-agent task")
48
+ run_parser.add_argument("prompt")
49
+ run_parser.add_argument("--agent")
50
+ run_parser.add_argument("--model")
51
+ run_parser.add_argument("--session-id")
52
+ run_parser.add_argument("--title")
53
+ run_parser.add_argument("--tool", action="append", default=[])
54
+ run_parser.add_argument("--mcp", action="append", default=[])
55
+ run_parser.add_argument("--stream", action="store_true")
56
+ run_parser.add_argument("--async", dest="async_mode", action="store_true")
57
+
58
+ orchestration_parser = subparsers.add_parser("orchestrate", help="Run a multi-agent task")
59
+ orchestration_parser.add_argument("prompt")
60
+ orchestration_parser.add_argument("--strategy", default="plan-explore-build-review")
61
+ orchestration_parser.add_argument("--session-id")
62
+ orchestration_parser.add_argument("--title")
63
+ orchestration_parser.add_argument("--tool", action="append", default=[])
64
+ orchestration_parser.add_argument("--mcp", action="append", default=[])
65
+ orchestration_parser.add_argument("--parallel", action="store_true")
66
+
67
+ sessions_parser = subparsers.add_parser("sessions", help="Inspect sessions")
68
+ sessions_subparsers = sessions_parser.add_subparsers(dest="sessions_command", required=True)
69
+ sessions_subparsers.add_parser("list", help="List sessions")
70
+ status_parser = sessions_subparsers.add_parser("status", help="Show session status")
71
+ status_parser.add_argument("session_id", nargs="?")
72
+ abort_parser = sessions_subparsers.add_parser("abort", help="Abort a session")
73
+ abort_parser.add_argument("session_id")
74
+
75
+ tool_parser = subparsers.add_parser("tools", help="Inspect tools")
76
+ tool_parser.add_argument("--provider")
77
+ tool_parser.add_argument("--model")
78
+ tool_parser.add_argument("--ids", action="store_true")
79
+
80
+ mcp_parser = subparsers.add_parser("mcp", help="Inspect or register MCP servers")
81
+ mcp_subparsers = mcp_parser.add_subparsers(dest="mcp_command", required=True)
82
+ mcp_subparsers.add_parser("list", help="List MCP servers")
83
+ register_parser = mcp_subparsers.add_parser("register", help="Register an MCP server")
84
+ register_parser.add_argument("name")
85
+ register_parser.add_argument("config_json")
86
+
87
+ shell_parser = subparsers.add_parser("shell", help="Run a shell command inside a session")
88
+ shell_parser.add_argument("session_id")
89
+ shell_parser.add_argument("command_text")
90
+ shell_parser.add_argument("--agent")
91
+ shell_parser.add_argument("--model")
92
+
93
+ stream_parser = subparsers.add_parser("stream", help="Read SSE events")
94
+ stream_parser.add_argument("--limit", type=int, default=10)
95
+
96
+ return parser
97
+
98
+
99
+ def main(argv: list[str] | None = None) -> int:
100
+ parser = build_parser()
101
+ args = parser.parse_args(argv)
102
+ config = OpenCodeConfig.from_env()
103
+ if args.base_url:
104
+ config.base_url = args.base_url
105
+ if args.project_path:
106
+ config.project_path = args.project_path
107
+ if args.verbose:
108
+ config.verbose = True
109
+ configure_logging(config.verbose)
110
+ runtime = _build_runtime(config)
111
+ client: OpenCodeClient = runtime["client"]
112
+ try:
113
+ if args.command == "run":
114
+ if args.async_mode:
115
+ result = runtime["single"].run_async(
116
+ args.prompt,
117
+ agent=args.agent,
118
+ model=args.model,
119
+ session_id=args.session_id,
120
+ title=args.title,
121
+ tools=args.tool or None,
122
+ )
123
+ _print_json(result)
124
+ return 0
125
+ result = runtime["single"].run(
126
+ args.prompt,
127
+ agent=args.agent,
128
+ model=args.model,
129
+ session_id=args.session_id,
130
+ title=args.title,
131
+ tools=args.tool or None,
132
+ mcp_servers=args.mcp or None,
133
+ streaming=args.stream,
134
+ )
135
+ _print_json({"output": result.output, "metadata": result.metadata, "message": result.message})
136
+ return 0
137
+ if args.command == "orchestrate":
138
+ result = runtime["multi"].run(
139
+ args.prompt,
140
+ strategy=args.strategy,
141
+ session_id=args.session_id,
142
+ title=args.title,
143
+ tools=args.tool or None,
144
+ mcp_servers=args.mcp or None,
145
+ parallel=args.parallel,
146
+ )
147
+ _print_json(result)
148
+ return 0
149
+ if args.command == "sessions":
150
+ if args.sessions_command == "list":
151
+ _print_json(client.list_sessions())
152
+ return 0
153
+ if args.sessions_command == "status":
154
+ status = client.get_session_status()
155
+ if args.session_id:
156
+ _print_json(status.get(args.session_id))
157
+ else:
158
+ _print_json(status)
159
+ return 0
160
+ if args.sessions_command == "abort":
161
+ client.abort_session(args.session_id)
162
+ print(f"aborted {args.session_id}")
163
+ return 0
164
+ if args.command == "tools":
165
+ _print_json(client.list_tool_ids() if args.ids else client.list_tools(provider=args.provider, model=args.model))
166
+ return 0
167
+ if args.command == "mcp":
168
+ if args.mcp_command == "list":
169
+ _print_json(runtime["mcp_manager"].list_servers())
170
+ return 0
171
+ if args.mcp_command == "register":
172
+ _print_json(runtime["mcp_manager"].register(args.name, json.loads(args.config_json)))
173
+ return 0
174
+ if args.command == "shell":
175
+ result = runtime["single"].run_shell(args.command_text, session_id=args.session_id, agent=args.agent, model=args.model)
176
+ _print_json(result)
177
+ return 0
178
+ if args.command == "stream":
179
+ _print_json(runtime["event_streamer"].stream(limit=args.limit))
180
+ return 0
181
+ return 1
182
+ finally:
183
+ client.close()
@@ -0,0 +1,331 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import time
6
+ from collections.abc import Iterator
7
+ from typing import Any
8
+
9
+ import httpx
10
+
11
+ from .config import OpenCodeConfig
12
+ from .exceptions import OpenCodeHTTPError
13
+ from .models import AgentDefinition, MCPServer, Message, Session, SessionState, StreamEvent, ToolSchema
14
+
15
+
16
+ def _normalize_tools(tools: list[str] | dict[str, Any] | None) -> dict[str, Any] | None:
17
+ if tools is None:
18
+ return None
19
+ if isinstance(tools, dict):
20
+ return tools
21
+ return {tool: True for tool in tools}
22
+
23
+
24
+ def _normalize_model(model: str | dict[str, Any] | None) -> dict[str, Any] | None:
25
+ if model is None:
26
+ return None
27
+ if isinstance(model, dict):
28
+ return model
29
+ if "/" in model:
30
+ provider_id, model_id = model.split("/", 1)
31
+ return {"providerID": provider_id, "modelID": model_id}
32
+ return {"modelID": model}
33
+
34
+
35
+ class OpenCodeClient:
36
+ def __init__(self, config: OpenCodeConfig | None = None, http_client: httpx.Client | None = None):
37
+ self.config = config or OpenCodeConfig.from_env()
38
+ self._client = http_client or httpx.Client(
39
+ base_url=self.config.base_url,
40
+ timeout=self.config.timeout_seconds,
41
+ )
42
+
43
+ def close(self) -> None:
44
+ self._client.close()
45
+
46
+ def _request(self, method: str, path: str, **kwargs: Any) -> Any:
47
+ last_error: Exception | None = None
48
+ for attempt in range(1, self.config.retry_policy.attempts + 1):
49
+ try:
50
+ response = self._client.request(method, path, **kwargs)
51
+ if response.status_code >= 400:
52
+ detail = response.text
53
+ try:
54
+ payload = response.json()
55
+ detail = payload.get("error", detail)
56
+ except json.JSONDecodeError:
57
+ pass
58
+ raise OpenCodeHTTPError(response.status_code, detail)
59
+ if response.status_code == 204:
60
+ return None
61
+ if not response.content:
62
+ return None
63
+ return response.json()
64
+ except (httpx.HTTPError, OpenCodeHTTPError) as exc:
65
+ last_error = exc
66
+ if attempt == self.config.retry_policy.attempts:
67
+ raise
68
+ time.sleep(self.config.retry_policy.backoff_seconds * attempt)
69
+ if last_error:
70
+ raise last_error
71
+ return None
72
+
73
+ def list_sessions(self) -> list[Session]:
74
+ return [Session.from_dict(item) for item in self._request("GET", "/session")]
75
+
76
+ def create_session(self, title: str | None = None, parent_id: str | None = None) -> Session:
77
+ payload = {}
78
+ if title:
79
+ payload["title"] = title
80
+ if parent_id:
81
+ payload["parentID"] = parent_id
82
+ return Session.from_dict(self._request("POST", "/session", json=payload))
83
+
84
+ def get_session(self, session_id: str) -> Session:
85
+ return Session.from_dict(self._request("GET", f"/session/{session_id}"))
86
+
87
+ def update_session(self, session_id: str, **fields: Any) -> Session:
88
+ return Session.from_dict(self._request("PATCH", f"/session/{session_id}", json=fields))
89
+
90
+ def delete_session(self, session_id: str) -> None:
91
+ self._request("DELETE", f"/session/{session_id}")
92
+
93
+ def abort_session(self, session_id: str) -> None:
94
+ self._request("POST", f"/session/{session_id}/abort")
95
+
96
+ def fork_session(self, session_id: str, message_id: str | None = None) -> Session:
97
+ payload = {"messageID": message_id} if message_id else {}
98
+ return Session.from_dict(self._request("POST", f"/session/{session_id}/fork", json=payload))
99
+
100
+ def get_session_children(self, session_id: str) -> list[Session]:
101
+ return [Session.from_dict(item) for item in self._request("GET", f"/session/{session_id}/children")]
102
+
103
+ def get_session_todos(self, session_id: str) -> list[dict[str, Any]]:
104
+ return self._request("GET", f"/session/{session_id}/todo")
105
+
106
+ def get_session_status(self) -> dict[str, SessionState]:
107
+ payload = self._request("GET", "/session/status")
108
+ return {
109
+ session_id: SessionState(
110
+ session_id=session_id,
111
+ status=details.get("status", "unknown"),
112
+ active_form=details.get("activeForm"),
113
+ )
114
+ for session_id, details in payload.items()
115
+ }
116
+
117
+ def list_messages(self, session_id: str) -> list[Message]:
118
+ return [Message.from_dict(item) for item in self._request("GET", f"/session/{session_id}/message")]
119
+
120
+ def get_message(self, session_id: str, message_id: str) -> Message:
121
+ return Message.from_dict(self._request("GET", f"/session/{session_id}/message/{message_id}"))
122
+
123
+ def send_message(
124
+ self,
125
+ session_id: str,
126
+ prompt: str,
127
+ *,
128
+ agent: str | None = None,
129
+ model: str | dict[str, Any] | None = None,
130
+ tools: list[str] | dict[str, Any] | None = None,
131
+ system: str | None = None,
132
+ message_id: str | None = None,
133
+ attachments: list[str] | None = None,
134
+ no_reply: bool = False,
135
+ ) -> Message:
136
+ parts: list[dict[str, Any]] = [{"type": "text", "text": prompt}]
137
+ for attachment in attachments or []:
138
+ parts.append({"type": "attach", "path": attachment})
139
+ payload: dict[str, Any] = {"parts": parts, "noReply": no_reply}
140
+ if agent:
141
+ payload["agent"] = agent
142
+ normalized_model = _normalize_model(model)
143
+ if normalized_model:
144
+ payload["model"] = normalized_model
145
+ normalized_tools = _normalize_tools(tools)
146
+ if normalized_tools:
147
+ payload["tools"] = normalized_tools
148
+ if system:
149
+ payload["system"] = system
150
+ if message_id:
151
+ payload["messageID"] = message_id
152
+ return Message.from_dict(self._request("POST", f"/session/{session_id}/message", json=payload))
153
+
154
+ def prompt_async(
155
+ self,
156
+ session_id: str,
157
+ prompt: str,
158
+ *,
159
+ agent: str | None = None,
160
+ model: str | dict[str, Any] | None = None,
161
+ tools: list[str] | dict[str, Any] | None = None,
162
+ ) -> None:
163
+ payload: dict[str, Any] = {"parts": [{"type": "text", "text": prompt}]}
164
+ if agent:
165
+ payload["agent"] = agent
166
+ normalized_model = _normalize_model(model)
167
+ if normalized_model:
168
+ payload["model"] = normalized_model
169
+ normalized_tools = _normalize_tools(tools)
170
+ if normalized_tools:
171
+ payload["tools"] = normalized_tools
172
+ self._request("POST", f"/session/{session_id}/prompt_async", json=payload)
173
+
174
+ def run_command(self, session_id: str, command: str, arguments: dict[str, Any] | None = None) -> Any:
175
+ return self._request(
176
+ "POST",
177
+ f"/session/{session_id}/command",
178
+ json={"command": command, "arguments": arguments or {}},
179
+ )
180
+
181
+ def run_shell(
182
+ self,
183
+ session_id: str,
184
+ command: str,
185
+ *,
186
+ agent: str | None = None,
187
+ model: str | dict[str, Any] | None = None,
188
+ ) -> Message:
189
+ payload = {"command": command}
190
+ if agent:
191
+ payload["agent"] = agent
192
+ normalized_model = _normalize_model(model)
193
+ if normalized_model:
194
+ payload["model"] = normalized_model
195
+ return Message.from_dict(self._request("POST", f"/session/{session_id}/shell", json=payload))
196
+
197
+ def list_tool_ids(self) -> list[str]:
198
+ payload = self._request("GET", "/experimental/tool/ids")
199
+ if isinstance(payload, list):
200
+ return payload
201
+ return payload.get("ids", [])
202
+
203
+ def list_tools(self, provider: str | None = None, model: str | None = None) -> list[ToolSchema]:
204
+ params = {}
205
+ if provider:
206
+ params["provider"] = provider
207
+ if model:
208
+ params["model"] = model
209
+ payload = self._request("GET", "/experimental/tool", params=params)
210
+ return [
211
+ ToolSchema(
212
+ name=item["name"],
213
+ description=item.get("description"),
214
+ input_schema=item.get("inputSchema", {}),
215
+ )
216
+ for item in payload.get("tools", [])
217
+ ]
218
+
219
+ def list_files(self, path: str | None = None) -> Any:
220
+ # Always use the server-side project path; never fall back to os.getcwd()
221
+ server_path = path or self.config.project_path
222
+ if not server_path:
223
+ raise ValueError("path is required: set OPENCODE_PROJECT_PATH or pass path explicitly")
224
+ return self._request("GET", "/file", params={"path": server_path})
225
+
226
+ def get_file_content(self, path: str) -> Any:
227
+ # Paths are resolved server-side; send as-is. If relative, OpenCode resolves
228
+ # against the session's working directory on the server.
229
+ return self._request("GET", "/file/content", params={"path": path})
230
+
231
+ def get_file_status(self) -> Any:
232
+ return self._request("GET", "/file/status")
233
+
234
+ def find_files(
235
+ self,
236
+ query: str,
237
+ *,
238
+ item_type: str | None = None,
239
+ directory: str | None = None,
240
+ limit: int | None = None,
241
+ ) -> Any:
242
+ params: dict[str, Any] = {"query": query}
243
+ if item_type:
244
+ params["type"] = item_type
245
+ # Use server-side project path; never resolve locally
246
+ server_dir = directory or self.config.project_path
247
+ if server_dir:
248
+ params["directory"] = server_dir
249
+ if limit is not None:
250
+ params["limit"] = limit
251
+ return self._request("GET", "/find/file", params=params)
252
+
253
+ def find_symbols(self, query: str) -> Any:
254
+ return self._request("GET", "/find/symbol", params={"query": query})
255
+
256
+ def search(self, pattern: str) -> Any:
257
+ return self._request("GET", "/find", params={"pattern": pattern})
258
+
259
+ def get_mcp_status(self) -> list[MCPServer]:
260
+ payload = self._request("GET", "/mcp")
261
+ return [
262
+ MCPServer(name=name, status=details.get("status"), tools=details.get("tools", []), raw=details)
263
+ for name, details in payload.items()
264
+ ]
265
+
266
+ def register_mcp_server(self, name: str, config: dict[str, Any]) -> Any:
267
+ return self._request("POST", "/mcp", json={"name": name, "config": config})
268
+
269
+ def list_commands(self) -> Any:
270
+ return self._request("GET", "/command")
271
+
272
+ def get_current_path(self) -> Any:
273
+ return self._request("GET", "/path")
274
+
275
+ def get_current_project(self) -> Any:
276
+ return self._request("GET", "/project/current")
277
+
278
+ def list_agents(self) -> list[AgentDefinition]:
279
+ return [
280
+ AgentDefinition(id=item["id"], name=item.get("name"), description=item.get("description"))
281
+ for item in self._request("GET", "/agent")
282
+ ]
283
+
284
+ def get_provider_config(self) -> dict[str, Any]:
285
+ return self._request("GET", "/config/providers")
286
+
287
+ def list_available_models(self, include_details: bool = False) -> list[Any]:
288
+ payload = self.get_provider_config()
289
+ models: list[Any] = []
290
+ for provider in payload.get("providers", []):
291
+ if not self._provider_is_ready(provider):
292
+ continue
293
+ provider_id = provider.get("id", "")
294
+ for model_id, details in provider.get("models", {}).items():
295
+ model_ref = f"{provider_id}/{model_id}"
296
+ if include_details:
297
+ models.append(
298
+ {
299
+ "provider": provider_id,
300
+ "model": model_id,
301
+ "ref": model_ref,
302
+ "name": details.get("name", model_id),
303
+ "status": details.get("status"),
304
+ "context_window": details.get("limit", {}).get("context"),
305
+ "raw": details,
306
+ }
307
+ )
308
+ else:
309
+ models.append(model_ref)
310
+ return models
311
+
312
+ @staticmethod
313
+ def _provider_is_ready(provider: dict[str, Any]) -> bool:
314
+ env_keys = provider.get("env", [])
315
+ options = provider.get("options", {})
316
+ api_key = options.get("apiKey")
317
+ if isinstance(api_key, str) and api_key.strip():
318
+ return True
319
+ if env_keys and all(os.getenv(key) for key in env_keys):
320
+ return True
321
+ return not env_keys
322
+
323
+ def iter_events(self) -> Iterator[StreamEvent]:
324
+ with self._client.stream("GET", "/event", headers={"Accept": "text/event-stream"}) as response:
325
+ response.raise_for_status()
326
+ for line in response.iter_lines():
327
+ if not line:
328
+ continue
329
+ if line.startswith("data:"):
330
+ payload = json.loads(line[5:].strip())
331
+ yield StreamEvent(type=payload.get("type", "message"), payload=payload.get("payload", {}))