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.
- opencode_agent/__init__.py +26 -0
- opencode_agent/agent.py +137 -0
- opencode_agent/cli.py +183 -0
- opencode_agent/client.py +331 -0
- opencode_agent/config.py +108 -0
- opencode_agent/exceptions.py +25 -0
- opencode_agent/logging.py +16 -0
- opencode_agent/managers.py +147 -0
- opencode_agent/models.py +141 -0
- opencode_agent/orchestrator.py +293 -0
- opencode_agent-0.1.0.dist-info/METADATA +209 -0
- opencode_agent-0.1.0.dist-info/RECORD +15 -0
- opencode_agent-0.1.0.dist-info/WHEEL +5 -0
- opencode_agent-0.1.0.dist-info/entry_points.txt +2 -0
- opencode_agent-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
]
|
opencode_agent/agent.py
ADDED
|
@@ -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()
|
opencode_agent/client.py
ADDED
|
@@ -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", {}))
|