agent-orchestration-process 0.2.0__tar.gz → 0.2.1__tar.gz

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 (28) hide show
  1. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/PKG-INFO +1 -1
  2. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/pyproject.toml +1 -1
  3. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/pyproject.toml.orig +1 -1
  4. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/cli.py +40 -8
  5. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/isolation.py +1 -1
  6. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/models.py +16 -0
  7. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/prompt_transport.py +25 -0
  8. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/runner.py +41 -2
  9. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/LICENSE +0 -0
  10. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/README.md +0 -0
  11. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/__init__.py +0 -0
  12. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/__main__.py +0 -0
  13. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/batch.py +0 -0
  14. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/codex_auth.py +0 -0
  15. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/codex_routes.py +0 -0
  16. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/dsh_runner.mjs +0 -0
  17. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/egress.py +0 -0
  18. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/egress_bridge.py +0 -0
  19. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/integration.py +0 -0
  20. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/locks.py +0 -0
  21. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/migration.py +0 -0
  22. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/model_catalog.py +0 -0
  23. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/model_listing.py +0 -0
  24. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/pricing.py +0 -0
  25. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/provider_versions.py +0 -0
  26. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/worktrees.py +0 -0
  27. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/zcode.py +0 -0
  28. {agent_orchestration_process-0.2.0 → agent_orchestration_process-0.2.1}/src/agent_orchestration_process/zcode_protocol.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-orchestration-process
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Run bounded, non-interactive jobs across agent harnesses with isolation, exact resume, and provenance
5
5
  Keywords: agent-orchestration,ai-agents,autoresearch,coding-agents,git-worktrees
6
6
  Author: AOP contributors
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "agent-orchestration-process"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Run bounded, non-interactive jobs across agent harnesses with isolation, exact resume, and provenance"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "agent-orchestration-process"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Run bounded, non-interactive jobs across agent harnesses with isolation, exact resume, and provenance"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -22,7 +22,7 @@ from .isolation import PROFILES, explain_profile, resolve_policy
22
22
  from .locks import exclusive_lock, task_lock_path
23
23
  from .model_catalog import ModelCatalog, ensure_catalog_fresh
24
24
  from .model_listing import AGENTS, AvailableModel, list_models
25
- from .models import RunRequest, RunResult
25
+ from .models import PromptPart, RunRequest, RunResult
26
26
  from .runner import AgentRunner, RunStore, adapter_for, provider_runtime_record
27
27
  from .migration import migrate_runs, migration_roots, print_counts
28
28
  from .worktrees import (
@@ -409,9 +409,11 @@ def main(argv: Sequence[str] | None = None) -> int:
409
409
  prefix="aop: ",
410
410
  stream=sys.stderr,
411
411
  )
