orcho-core 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.
- agents/__init__.py +41 -0
- agents/command_guard.py +336 -0
- agents/entities.py +70 -0
- agents/protocols.py +135 -0
- agents/pty_diagnostics.py +43 -0
- agents/registry.py +267 -0
- agents/runtimes/__init__.py +32 -0
- agents/runtimes/_failures.py +232 -0
- agents/runtimes/_strategy.py +1952 -0
- agents/runtimes/auth.py +180 -0
- agents/runtimes/claude.py +789 -0
- agents/runtimes/codex.py +790 -0
- agents/runtimes/codex_telemetry.py +225 -0
- agents/runtimes/gemini.py +494 -0
- agents/runtimes/identity.py +125 -0
- agents/stall_protocol.py +223 -0
- agents/stream.py +759 -0
- agents/stream_parsers/__init__.py +47 -0
- agents/stream_parsers/claude_jsonl.py +273 -0
- agents/stream_parsers/codex_jsonl.py +239 -0
- agents/stream_parsers/gemini_jsonl.py +226 -0
- agents/stream_parsers/skill_registry.py +94 -0
- agents/stream_parsers/tool_invocations.py +411 -0
- agents/stream_stall.py +131 -0
- cli/_formatters.py +1410 -0
- cli/_help.py +193 -0
- cli/_profile_menu.py +310 -0
- cli/_profile_prompt.py +371 -0
- cli/_repair_state.py +116 -0
- cli/_run.py +30 -0
- cli/_task_prompt.py +61 -0
- cli/install_guard.py +138 -0
- cli/orcho.py +1693 -0
- core/__init__.py +16 -0
- core/_config/config.defaults.json +121 -0
- core/_config/pipeline_profiles_v2.json +596 -0
- core/_config/pricing.openai.snapshot.json +9 -0
- core/_prompts/AGENTS.md +64 -0
- core/_prompts/CLAUDE.md +1 -0
- core/_prompts/README.md +192 -0
- core/_prompts/formats/bullets.md +2 -0
- core/_prompts/formats/compact.md +4 -0
- core/_prompts/formats/detailed.md +3 -0
- core/_prompts/formats/handoff.md +2 -0
- core/_prompts/formats/terse.md +2 -0
- core/_prompts/roles/code_reviewer.md +13 -0
- core/_prompts/roles/implementation_engineer.md +11 -0
- core/_prompts/roles/plan_reviewer.md +21 -0
- core/_prompts/roles/product_owner.md +11 -0
- core/_prompts/roles/release_manager.md +14 -0
- core/_prompts/roles/systems_architect.md +22 -0
- core/_prompts/tasks/code_review.md +17 -0
- core/_prompts/tasks/commit_message.md +30 -0
- core/_prompts/tasks/correction_triage.md +25 -0
- core/_prompts/tasks/cross_contract_bundle.md +11 -0
- core/_prompts/tasks/cross_final_acceptance.md +40 -0
- core/_prompts/tasks/cross_plan.md +28 -0
- core/_prompts/tasks/cross_replan.md +21 -0
- core/_prompts/tasks/cross_validate_plan.md +31 -0
- core/_prompts/tasks/decompose.md +21 -0
- core/_prompts/tasks/final_acceptance.md +59 -0
- core/_prompts/tasks/handoff_advice.md +32 -0
- core/_prompts/tasks/hypothesis.md +10 -0
- core/_prompts/tasks/implement.md +21 -0
- core/_prompts/tasks/plan.md +24 -0
- core/_prompts/tasks/readonly_plan.md +2 -0
- core/_prompts/tasks/repair_changes.md +20 -0
- core/_prompts/tasks/replan.md +18 -0
- core/_prompts/tasks/review_uncommitted.md +8 -0
- core/_prompts/tasks/validate_hypothesis.md +13 -0
- core/_prompts/tasks/validate_plan.md +25 -0
- core/context/__init__.py +330 -0
- core/contracts/__init__.py +0 -0
- core/contracts/commit_decision_schema.py +803 -0
- core/contracts/cross_plan_schema.py +259 -0
- core/contracts/plan_schema.py +249 -0
- core/contracts/release_schema.py +283 -0
- core/contracts/review_schema.py +153 -0
- core/contracts/subtask_attestation_schema.py +141 -0
- core/infra/__init__.py +20 -0
- core/infra/config.py +859 -0
- core/infra/lazy.py +43 -0
- core/infra/paths.py +72 -0
- core/infra/platform.py +223 -0
- core/io/__init__.py +5 -0
- core/io/ansi.py +165 -0
- core/io/git_helpers.py +364 -0
- core/io/journey_prompt.py +79 -0
- core/io/output_elision.py +200 -0
- core/io/pipeline_block.py +362 -0
- core/io/prompt_loader.py +246 -0
- core/io/retry.py +784 -0
- core/io/stdout_render.py +281 -0
- core/io/terminal_input.py +86 -0
- core/io/transcript.py +1584 -0
- core/io/verification_header.py +471 -0
- core/observability/__init__.py +4 -0
- core/observability/event_hub.py +535 -0
- core/observability/event_kinds.py +387 -0
- core/observability/events.py +504 -0
- core/observability/live_card.py +399 -0
- core/observability/logging.py +254 -0
- core/observability/metrics.py +1321 -0
- core/observability/phases.py +39 -0
- core/observability/pricing.py +273 -0
- core/observability/pricing_scrapers.py +481 -0
- core/observability/prompt_trace.py +105 -0
- core/observability/trace.py +100 -0
- orcho_core-0.1.0.dist-info/METADATA +282 -0
- orcho_core-0.1.0.dist-info/RECORD +403 -0
- orcho_core-0.1.0.dist-info/WHEEL +5 -0
- orcho_core-0.1.0.dist-info/entry_points.txt +19 -0
- orcho_core-0.1.0.dist-info/licenses/LICENSE +201 -0
- orcho_core-0.1.0.dist-info/top_level.txt +5 -0
- pipeline/__init__.py +0 -0
- pipeline/agent_resolver.py +136 -0
- pipeline/allowed_modifications.py +58 -0
- pipeline/argv.py +214 -0
- pipeline/artifacts/__init__.py +29 -0
- pipeline/artifacts/types.py +125 -0
- pipeline/attachment_inject.py +101 -0
- pipeline/attachment_loader.py +149 -0
- pipeline/checkpoint.py +281 -0
- pipeline/commit_message_parser.py +174 -0
- pipeline/control/__init__.py +155 -0
- pipeline/control/from_run_plan.py +185 -0
- pipeline/control/handoff_banners.py +254 -0
- pipeline/control/handoff_decisions.py +168 -0
- pipeline/control/handoff_labels.py +119 -0
- pipeline/control/handoff_prompt.py +868 -0
- pipeline/control/implement_handoff_digest.py +232 -0
- pipeline/control/operator_decisions.py +152 -0
- pipeline/control/resume_context.py +1015 -0
- pipeline/control/resume_preflight.py +242 -0
- pipeline/control/resume_prompt.py +361 -0
- pipeline/control/reviewed_loop.py +191 -0
- pipeline/cross_project/__init__.py +82 -0
- pipeline/cross_project/agent_setup.py +350 -0
- pipeline/cross_project/app.py +72 -0
- pipeline/cross_project/app_types.py +183 -0
- pipeline/cross_project/artifact_bundle.py +391 -0
- pipeline/cross_project/cfa_gate.py +656 -0
- pipeline/cross_project/checkpoint.py +88 -0
- pipeline/cross_project/cli.py +905 -0
- pipeline/cross_project/constants.py +27 -0
- pipeline/cross_project/contract_check.py +591 -0
- pipeline/cross_project/cross_delivery.py +430 -0
- pipeline/cross_project/final_acceptance.py +1498 -0
- pipeline/cross_project/finalization.py +639 -0
- pipeline/cross_project/gate_decisions.py +136 -0
- pipeline/cross_project/gate_entries.py +115 -0
- pipeline/cross_project/handoff.py +333 -0
- pipeline/cross_project/handoff_payloads.py +372 -0
- pipeline/cross_project/orchestrator.py +328 -0
- pipeline/cross_project/path_alias.py +133 -0
- pipeline/cross_project/plan_parser.py +338 -0
- pipeline/cross_project/planning_loop.py +1415 -0
- pipeline/cross_project/profile_projection.py +498 -0
- pipeline/cross_project/profile_setup.py +157 -0
- pipeline/cross_project/project_dispatch.py +538 -0
- pipeline/cross_project/prompts.py +348 -0
- pipeline/cross_project/rendering.py +299 -0
- pipeline/cross_project/run_setup.py +336 -0
- pipeline/cross_project/session_invoke.py +118 -0
- pipeline/cross_project/session_run.py +700 -0
- pipeline/cross_project/task_plan.py +98 -0
- pipeline/cross_project/terminal.py +92 -0
- pipeline/cross_project/types.py +317 -0
- pipeline/cross_project/usage.py +327 -0
- pipeline/dag_runner.py +1125 -0
- pipeline/engine/__init__.py +42 -0
- pipeline/engine/artifact_mirror.py +135 -0
- pipeline/engine/commit_delivery.py +1898 -0
- pipeline/engine/companion_scope.py +460 -0
- pipeline/engine/delivery_scope.py +534 -0
- pipeline/engine/diff_apply_check.py +476 -0
- pipeline/engine/hypothesis.py +457 -0
- pipeline/engine/pre_run_dirty.py +610 -0
- pipeline/engine/run_diff.py +674 -0
- pipeline/engine/run_logging.py +81 -0
- pipeline/engine/scope_expansion.py +624 -0
- pipeline/engine/session.py +57 -0
- pipeline/engine/worktree.py +836 -0
- pipeline/engine/worktree_bootstrap.py +425 -0
- pipeline/engine/worktree_source.py +204 -0
- pipeline/entry_points.py +179 -0
- pipeline/evidence/__init__.py +48 -0
- pipeline/evidence/bundle.py +159 -0
- pipeline/evidence/collector.py +1048 -0
- pipeline/evidence/prompt_render.py +136 -0
- pipeline/evidence/render_md.py +563 -0
- pipeline/evidence/schema.py +570 -0
- pipeline/evidence/verification_receipt.py +822 -0
- pipeline/json_contract.py +157 -0
- pipeline/lifecycle.py +651 -0
- pipeline/observability/__init__.py +10 -0
- pipeline/observability/context_clearing.py +406 -0
- pipeline/observability/context_growth.py +277 -0
- pipeline/observability/context_pressure.py +690 -0
- pipeline/observability/invocation_outcome.py +164 -0
- pipeline/observability/output_class.py +309 -0
- pipeline/observability/prompt_render.py +430 -0
- pipeline/observability/runtime_compaction.py +572 -0
- pipeline/participant_promotion.py +644 -0
- pipeline/participants.py +341 -0
- pipeline/phases/__init__.py +45 -0
- pipeline/phases/adapters.py +459 -0
- pipeline/phases/builtin/__init__.py +90 -0
- pipeline/phases/builtin/handlers/__init__.py +10 -0
- pipeline/phases/builtin/handlers/compliance_check.py +25 -0
- pipeline/phases/builtin/handlers/correction_triage.py +356 -0
- pipeline/phases/builtin/handlers/final_acceptance.py +396 -0
- pipeline/phases/builtin/handlers/implement.py +207 -0
- pipeline/phases/builtin/handlers/plan.py +491 -0
- pipeline/phases/builtin/handlers/repair_changes.py +367 -0
- pipeline/phases/builtin/handlers/review_changes.py +402 -0
- pipeline/phases/builtin/handlers/validate_plan.py +235 -0
- pipeline/phases/builtin/lifecycle.py +165 -0
- pipeline/phases/builtin/plan_artifact.py +375 -0
- pipeline/phases/builtin/prompt_parts.py +236 -0
- pipeline/phases/builtin/registry.py +103 -0
- pipeline/phases/builtin/review_support.py +661 -0
- pipeline/phases/builtin/scope_expansion_support.py +565 -0
- pipeline/phases/builtin/session_invoke.py +469 -0
- pipeline/phases/builtin/session_keys.py +504 -0
- pipeline/phases/builtin/session_observability.py +344 -0
- pipeline/phases/builtin/subtask_dag.py +944 -0
- pipeline/phases/builtin/subtask_dag_handoff.py +402 -0
- pipeline/phases/review_contract_recovery.py +102 -0
- pipeline/plan_artifacts.py +441 -0
- pipeline/plan_contract.py +100 -0
- pipeline/plan_markdown.py +137 -0
- pipeline/plan_parser.py +504 -0
- pipeline/plugins.py +485 -0
- pipeline/presentation.py +52 -0
- pipeline/profiles/__init__.py +31 -0
- pipeline/profiles/loader.py +1031 -0
- pipeline/profiles/session_split_override.py +93 -0
- pipeline/project/__init__.py +15 -0
- pipeline/project/app.py +234 -0
- pipeline/project/auto_detect.py +586 -0
- pipeline/project/bootstrap.py +644 -0
- pipeline/project/cli.py +1417 -0
- pipeline/project/constants.py +20 -0
- pipeline/project/correction_fixed_point.py +242 -0
- pipeline/project/correction_followup.py +554 -0
- pipeline/project/correction_gate_rerun.py +184 -0
- pipeline/project/correction_route.py +131 -0
- pipeline/project/correction_route_display.py +209 -0
- pipeline/project/finalization.py +2386 -0
- pipeline/project/followup_worktree.py +469 -0
- pipeline/project/gate_repair.py +909 -0
- pipeline/project/handoff.py +2047 -0
- pipeline/project/handoff_advice.py +713 -0
- pipeline/project/handoff_advice_artifact.py +165 -0
- pipeline/project/handoff_advice_ci.py +147 -0
- pipeline/project/handoff_advice_dispatch.py +166 -0
- pipeline/project/handoff_advice_evidence.py +799 -0
- pipeline/project/handoff_advice_policy.py +251 -0
- pipeline/project/handoff_waiver.py +110 -0
- pipeline/project/isolation_setup.py +589 -0
- pipeline/project/phase_config.py +114 -0
- pipeline/project/profile_dispatch.py +970 -0
- pipeline/project/profile_setup.py +451 -0
- pipeline/project/project_aliases.py +116 -0
- pipeline/project/project_discovery_prompt.py +231 -0
- pipeline/project/provider_recovery.py +141 -0
- pipeline/project/resume_artifacts.py +301 -0
- pipeline/project/resume_worktree.py +253 -0
- pipeline/project/retry_subject.py +146 -0
- pipeline/project/run.py +1486 -0
- pipeline/project/run_setup.py +574 -0
- pipeline/project/runtime_setup.py +438 -0
- pipeline/project/session_run.py +562 -0
- pipeline/project/state_setup.py +542 -0
- pipeline/project/types.py +211 -0
- pipeline/project/verification_autorun.py +475 -0
- pipeline/project/verification_timeline.py +902 -0
- pipeline/project/workspace_picker.py +194 -0
- pipeline/project_orchestrator.py +36 -0
- pipeline/project_testing.py +177 -0
- pipeline/prompts/__init__.py +103 -0
- pipeline/prompts/builders.py +2083 -0
- pipeline/prompts/composer.py +372 -0
- pipeline/prompts/contract_templates.py +724 -0
- pipeline/prompts/contracts.py +713 -0
- pipeline/prompts/delta.py +375 -0
- pipeline/prompts/envelope.py +264 -0
- pipeline/prompts/minimal_intents.py +283 -0
- pipeline/prompts/modes.py +112 -0
- pipeline/prompts/session.py +249 -0
- pipeline/prompts/spec.py +86 -0
- pipeline/prompts/subtask.py +294 -0
- pipeline/prompts/turn.py +402 -0
- pipeline/prompts/types.py +276 -0
- pipeline/quality_gates.py +337 -0
- pipeline/release_markdown.py +130 -0
- pipeline/release_parser.py +239 -0
- pipeline/repair_protocol.py +297 -0
- pipeline/review_markdown.py +75 -0
- pipeline/review_parser.py +140 -0
- pipeline/run_state/__init__.py +139 -0
- pipeline/run_state/consistency.py +258 -0
- pipeline/run_state/cross.py +398 -0
- pipeline/run_state/cross_repair.py +238 -0
- pipeline/run_state/handoff.py +289 -0
- pipeline/run_state/phase_outcome.py +63 -0
- pipeline/run_state/projector.py +56 -0
- pipeline/run_state/provider_runtime.py +118 -0
- pipeline/run_state/reducer.py +236 -0
- pipeline/run_state/release_verdict.py +76 -0
- pipeline/run_state/repair.py +426 -0
- pipeline/run_state/setup_failure.py +390 -0
- pipeline/run_state/stalled_command.py +113 -0
- pipeline/run_state/status_vocab.py +56 -0
- pipeline/run_state/subtask_progress.py +81 -0
- pipeline/run_state/terminal.py +388 -0
- pipeline/run_state/terminal_outcome.py +653 -0
- pipeline/run_state/types.py +213 -0
- pipeline/runtime/__init__.py +166 -0
- pipeline/runtime/handoff.py +684 -0
- pipeline/runtime/profile.py +549 -0
- pipeline/runtime/results.py +63 -0
- pipeline/runtime/roles.py +343 -0
- pipeline/runtime/run_shape.py +440 -0
- pipeline/runtime/runner.py +1192 -0
- pipeline/runtime/scope_expansion_sanction.py +292 -0
- pipeline/runtime/semantic_mode_defaults.py +83 -0
- pipeline/runtime/session_disposition.py +149 -0
- pipeline/runtime/state.py +105 -0
- pipeline/runtime/steps.py +388 -0
- pipeline/runtime/topology_detection.py +212 -0
- pipeline/runtime/work_kind_detection.py +634 -0
- pipeline/sandbox/__init__.py +64 -0
- pipeline/sandbox/backends/__init__.py +7 -0
- pipeline/sandbox/backends/_env_filter.py +70 -0
- pipeline/sandbox/backends/env_unix.py +132 -0
- pipeline/sandbox/backends/env_windows.py +148 -0
- pipeline/sandbox/backends/null_backend.py +45 -0
- pipeline/sandbox/capabilities.py +76 -0
- pipeline/sandbox/context.py +59 -0
- pipeline/sandbox/defaults.py +128 -0
- pipeline/sandbox/launcher.py +129 -0
- pipeline/sandbox/masking.py +100 -0
- pipeline/sandbox/policy.py +168 -0
- pipeline/sandbox/resolver.py +255 -0
- pipeline/session_adapters.py +780 -0
- pipeline/skills/__init__.py +56 -0
- pipeline/skills/discover.py +296 -0
- pipeline/skills/inject.py +202 -0
- pipeline/skills/loader.py +531 -0
- pipeline/skills/migrate.py +351 -0
- pipeline/skills/types.py +113 -0
- pipeline/subtask_attestation_parser.py +154 -0
- pipeline/subtask_attestation_repair.py +123 -0
- pipeline/subtask_substance_repair.py +169 -0
- pipeline/verification_command.py +206 -0
- pipeline/verification_contract.py +1002 -0
- pipeline/verification_delivery.py +605 -0
- pipeline/verification_dependencies.py +202 -0
- pipeline/verification_env.py +353 -0
- pipeline/verification_policy.py +256 -0
- pipeline/verification_readiness.py +1405 -0
- pipeline/verification_receipt_index.py +178 -0
- pipeline/verification_selection.py +408 -0
- pipeline/verification_waiver.py +154 -0
- sdk/__init__.py +312 -0
- sdk/_jsonable.py +41 -0
- sdk/_time.py +48 -0
- sdk/actions.py +526 -0
- sdk/cost.py +239 -0
- sdk/errors.py +94 -0
- sdk/events.py +36 -0
- sdk/evidence.py +105 -0
- sdk/evidence_slices.py +1069 -0
- sdk/fine_tune.py +278 -0
- sdk/handoff_advice.py +339 -0
- sdk/history.py +57 -0
- sdk/metrics.py +79 -0
- sdk/phase_handoff.py +837 -0
- sdk/pricing.py +91 -0
- sdk/prompts.py +62 -0
- sdk/run_control/__init__.py +74 -0
- sdk/run_control/commands.py +91 -0
- sdk/run_control/delivery.py +944 -0
- sdk/run_control/diagnosis.py +511 -0
- sdk/run_control/events.py +82 -0
- sdk/run_control/recovery_lineage.py +703 -0
- sdk/run_control/recovery_lineage_resolve.py +69 -0
- sdk/run_control/runtime_override.py +194 -0
- sdk/run_control/service.py +472 -0
- sdk/run_control/snapshots.py +225 -0
- sdk/run_control/types.py +425 -0
- sdk/run_diff.py +320 -0
- sdk/runner.py +230 -0
- sdk/runs.py +196 -0
- sdk/runtimes.py +70 -0
- sdk/status.py +195 -0
- sdk/types.py +340 -0
- sdk/verification_timeline.py +542 -0
- sdk/verify.py +516 -0
- sdk/workspace.py +818 -0
- sdk/workspace_scaffold.py +190 -0
agents/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agents — Provider wrappers, Protocols, entities, and the agent registry.
|
|
3
|
+
|
|
4
|
+
Public surface (re-exported here so callers can ``from agents import X``):
|
|
5
|
+
|
|
6
|
+
Protocol IAgentRuntime, SessionMode
|
|
7
|
+
Entities SubTask, TestResult
|
|
8
|
+
Registry AgentRegistry, PhaseAgentConfig
|
|
9
|
+
Runtimes ClaudeAgent, CodexAgent
|
|
10
|
+
Streaming _stream_run, set_agent_log, set_stdout_echo
|
|
11
|
+
subprocess re-exported for tests that monkeypatch agents.subprocess
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import subprocess # re-exported so tests can `monkeypatch.setattr(agents.subprocess, ...)`
|
|
15
|
+
|
|
16
|
+
from agents.entities import SubTask, TestResult
|
|
17
|
+
from agents.protocols import IAgentRuntime, SessionMode
|
|
18
|
+
from agents.registry import AgentRegistry, PhaseAgentConfig
|
|
19
|
+
from agents.runtimes import ClaudeAgent, CodexAgent
|
|
20
|
+
from agents.stream import StreamAbort, _stream_run, set_agent_log, set_stdout_echo
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
# Protocol
|
|
24
|
+
"IAgentRuntime",
|
|
25
|
+
"SessionMode",
|
|
26
|
+
# Entities
|
|
27
|
+
"SubTask",
|
|
28
|
+
"TestResult",
|
|
29
|
+
# Registry
|
|
30
|
+
"AgentRegistry",
|
|
31
|
+
"PhaseAgentConfig",
|
|
32
|
+
# Providers
|
|
33
|
+
"ClaudeAgent",
|
|
34
|
+
"CodexAgent",
|
|
35
|
+
# Streaming
|
|
36
|
+
"_stream_run",
|
|
37
|
+
"StreamAbort",
|
|
38
|
+
"set_agent_log",
|
|
39
|
+
"set_stdout_echo",
|
|
40
|
+
"subprocess",
|
|
41
|
+
]
|
agents/command_guard.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Runtime guardrails for agent-issued shell commands.
|
|
2
|
+
|
|
3
|
+
The prompts tell write-capable agents not to discard user-owned work, but
|
|
4
|
+
prompts are advisory. This module is the runtime backstop for the specific
|
|
5
|
+
class of failure that live repeated-run testing exposed: destructive git
|
|
6
|
+
rollback commands issued through Claude's Bash tool.
|
|
7
|
+
|
|
8
|
+
A second, *diagnostic-only* guard flags unsafe free-text process polling
|
|
9
|
+
(``pgrep -f`` / ``pkill -f``). Unlike the destructive-git guard — which aborts
|
|
10
|
+
the call — the process-polling guard never aborts and never kills: it carries
|
|
11
|
+
the :class:`agents.stall_protocol.StallReason` ``unsafe_process_polling``
|
|
12
|
+
category so the runtime can emit a ``warn`` diagnostic and keep running.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
import shlex
|
|
19
|
+
from collections.abc import Iterator
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from agents.stall_protocol import StallReason
|
|
24
|
+
|
|
25
|
+
ORCHO_GUARDRAIL_BLOCKED = "ORCHO_GUARDRAIL_BLOCKED"
|
|
26
|
+
|
|
27
|
+
#: Guardrail category for the destructive-git guard (abort semantics).
|
|
28
|
+
GUARDRAIL_DESTRUCTIVE_GIT = "destructive_git"
|
|
29
|
+
#: Guardrail category for the unsafe free-text process-polling guard
|
|
30
|
+
#: (diagnostic ``warn`` only — never abort/kill). Equal to the
|
|
31
|
+
#: ``StallReason.unsafe_process_polling`` value so the two layers agree.
|
|
32
|
+
GUARDRAIL_UNSAFE_PROCESS_POLLING = str(StallReason.UNSAFE_PROCESS_POLLING)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class BlockedCommand:
|
|
37
|
+
"""A shell command flagged by a runtime guardrail.
|
|
38
|
+
|
|
39
|
+
``guardrail`` names the category so callers can route the verdict: the
|
|
40
|
+
default ``destructive_git`` is an abort, while ``unsafe_process_polling``
|
|
41
|
+
is a diagnostic warn that must NOT abort or kill the subprocess.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
command: str
|
|
45
|
+
reason: str
|
|
46
|
+
guardrail: str = GUARDRAIL_DESTRUCTIVE_GIT
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_SEGMENT_SPLIT_RE = re.compile(r"(?:&&|\|\||;|\n)")
|
|
50
|
+
_ENV_ASSIGN_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
|
|
51
|
+
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
|
|
52
|
+
_TEXT_PREFIX_RE = re.compile(
|
|
53
|
+
r"^(?:tool\s+Bash:|Bash:|command:|cmd:|shell:|run:|exec:)\s*",
|
|
54
|
+
re.IGNORECASE,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def blocked_destructive_git_command(
|
|
59
|
+
command: str,
|
|
60
|
+
*,
|
|
61
|
+
worktree_cwd_path: str | Path | None = None,
|
|
62
|
+
) -> BlockedCommand | None:
|
|
63
|
+
"""Return a block reason when ``command`` contains risky git mutation.
|
|
64
|
+
|
|
65
|
+
The guard is intentionally small and targeted. It does not try to be a
|
|
66
|
+
general shell sandbox; it catches the rollback/switch commands that can
|
|
67
|
+
erase or hide pre-existing working-tree state during Orcho write phases.
|
|
68
|
+
|
|
69
|
+
When ``worktree_cwd_path`` is set, the agent's effective cwd is an
|
|
70
|
+
orcho-managed isolated worktree (ADR 0033). Destructive git inside the
|
|
71
|
+
worktree cannot harm user-owned work, so the block is lifted — the agent
|
|
72
|
+
has full git freedom inside its own sandbox. Outside the worktree (or
|
|
73
|
+
when the parameter is None) the existing detection stands unchanged.
|
|
74
|
+
"""
|
|
75
|
+
if worktree_cwd_path is not None:
|
|
76
|
+
return None
|
|
77
|
+
for segment in _SEGMENT_SPLIT_RE.split(command or ""):
|
|
78
|
+
argv = _safe_split(segment)
|
|
79
|
+
if not argv:
|
|
80
|
+
continue
|
|
81
|
+
argv = _strip_prefixes(argv)
|
|
82
|
+
if len(argv) < 2 or argv[0] != "git":
|
|
83
|
+
continue
|
|
84
|
+
subcommand = argv[1]
|
|
85
|
+
if subcommand in {"reset", "restore", "clean", "revert", "switch"}:
|
|
86
|
+
return BlockedCommand(
|
|
87
|
+
command=command,
|
|
88
|
+
reason=f"git {subcommand} can discard or hide working-tree state",
|
|
89
|
+
)
|
|
90
|
+
if subcommand == "checkout":
|
|
91
|
+
return BlockedCommand(
|
|
92
|
+
command=command,
|
|
93
|
+
reason="git checkout can discard file changes or switch context",
|
|
94
|
+
)
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ── unsafe free-text process polling guard ───────────────────────────────────
|
|
99
|
+
|
|
100
|
+
# A pgrep/pkill invocation, even when wrapped in command substitution or
|
|
101
|
+
# backticks. ``\b`` matches at the start of the binary name when preceded by
|
|
102
|
+
# ``$(`` or `` ` `` (both non-word) just as well as at a line/whitespace start.
|
|
103
|
+
_PGREP_PKILL_RE = re.compile(r"\b(?:pgrep|pkill)\b")
|
|
104
|
+
# A short-option cluster that includes the ``-f`` (full-command-line match)
|
|
105
|
+
# flag — ``-f``, ``-fl``, ``-lf``, ``-af`` … A leading boundary keeps this a
|
|
106
|
+
# flag rather than a substring of an argument.
|
|
107
|
+
_F_FLAG_RE = re.compile(r"(?:^|\s)-[A-Za-z]*f[A-Za-z]*\b")
|
|
108
|
+
# Characters that end the pgrep/pkill argument scope, so an unrelated ``-f``
|
|
109
|
+
# elsewhere in a compound line is not mis-attributed to the pgrep call.
|
|
110
|
+
_CMD_BOUNDARY_RE = re.compile(r"[;&|)`\n]")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def blocked_unsafe_process_polling(
|
|
114
|
+
command: str,
|
|
115
|
+
*,
|
|
116
|
+
worktree_cwd_path: str | Path | None = None,
|
|
117
|
+
) -> BlockedCommand | None:
|
|
118
|
+
"""Flag unsafe free-text process polling (``pgrep -f`` / ``pkill -f``).
|
|
119
|
+
|
|
120
|
+
Detects the full-command-line, free-text process matching an agent reaches
|
|
121
|
+
for when it wants to wait on a backgrounded command, including the compound
|
|
122
|
+
and loop forms:
|
|
123
|
+
|
|
124
|
+
* ``pgrep -f "pytest -q -m"`` / ``pkill -f ...``
|
|
125
|
+
* ``kill -0 $(pgrep -f "pytest -q -m")``
|
|
126
|
+
* ``while kill -0 $(pgrep -f ...); do sleep 1; done``
|
|
127
|
+
|
|
128
|
+
Free-text argv matching is unsafe because it can match *unrelated* host
|
|
129
|
+
processes — the dogfood hazard is a real ``pytest -q -m`` running elsewhere
|
|
130
|
+
on the machine being mistaken for the run's own command. Polling the run's
|
|
131
|
+
OWN child by PID (``kill -0 <pid>``) matches nothing here and is correctly
|
|
132
|
+
left alone.
|
|
133
|
+
|
|
134
|
+
Unlike :func:`blocked_destructive_git_command`, an orcho-managed worktree
|
|
135
|
+
does **not** relax this check — matching foreign processes by argv is a
|
|
136
|
+
hazard regardless of cwd. ``worktree_cwd_path`` is accepted only for
|
|
137
|
+
call-site symmetry and is intentionally ignored.
|
|
138
|
+
|
|
139
|
+
The verdict is derived purely from the command TEXT; this guard never
|
|
140
|
+
scans, signals, or kills any process. The returned ``BlockedCommand``
|
|
141
|
+
carries ``guardrail=unsafe_process_polling`` so the runtime routes it as a
|
|
142
|
+
diagnostic ``warn``, never an abort.
|
|
143
|
+
"""
|
|
144
|
+
text = command or ""
|
|
145
|
+
for match in _PGREP_PKILL_RE.finditer(text):
|
|
146
|
+
tail = text[match.end():]
|
|
147
|
+
boundary = _CMD_BOUNDARY_RE.search(tail)
|
|
148
|
+
scope = tail[: boundary.start()] if boundary else tail
|
|
149
|
+
if _F_FLAG_RE.search(scope):
|
|
150
|
+
return BlockedCommand(
|
|
151
|
+
command=command,
|
|
152
|
+
reason=(
|
|
153
|
+
"free-text process polling (pgrep/pkill -f) can match "
|
|
154
|
+
"unrelated processes by command line; poll the run's own "
|
|
155
|
+
"child by PID instead"
|
|
156
|
+
),
|
|
157
|
+
guardrail=GUARDRAIL_UNSAFE_PROCESS_POLLING,
|
|
158
|
+
)
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def claude_bash_commands_from_jsonl(line: str) -> Iterator[str]:
|
|
163
|
+
"""Yield Bash command strings from one Claude stream-json line."""
|
|
164
|
+
line = (line or "").strip()
|
|
165
|
+
if not line.startswith("{"):
|
|
166
|
+
return
|
|
167
|
+
try:
|
|
168
|
+
payload = json.loads(line)
|
|
169
|
+
except json.JSONDecodeError:
|
|
170
|
+
return
|
|
171
|
+
if payload.get("type") != "assistant":
|
|
172
|
+
return
|
|
173
|
+
message = payload.get("message")
|
|
174
|
+
if not isinstance(message, dict):
|
|
175
|
+
return
|
|
176
|
+
for block in message.get("content", []) or []:
|
|
177
|
+
if not isinstance(block, dict):
|
|
178
|
+
continue
|
|
179
|
+
if block.get("type") != "tool_use" or block.get("name") != "Bash":
|
|
180
|
+
continue
|
|
181
|
+
tool_input = block.get("input")
|
|
182
|
+
if not isinstance(tool_input, dict):
|
|
183
|
+
continue
|
|
184
|
+
command = tool_input.get("command")
|
|
185
|
+
if isinstance(command, str) and command.strip():
|
|
186
|
+
yield command
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def blocked_claude_write_tool_use(
|
|
190
|
+
line: str,
|
|
191
|
+
*,
|
|
192
|
+
worktree_cwd_path: str | Path | None = None,
|
|
193
|
+
) -> BlockedCommand | None:
|
|
194
|
+
"""Detect blocked Bash tool use in one Claude stream-json line."""
|
|
195
|
+
for command in claude_bash_commands_from_jsonl(line):
|
|
196
|
+
blocked = blocked_destructive_git_command(command, worktree_cwd_path=worktree_cwd_path)
|
|
197
|
+
if blocked is not None:
|
|
198
|
+
return blocked
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def gemini_shell_commands_from_jsonl(line: str) -> Iterator[str]:
|
|
203
|
+
"""Yield shell command strings from one Gemini stream-json line.
|
|
204
|
+
|
|
205
|
+
Gemini CLI 0.40 emits ``tool_use`` events as top-level JSONL records
|
|
206
|
+
(not wrapped in an assistant content array like Claude). The
|
|
207
|
+
shell-executing tool is ``run_shell_command`` and the command lands
|
|
208
|
+
under ``parameters.command``. Tool names may change across CLI
|
|
209
|
+
releases — ``run_shell_command`` is the documented 0.40 name. The
|
|
210
|
+
yield is empty for every other tool (``read_file``, ``glob``, ...)
|
|
211
|
+
and for malformed input.
|
|
212
|
+
"""
|
|
213
|
+
line = (line or "").strip()
|
|
214
|
+
if not line.startswith("{"):
|
|
215
|
+
return
|
|
216
|
+
try:
|
|
217
|
+
payload = json.loads(line)
|
|
218
|
+
except json.JSONDecodeError:
|
|
219
|
+
return
|
|
220
|
+
if not isinstance(payload, dict) or payload.get("type") != "tool_use":
|
|
221
|
+
return
|
|
222
|
+
if payload.get("tool_name") != "run_shell_command":
|
|
223
|
+
return
|
|
224
|
+
params = payload.get("parameters")
|
|
225
|
+
if not isinstance(params, dict):
|
|
226
|
+
return
|
|
227
|
+
command = params.get("command")
|
|
228
|
+
if isinstance(command, str) and command.strip():
|
|
229
|
+
yield command
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def blocked_gemini_write_tool_use(
|
|
233
|
+
line: str,
|
|
234
|
+
*,
|
|
235
|
+
worktree_cwd_path: str | Path | None = None,
|
|
236
|
+
) -> BlockedCommand | None:
|
|
237
|
+
"""Detect blocked shell tool use in one Gemini stream-json line."""
|
|
238
|
+
for command in gemini_shell_commands_from_jsonl(line):
|
|
239
|
+
blocked = blocked_destructive_git_command(
|
|
240
|
+
command, worktree_cwd_path=worktree_cwd_path,
|
|
241
|
+
)
|
|
242
|
+
if blocked is not None:
|
|
243
|
+
return blocked
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def blocked_agent_stream_line(
|
|
248
|
+
line: str,
|
|
249
|
+
*,
|
|
250
|
+
worktree_cwd_path: str | Path | None = None,
|
|
251
|
+
) -> BlockedCommand | None:
|
|
252
|
+
"""Detect blocked commands in a write-agent stream line.
|
|
253
|
+
|
|
254
|
+
Claude and Gemini emit structured stream-json with tool calls;
|
|
255
|
+
Codex prints human-readable command lines. This function supports
|
|
256
|
+
every shape so provider wrappers can share one guardrail.
|
|
257
|
+
"""
|
|
258
|
+
# Destructive-git guard first: it aborts the call and is worktree-relaxable.
|
|
259
|
+
structured = blocked_claude_write_tool_use(line, worktree_cwd_path=worktree_cwd_path)
|
|
260
|
+
if structured is not None:
|
|
261
|
+
return structured
|
|
262
|
+
structured = blocked_gemini_write_tool_use(line, worktree_cwd_path=worktree_cwd_path)
|
|
263
|
+
if structured is not None:
|
|
264
|
+
return structured
|
|
265
|
+
for candidate in shell_command_candidates_from_text(line):
|
|
266
|
+
blocked = blocked_destructive_git_command(candidate, worktree_cwd_path=worktree_cwd_path)
|
|
267
|
+
if blocked is not None:
|
|
268
|
+
return blocked
|
|
269
|
+
# Unsafe free-text process polling: a diagnostic warn (never abort/kill),
|
|
270
|
+
# provider-neutral over every command shape, and never worktree-relaxed.
|
|
271
|
+
for command in agent_commands_from_stream_line(line):
|
|
272
|
+
unsafe = blocked_unsafe_process_polling(
|
|
273
|
+
command, worktree_cwd_path=worktree_cwd_path,
|
|
274
|
+
)
|
|
275
|
+
if unsafe is not None:
|
|
276
|
+
return unsafe
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _clean_stream_text(line: str) -> str:
|
|
281
|
+
"""Normalise one human-readable stream line into a bare command string.
|
|
282
|
+
|
|
283
|
+
Strips ANSI, surrounding backticks, the ``tool Bash:`` / ``command:`` …
|
|
284
|
+
prefixes, and a leading shell-prompt sigil (``$`` / ``>`` / ``+``). Returns
|
|
285
|
+
``""`` for an empty / whitespace-only line.
|
|
286
|
+
"""
|
|
287
|
+
text = _ANSI_RE.sub("", line or "").strip()
|
|
288
|
+
if not text:
|
|
289
|
+
return ""
|
|
290
|
+
text = text.strip("`")
|
|
291
|
+
text = _TEXT_PREFIX_RE.sub("", text).strip()
|
|
292
|
+
for prefix in ("$", ">", "+"):
|
|
293
|
+
if text.startswith(prefix):
|
|
294
|
+
text = text[len(prefix):].strip()
|
|
295
|
+
if text.startswith("tool Bash:"):
|
|
296
|
+
text = text.removeprefix("tool Bash:").strip()
|
|
297
|
+
return text
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def shell_command_candidates_from_text(line: str) -> Iterator[str]:
|
|
301
|
+
"""Yield git-command-looking strings from one human-readable stream line."""
|
|
302
|
+
text = _clean_stream_text(line)
|
|
303
|
+
if text.startswith("git ") or text.startswith("command git ") or text.startswith("sudo git "):
|
|
304
|
+
yield text
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def agent_commands_from_stream_line(line: str) -> Iterator[str]:
|
|
308
|
+
"""Yield every candidate shell command from one stream line, any provider.
|
|
309
|
+
|
|
310
|
+
Unions the structured tool-use commands (Claude Bash, Gemini
|
|
311
|
+
``run_shell_command``) with the cleaned human-readable text (Codex). Unlike
|
|
312
|
+
:func:`shell_command_candidates_from_text`, the text candidate is yielded
|
|
313
|
+
regardless of leading binary, because process-polling forms
|
|
314
|
+
(``pgrep -f`` / ``kill -0 $(pgrep -f ...)``) do not start with ``git``.
|
|
315
|
+
"""
|
|
316
|
+
yield from claude_bash_commands_from_jsonl(line)
|
|
317
|
+
yield from gemini_shell_commands_from_jsonl(line)
|
|
318
|
+
text = _clean_stream_text(line)
|
|
319
|
+
if text:
|
|
320
|
+
yield text
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _safe_split(segment: str) -> list[str]:
|
|
324
|
+
try:
|
|
325
|
+
return shlex.split(segment)
|
|
326
|
+
except ValueError:
|
|
327
|
+
return segment.strip().split()
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _strip_prefixes(argv: list[str]) -> list[str]:
|
|
331
|
+
out = list(argv)
|
|
332
|
+
while out and _ENV_ASSIGN_RE.match(out[0]):
|
|
333
|
+
out.pop(0)
|
|
334
|
+
while out and out[0] in {"command", "sudo"}:
|
|
335
|
+
out.pop(0)
|
|
336
|
+
return out
|
agents/entities.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""agents/entities.py — Domain entities for the agent pipeline.
|
|
2
|
+
|
|
3
|
+
Plan parsing now lives in :mod:`pipeline.plan_parser`; the canonical PLAN
|
|
4
|
+
object is ``ParsedPlan``. This module keeps the agent-adjacent entities that
|
|
5
|
+
do not depend on pipeline runtime wiring.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class SubTask:
|
|
13
|
+
"""A team-lead-decomposed unit of work with explicit DAG dependencies.
|
|
14
|
+
|
|
15
|
+
Produced by the PLAN phase when the architect emits the structured
|
|
16
|
+
``## Task N`` / JSON-fence format. ``depends_on`` references other SubTask
|
|
17
|
+
``id``s — the runner topo-sorts and (later) groups into parallel waves.
|
|
18
|
+
``skill`` is an optional name resolved against the plugin's skill registry
|
|
19
|
+
(auto-discovered from the plugin folder); when empty, the runner falls
|
|
20
|
+
back to the per-phase build agent. ``done_criteria`` are checkable
|
|
21
|
+
conditions the per-task QA gate validates before marking the subtask done.
|
|
22
|
+
|
|
23
|
+
Phase 1 redesign extensions (additive):
|
|
24
|
+
* ``owned_files`` — glob patterns the subtask writes to. Used by
|
|
25
|
+
Milestone 9 wave executor for proactive conflict avoidance:
|
|
26
|
+
subtasks with overlapping owned_files are scheduled in different
|
|
27
|
+
waves rather than racing on merge.
|
|
28
|
+
* ``architectural_decision`` — flag for the artifact pipeline. When
|
|
29
|
+
``ArtifactProfile.ADR`` (or higher) is selected, an ADR is emitted
|
|
30
|
+
per subtask carrying this flag (Milestone 11).
|
|
31
|
+
"""
|
|
32
|
+
id: str
|
|
33
|
+
goal: str
|
|
34
|
+
spec: str = ""
|
|
35
|
+
files: tuple[str, ...] = field(default_factory=tuple)
|
|
36
|
+
skill: str | None = None
|
|
37
|
+
model: str | None = None
|
|
38
|
+
depends_on: tuple[str, ...] = field(default_factory=tuple)
|
|
39
|
+
done_criteria: tuple[str, ...] = field(default_factory=tuple)
|
|
40
|
+
owned_files: tuple[str, ...] = field(default_factory=tuple)
|
|
41
|
+
# Companion modifications the reviewer may accept for THIS task beyond
|
|
42
|
+
# the project-wide ``allowed_modifications`` (lockfiles, regenerated
|
|
43
|
+
# snapshots, derived artifacts). This is an *informational review
|
|
44
|
+
# allowance* surfaced in the Plan Contract — NOT a write-scope
|
|
45
|
+
# primitive for the wave planner, which schedules on ``owned_files``.
|
|
46
|
+
allowed_modifications: tuple[str, ...] = field(default_factory=tuple)
|
|
47
|
+
architectural_decision: bool = False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class TestResult:
|
|
52
|
+
"""Result of running the project's test suite as a shell step.
|
|
53
|
+
|
|
54
|
+
Produced by the orchestrator's ``run_tests()`` shell wrapper, not by an
|
|
55
|
+
LLM. ``output`` is the combined stdout+stderr fed back into the repair_changes prompt
|
|
56
|
+
when tests fail. ``skipped`` is True when the plugin defines no
|
|
57
|
+
``run_command`` — the pipeline must treat that as success.
|
|
58
|
+
"""
|
|
59
|
+
# Tell pytest not to collect this as a test class (the leading "Test"
|
|
60
|
+
# in the name would otherwise trigger PytestCollectionWarning).
|
|
61
|
+
__test__ = False
|
|
62
|
+
|
|
63
|
+
skipped: bool = False
|
|
64
|
+
passed: bool = True
|
|
65
|
+
output: str = ""
|
|
66
|
+
duration: float = 0.0
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def failed(self) -> bool:
|
|
70
|
+
return not self.skipped and not self.passed
|
agents/protocols.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agents/protocols.py — Public agent contract.
|
|
3
|
+
|
|
4
|
+
:class:`IAgentRuntime` is the single Protocol every runtime implements. One
|
|
5
|
+
``invoke()`` entry point: it receives a fully-composed prompt (the composer
|
|
6
|
+
builds it) plus per-call flags and returns raw text. Callers parse plans /
|
|
7
|
+
critiques / etc. from the returned string.
|
|
8
|
+
|
|
9
|
+
``session_id`` is the bridge handle. A runtime instance carries it across
|
|
10
|
+
phase boundaries so subsequent invocations can ``--resume`` the same
|
|
11
|
+
conversation. Runtimes without resumable sessions leave it ``None``. The
|
|
12
|
+
orchestrator decides per-call whether to resume via ``continue_session=True``;
|
|
13
|
+
the runtime is mechanical and never raises on cross-mutation resume — that
|
|
14
|
+
policy lives in the orchestrator.
|
|
15
|
+
|
|
16
|
+
``mutates_artifacts`` declares whether the call may modify project artifacts
|
|
17
|
+
on disk (code, configs, generated files in ``cwd``). Maps to the CLI write
|
|
18
|
+
flags. Default ``False`` is safe; ``build`` / ``fix`` phases opt in. The flag
|
|
19
|
+
is per-call, not per-session — one bridge can carry both mutating and
|
|
20
|
+
non-mutating invocations.
|
|
21
|
+
|
|
22
|
+
``attachments`` is multimodal-only (IMAGE / BINARY). TEXT attachments are
|
|
23
|
+
rendered into the prompt outside the runtime by
|
|
24
|
+
``builtin._plan_prompt_prefix`` via ``render_text_block``; passing TEXT into
|
|
25
|
+
``invoke(attachments=...)`` is a caller bug and the runtime raises
|
|
26
|
+
``ValueError`` to prevent double injection.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from enum import Enum
|
|
32
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from pipeline.runtime.steps import Attachment
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SessionMode(str, Enum): # noqa: UP042 # StrEnum changes __str__; appears in SDK schema snapshot, keep stable
|
|
39
|
+
"""How the orchestrator should chain calls between phases."""
|
|
40
|
+
|
|
41
|
+
AUTO = "auto"
|
|
42
|
+
STATELESS = "stateless"
|
|
43
|
+
CHAIN = "chain"
|
|
44
|
+
HYBRID = "hybrid"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@runtime_checkable
|
|
48
|
+
class IAgentRuntime(Protocol):
|
|
49
|
+
"""Unified agent runtime contract.
|
|
50
|
+
|
|
51
|
+
A concrete implementation backs one CLI / SDK (Claude Code, Codex CLI,
|
|
52
|
+
Gemini, …). It receives a fully-composed prompt and returns the agent's
|
|
53
|
+
raw textual response. Plan parsing, review verdict extraction, and any
|
|
54
|
+
other downstream interpretation happen in the caller.
|
|
55
|
+
|
|
56
|
+
Fields:
|
|
57
|
+
model: identifier of the underlying model.
|
|
58
|
+
session_id: bridge handle. ``None`` until the first successful call,
|
|
59
|
+
or for runtimes without resumable sessions. Same instance, same
|
|
60
|
+
session through the pipeline; reset with :meth:`reset_session`.
|
|
61
|
+
_followup_resume_pending: optional follow-up seed flag. When true and
|
|
62
|
+
``session_id`` is set, the next ``invoke`` must pass that id to the
|
|
63
|
+
runtime resume flag even if the caller did not request continuation.
|
|
64
|
+
_last_resumed_session_id: the exact id passed to the runtime resume
|
|
65
|
+
flag on the most recent call, or ``None`` when the call was fresh.
|
|
66
|
+
_last_followup_parent_session_id: the parent-run session id consumed by
|
|
67
|
+
a follow-up seed on the most recent call, or ``None`` otherwise.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
model: str
|
|
71
|
+
session_id: str | None
|
|
72
|
+
_followup_resume_pending: bool
|
|
73
|
+
_last_resumed_session_id: str | None
|
|
74
|
+
_last_followup_parent_session_id: str | None
|
|
75
|
+
|
|
76
|
+
def invoke(
|
|
77
|
+
self,
|
|
78
|
+
prompt: str,
|
|
79
|
+
cwd: str,
|
|
80
|
+
*,
|
|
81
|
+
mutates_artifacts: bool = False,
|
|
82
|
+
continue_session: bool = False,
|
|
83
|
+
attachments: tuple[Attachment, ...] = (),
|
|
84
|
+
) -> str:
|
|
85
|
+
"""Run the agent on ``prompt`` inside working directory ``cwd``.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
prompt: fully-composed prompt text (composer built it).
|
|
89
|
+
cwd: project working directory.
|
|
90
|
+
mutates_artifacts: when ``True`` the call may modify files on
|
|
91
|
+
disk in ``cwd``. Maps to the underlying CLI's write flags.
|
|
92
|
+
Default ``False`` is read-only.
|
|
93
|
+
continue_session: when ``True`` and ``self.session_id`` is set,
|
|
94
|
+
resume that session (``--resume``) so this call inherits the
|
|
95
|
+
bridge's accumulated context. Otherwise start fresh and
|
|
96
|
+
capture a new ``session_id``.
|
|
97
|
+
attachments: multimodal attachments (IMAGE / BINARY) the runtime
|
|
98
|
+
must hand to the CLI. **TEXT attachments are forbidden here**
|
|
99
|
+
— they are rendered into ``prompt`` outside the runtime; if
|
|
100
|
+
a TEXT entry leaks in, the runtime raises ``ValueError``.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Raw assistant text. Callers parse plans/critiques/etc. from it.
|
|
104
|
+
"""
|
|
105
|
+
...
|
|
106
|
+
|
|
107
|
+
def reset_session(self) -> None:
|
|
108
|
+
"""Clear ``session_id`` so the next ``invoke()`` starts fresh.
|
|
109
|
+
|
|
110
|
+
Use to deliberately burn the bridge (e.g. when a human operator
|
|
111
|
+
requested a clean restart from a paused run).
|
|
112
|
+
"""
|
|
113
|
+
...
|
|
114
|
+
|
|
115
|
+
# ── Optional capability: account identity diagnostics ────────────────
|
|
116
|
+
#
|
|
117
|
+
# A runtime MAY implement ``probe_identity(self) -> RuntimeIdentity`` to
|
|
118
|
+
# report which provider account / organization it is executing under. It
|
|
119
|
+
# is intentionally NOT declared as a required method on this Protocol:
|
|
120
|
+
# the capability is structural, so a third-party runtime that omits it
|
|
121
|
+
# simply yields an ``unavailable`` identity via
|
|
122
|
+
# :func:`agents.runtimes.identity.probe_runtime_identity`.
|
|
123
|
+
#
|
|
124
|
+
# Contract for implementers:
|
|
125
|
+
# * Diagnostic only — never an authorization or delivery decision.
|
|
126
|
+
# * Best-effort and non-interactive: read a status surface the provider
|
|
127
|
+
# already shows users, with a short timeout. Any failure (missing
|
|
128
|
+
# binary, timeout, non-zero exit, unparsable output, no account field)
|
|
129
|
+
# returns ``RuntimeIdentity.unavailable(...)``; it must never raise.
|
|
130
|
+
# * Sanitized: populate only ``account_label`` / ``email`` (values the
|
|
131
|
+
# provider already surfaces). Never read or return tokens, cookies,
|
|
132
|
+
# auth-file paths, or raw auth JSON.
|
|
133
|
+
# * Lazy: must not be called from the constructor, profile listing, or
|
|
134
|
+
# dry-run rendering. Resolving the CLI binary here (first real use) is
|
|
135
|
+
# fine; the orchestrator only calls it during real run setup.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Diagnostics for startup-time pseudo-terminal allocation failures."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import errno
|
|
6
|
+
|
|
7
|
+
PTY_EXHAUSTED_SENTINEL = "ORCHO_SYSTEM_PTY_EXHAUSTED"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_pty_exhaustion(exc: OSError) -> bool:
|
|
11
|
+
"""Return True when ``pty.openpty`` failed because no PTYs are available."""
|
|
12
|
+
message = str(exc).lower()
|
|
13
|
+
return (
|
|
14
|
+
exc.errno in {errno.ENOSPC, errno.EAGAIN}
|
|
15
|
+
or "out of pty devices" in message
|
|
16
|
+
or "no available pty" in message
|
|
17
|
+
or "device not configured" in message
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def render_pty_exhaustion_diagnostic(exc: OSError) -> str:
|
|
22
|
+
"""Human recovery text for a system PTY pool exhaustion blocker."""
|
|
23
|
+
return "\n".join((
|
|
24
|
+
f"{PTY_EXHAUSTED_SENTINEL}: PTY pool exhausted before agent startup.",
|
|
25
|
+
"",
|
|
26
|
+
"Orcho could not allocate a pseudo-terminal for the agent runtime.",
|
|
27
|
+
"This is a system resource blocker, not a task, plan, or code-review failure.",
|
|
28
|
+
"It is usually caused by external orphaned PTY holders, such as terminal",
|
|
29
|
+
"sessions or computer-use/browser automation clients that were not cleaned up.",
|
|
30
|
+
"",
|
|
31
|
+
"Diagnostics:",
|
|
32
|
+
" python -c \"import pty; print(pty.openpty())\"",
|
|
33
|
+
" lsof 2>/dev/null | grep -E '/dev/(ttys|ptmx|pty)' | "
|
|
34
|
+
"awk '{print $1, $2, $9}' | sort | uniq -c | sort -nr | head",
|
|
35
|
+
" ps aux | grep '[S]kyComputerUseClient'",
|
|
36
|
+
" ps aux | grep '[p]ty'",
|
|
37
|
+
"",
|
|
38
|
+
"Recovery:",
|
|
39
|
+
" close or restart the leaking client, or terminate orphaned PTY holders;",
|
|
40
|
+
" then rerun the same Orcho command.",
|
|
41
|
+
"",
|
|
42
|
+
f"Original error: {exc}",
|
|
43
|
+
))
|