plato-sdk-v2 2.0.64__py3-none-any.whl → 2.3.4__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.
- plato/__init__.py +0 -9
- plato/_sims_generator/__init__.py +19 -4
- plato/_sims_generator/instruction.py +203 -0
- plato/_sims_generator/templates/instruction/helpers.py.jinja +161 -0
- plato/_sims_generator/templates/instruction/init.py.jinja +43 -0
- plato/agents/__init__.py +99 -430
- plato/agents/base.py +145 -0
- plato/agents/build.py +61 -0
- plato/agents/config.py +160 -0
- plato/agents/logging.py +515 -0
- plato/agents/runner.py +191 -0
- plato/agents/trajectory.py +266 -0
- plato/chronos/models/__init__.py +1 -1
- plato/sims/cli.py +299 -123
- plato/sims/registry.py +77 -4
- plato/v1/cli/agent.py +88 -84
- plato/v1/cli/pm.py +84 -44
- plato/v1/cli/sandbox.py +241 -61
- plato/v1/cli/ssh.py +16 -4
- plato/v1/cli/verify.py +685 -0
- plato/v1/cli/world.py +3 -0
- plato/v1/flow_executor.py +21 -17
- plato/v1/models/env.py +11 -11
- plato/v1/sdk.py +2 -2
- plato/v1/sync_env.py +11 -11
- plato/v1/sync_flow_executor.py +21 -17
- plato/v1/sync_sdk.py +4 -2
- plato/v2/__init__.py +2 -0
- plato/v2/async_/environment.py +31 -0
- plato/v2/async_/session.py +72 -4
- plato/v2/sync/environment.py +31 -0
- plato/v2/sync/session.py +72 -4
- plato/worlds/README.md +71 -56
- plato/worlds/__init__.py +56 -18
- plato/worlds/base.py +578 -93
- plato/worlds/config.py +276 -74
- plato/worlds/runner.py +475 -80
- {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/METADATA +3 -3
- {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/RECORD +41 -36
- {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/entry_points.txt +1 -0
- plato/agents/callback.py +0 -246
- plato/world/__init__.py +0 -44
- plato/world/base.py +0 -267
- plato/world/config.py +0 -139
- plato/world/types.py +0 -47
- {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/WHEEL +0 -0
plato/agents/__init__.py
CHANGED
|
@@ -1,453 +1,122 @@
|
|
|
1
|
-
"""Plato agent
|
|
1
|
+
"""Plato agent framework.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Provides base classes and utilities for building and running agents.
|
|
4
|
+
|
|
5
|
+
Base Classes:
|
|
6
|
+
- BaseAgent: Abstract base class for agents
|
|
7
|
+
- AgentConfig: Base configuration class
|
|
8
|
+
- Secret: Annotation marker for secrets
|
|
9
|
+
|
|
10
|
+
Registry:
|
|
11
|
+
- register_agent: Decorator to register an agent
|
|
12
|
+
- get_agent: Get an agent by name
|
|
13
|
+
- get_registered_agents: Get all registered agents
|
|
5
14
|
|
|
6
15
|
Runner:
|
|
7
|
-
- AgentRunner:
|
|
16
|
+
- AgentRunner: Run agents in Docker containers
|
|
8
17
|
- AgentRunResult: Async iterator for agent output
|
|
9
18
|
|
|
19
|
+
Trajectory (ATIF):
|
|
20
|
+
- Trajectory: ATIF trajectory model
|
|
21
|
+
- Step, Agent, ToolCall, etc.: ATIF components
|
|
22
|
+
|
|
10
23
|
Callback:
|
|
11
|
-
- ChronosCallback: Utility for
|
|
24
|
+
- ChronosCallback: Utility for Chronos communication
|
|
25
|
+
|
|
26
|
+
Example (direct execution):
|
|
27
|
+
from plato.agents import BaseAgent, AgentConfig, Secret, register_agent
|
|
28
|
+
from typing import Annotated
|
|
12
29
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
30
|
+
class MyAgentConfig(AgentConfig):
|
|
31
|
+
model_name: str = "anthropic/claude-sonnet-4"
|
|
32
|
+
api_key: Annotated[str, Secret(description="API key")]
|
|
16
33
|
|
|
17
|
-
|
|
34
|
+
@register_agent("my-agent")
|
|
35
|
+
class MyAgent(BaseAgent[MyAgentConfig]):
|
|
36
|
+
name = "my-agent"
|
|
37
|
+
description = "My custom agent"
|
|
38
|
+
|
|
39
|
+
async def run(self, instruction: str) -> None:
|
|
40
|
+
# Agent implementation
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
Example (Docker execution):
|
|
18
44
|
from plato.agents import AgentRunner
|
|
19
45
|
|
|
20
|
-
# Run agent with automatic Chronos callbacks
|
|
21
46
|
async for line in AgentRunner.run(
|
|
22
|
-
image="
|
|
47
|
+
image="my-agent:latest",
|
|
23
48
|
config={"model_name": "anthropic/claude-sonnet-4"},
|
|
24
|
-
secrets={"
|
|
25
|
-
instruction="Fix the bug
|
|
49
|
+
secrets={"api_key": "sk-..."},
|
|
50
|
+
instruction="Fix the bug",
|
|
26
51
|
workspace="/path/to/repo",
|
|
27
|
-
callback_url="http://chronos.example.com/api/callback",
|
|
28
|
-
session_id="abc123",
|
|
29
52
|
):
|
|
30
53
|
print(line)
|
|
31
|
-
# Logs are automatically pushed and artifacts uploaded to Chronos
|
|
32
54
|
"""
|
|
33
55
|
|
|
34
56
|
from __future__ import annotations
|
|
35
57
|
|
|
36
58
|
__all__ = [
|
|
37
|
-
#
|
|
38
|
-
"
|
|
39
|
-
"
|
|
59
|
+
# Config
|
|
60
|
+
"AgentConfig",
|
|
61
|
+
"Secret",
|
|
62
|
+
# Build
|
|
63
|
+
"BuildConfig",
|
|
64
|
+
"load_build_config",
|
|
65
|
+
# Base
|
|
66
|
+
"BaseAgent",
|
|
67
|
+
"ConfigT",
|
|
68
|
+
"register_agent",
|
|
69
|
+
"get_agent",
|
|
70
|
+
"get_registered_agents",
|
|
40
71
|
# Runner
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
72
|
+
"run_agent",
|
|
73
|
+
# Trajectory (ATIF)
|
|
74
|
+
"Trajectory",
|
|
75
|
+
"Step",
|
|
76
|
+
"Agent",
|
|
77
|
+
"ToolCall",
|
|
78
|
+
"Observation",
|
|
79
|
+
"ObservationResult",
|
|
80
|
+
"Metrics",
|
|
81
|
+
"FinalMetrics",
|
|
82
|
+
"SCHEMA_VERSION",
|
|
83
|
+
# Logging
|
|
84
|
+
"init_logging",
|
|
85
|
+
"span",
|
|
86
|
+
"log_event",
|
|
87
|
+
"upload_artifacts",
|
|
88
|
+
"upload_artifact",
|
|
89
|
+
"upload_checkpoint",
|
|
90
|
+
"reset_logging",
|
|
45
91
|
]
|
|
46
92
|
|
|
47
|
-
from plato.agents.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"model_name": {
|
|
78
|
-
"type": "string",
|
|
79
|
-
"description": "LLM model to use (e.g., 'anthropic/claude-sonnet-4')",
|
|
80
|
-
},
|
|
81
|
-
"disable_tool_calls": {
|
|
82
|
-
"type": "boolean",
|
|
83
|
-
"default": False,
|
|
84
|
-
"description": "Whether to disable native function calling",
|
|
85
|
-
},
|
|
86
|
-
"reasoning_effort": {
|
|
87
|
-
"type": ["string", "null"],
|
|
88
|
-
"enum": ["low", "medium", "high", None],
|
|
89
|
-
"default": "medium",
|
|
90
|
-
"description": "Reasoning effort level for the model",
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
"required": [],
|
|
94
|
-
},
|
|
95
|
-
"codex": {
|
|
96
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
97
|
-
"$id": "codex",
|
|
98
|
-
"title": "CodexConfig",
|
|
99
|
-
"description": "Configuration for Codex CLI agent.",
|
|
100
|
-
"type": "object",
|
|
101
|
-
"properties": {
|
|
102
|
-
"model_name": {
|
|
103
|
-
"type": "string",
|
|
104
|
-
"description": "LLM model to use (e.g., 'openai/gpt-4o')",
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
"required": [],
|
|
108
|
-
},
|
|
109
|
-
"aider": {
|
|
110
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
111
|
-
"$id": "aider",
|
|
112
|
-
"title": "AiderConfig",
|
|
113
|
-
"description": "Configuration for Aider agent.",
|
|
114
|
-
"type": "object",
|
|
115
|
-
"properties": {
|
|
116
|
-
"model_name": {
|
|
117
|
-
"type": "string",
|
|
118
|
-
"description": "LLM model to use",
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
"required": [],
|
|
122
|
-
},
|
|
123
|
-
"gemini-cli": {
|
|
124
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
125
|
-
"$id": "gemini-cli",
|
|
126
|
-
"title": "GeminiCliConfig",
|
|
127
|
-
"description": "Configuration for Gemini CLI agent.",
|
|
128
|
-
"type": "object",
|
|
129
|
-
"properties": {
|
|
130
|
-
"model_name": {
|
|
131
|
-
"type": "string",
|
|
132
|
-
"description": "LLM model to use (e.g., 'google/gemini-2.5-pro')",
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
"required": [],
|
|
136
|
-
},
|
|
137
|
-
"goose": {
|
|
138
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
139
|
-
"$id": "goose",
|
|
140
|
-
"title": "GooseConfig",
|
|
141
|
-
"description": "Configuration for Block Goose agent.",
|
|
142
|
-
"type": "object",
|
|
143
|
-
"properties": {
|
|
144
|
-
"model_name": {
|
|
145
|
-
"type": "string",
|
|
146
|
-
"description": "LLM model to use",
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
"required": [],
|
|
150
|
-
},
|
|
151
|
-
"swe-agent": {
|
|
152
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
153
|
-
"$id": "swe-agent",
|
|
154
|
-
"title": "SweAgentConfig",
|
|
155
|
-
"description": "Configuration for SWE-agent.",
|
|
156
|
-
"type": "object",
|
|
157
|
-
"properties": {
|
|
158
|
-
"model_name": {
|
|
159
|
-
"type": "string",
|
|
160
|
-
"description": "LLM model to use",
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
"required": [],
|
|
164
|
-
},
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def get_agent_schema(agent_name: str) -> dict | None:
|
|
169
|
-
"""Get the JSON schema for an agent.
|
|
170
|
-
|
|
171
|
-
Args:
|
|
172
|
-
agent_name: The agent name (e.g., 'claude-code', 'openhands')
|
|
173
|
-
|
|
174
|
-
Returns:
|
|
175
|
-
JSON schema dict or None if agent not found
|
|
176
|
-
"""
|
|
177
|
-
return AGENT_SCHEMAS.get(agent_name)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
class AgentRunResult:
|
|
181
|
-
"""Result of running an agent.
|
|
182
|
-
|
|
183
|
-
This class is an async iterator that yields output lines from the agent.
|
|
184
|
-
It also provides access to the logs directory where agent logs are stored.
|
|
185
|
-
|
|
186
|
-
If callback_url and session_id are provided, logs are automatically
|
|
187
|
-
pushed to Chronos during execution, and artifacts are uploaded after
|
|
188
|
-
the agent completes.
|
|
189
|
-
|
|
190
|
-
Example:
|
|
191
|
-
result = AgentRunner.run(...)
|
|
192
|
-
async for line in result:
|
|
193
|
-
print(line)
|
|
194
|
-
print(f"Logs at: {result.logs_dir}")
|
|
195
|
-
"""
|
|
196
|
-
|
|
197
|
-
def __init__(
|
|
198
|
-
self,
|
|
199
|
-
image: str,
|
|
200
|
-
config: dict,
|
|
201
|
-
secrets: dict[str, str],
|
|
202
|
-
instruction: str,
|
|
203
|
-
workspace: str,
|
|
204
|
-
logs_dir: str | None,
|
|
205
|
-
pull: bool,
|
|
206
|
-
callback_url: str,
|
|
207
|
-
session_id: str,
|
|
208
|
-
):
|
|
209
|
-
self._image = image
|
|
210
|
-
self._config = config
|
|
211
|
-
self._secrets = secrets
|
|
212
|
-
self._instruction = instruction
|
|
213
|
-
self._workspace = workspace
|
|
214
|
-
self._pull = pull
|
|
215
|
-
|
|
216
|
-
# Chronos callback
|
|
217
|
-
self._callback = ChronosCallback(
|
|
218
|
-
callback_url=callback_url,
|
|
219
|
-
session_id=session_id,
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
# Create logs dir if not provided
|
|
223
|
-
if logs_dir is None:
|
|
224
|
-
import tempfile
|
|
225
|
-
|
|
226
|
-
self._logs_dir = tempfile.mkdtemp(prefix="agent_logs_")
|
|
227
|
-
else:
|
|
228
|
-
self._logs_dir = logs_dir
|
|
229
|
-
|
|
230
|
-
@property
|
|
231
|
-
def logs_dir(self) -> str:
|
|
232
|
-
"""Host path where agent logs are stored."""
|
|
233
|
-
return self._logs_dir
|
|
234
|
-
|
|
235
|
-
@property
|
|
236
|
-
def callback(self) -> ChronosCallback:
|
|
237
|
-
"""The Chronos callback client (for manual use if needed)."""
|
|
238
|
-
return self._callback
|
|
239
|
-
|
|
240
|
-
def __aiter__(self):
|
|
241
|
-
return self._stream()
|
|
242
|
-
|
|
243
|
-
async def _stream(self):
|
|
244
|
-
"""Stream output from the agent."""
|
|
245
|
-
import asyncio
|
|
246
|
-
import json
|
|
247
|
-
import os
|
|
248
|
-
import tempfile
|
|
249
|
-
|
|
250
|
-
# Log buffer for batching
|
|
251
|
-
log_buffer: list[dict] = []
|
|
252
|
-
|
|
253
|
-
async def flush_logs():
|
|
254
|
-
"""Push buffered logs to Chronos."""
|
|
255
|
-
if not log_buffer or not self._callback.enabled:
|
|
256
|
-
return
|
|
257
|
-
await self._callback.push_logs(log_buffer.copy())
|
|
258
|
-
log_buffer.clear()
|
|
259
|
-
|
|
260
|
-
# Push start log
|
|
261
|
-
agent_name = self._image.split("/")[-1].split(":")[0]
|
|
262
|
-
await self._callback.push_log(f"Starting agent: {agent_name} ({self._image})")
|
|
263
|
-
|
|
264
|
-
# Pull the image if requested
|
|
265
|
-
if self._pull:
|
|
266
|
-
pull_proc = await asyncio.create_subprocess_exec(
|
|
267
|
-
"docker",
|
|
268
|
-
"pull",
|
|
269
|
-
self._image,
|
|
270
|
-
stdout=asyncio.subprocess.PIPE,
|
|
271
|
-
stderr=asyncio.subprocess.STDOUT,
|
|
272
|
-
)
|
|
273
|
-
assert pull_proc.stdout is not None # stdout is set via PIPE
|
|
274
|
-
while True:
|
|
275
|
-
line = await pull_proc.stdout.readline()
|
|
276
|
-
if not line:
|
|
277
|
-
break
|
|
278
|
-
yield f"[pull] {line.decode().rstrip()}"
|
|
279
|
-
await pull_proc.wait()
|
|
280
|
-
|
|
281
|
-
# Create agent subdirectory for logs (Harbor writes to /logs/agent/)
|
|
282
|
-
agent_logs_subdir = os.path.join(self._logs_dir, "agent")
|
|
283
|
-
os.makedirs(agent_logs_subdir, exist_ok=True)
|
|
284
|
-
|
|
285
|
-
# Write config to a temp file
|
|
286
|
-
config_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)
|
|
287
|
-
json.dump(self._config, config_file)
|
|
288
|
-
config_file.close()
|
|
289
|
-
|
|
290
|
-
agent_failed = False
|
|
291
|
-
error_message = ""
|
|
292
|
-
|
|
293
|
-
try:
|
|
294
|
-
# Build docker command
|
|
295
|
-
docker_cmd = [
|
|
296
|
-
"docker",
|
|
297
|
-
"run",
|
|
298
|
-
"--rm",
|
|
299
|
-
"-v",
|
|
300
|
-
f"{self._workspace}:/workspace",
|
|
301
|
-
"-v",
|
|
302
|
-
f"{self._logs_dir}:/logs",
|
|
303
|
-
"-v",
|
|
304
|
-
f"{config_file.name}:/config.json:ro",
|
|
305
|
-
"-w",
|
|
306
|
-
"/workspace",
|
|
307
|
-
]
|
|
308
|
-
|
|
309
|
-
# Add secrets as environment variables
|
|
310
|
-
for key, value in self._secrets.items():
|
|
311
|
-
docker_cmd.extend(["-e", f"{key.upper()}={value}"])
|
|
312
|
-
|
|
313
|
-
# Add the image and instruction argument
|
|
314
|
-
docker_cmd.append(self._image)
|
|
315
|
-
docker_cmd.extend(["--instruction", self._instruction])
|
|
316
|
-
|
|
317
|
-
# Run the container
|
|
318
|
-
process = await asyncio.create_subprocess_exec(
|
|
319
|
-
*docker_cmd,
|
|
320
|
-
stdout=asyncio.subprocess.PIPE,
|
|
321
|
-
stderr=asyncio.subprocess.STDOUT,
|
|
322
|
-
)
|
|
323
|
-
assert process.stdout is not None # stdout is set via PIPE
|
|
324
|
-
|
|
325
|
-
# Stream output
|
|
326
|
-
while True:
|
|
327
|
-
line = await process.stdout.readline()
|
|
328
|
-
if not line:
|
|
329
|
-
break
|
|
330
|
-
decoded = line.decode().rstrip()
|
|
331
|
-
yield decoded
|
|
332
|
-
|
|
333
|
-
# Buffer logs for Chronos callback
|
|
334
|
-
if self._callback.enabled:
|
|
335
|
-
log_buffer.append({"level": "info", "message": decoded})
|
|
336
|
-
if len(log_buffer) >= 10:
|
|
337
|
-
await flush_logs()
|
|
338
|
-
|
|
339
|
-
await process.wait()
|
|
340
|
-
|
|
341
|
-
if process.returncode != 0:
|
|
342
|
-
agent_failed = True
|
|
343
|
-
error_message = f"Agent failed with exit code {process.returncode}"
|
|
344
|
-
except Exception as e:
|
|
345
|
-
agent_failed = True
|
|
346
|
-
error_message = str(e)
|
|
347
|
-
raise
|
|
348
|
-
finally:
|
|
349
|
-
# Clean up config file
|
|
350
|
-
os.unlink(config_file.name)
|
|
351
|
-
|
|
352
|
-
# Flush any remaining logs
|
|
353
|
-
await flush_logs()
|
|
354
|
-
|
|
355
|
-
# Push final status and upload artifacts
|
|
356
|
-
if self._callback.enabled:
|
|
357
|
-
if agent_failed:
|
|
358
|
-
await self._callback.push_log(error_message, level="error")
|
|
359
|
-
else:
|
|
360
|
-
await self._callback.push_log("Agent completed successfully")
|
|
361
|
-
|
|
362
|
-
# Upload trajectory and logs
|
|
363
|
-
await self._callback.upload_artifacts(logs_dir=self._logs_dir)
|
|
364
|
-
|
|
365
|
-
if agent_failed:
|
|
366
|
-
raise RuntimeError(error_message)
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
class AgentRunner:
|
|
370
|
-
"""Utility for running agents in Docker containers.
|
|
371
|
-
|
|
372
|
-
Each agent image has its own entrypoint that:
|
|
373
|
-
- Reads config from /config.json
|
|
374
|
-
- Reads secrets from environment variables
|
|
375
|
-
- Takes instruction via --instruction argument
|
|
376
|
-
|
|
377
|
-
When callback_url and session_id are provided, the runner automatically:
|
|
378
|
-
- Pushes logs to Chronos during execution
|
|
379
|
-
- Uploads trajectory and zipped logs after completion
|
|
380
|
-
|
|
381
|
-
Example:
|
|
382
|
-
import asyncio
|
|
383
|
-
from plato.agents import AgentRunner
|
|
384
|
-
|
|
385
|
-
async def main():
|
|
386
|
-
async for line in AgentRunner.run(
|
|
387
|
-
image="us-docker.pkg.dev/plato-prod/agents/openhands:latest",
|
|
388
|
-
config={"model_name": "anthropic/claude-sonnet-4"},
|
|
389
|
-
secrets={"anthropic_api_key": "sk-..."},
|
|
390
|
-
instruction="Fix the bug in main.py",
|
|
391
|
-
workspace="/path/to/repo",
|
|
392
|
-
callback_url="http://chronos.example.com/api/callback",
|
|
393
|
-
session_id="abc123",
|
|
394
|
-
):
|
|
395
|
-
print(line)
|
|
396
|
-
# Logs and artifacts are automatically uploaded to Chronos
|
|
397
|
-
|
|
398
|
-
asyncio.run(main())
|
|
399
|
-
"""
|
|
400
|
-
|
|
401
|
-
@staticmethod
|
|
402
|
-
def run(
|
|
403
|
-
image: str,
|
|
404
|
-
config: dict,
|
|
405
|
-
secrets: dict[str, str],
|
|
406
|
-
instruction: str,
|
|
407
|
-
workspace: str,
|
|
408
|
-
logs_dir: str | None = None,
|
|
409
|
-
pull: bool = True,
|
|
410
|
-
callback_url: str = "",
|
|
411
|
-
session_id: str = "",
|
|
412
|
-
) -> AgentRunResult:
|
|
413
|
-
"""Run an agent in a Docker container.
|
|
414
|
-
|
|
415
|
-
Pulls the image if needed, then runs the agent with proper
|
|
416
|
-
configuration. Returns a result object that can be iterated
|
|
417
|
-
to stream output lines.
|
|
418
|
-
|
|
419
|
-
Args:
|
|
420
|
-
image: Docker image URI
|
|
421
|
-
config: Agent-specific configuration dict (matches agent schema)
|
|
422
|
-
secrets: Secret values (API keys, tokens, etc.)
|
|
423
|
-
instruction: The task instruction/prompt for the agent
|
|
424
|
-
workspace: Host directory to mount as /workspace
|
|
425
|
-
logs_dir: Host directory for agent logs. If None, creates a temp dir.
|
|
426
|
-
pull: Whether to pull the image before running
|
|
427
|
-
callback_url: Full callback URL for Chronos (e.g., http://server/api/callback)
|
|
428
|
-
session_id: Chronos session ID for callbacks
|
|
429
|
-
|
|
430
|
-
Returns:
|
|
431
|
-
AgentRunResult that can be async-iterated for output lines.
|
|
432
|
-
Access result.logs_dir to get the host path where logs are stored.
|
|
433
|
-
|
|
434
|
-
Raises:
|
|
435
|
-
RuntimeError: If agent fails with non-zero exit code
|
|
436
|
-
|
|
437
|
-
Example:
|
|
438
|
-
result = AgentRunner.run(...)
|
|
439
|
-
async for line in result:
|
|
440
|
-
print(line)
|
|
441
|
-
print(f"Logs at: {result.logs_dir}")
|
|
442
|
-
"""
|
|
443
|
-
return AgentRunResult(
|
|
444
|
-
image=image,
|
|
445
|
-
config=config,
|
|
446
|
-
secrets=secrets,
|
|
447
|
-
instruction=instruction,
|
|
448
|
-
workspace=workspace,
|
|
449
|
-
logs_dir=logs_dir,
|
|
450
|
-
pull=pull,
|
|
451
|
-
callback_url=callback_url,
|
|
452
|
-
session_id=session_id,
|
|
453
|
-
)
|
|
93
|
+
from plato.agents.base import (
|
|
94
|
+
BaseAgent,
|
|
95
|
+
ConfigT,
|
|
96
|
+
get_agent,
|
|
97
|
+
get_registered_agents,
|
|
98
|
+
register_agent,
|
|
99
|
+
)
|
|
100
|
+
from plato.agents.build import BuildConfig, load_build_config
|
|
101
|
+
from plato.agents.config import AgentConfig, Secret
|
|
102
|
+
from plato.agents.logging import (
|
|
103
|
+
init_logging,
|
|
104
|
+
log_event,
|
|
105
|
+
reset_logging,
|
|
106
|
+
span,
|
|
107
|
+
upload_artifact,
|
|
108
|
+
upload_artifacts,
|
|
109
|
+
upload_checkpoint,
|
|
110
|
+
)
|
|
111
|
+
from plato.agents.runner import run_agent
|
|
112
|
+
from plato.agents.trajectory import (
|
|
113
|
+
SCHEMA_VERSION,
|
|
114
|
+
Agent,
|
|
115
|
+
FinalMetrics,
|
|
116
|
+
Metrics,
|
|
117
|
+
Observation,
|
|
118
|
+
ObservationResult,
|
|
119
|
+
Step,
|
|
120
|
+
ToolCall,
|
|
121
|
+
Trajectory,
|
|
122
|
+
)
|
plato/agents/base.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Base agent class and registry for Plato agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, ClassVar, Generic, TypeVar, get_args, get_origin
|
|
10
|
+
|
|
11
|
+
from plato.agents.config import AgentConfig
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Global registry of agents
|
|
16
|
+
_AGENT_REGISTRY: dict[str, type[BaseAgent]] = {}
|
|
17
|
+
|
|
18
|
+
# Type variable for config
|
|
19
|
+
ConfigT = TypeVar("ConfigT", bound=AgentConfig)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def register_agent(name: str | None = None):
|
|
23
|
+
"""Decorator to register an agent class.
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
@register_agent("openhands")
|
|
27
|
+
class OpenHandsAgent(BaseAgent[OpenHandsConfig]):
|
|
28
|
+
...
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def decorator(cls: type[BaseAgent]) -> type[BaseAgent]:
|
|
32
|
+
agent_name = name or getattr(cls, "name", cls.__name__.lower().replace("agent", ""))
|
|
33
|
+
_AGENT_REGISTRY[agent_name] = cls
|
|
34
|
+
logger.debug(f"Registered agent: {agent_name} -> {cls.__name__}")
|
|
35
|
+
return cls
|
|
36
|
+
|
|
37
|
+
return decorator
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_registered_agents() -> dict[str, type[BaseAgent]]:
|
|
41
|
+
"""Get all registered agents."""
|
|
42
|
+
return _AGENT_REGISTRY.copy()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_agent(name: str) -> type[BaseAgent] | None:
|
|
46
|
+
"""Get an agent by name."""
|
|
47
|
+
return _AGENT_REGISTRY.get(name)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BaseAgent(ABC, Generic[ConfigT]):
|
|
51
|
+
"""Base class for Plato agents.
|
|
52
|
+
|
|
53
|
+
Subclass with a config type parameter for fully typed config access:
|
|
54
|
+
|
|
55
|
+
class OpenHandsConfig(AgentConfig):
|
|
56
|
+
model_name: str = "anthropic/claude-sonnet-4"
|
|
57
|
+
anthropic_api_key: Annotated[str | None, Secret(description="API key")] = None
|
|
58
|
+
|
|
59
|
+
@register_agent("openhands")
|
|
60
|
+
class OpenHandsAgent(BaseAgent[OpenHandsConfig]):
|
|
61
|
+
name = "openhands"
|
|
62
|
+
description = "OpenHands AI software engineer"
|
|
63
|
+
|
|
64
|
+
async def run(self, instruction: str) -> None:
|
|
65
|
+
# self.config is typed as OpenHandsConfig
|
|
66
|
+
model = self.config.model_name
|
|
67
|
+
...
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# Class attributes
|
|
71
|
+
name: ClassVar[str] = "base"
|
|
72
|
+
description: ClassVar[str] = ""
|
|
73
|
+
|
|
74
|
+
# Instance attributes
|
|
75
|
+
config: ConfigT
|
|
76
|
+
|
|
77
|
+
def __init__(self) -> None:
|
|
78
|
+
self.logger = logging.getLogger(f"plato.agents.{self.name}")
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def get_config_class(cls) -> type[AgentConfig]:
|
|
82
|
+
"""Get the config class from the generic parameter."""
|
|
83
|
+
for base in getattr(cls, "__orig_bases__", []):
|
|
84
|
+
origin = get_origin(base)
|
|
85
|
+
if origin is BaseAgent:
|
|
86
|
+
args = get_args(base)
|
|
87
|
+
if args and isinstance(args[0], type) and issubclass(args[0], AgentConfig):
|
|
88
|
+
return args[0]
|
|
89
|
+
return AgentConfig
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def get_version(cls) -> str:
|
|
93
|
+
"""Get version from package metadata."""
|
|
94
|
+
import importlib.metadata
|
|
95
|
+
|
|
96
|
+
for pkg_name in [cls.__module__.split(".")[0], f"plato-agent-{cls.name}"]:
|
|
97
|
+
try:
|
|
98
|
+
return importlib.metadata.version(pkg_name)
|
|
99
|
+
except importlib.metadata.PackageNotFoundError:
|
|
100
|
+
continue
|
|
101
|
+
return "0.0.0"
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def get_schema(cls) -> dict:
|
|
105
|
+
"""Get full schema for the agent including config and build schemas."""
|
|
106
|
+
from plato.agents.build import BuildConfig
|
|
107
|
+
|
|
108
|
+
config_class = cls.get_config_class()
|
|
109
|
+
return {
|
|
110
|
+
"config": config_class.get_json_schema(),
|
|
111
|
+
"build": BuildConfig.get_json_schema(),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@abstractmethod
|
|
115
|
+
async def run(self, instruction: str) -> None:
|
|
116
|
+
"""Run the agent with the given instruction.
|
|
117
|
+
|
|
118
|
+
This is the main entry point for agent execution. Implementations should:
|
|
119
|
+
1. Set up the environment using self.config
|
|
120
|
+
2. Execute the agent's core logic
|
|
121
|
+
3. Write trajectory to logs_dir if applicable
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
instruction: The task instruction/prompt for the agent.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
RuntimeError: If agent execution fails.
|
|
128
|
+
"""
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
async def write_trajectory(self, trajectory: dict[str, Any]) -> None:
|
|
132
|
+
"""Write ATIF trajectory to the logs directory.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
trajectory: ATIF-formatted trajectory dictionary.
|
|
136
|
+
"""
|
|
137
|
+
logs_dir = Path(self.config.logs_dir)
|
|
138
|
+
agent_logs = logs_dir / "agent"
|
|
139
|
+
agent_logs.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
|
|
141
|
+
trajectory_path = agent_logs / "trajectory.json"
|
|
142
|
+
with open(trajectory_path, "w") as f:
|
|
143
|
+
json.dump(trajectory, f, indent=2)
|
|
144
|
+
|
|
145
|
+
self.logger.info(f"Wrote trajectory to {trajectory_path}")
|