412
+ prompt, prompt_parts = _read_prompt(args)
412
413
  result = AgentRunner(manager, adapter_for(args.agent)).run(
413
414
  task=task,
414
- prompt=_read_prompt(args),
415
+ prompt=prompt,
416
+ prompt_parts=prompt_parts,
415
417
  base=args.base,
416
418
  model=args.model,
417
419
  inference_provider=args.inference_provider,
@@ -432,9 +434,11 @@ def main(argv: Sequence[str] | None = None) -> int:
432
434
  return _report_run(result, manager, json_output=args.json)
433
435
 
434
436
  if args.command == "resume":
437
+ prompt, prompt_parts = _read_prompt(args)
435
438
  result = AgentRunner(manager).resume(
436
439
  run_id=args.run_id,
437
- prompt=_read_prompt(args),
440
+ prompt=prompt,
441
+ prompt_parts=prompt_parts,
438
442
  timeout_seconds=args.timeout,
439
443
  artifacts=args.artifact,
440
444
  input_paths=args.input_paths,
@@ -472,6 +476,13 @@ def _add_prompt_arguments(parser: argparse.ArgumentParser) -> None:
472
476
  prompts = parser.add_mutually_exclusive_group(required=True)
473
477
  prompts.add_argument("--prompt")
474
478
  prompts.add_argument("--prompt-file", type=Path)
479
+ prompts.add_argument(
480
+ "--prompt-parts",
481
+ type=Path,
482
+ metavar="PATH",
483
+ help='JSON list of prompt blocks, each {"text": ..., "cache": true|false}; '
484
+ "Claude caches the prompt through the one cached block (claude only)",
485
+ )
475
486
 
476
487
 
477
488
  def _add_artifact_arguments(parser: argparse.ArgumentParser) -> None:
@@ -520,15 +531,36 @@ def _read_system_prompt(args: argparse.Namespace) -> str | None:
520
531
  ) from error
521
532
 
522
533
 
523
- def _read_prompt(args: argparse.Namespace) -> str:
534
+ def _read_prompt(args: argparse.Namespace) -> tuple[str, tuple[PromptPart, ...] | None]:
524
535
  if args.prompt is not None:
525
- return args.prompt
536
+ return args.prompt, None
537
+ path = args.prompt_file or args.prompt_parts
526
538
  try:
527
- return args.prompt_file.read_text()
539
+ text = path.read_text()
528
540
  except OSError as error:
541
+ raise AOPError(f"could not read prompt file {path}: {error}") from error
542
+ if args.prompt_file is not None:
543
+ return text, None
544
+ try:
545
+ blocks = json.loads(text)
546
+ except json.JSONDecodeError as error:
547
+ raise AOPError(f"prompt parts file {path} is not JSON: {error}") from error
548
+ if not isinstance(blocks, list) or not all(
549
+ isinstance(block, dict)
550
+ and set(block) <= {"text", "cache"}
551
+ and isinstance(block.get("text"), str)
552
+ and isinstance(block.get("cache", False), bool)
553
+ for block in blocks
554
+ ):
529
555
  raise AOPError(
530
- f"could not read prompt file {args.prompt_file}: {error}"
531
- ) from error
556
+ f"prompt parts file {path} must be a list of "
557
+ '{"text": string, "cache": boolean} objects'
558
+ )
559
+ parts = tuple(
560
+ PromptPart(chars=len(block["text"]), cache=block.get("cache", False))
561
+ for block in blocks
562
+ )
563
+ return "".join(block["text"] for block in blocks), parts
532
564
 
533
565
 
