plato-sdk-v2 2.3.8__py3-none-any.whl → 2.3.11__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/agents/runner.py CHANGED
@@ -152,21 +152,34 @@ async def run_agent(
152
152
  docker_cmd.extend(["--instruction", instruction])
153
153
 
154
154
  # Run container - agents emit their own OTel spans
155
+ # Use large limit to handle agents that output long lines (e.g., JSON with file contents)
155
156
  process = await asyncio.create_subprocess_exec(
156
157
  *docker_cmd,
157
158
  stdout=asyncio.subprocess.PIPE,
158
159
  stderr=asyncio.subprocess.STDOUT,
160
+ limit=100 * 1024 * 1024, # 100MB buffer limit
159
161
  )
160
162
 
161
- # Capture output for error reporting
163
+ # Capture output for error reporting using chunked reads to handle large lines
162
164
  output_lines: list[str] = []
163
165
  assert process.stdout is not None
166
+ buffer = ""
164
167
  while True:
165
- line = await process.stdout.readline()
166
- if not line:
168
+ try:
169
+ chunk = await process.stdout.read(65536)
170
+ except Exception:
171
+ break
172
+ if not chunk:
167
173
  break
168
- decoded_line = line.decode().rstrip()
169
- output_lines.append(decoded_line)
174
+ buffer += chunk.decode(errors="replace")
175
+
176
+ while "\n" in buffer:
177
+ line, buffer = buffer.split("\n", 1)
178
+ output_lines.append(line)
179
+
180
+ # Handle any remaining content in buffer
181
+ if buffer.strip():
182
+ output_lines.append(buffer)
170
183
 
171
184
  await process.wait()
172
185
 
@@ -10,10 +10,18 @@ from pydantic import AwareDatetime, BaseModel, ConfigDict, Field
10
10
 
11
11
 
12
12
  class AgentConfig(BaseModel):
13
+ """Agent config - supports multiple formats.
14
+
15
+ New format: agent + version (version optional, defaults to latest)
16
+ Legacy format: agent_id (public_id)
17
+ """
18
+
13
19
  model_config = ConfigDict(
14
20
  extra="allow",
15
21
  )
16
- agent_id: Annotated[str, Field(title="Agent Id")]
22
+ agent: Annotated[str | None, Field(title="Agent")] = None
23
+ version: Annotated[str | None, Field(title="Version")] = None
24
+ agent_id: Annotated[str | None, Field(title="Agent Id")] = None # backwards compat
17
25
  config: Annotated[dict[str, Any] | None, Field(title="Config")] = {}
18
26
 
19
27
 
@@ -0,0 +1,219 @@
1
+ """Plato Chronos CLI - Launch and manage Chronos jobs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import typer
9
+
10
+ from plato.v1.cli.utils import console
11
+
12
+ chronos_app = typer.Typer(help="Chronos job management commands.")
13
+
14
+
15
+ @chronos_app.command()
16
+ def launch(
17
+ config: Path = typer.Argument(
18
+ ...,
19
+ help="Path to job config JSON file",
20
+ exists=True,
21
+ readable=True,
22
+ ),
23
+ chronos_url: str = typer.Option(
24
+ None,
25
+ "--url",
26
+ "-u",
27
+ envvar="CHRONOS_URL",
28
+ help="Chronos API URL (default: https://chronos.plato.so)",
29
+ ),
30
+ api_key: str = typer.Option(
31
+ None,
32
+ "--api-key",
33
+ "-k",
34
+ envvar="PLATO_API_KEY",
35
+ help="Plato API key for authentication",
36
+ ),
37
+ wait: bool = typer.Option(
38
+ False,
39
+ "--wait",
40
+ "-w",
41
+ help="Wait for job completion and stream logs",
42
+ ),
43
+ ):
44
+ """
45
+ Launch a Chronos job from a config file.
46
+
47
+ The config file should be a JSON file with the following structure:
48
+
49
+ \b
50
+ {
51
+ "world_package": "plato-world-structured-execution",
52
+ "world_version": "0.1.13", // optional, uses latest if not specified
53
+ "world_config": { ... },
54
+ "agent_configs": {
55
+ "skill_runner": {
56
+ "agent_id": "claude-code:2.1.5",
57
+ "config": { ... }
58
+ }
59
+ },
60
+ "secret_ids": [1, 2, 3] // IDs of secrets from Chronos
61
+ }
62
+
63
+ Examples:
64
+ plato chronos launch config.json
65
+ plato chronos launch config.json --wait
66
+ plato chronos launch config.json --url https://chronos.example.com
67
+ """
68
+ import httpx
69
+
70
+ from plato.chronos.api.jobs import launch_job
71
+ from plato.chronos.models import AgentConfig, LaunchJobRequest
72
+
73
+ # Set defaults
74
+ if not chronos_url:
75
+ chronos_url = "https://chronos.plato.so"
76
+
77
+ if not api_key:
78
+ console.print("[red]❌ No API key provided[/red]")
79
+ console.print("Set PLATO_API_KEY environment variable or use --api-key")
80
+ raise typer.Exit(1)
81
+
82
+ # Load config
83
+ try:
84
+ with open(config) as f:
85
+ job_config = json.load(f)
86
+ except json.JSONDecodeError as e:
87
+ console.print(f"[red]❌ Invalid JSON in config file: {e}[/red]")
88
+ raise typer.Exit(1)
89
+
90
+ # Validate required fields
91
+ if "world_package" not in job_config:
92
+ console.print("[red]❌ Missing required field: world_package[/red]")
93
+ raise typer.Exit(1)
94
+
95
+ if "agent_configs" not in job_config:
96
+ console.print("[red]❌ Missing required field: agent_configs[/red]")
97
+ raise typer.Exit(1)
98
+
99
+ # Build agent configs - supports both formats:
100
+ # - {"agent": "claude-code", "version": "2.1.5"} (new)
101
+ # - {"agent_id": "ag_xxxxx"} (backwards compat)
102
+ agent_configs = {}
103
+ for slot_name, agent_cfg in job_config.get("agent_configs", {}).items():
104
+ if "agent" not in agent_cfg and "agent_id" not in agent_cfg:
105
+ console.print(f"[red]❌ Missing 'agent' or 'agent_id' for slot: {slot_name}[/red]")
106
+ raise typer.Exit(1)
107
+
108
+ config_kwargs = {"config": agent_cfg.get("config", {})}
109
+ if "agent" in agent_cfg:
110
+ config_kwargs["agent"] = agent_cfg["agent"]
111
+ config_kwargs["version"] = agent_cfg.get("version")
112
+ else:
113
+ config_kwargs["agent_id"] = agent_cfg["agent_id"]
114
+
115
+ agent_configs[slot_name] = AgentConfig(**config_kwargs)
116
+
117
+ # Build request
118
+ request = LaunchJobRequest(
119
+ world_package=job_config["world_package"],
120
+ world_version=job_config.get("world_version"),
121
+ world_config=job_config.get("world_config"),
122
+ agent_configs=agent_configs,
123
+ secret_ids=job_config.get("secret_ids"),
124
+ runtime_artifact_id=job_config.get("runtime_artifact_id"),
125
+ )
126
+
127
+ console.print("[blue]🚀 Launching job...[/blue]")
128
+ console.print(f" World: {request.world_package}")
129
+ if request.world_version:
130
+ console.print(f" Version: {request.world_version}")
131
+ console.print(f" Agents: {list(agent_configs.keys())}")
132
+
133
+ try:
134
+ with httpx.Client(base_url=chronos_url, timeout=60) as client:
135
+ response = launch_job.sync(client, request, x_api_key=api_key)
136
+
137
+ console.print("\n[green]✅ Job launched successfully![/green]")
138
+ console.print(f" Session ID: {response.session_id}")
139
+ console.print(f" Plato Session: {response.plato_session_id}")
140
+ console.print(f" Status: {response.status}")
141
+ console.print(f"\n[dim]View at: {chronos_url}/sessions/{response.session_id}[/dim]")
142
+
143
+ if wait:
144
+ console.print("\n[yellow]--wait not yet implemented[/yellow]")
145
+
146
+ except Exception as e:
147
+ console.print(f"[red]❌ Failed to launch job: {e}[/red]")
148
+ raise typer.Exit(1)
149
+
150
+
151
+ @chronos_app.command()
152
+ def example(
153
+ world: str = typer.Argument(
154
+ "structured-execution",
155
+ help="World to generate example config for",
156
+ ),
157
+ output: Path = typer.Option(
158
+ None,
159
+ "--output",
160
+ "-o",
161
+ help="Output file path (prints to stdout if not specified)",
162
+ ),
163
+ ):
164
+ """
165
+ Generate an example job config file.
166
+
167
+ Examples:
168
+ plato chronos example
169
+ plato chronos example structured-execution -o config.json
170
+ """
171
+ examples = {
172
+ "structured-execution": {
173
+ "world_package": "plato-world-structured-execution",
174
+ "world_version": "0.1.13",
175
+ "world_config": {
176
+ "sim_name": "my-sim",
177
+ "github_url": "https://github.com/example/repo",
178
+ "max_attempts": 3,
179
+ "use_backtrack": True,
180
+ "skill_runner": {
181
+ "image": "claude-code:2.1.6",
182
+ "config": {"model_name": "anthropic/claude-sonnet-4-20250514", "max_turns": 100},
183
+ },
184
+ },
185
+ "agent_configs": {
186
+ "skill_runner": {
187
+ "agent": "claude-code",
188
+ "version": "2.1.6",
189
+ "config": {"model_name": "anthropic/claude-sonnet-4-20250514", "max_turns": 100},
190
+ }
191
+ },
192
+ "secret_ids": [],
193
+ "_comment": "Add secret IDs from Chronos. Secrets should include: plato_api_key, anthropic_api_key (or claude_oauth_credentials for the agent)",
194
+ },
195
+ "code-world": {
196
+ "world_package": "plato-world-code",
197
+ "world_config": {"task": "Fix the bug in src/main.py", "repo_url": "https://github.com/example/repo"},
198
+ "agent_configs": {
199
+ "coder": {"agent": "claude-code", "config": {"model_name": "anthropic/claude-sonnet-4-20250514"}}
200
+ },
201
+ "secret_ids": [],
202
+ "_comment": "version is optional - uses latest if not specified",
203
+ },
204
+ }
205
+
206
+ if world not in examples:
207
+ console.print(f"[red]❌ Unknown world: {world}[/red]")
208
+ console.print(f"Available examples: {list(examples.keys())}")
209
+ raise typer.Exit(1)
210
+
211
+ example_config = examples[world]
212
+ json_output = json.dumps(example_config, indent=2)
213
+
214
+ if output:
215
+ with open(output, "w") as f:
216
+ f.write(json_output)
217
+ console.print(f"[green]✅ Example config written to {output}[/green]")
218
+ else:
219
+ console.print(json_output)
plato/v1/cli/main.py CHANGED
@@ -9,6 +9,7 @@ import typer
9
9
  from dotenv import load_dotenv
10
10
 
11
11
  from plato.v1.cli.agent import agent_app
12
+ from plato.v1.cli.chronos import chronos_app
12
13
  from plato.v1.cli.pm import pm_app
13
14
  from plato.v1.cli.sandbox import sandbox_app
14
15
  from plato.v1.cli.utils import console
@@ -71,6 +72,7 @@ app.add_typer(sandbox_app, name="sandbox")
71
72
  app.add_typer(pm_app, name="pm")
72
73
  app.add_typer(agent_app, name="agent")
73
74
  app.add_typer(world_app, name="world")
75
+ app.add_typer(chronos_app, name="chronos")
74
76
 
75
77
 
76
78
  # =============================================================================
plato/v1/cli/pm.py CHANGED
@@ -753,16 +753,16 @@ def review_data(
753
753
  is_installed = "site-packages" in str(package_dir)
754
754
 
755
755
  if is_installed:
756
- extension_source_path = package_dir / "extensions" / "envgen-recorder"
756
+ extension_source_path = package_dir / "extensions" / "envgen-recorder-old"
757
757
  else:
758
758
  repo_root = package_dir.parent.parent.parent # plato-client/
759
- extension_source_path = repo_root / "extensions" / "envgen-recorder"
759
+ extension_source_path = repo_root / "extensions" / "envgen-recorder-old"
760
760
 
761
761
  # Fallback to env var
762
762
  if not extension_source_path.exists():
763
763
  plato_client_dir_env = os.getenv("PLATO_CLIENT_DIR")
764
764
  if plato_client_dir_env:
765
- env_path = Path(plato_client_dir_env) / "extensions" / "envgen-recorder"
765
+ env_path = Path(plato_client_dir_env) / "extensions" / "envgen-recorder-old"
766
766
  if env_path.exists():
767
767
  extension_source_path = env_path
768
768
 
plato/v1/cli/sandbox.py CHANGED
@@ -1819,8 +1819,6 @@ def sandbox_clear_audit(
1819
1819
 
1820
1820
  for name, db_config in db_listeners:
1821
1821
  db_type = db_config.get("db_type", "postgresql").lower()
1822
- db_host = db_config.get("db_host", "127.0.0.1")
1823
- db_port = db_config.get("db_port", 5432 if db_type == "postgresql" else 3306)
1824
1822
  db_user = db_config.get("db_user", "postgres" if db_type == "postgresql" else "root")
1825
1823
  db_password = db_config.get("db_password", "")
1826
1824
  db_database = db_config.get("db_database", "postgres")
@@ -1829,10 +1827,15 @@ def sandbox_clear_audit(
1829
1827
  console.print(f"[cyan]Clearing audit_log for listener '{name}' ({db_type})...[/cyan]")
1830
1828
 
1831
1829
  # Build SQL command based on db_type
1830
+ # Use docker exec since psql/mysql aren't installed on the VM directly
1832
1831
  if db_type == "postgresql":
1833
- sql_cmd = f"PGPASSWORD='{db_password}' psql -h {db_host} -p {db_port} -U {db_user} -d {db_database} -c 'TRUNCATE TABLE audit_log RESTART IDENTITY CASCADE'"
1832
+ # Find the postgres container and truncate all audit_log tables across all schemas
1833
+ # Use $body$ delimiter instead of $$ to avoid shell expansion
1834
+ truncate_sql = "DO \\$body\\$ DECLARE r RECORD; BEGIN FOR r IN SELECT schemaname FROM pg_tables WHERE tablename = 'audit_log' LOOP EXECUTE format('TRUNCATE TABLE %I.audit_log RESTART IDENTITY CASCADE', r.schemaname); END LOOP; END \\$body\\$;"
1835
+ sql_cmd = f"CONTAINER=$(docker ps --format '{{{{.Names}}}}\\t{{{{.Image}}}}' | grep -i postgres | head -1 | cut -f1) && docker exec $CONTAINER psql -U {db_user} -d {db_database} -c \"{truncate_sql}\""
1834
1836
  elif db_type in ("mysql", "mariadb"):
1835
- sql_cmd = f"mysql -h {db_host} -P {db_port} -u {db_user} -p'{db_password}' {db_database} -e 'SET FOREIGN_KEY_CHECKS=0; DELETE FROM audit_log; SET FOREIGN_KEY_CHECKS=1;'"
1837
+ # Find the mysql/mariadb container and exec into it
1838
+ sql_cmd = f"CONTAINER=$(docker ps --format '{{{{.Names}}}}\\t{{{{.Image}}}}' | grep -iE 'mysql|mariadb' | head -1 | cut -f1) && docker exec $CONTAINER mysql -u {db_user} -p'{db_password}' {db_database} -e 'SET FOREIGN_KEY_CHECKS=0; DELETE FROM audit_log; SET FOREIGN_KEY_CHECKS=1;'"
1836
1839
  else:
1837
1840
  if not json_output:
1838
1841
  console.print(f"[yellow]⚠ Unsupported db_type '{db_type}' for listener '{name}'[/yellow]")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plato-sdk-v2
3
- Version: 2.3.8
3
+ Version: 2.3.11
4
4
  Summary: Python SDK for the Plato API
5
5
  Author-email: Plato <support@plato.so>
6
6
  License-Expression: MIT
@@ -303,7 +303,7 @@ plato/agents/base.py,sha256=vUbPQuNSo6Ka2lIB_ZOXgi4EoAjtAD7GIj9LnNotam0,4577
303
303
  plato/agents/build.py,sha256=CNMbVQFs2_pYit1dA29Davve28Yi4c7TNK9wBB7odrE,1621
304
304
  plato/agents/config.py,sha256=CmRS6vOAg7JeqX4Hgp_KpA1YWBX_LuMicHm7SBjQEbs,5077
305
305
  plato/agents/otel.py,sha256=UOEBeMyyfbffsC3tRXlHAydoj9bXwJupA_UeLZ5h97w,8585
306
- plato/agents/runner.py,sha256=Ei20Ib-Fn5XOaS6V1Rtw0UEw34XflEWaXMpazPjmnrE,6061
306
+ plato/agents/runner.py,sha256=ZzEK-O0tCZMBb-y-d2Y8f0BkfPgC-vKkZVOnHfZd7e8,6560
307
307
  plato/agents/trajectory.py,sha256=WdiBmua0KvCrNaM3qgPI7-7B4xmSkfbP4oZ_9_8qHzU,10529
308
308
  plato/chronos/__init__.py,sha256=RHMvSrQS_-vkKOyTRuAkp2gKDP1HEuBLDnw8jcZs1Jg,739
309
309
  plato/chronos/client.py,sha256=YcOGtHWERyOD9z8LKt8bRMVL0cEwL2hiAP4qQgdZlUI,5495
@@ -367,7 +367,7 @@ plato/chronos/api/worlds/create_world.py,sha256=H6yl5QIazNXgryOR5rvscSIMf8Y9kjc6
367
367
  plato/chronos/api/worlds/delete_world.py,sha256=UETu3Zk0e2VkDdAyMilv1ev-0g_j-oujH1Dc8DBqQOc,1239
368
368
  plato/chronos/api/worlds/get_world.py,sha256=eHTM1U5JiNTaZwYLh7x4QVBoRQeI5kaJ9o6xSi4-nos,1356
369
369
  plato/chronos/api/worlds/list_worlds.py,sha256=hBAuGb69tlasyn-kV_LNr9x6Rr7SHhST5hXJn1uqMf8,1253
370
- plato/chronos/models/__init__.py,sha256=5Hil8v_jFX1YU6LpOfqyJM4WV867Ckv6CX052Q4SCso,20996
370
+ plato/chronos/models/__init__.py,sha256=t9Kn9qwMBm2S9qs9weF0-CBCg4o1-u_W_3kDFQb-aDU,21328
371
371
  plato/sims/README.md,sha256=FIbJhNVNAV-SO6dq_cXX3Rg0C7HdQCfEY9YxGlkCmsM,6902
372
372
  plato/sims/__init__.py,sha256=tnoCGKZwNx6h22tEWLujdpLv6K4PpFU2RnDOhL1o-Uc,1494
373
373
  plato/sims/agent_helpers.py,sha256=kITvQywoTCS8mGhro3jZWuPJHDlje-UZujhjoahqhd0,10291
@@ -387,9 +387,10 @@ plato/v1/sync_flow_executor.py,sha256=kgvNYOtA9FHeNfP7qb8ZPUIlTsfIss_Z98W8uX5vec
387
387
  plato/v1/sync_sdk.py,sha256=2sedg1QJiSxr1I3kCyfaLAnlAgHlbblc3QQP_47O30k,25697
388
388
  plato/v1/cli/__init__.py,sha256=om4b7PxgsoI7rEwuQelmQkqPdhMVn53_5qEN8kvksYw,105
389
389
  plato/v1/cli/agent.py,sha256=G6TV3blG_BqMDBWS-CG7GwzqoqcJTMsIKQ88jvLXb4k,43745
390
- plato/v1/cli/main.py,sha256=ktPtBvMwykR7AjXmTQ6bmZkHdzpAjhX5Fq66cDbGSzA,6844
391
- plato/v1/cli/pm.py,sha256=uLM6WszKqxq9Czg1FraDyWb9_INUuHZq63imvRYfRLw,49734
392
- plato/v1/cli/sandbox.py,sha256=N7DIpXsxExtZB47tWKxp-3cV0_gLnI7C-mTKW3tTi8Y,95360
390
+ plato/v1/cli/chronos.py,sha256=QfVLbnFxhJcVdLNvaJkIyAlb-QgLlUdgvH3P4MfgJT8,7335
391
+ plato/v1/cli/main.py,sha256=iKUz6Mu-4-dgr29qOUmDqBaumOCzNQKZsHAalVtaH0Q,6932
392
+ plato/v1/cli/pm.py,sha256=TIvXBIWFDjr4s1girMMCuvHWQJkjpmsS-igAamddIWE,49746
393
+ plato/v1/cli/sandbox.py,sha256=22WbT66hFlpGIpN9V8NlNdncKpd098hY5WJxicTnggo,95944
393
394
  plato/v1/cli/ssh.py,sha256=enrf7Y01ZeRIyHDEX0Yt7up5zEe7MCvE9u8SP4Oqiz4,6926
394
395
  plato/v1/cli/utils.py,sha256=ba7Crv4OjDmgCv4SeB8UeZDin-iOdQw_3N6fd-g5XVk,4572
395
396
  plato/v1/cli/verify.py,sha256=7QmQwfOOkr8a51f8xfVIr2zif7wGl2E8HOZTbOaIoV0,20671
@@ -462,7 +463,7 @@ plato/worlds/base.py,sha256=kIX02gMQR43ZsZK25vZvCoNkYtICVRMeofNk-im5IRM,28215
462
463
  plato/worlds/build_hook.py,sha256=KSoW0kqa5b7NyZ7MYOw2qsZ_2FkWuz0M3Ru7AKOP7Qw,3486
463
464
  plato/worlds/config.py,sha256=a5frj3mt06rSlT25kE-L8Q2b2MTWkR-8cUoBKpC8tG4,11036
464
465
  plato/worlds/runner.py,sha256=2H5EV77bTYrMyI7qez0kwxOp9EApQxG19Ob9a_GTdbw,19383
465
- plato_sdk_v2-2.3.8.dist-info/METADATA,sha256=GnoZAAlPmFfWxSwHFjkab7s6MmWfk8rXOvOynTb2r28,8652
466
- plato_sdk_v2-2.3.8.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
467
- plato_sdk_v2-2.3.8.dist-info/entry_points.txt,sha256=upGMbJCx6YWUTKrPoYvYUYfFCqYr75nHDwhA-45m6p8,136
468
- plato_sdk_v2-2.3.8.dist-info/RECORD,,
466
+ plato_sdk_v2-2.3.11.dist-info/METADATA,sha256=31I2bshYErJviipSAqW8gMKPoOk3z2Xef7ULUO7rSds,8653
467
+ plato_sdk_v2-2.3.11.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
468
+ plato_sdk_v2-2.3.11.dist-info/entry_points.txt,sha256=upGMbJCx6YWUTKrPoYvYUYfFCqYr75nHDwhA-45m6p8,136
469
+ plato_sdk_v2-2.3.11.dist-info/RECORD,,