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.
Files changed (46) hide show
  1. plato/__init__.py +0 -9
  2. plato/_sims_generator/__init__.py +19 -4
  3. plato/_sims_generator/instruction.py +203 -0
  4. plato/_sims_generator/templates/instruction/helpers.py.jinja +161 -0
  5. plato/_sims_generator/templates/instruction/init.py.jinja +43 -0
  6. plato/agents/__init__.py +99 -430
  7. plato/agents/base.py +145 -0
  8. plato/agents/build.py +61 -0
  9. plato/agents/config.py +160 -0
  10. plato/agents/logging.py +515 -0
  11. plato/agents/runner.py +191 -0
  12. plato/agents/trajectory.py +266 -0
  13. plato/chronos/models/__init__.py +1 -1
  14. plato/sims/cli.py +299 -123
  15. plato/sims/registry.py +77 -4
  16. plato/v1/cli/agent.py +88 -84
  17. plato/v1/cli/pm.py +84 -44
  18. plato/v1/cli/sandbox.py +241 -61
  19. plato/v1/cli/ssh.py +16 -4
  20. plato/v1/cli/verify.py +685 -0
  21. plato/v1/cli/world.py +3 -0
  22. plato/v1/flow_executor.py +21 -17
  23. plato/v1/models/env.py +11 -11
  24. plato/v1/sdk.py +2 -2
  25. plato/v1/sync_env.py +11 -11
  26. plato/v1/sync_flow_executor.py +21 -17
  27. plato/v1/sync_sdk.py +4 -2
  28. plato/v2/__init__.py +2 -0
  29. plato/v2/async_/environment.py +31 -0
  30. plato/v2/async_/session.py +72 -4
  31. plato/v2/sync/environment.py +31 -0
  32. plato/v2/sync/session.py +72 -4
  33. plato/worlds/README.md +71 -56
  34. plato/worlds/__init__.py +56 -18
  35. plato/worlds/base.py +578 -93
  36. plato/worlds/config.py +276 -74
  37. plato/worlds/runner.py +475 -80
  38. {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/METADATA +3 -3
  39. {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/RECORD +41 -36
  40. {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/entry_points.txt +1 -0
  41. plato/agents/callback.py +0 -246
  42. plato/world/__init__.py +0 -44
  43. plato/world/base.py +0 -267
  44. plato/world/config.py +0 -139
  45. plato/world/types.py +0 -47
  46. {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 runner utilities.
1
+ """Plato agent framework.
2
2
 
3
- This module provides utilities for running coding agents in Docker containers.
4
- Each agent image has its own entrypoint that handles execution internally.
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: Utility for running agents in Docker containers
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 communicating with Chronos server
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
- Schemas:
14
- - AGENT_SCHEMAS: JSON schemas for agent configuration
15
- - get_agent_schema: Get schema for a specific agent
30
+ class MyAgentConfig(AgentConfig):
31
+ model_name: str = "anthropic/claude-sonnet-4"
32
+ api_key: Annotated[str, Secret(description="API key")]
16
33
 
17
- Example:
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="us-docker.pkg.dev/plato-prod/agents/openhands:latest",
47
+ image="my-agent:latest",
23
48
  config={"model_name": "anthropic/claude-sonnet-4"},
24
- secrets={"anthropic_api_key": "sk-..."},
25
- instruction="Fix the bug in main.py",
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
- # Schemas
38
- "get_agent_schema",
39
- "AGENT_SCHEMAS",
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
- "AgentRunner",
42
- "AgentRunResult",
43
- # Callback
44
- "ChronosCallback",
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.callback import ChronosCallback
48
-
49
- # JSON Schemas for agent configuration
50
- AGENT_SCHEMAS: dict[str, dict] = {
51
- "claude-code": {
52
- "$schema": "https://json-schema.org/draft/2020-12/schema",
53
- "$id": "claude-code",
54
- "title": "ClaudeCodeConfig",
55
- "description": "Configuration for Claude Code agent.",
56
- "type": "object",
57
- "properties": {
58
- "model_name": {
59
- "type": "string",
60
- "description": "LLM model to use (e.g., 'anthropic/claude-sonnet-4')",
61
- },
62
- "max_thinking_tokens": {
63
- "type": ["integer", "null"],
64
- "default": None,
65
- "description": "Maximum tokens for extended thinking mode",
66
- },
67
- },
68
- "required": [],
69
- },
70
- "openhands": {
71
- "$schema": "https://json-schema.org/draft/2020-12/schema",
72
- "$id": "openhands",
73
- "title": "OpenHandsConfig",
74
- "description": "Configuration for OpenHands agent.",
75
- "type": "object",
76
- "properties": {
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}")