534
566
  def _report_policy(
@@ -157,7 +157,7 @@ def resolve_policy(
157
157
  host={
158
158
  "access": selected.host_access,
159
159
  "runtime_paths": (
160
- ["/usr", "/bin", "/lib", "/lib64", "/etc runtime files"]
160
+ ["/usr", "/bin", "/sbin", "/lib", "/lib64", "/etc runtime files"]
161
161
  if selected.host_access == "runtime-only"
162
162
  else ["/"]
163
163
  ),
@@ -57,6 +57,15 @@ class InferenceRoute:
57
57
  return cls(**fields)
58
58
 
59
59
 
60
+ @dataclass(frozen=True)
61
+ class PromptPart:
62
+ """One content block of a prompt; `cache` asks Claude to cache the prompt
63
+ up to and including this block. The text itself stays in RunRequest.prompt."""
64
+
65
+ chars: int
66
+ cache: bool = False
67
+
68
+
60
69
  @dataclass(frozen=True)
61
70
  class RunRequest:
62
71
  run_id: str
@@ -90,6 +99,9 @@ class RunRequest:
90
99
  allowed_urls: tuple[str, ...] = ()
91
100
  # A sealed run's private, initially empty /workspace is writable.
92
101
  writable_workspace: bool = False
102
+ # The prompt split into content blocks, in order; their lengths add up to
103
+ # len(prompt). None sends the prompt as one block.
104
+ prompt_parts: tuple[PromptPart, ...] | None = None
93
105
  # How `aop migrate-results` converted an older record, if it did.
94
106
  migration: dict[str, Any] | None = None
95
107
 
@@ -106,6 +118,10 @@ class RunRequest:
106
118
  if fields.get("tools") is not None:
107
119
  fields["tools"] = tuple(fields["tools"])
108
120
  fields["allowed_urls"] = tuple(fields.get("allowed_urls", ()))
121
+ if fields.get("prompt_parts") is not None:
122
+ fields["prompt_parts"] = tuple(
123
+ PromptPart(**part) for part in fields["prompt_parts"]
124
+ )
109
125
  fields["inputs"] = tuple(
110
126
  InputSnapshot.from_dict(item) for item in fields["inputs"]
111
127
  )
@@ -1,11 +1,14 @@
1
1
  """Prompt transport independent of the OS argument and environment limits."""
2
2
 
3
+ from collections.abc import Sequence
3
4
  from contextlib import contextmanager
4
5
  import json
5
6
  import os
6
7
  from pathlib import Path
7
8
  import tempfile
8
9
 
10
+ from .models import PromptPart
11
+
9
12
 
10
13
  # Native protocols remain in their adapters. Runtime-file is only for CLIs
11
14
  # whose one-shot parser accepts text exclusively as an argument.
@@ -47,6 +50,28 @@ def stdin_prompt(provider: str, prompt: str) -> str | None:
47
50
  return None
48
51
 
49
52
 
53
+ def claude_message(prompt: str, parts: Sequence[PromptPart]) -> str:
54
+ """A stream-json user message whose content blocks follow `parts`.
55
+
56
+ Claude Code marks its own system blocks and the context it appends after
57
+ the prompt with one-hour cache marks, and a five-minute mark must not
58
+ precede a one-hour one, so a caller's mark uses one hour too.
59
+ """
60
+ content = []
61
+ start = 0
62
+ for part in parts:
63
+ block: dict[str, object] = {
64
+ "type": "text",
65
+ "text": prompt[start : start + part.chars],
66
+ }
67
+ if part.cache:
68
+ block["cache_control"] = {"type": "ephemeral", "ttl": "1h"}
69
+ content.append(block)
70
+ start += part.chars
71
+ message = {"type": "user", "message": {"role": "user", "content": content}}
72
+ return json.dumps(message) + "\n"
73
+
74
+
50
75
  @contextmanager
51
76
  def prompt_stream(prompt: str | None):
52
77
  """A seekable stdin avoids blocking on pipe writes before the timeout starts."""
@@ -39,6 +39,7 @@ from .models import (
39
39
  InputFile,
40
40
  InputSnapshot,
41
41
  InferenceRoute,
42
+ PromptPart,
42
43
  RunArtifact,
43
44
  RunRequest,
44
45
  RunResult,
@@ -1221,7 +1222,11 @@ class ClaudeAdapter:
1221
1222
  command,
1222
1223
  cwd=worktree.path,
1223
1224
  environment=environment,
1224
- prompt=prompt_transport.stdin_prompt(self.provider, request.prompt),
1225
+ prompt=(
1226
+ prompt_transport.claude_message(request.prompt, request.prompt_parts)
1227
+ if request.prompt_parts is not None
1228
+ else prompt_transport.stdin_prompt(self.provider, request.prompt)
1229
+ ),
1225
1230
  timeout_seconds=request.timeout_seconds,
1226
1231
  is_response=self._is_response,
1227
1232
  )
@@ -1396,6 +1401,8 @@ class ClaudeAdapter:
1396
1401
  command.extend(["--system-prompt", request.system_prompt])
1397
1402
  if request.thinking_display is not None:
1398
1403
  command.extend(["--thinking-display", request.thinking_display])
1404
+ if request.prompt_parts is not None:
1405
+ command.extend(["--input-format", "stream-json"])
1399
1406
  if request.session_id:
1400
1407
  command.extend(["--resume", request.session_id])
1401
1408
  else:
@@ -4205,7 +4212,7 @@ def _isolated_root_command(bwrap: str) -> list[str]:
4205
4212
  "--tmpfs",
4206
4213
  "/tmp",
4207
4214
  ]
4208
- for path in ("/usr", "/bin", "/lib", "/lib64"):
4215
+ for path in ("/usr", "/bin", "/sbin", "/lib", "/lib64"):
4209
4216
  source = Path(path)
4210
4217
  if source.exists():
4211
4218
  command.extend(["--ro-bind", path, path])
@@ -4214,6 +4221,9 @@ def _isolated_root_command(bwrap: str) -> list[str]:
4214
4221
  "/etc/ca-certificates",
4215
4222
  "/etc/crypto-policies",
4216
4223
  "/etc/hosts",
4224
+ # The dynamic linker finds libraries in folders registered under
4225
+ # /etc/ld.so.conf.d only through this cache.
4226
+ "/etc/ld.so.cache",
4217
4227
  "/etc/localtime",
4218
4228
  "/etc/nsswitch.conf",
4219
4229
  "/etc/pki",
@@ -5775,8 +5785,10 @@ class AgentRunner:
5775
5785
  allowed_urls: Sequence[str] = (),
5776
5786
  writable_workspace: bool = False,
5777
5787
  thinking_display: str | None = None,
5788
+ prompt_parts: Sequence[PromptPart] | None = None,
5778
5789
  ) -> RunResult:
5779
5790
  self._validate_mode(mode)
5791
+ prompt_parts = self._validated_prompt_parts(prompt, prompt_parts)
5780
5792
  if writable_workspace and profile != "sealed":
5781
5793
  raise AOPError("--writable-workspace applies only to the sealed profile")
5782
5794
  if thinking_display is not None:
@@ -5822,6 +5834,7 @@ class AgentRunner:
5822
5834
  allowed_urls=allowed_urls,
5823
5835
  writable_workspace=writable_workspace,
5824
5836
  thinking_display=thinking_display,
5837
+ prompt_parts=prompt_parts,
5825
5838
  )
5826
5839
  worktree = (
5827
5840
  self._create_sealed_workspace(request.run_id)
@@ -5854,6 +5867,7 @@ class AgentRunner:
5854
5867
  timeout_seconds: float | None = None,
5855
5868
  artifacts: Sequence[str] = (),
5856
5869
  input_paths: Sequence[str | os.PathLike[str]] | None = None,
5870
+ prompt_parts: Sequence[PromptPart] | None = None,
5857
5871
  _task_lock_held: bool = False,
5858
5872
  ) -> RunResult:
5859
5873
  parent_request = self.store.load_request(run_id)
@@ -5865,6 +5879,7 @@ class AgentRunner:
5865
5879
  )
5866
5880
  self.adapter = adapter_for(parent_request.provider)
5867
5881
  self._validate_mode(parent_request.mode)
5882
+ prompt_parts = self._validated_prompt_parts(prompt, prompt_parts)
5868
5883
  self._validate_no_web(
5869
5884
  parent_request.no_web, parent_request.mode, parent_request.profile
5870
5885
  )
@@ -5952,6 +5967,7 @@ class AgentRunner:
5952
5967
  allowed_urls=parent_request.allowed_urls,
5953
5968
  writable_workspace=parent_request.writable_workspace,
5954
5969
  thinking_display=parent_request.thinking_display,
5970
+ prompt_parts=prompt_parts,
5955
5971
  )
5956
5972
  return self._execute(
5957
5973
  request,
@@ -6308,6 +6324,7 @@ class AgentRunner:
6308
6324
  allowed_urls: tuple[str, ...] = (),
6309
6325
  writable_workspace: bool = False,
6310
6326
  thinking_display: str | None = None,
6327
+ prompt_parts: tuple[PromptPart, ...] | None = None,
6311
6328
  ) -> RunRequest:
6312
6329
  return RunRequest(
6313
6330
  run_id=str(uuid.uuid4()),
@@ -6335,6 +6352,7 @@ class AgentRunner:
6335
6352
  allowed_urls=allowed_urls,
6336
6353
  writable_workspace=writable_workspace,
6337
6354
  thinking_display=thinking_display,
6355
+ prompt_parts=prompt_parts,
6338
6356
  )
6339
6357
 
6340
6358
  def _validate_provider_base_url(self, url: str | None, profile: str) -> None:
@@ -6356,6 +6374,27 @@ class AgentRunner:
6356
6374
  if parsed.scheme not in {"http", "https"} or not parsed.hostname:
6357
6375
  raise AOPError(f"--provider-base-url must be an http(s) URL: {url!r}")
6358
6376
 
6377
+ def _validated_prompt_parts(
6378
+ self, prompt: str, parts: Sequence[PromptPart] | None
6379
+ ) -> tuple[PromptPart, ...] | None:
6380
+ if parts is None:
6381
+ return None
6382
+ if self.adapter.provider != "claude":
6383
+ raise AOPError(f"{self.adapter.provider} does not support --prompt-parts")
6384
+ parts = tuple(parts)
6385
+ if not parts or any(part.chars <= 0 for part in parts):
6386
+ raise AOPError("--prompt-parts needs at least one part, none of them empty")
6387
+ if sum(part.chars for part in parts) != len(prompt):
6388
+ raise AOPError("prompt parts do not add up to the prompt")
6389
+ # Anthropic accepts four cache marks per request, and Claude Code
6390
+ # places three: two on its system prompt and one on the context it
6391
+ # appends after the prompt.
6392
+ if sum(part.cache for part in parts) > 1:
6393
+ raise AOPError(
6394
+ "Claude allows one cached prompt part; Claude Code uses the other marks"
6395
+ )
6396
+ return parts
6397
+
6359
6398
  def _validated_framing(
6360
6399
  self, system_prompt: str | None, tools: Sequence[str] | None, no_web: bool
6361
6400
  ) -> tuple[str, ...] | None: