vidbyte-cli 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.
- vidbyte_cli/__init__.py +11 -0
- vidbyte_cli/__main__.py +6 -0
- vidbyte_cli/cli.py +18 -0
- vidbyte_cli/commands/__init__.py +86 -0
- vidbyte_cli/commands/agent_options.py +53 -0
- vidbyte_cli/commands/agents/__init__.py +31 -0
- vidbyte_cli/commands/agents/suggestion/__init__.py +38 -0
- vidbyte_cli/commands/agents/suggestion/prompts/__init__.py +3 -0
- vidbyte_cli/commands/agents/suggestion/prompts/all_categories.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/approaches.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/avoid.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/blockers.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/capability.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/categories.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/category.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/completed.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/constraint.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/context.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/count.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/critic_model.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/decision.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/dry_run.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/extra_compute.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_accept.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_group.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_project.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_reason.md +98 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_reject.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/feedback_suggestion.md +101 -0
- vidbyte_cli/commands/agents/suggestion/prompts/files.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/forbidden.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/goal.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/handoff.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/horizon.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/hypotheses.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/idea.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/in_progress.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/input.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/library.py +29 -0
- vidbyte_cli/commands/agents/suggestion/prompts/max_messages.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/max_output_tokens.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/max_total_tokens.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/mistakes.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/outcomes.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_create.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_description.md +98 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_group.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_key.md +96 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_list.md +7 -0
- vidbyte_cli/commands/agents/suggestion/prompts/project_title.md +99 -0
- vidbyte_cli/commands/agents/suggestion/prompts/provider.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/question.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/risks.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/rounds.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/run.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/success.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/timeout.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/trajectory.md +100 -0
- vidbyte_cli/commands/agents/suggestion/prompts/view.md +5 -0
- vidbyte_cli/commands/agents/suggestion/prompts/view_all.md +5 -0
- vidbyte_cli/commands/agents/suggestion/render.py +77 -0
- vidbyte_cli/commands/agents/suggestion/request_builder.py +240 -0
- vidbyte_cli/commands/agents/suggestion/suggest.py +154 -0
- vidbyte_cli/commands/agents/suggestion/suggestion_categories.py +77 -0
- vidbyte_cli/commands/agents/suggestion/suggestion_feedback.py +85 -0
- vidbyte_cli/commands/agents/suggestion/suggestion_handoff.py +67 -0
- vidbyte_cli/commands/agents/suggestion/suggestion_projects.py +95 -0
- vidbyte_cli/commands/auth/__init__.py +0 -0
- vidbyte_cli/commands/auth/login.py +100 -0
- vidbyte_cli/commands/auth/logout.py +41 -0
- vidbyte_cli/commands/auth/whoami.py +51 -0
- vidbyte_cli/commands/billing/__init__.py +20 -0
- vidbyte_cli/commands/billing/prompts/__init__.py +1 -0
- vidbyte_cli/commands/billing/prompts/confirm.md +16 -0
- vidbyte_cli/commands/billing/prompts/group.md +13 -0
- vidbyte_cli/commands/billing/prompts/idempotency_key.md +16 -0
- vidbyte_cli/commands/billing/prompts/library.py +18 -0
- vidbyte_cli/commands/billing/prompts/top_up.md +16 -0
- vidbyte_cli/commands/billing/top_up.py +65 -0
- vidbyte_cli/commands/config/__init__.py +0 -0
- vidbyte_cli/commands/config/get.py +45 -0
- vidbyte_cli/commands/config/set.py +39 -0
- vidbyte_cli/commands/provider/__init__.py +9 -0
- vidbyte_cli/commands/provider/login.py +102 -0
- vidbyte_cli/commands/provider/logout.py +45 -0
- vidbyte_cli/commands/provider/whoami.py +49 -0
- vidbyte_cli/commands/research/__init__.py +5 -0
- vidbyte_cli/commands/research/add.py +41 -0
- vidbyte_cli/commands/research/options.py +134 -0
- vidbyte_cli/commands/research/render.py +157 -0
- vidbyte_cli/commands/research/resume.py +37 -0
- vidbyte_cli/commands/research/start.py +37 -0
- vidbyte_cli/commands/research/status.py +32 -0
- vidbyte_cli/commands/research/thread.py +30 -0
- vidbyte_cli/commands/research/threads.py +35 -0
- vidbyte_cli/commands/research/watch.py +93 -0
- vidbyte_cli/commands/runtime/__init__.py +23 -0
- vidbyte_cli/commands/runtime/adversarial_team.py +41 -0
- vidbyte_cli/commands/runtime/doctor.py +44 -0
- vidbyte_cli/commands/runtime/list.py +47 -0
- vidbyte_cli/commands/runtime/persistence.py +141 -0
- vidbyte_cli/commands/runtime/same_host_ensemble.py +221 -0
- vidbyte_cli/commands/runtime/stages.py +553 -0
- vidbyte_cli/commands/runtime/task_board.py +1342 -0
- vidbyte_cli/commands/setup/__init__.py +0 -0
- vidbyte_cli/commands/setup/doctor.py +64 -0
- vidbyte_cli/commands/setup/prompts/__init__.py +1 -0
- vidbyte_cli/commands/setup/prompts/browser.md +7 -0
- vidbyte_cli/commands/setup/prompts/library.py +18 -0
- vidbyte_cli/commands/setup/prompts/star.md +13 -0
- vidbyte_cli/commands/setup/star.py +132 -0
- vidbyte_cli/lib/__init__.py +0 -0
- vidbyte_cli/lib/api/__init__.py +0 -0
- vidbyte_cli/lib/api/client.py +236 -0
- vidbyte_cli/lib/api/endpoints/__init__.py +0 -0
- vidbyte_cli/lib/api/endpoints/auth.py +24 -0
- vidbyte_cli/lib/api/endpoints/billing.py +36 -0
- vidbyte_cli/lib/api/endpoints/research.py +103 -0
- vidbyte_cli/lib/api/endpoints/runtime.py +113 -0
- vidbyte_cli/lib/api/problem.py +86 -0
- vidbyte_cli/lib/api/response.py +101 -0
- vidbyte_cli/lib/api/retry.py +116 -0
- vidbyte_cli/lib/api/runtime_payment.py +89 -0
- vidbyte_cli/lib/auth/__init__.py +16 -0
- vidbyte_cli/lib/auth/credentials.py +223 -0
- vidbyte_cli/lib/auth/input.py +59 -0
- vidbyte_cli/lib/auth/keyring_store.py +126 -0
- vidbyte_cli/lib/auth/provider_credentials.py +157 -0
- vidbyte_cli/lib/auth/provider_input.py +54 -0
- vidbyte_cli/lib/auth/provider_resolver.py +89 -0
- vidbyte_cli/lib/auth/provider_store.py +149 -0
- vidbyte_cli/lib/auth/provider_verifier.py +219 -0
- vidbyte_cli/lib/auth/resolver.py +63 -0
- vidbyte_cli/lib/auth/verifier.py +38 -0
- vidbyte_cli/lib/config/__init__.py +25 -0
- vidbyte_cli/lib/config/atomic.py +47 -0
- vidbyte_cli/lib/config/config.py +141 -0
- vidbyte_cli/lib/config/migration.py +81 -0
- vidbyte_cli/lib/config/models.py +145 -0
- vidbyte_cli/lib/config/paths.py +66 -0
- vidbyte_cli/lib/config/resolver.py +137 -0
- vidbyte_cli/lib/constants/__init__.py +1 -0
- vidbyte_cli/lib/constants/runtime.py +186 -0
- vidbyte_cli/lib/errors/__init__.py +12 -0
- vidbyte_cli/lib/errors/cli_error.py +86 -0
- vidbyte_cli/lib/errors/codes.py +43 -0
- vidbyte_cli/lib/errors/failures.py +2910 -0
- vidbyte_cli/lib/errors/handler.py +72 -0
- vidbyte_cli/lib/files/__init__.py +6 -0
- vidbyte_cli/lib/files/documents.py +94 -0
- vidbyte_cli/lib/files/store.py +156 -0
- vidbyte_cli/lib/io/__init__.py +16 -0
- vidbyte_cli/lib/io/attachments.py +129 -0
- vidbyte_cli/lib/io/codex_attachments.py +47 -0
- vidbyte_cli/lib/io/prompt.py +71 -0
- vidbyte_cli/lib/io/streams.py +37 -0
- vidbyte_cli/lib/io/terminal.py +66 -0
- vidbyte_cli/lib/output/__init__.py +7 -0
- vidbyte_cli/lib/output/formats.py +29 -0
- vidbyte_cli/lib/output/manager.py +95 -0
- vidbyte_cli/lib/output/models.py +49 -0
- vidbyte_cli/lib/runtime/__init__.py +9 -0
- vidbyte_cli/lib/runtime/application.py +134 -0
- vidbyte_cli/lib/runtime/context.py +250 -0
- vidbyte_cli/lib/runtime/options.py +159 -0
- vidbyte_cli/lib/runtime/version.py +30 -0
- vidbyte_cli/lib/runtime_primitives/__init__.py +28 -0
- vidbyte_cli/lib/runtime_primitives/executor.py +54 -0
- vidbyte_cli/lib/runtime_primitives/gate.py +105 -0
- vidbyte_cli/lib/runtime_primitives/hosts.py +45 -0
- vidbyte_cli/lib/runtime_primitives/planner.py +79 -0
- vidbyte_cli/lib/runtime_primitives/stages.py +338 -0
- vidbyte_cli/lib/runtime_primitives/task_board.py +982 -0
- vidbyte_cli/lib/runtime_primitives/task_board_checkpoints.py +473 -0
- vidbyte_cli/lib/runtime_primitives/task_board_system.md +33 -0
- vidbyte_cli/lib/runtime_primitives/task_board_tasks.py +111 -0
- vidbyte_cli/lib/runtime_primitives/verification.py +68 -0
- vidbyte_cli/services/__init__.py +5 -0
- vidbyte_cli/services/ensemble/__init__.py +7 -0
- vidbyte_cli/services/ensemble/prompts/__init__.py +6 -0
- vidbyte_cli/services/ensemble/prompts/implementer_system.md +546 -0
- vidbyte_cli/services/ensemble/prompts/implementer_turn.md +49 -0
- vidbyte_cli/services/ensemble/prompts/library.py +188 -0
- vidbyte_cli/services/ensemble/prompts/planner_system.md +728 -0
- vidbyte_cli/services/ensemble/prompts/planner_turn.md +6 -0
- vidbyte_cli/services/ensemble/prompts/role_system.md +104 -0
- vidbyte_cli/services/ensemble/prompts/role_turn.md +19 -0
- vidbyte_cli/services/ensemble/prompts/selector_final_turn.md +19 -0
- vidbyte_cli/services/ensemble/prompts/selector_round_turn.md +17 -0
- vidbyte_cli/services/ensemble/prompts/selector_system.md +89 -0
- vidbyte_cli/services/ensemble/runner.py +30 -0
- vidbyte_cli/services/ensemble/sdk.py +134 -0
- vidbyte_cli/services/ensemble/service.py +233 -0
- vidbyte_cli/services/ensemble/settings.py +60 -0
- vidbyte_cli/services/persistence/__init__.py +6 -0
- vidbyte_cli/services/persistence/prompts/__init__.py +6 -0
- vidbyte_cli/services/persistence/prompts/library.py +46 -0
- vidbyte_cli/services/persistence/prompts/persistence_system.md +1 -0
- vidbyte_cli/services/persistence/prompts/persistence_turn.md +3 -0
- vidbyte_cli/services/persistence/runner.py +35 -0
- vidbyte_cli/services/persistence/session.py +116 -0
- vidbyte_cli/services/suggestions/__init__.py +7 -0
- vidbyte_cli/services/suggestions/categories.py +270 -0
- vidbyte_cli/services/suggestions/context.py +222 -0
- vidbyte_cli/services/suggestions/extra_compute.py +53 -0
- vidbyte_cli/services/suggestions/handoff.py +173 -0
- vidbyte_cli/services/suggestions/message_tools.py +47 -0
- vidbyte_cli/services/suggestions/project.py +253 -0
- vidbyte_cli/services/suggestions/prompts/__init__.py +1 -0
- vidbyte_cli/services/suggestions/prompts/categories/__init__.py +3 -0
- vidbyte_cli/services/suggestions/prompts/categories/adjacent_opportunity.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/alternative.md +38 -0
- vidbyte_cli/services/suggestions/prompts/categories/big_bets.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/bottleneck.md +38 -0
- vidbyte_cli/services/suggestions/prompts/categories/business_growth.md +38 -0
- vidbyte_cli/services/suggestions/prompts/categories/completion.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/continuation.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/coordination.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/creative_exploration.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/cross_domain.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/delegation.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/experiment.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/feedback.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/goal_clarification.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/immediate_next_steps.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/investigation.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/learning.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/leverage.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/long_term_directions.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/preparation.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/prerequisite.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/prioritization.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/product_experience.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/quick_wins.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/reframing.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/risk_prevention.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/simplification.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/stop_or_defer.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/strategy.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/technical_possibilities.md +37 -0
- vidbyte_cli/services/suggestions/prompts/categories/verification.md +37 -0
- vidbyte_cli/services/suggestions/prompts/critic.md +209 -0
- vidbyte_cli/services/suggestions/prompts/critic_message.md +8 -0
- vidbyte_cli/services/suggestions/prompts/generator.md +64 -0
- vidbyte_cli/services/suggestions/prompts/library.py +72 -0
- vidbyte_cli/services/suggestions/prompts/message_generator_argument.md +1 -0
- vidbyte_cli/services/suggestions/prompts/message_generator_tool.md +1 -0
- vidbyte_cli/services/suggestions/prompts/message_parent_argument.md +1 -0
- vidbyte_cli/services/suggestions/prompts/message_parent_tool.md +1 -0
- vidbyte_cli/services/suggestions/prompts/message_receipt.md +1 -0
- vidbyte_cli/services/suggestions/prompts/revision.md +8 -0
- vidbyte_cli/services/suggestions/sdk.py +228 -0
- vidbyte_cli/services/suggestions/selection.py +78 -0
- vidbyte_cli/services/suggestions/service.py +540 -0
- vidbyte_cli/types/__init__.py +0 -0
- vidbyte_cli/types/api.py +101 -0
- vidbyte_cli/types/attachments.py +79 -0
- vidbyte_cli/types/ensemble.py +189 -0
- vidbyte_cli/types/provider.py +62 -0
- vidbyte_cli/types/research.py +260 -0
- vidbyte_cli/types/runtime.py +1098 -0
- vidbyte_cli/types/suggestions.py +518 -0
- vidbyte_cli-0.1.0.dist-info/METADATA +335 -0
- vidbyte_cli-0.1.0.dist-info/RECORD +269 -0
- vidbyte_cli-0.1.0.dist-info/WHEEL +5 -0
- vidbyte_cli-0.1.0.dist-info/entry_points.txt +2 -0
- vidbyte_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- vidbyte_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1098 @@
|
|
|
1
|
+
"""Wire and local planning contracts for Vidbyte runtime primitives.
|
|
2
|
+
|
|
3
|
+
The backend sees only admission metadata; task and machine context stay local. Frozen,
|
|
4
|
+
extra-forbid models make contract drift fail before a paid execution can begin.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from enum import IntEnum, StrEnum
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Literal
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
18
|
+
|
|
19
|
+
from ..lib.constants.runtime import AdmissionReason, StagesLimit
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RuntimeHost(StrEnum):
|
|
23
|
+
"""Native coding-agent hosts supported by the first runtime shell."""
|
|
24
|
+
|
|
25
|
+
CODEX = "codex"
|
|
26
|
+
CLAUDE = "claude"
|
|
27
|
+
OPENCODE = "opencode"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RuntimeCapability(BaseModel):
|
|
31
|
+
"""One local runtime product published by the backend."""
|
|
32
|
+
|
|
33
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
34
|
+
capability_id: str = Field(min_length=1, max_length=128)
|
|
35
|
+
version: str = Field(min_length=1, max_length=32)
|
|
36
|
+
execution_location: Literal["local"]
|
|
37
|
+
supported_hosts: tuple[RuntimeHost, ...] = Field(min_length=1)
|
|
38
|
+
admission_price_cents: int = Field(ge=1)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RuntimeCapabilityCatalog(BaseModel):
|
|
42
|
+
"""The runtime-only catalog plus its central wallet funding route."""
|
|
43
|
+
|
|
44
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
45
|
+
capabilities: tuple[RuntimeCapability, ...]
|
|
46
|
+
topup_path: str = Field(pattern=r"^/[^\s]*$")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RuntimeAdmissionRequest(BaseModel):
|
|
50
|
+
"""Safe metadata required to buy one local execution admission."""
|
|
51
|
+
|
|
52
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
53
|
+
client_runtime_version: Literal["1"] = "1"
|
|
54
|
+
host: RuntimeHost
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class RuntimeX402AdmissionRequest(RuntimeAdmissionRequest):
|
|
58
|
+
"""Explicit payment opt-in; ordinary wallet requests retain their original wire shape."""
|
|
59
|
+
|
|
60
|
+
with_x402_payment: Literal[True] = True
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RuntimeAdmissionGrant(BaseModel):
|
|
64
|
+
"""Receipt returned after the backend durably charges admission."""
|
|
65
|
+
|
|
66
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
67
|
+
admission_id: str = Field(min_length=1, max_length=128)
|
|
68
|
+
capability_id: str = Field(min_length=1, max_length=160)
|
|
69
|
+
execution_location: Literal["local"]
|
|
70
|
+
charged_cents: int = Field(ge=1)
|
|
71
|
+
admitted_at: datetime
|
|
72
|
+
expires_at: datetime | None = None
|
|
73
|
+
grant_token: str | None = Field(default=None, min_length=10, max_length=8192)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class RuntimeHostStatus(BaseModel):
|
|
77
|
+
"""Non-secret PATH discovery result for one native coding-agent host."""
|
|
78
|
+
|
|
79
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
80
|
+
host: RuntimeHost
|
|
81
|
+
available: bool
|
|
82
|
+
executable: str | None = None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class RuntimeLaunchPlan(BaseModel):
|
|
86
|
+
"""Local-only handoff a future executor will turn into an agent topology."""
|
|
87
|
+
|
|
88
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid", frozen=True)
|
|
89
|
+
capability_id: Literal[
|
|
90
|
+
"runtime.review.adversarial-team@1",
|
|
91
|
+
"runtime.adversarial-team@1",
|
|
92
|
+
"runtime.same-host-ensemble@1",
|
|
93
|
+
"runtime.persistence@1",
|
|
94
|
+
"runtime.task-board@1",
|
|
95
|
+
"runtime.stages@1",
|
|
96
|
+
] = "runtime.review.adversarial-team@1"
|
|
97
|
+
host: RuntimeHost
|
|
98
|
+
executable: Path
|
|
99
|
+
working_directory: Path
|
|
100
|
+
task: str = Field(min_length=1, max_length=20_000)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TaskBoardSummaryMode(StrEnum):
|
|
104
|
+
"""How one prior task result is shrunk before the next agent reads it."""
|
|
105
|
+
|
|
106
|
+
TRUNCATE_TAIL = "truncate-tail"
|
|
107
|
+
HEAD_TAIL = "head-tail"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TaskBoardContextMode(StrEnum):
|
|
111
|
+
"""Whether a task agent reads prior task results at all."""
|
|
112
|
+
|
|
113
|
+
WINDOWED_SUMMARIES = "windowed-summaries"
|
|
114
|
+
ISOLATED = "isolated"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class TaskBoardHandoffMode(StrEnum):
|
|
118
|
+
"""What a finished task hands forward to the tasks allowed to read it."""
|
|
119
|
+
|
|
120
|
+
SUMMARY = "summary"
|
|
121
|
+
TASK_AND_SUMMARY = "task-and-summary"
|
|
122
|
+
FULL_RESULT = "full-result"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TaskBoardCheckpointMode(StrEnum):
|
|
126
|
+
"""What happens, beyond the durable save, each time a step is checkpointed."""
|
|
127
|
+
|
|
128
|
+
SAVE_ONLY = "save-only"
|
|
129
|
+
STREAM = "stream"
|
|
130
|
+
EXPORT = "export"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TaskBoardSandbox(StrEnum):
|
|
134
|
+
"""Filesystem authority granted to every task agent on one board."""
|
|
135
|
+
|
|
136
|
+
READ_ONLY = "read-only"
|
|
137
|
+
WORKSPACE_WRITE = "workspace-write"
|
|
138
|
+
FULL_ACCESS = "full-access"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TaskBoardReasoningEffort(StrEnum):
|
|
142
|
+
"""Reasoning budget requested per task turn; provider-default leaves it unset."""
|
|
143
|
+
|
|
144
|
+
PROVIDER_DEFAULT = ""
|
|
145
|
+
MINIMAL = "minimal"
|
|
146
|
+
LOW = "low"
|
|
147
|
+
MEDIUM = "medium"
|
|
148
|
+
HIGH = "high"
|
|
149
|
+
XHIGH = "xhigh"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TaskBoardAgentSettings(BaseModel):
|
|
153
|
+
"""The Codex agent configuration every iteration of one board is built with."""
|
|
154
|
+
|
|
155
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
156
|
+
model: str = Field(
|
|
157
|
+
default="",
|
|
158
|
+
max_length=100,
|
|
159
|
+
description=(
|
|
160
|
+
"Which Codex model every task agent on this board runs, given as the provider's "
|
|
161
|
+
"own model identifier. Leaving it empty keeps whatever model the installed Codex "
|
|
162
|
+
"host defaults to, which is the right choice when the board is not model-sensitive. "
|
|
163
|
+
"Set it when a board needs a specific capability or price point, because every "
|
|
164
|
+
"iteration of the board is built with the same value and no task can override it. "
|
|
165
|
+
"The model is charged to the caller's own provider account, not to Vidbyte."
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
sandbox: TaskBoardSandbox = Field(
|
|
169
|
+
default=TaskBoardSandbox.WORKSPACE_WRITE,
|
|
170
|
+
description=(
|
|
171
|
+
"How much of the filesystem every task agent on this board may change. "
|
|
172
|
+
"workspace-write, the default, lets a task edit files inside the working directory "
|
|
173
|
+
"and is what a board of implementation tasks needs. read-only lets a task inspect "
|
|
174
|
+
"the tree without writing to it, which is the correct setting for review, audit, "
|
|
175
|
+
"or planning boards. full-access removes the sandbox entirely and should be "
|
|
176
|
+
"reserved for boards whose tasks genuinely have to reach outside the workspace."
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
reasoning_effort: TaskBoardReasoningEffort = Field(
|
|
180
|
+
default=TaskBoardReasoningEffort.PROVIDER_DEFAULT,
|
|
181
|
+
description=(
|
|
182
|
+
"How much reasoning each task turn asks the model to spend before answering. An "
|
|
183
|
+
"empty value leaves the provider default in place, which is what most boards want. "
|
|
184
|
+
"Raising it to high or xhigh suits a board of a few hard tasks, while minimal or "
|
|
185
|
+
"low suits a long board of mechanical ones. Effort multiplies the caller's own "
|
|
186
|
+
"provider bill per task, so a 500-task board at xhigh is a real cost decision."
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
turn_timeout_seconds: int = Field(
|
|
190
|
+
ge=60,
|
|
191
|
+
le=5 * 24 * 60 * 60,
|
|
192
|
+
default=5 * 24 * 60 * 60,
|
|
193
|
+
description=(
|
|
194
|
+
"How long one task turn may run before the board cancels it and counts the attempt "
|
|
195
|
+
"as failed. The default of five days is a ceiling on a wedged child process rather "
|
|
196
|
+
"than a budget any task is expected to approach. Lower it when a board's tasks are "
|
|
197
|
+
"small and a stuck agent should surface quickly instead of blocking the run. The "
|
|
198
|
+
"timeout applies per attempt, so retries each get the full allowance again."
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class TaskBoardRunControls(BaseModel):
|
|
204
|
+
"""Where one invocation starts, how far it may go, and what it may spend."""
|
|
205
|
+
|
|
206
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
207
|
+
checkpoint_mode: TaskBoardCheckpointMode = TaskBoardCheckpointMode.SAVE_ONLY
|
|
208
|
+
export_file: str = Field(default="", max_length=4096)
|
|
209
|
+
report_file: str = Field(default="", max_length=4096)
|
|
210
|
+
on_checkpoint: str = Field(default="", max_length=4096)
|
|
211
|
+
start_from: int = Field(ge=0, default=0)
|
|
212
|
+
replay_index: int | None = Field(ge=0, default=None)
|
|
213
|
+
stop_after: int | None = Field(ge=1, default=None)
|
|
214
|
+
retry_failed_only: bool = False
|
|
215
|
+
max_tokens: int | None = Field(ge=1, default=None)
|
|
216
|
+
max_cost_usd: float | None = Field(gt=0, default=None)
|
|
217
|
+
usd_per_million_tokens: float = Field(ge=0, default=10.0)
|
|
218
|
+
|
|
219
|
+
def cost_of(self, tokens: int) -> float:
|
|
220
|
+
# One rate for the whole board, so a per-step estimate and the total never disagree.
|
|
221
|
+
return round(tokens / 1_000_000 * self.usd_per_million_tokens, 6)
|
|
222
|
+
|
|
223
|
+
def exhausted(self, tokens: int) -> str | None:
|
|
224
|
+
# Names the first breached budget so the caller learns why the board stopped early.
|
|
225
|
+
if self.max_tokens is not None and tokens >= self.max_tokens:
|
|
226
|
+
return "max-tokens"
|
|
227
|
+
if self.max_cost_usd is not None and self.cost_of(tokens) >= self.max_cost_usd:
|
|
228
|
+
return "max-cost"
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@dataclass(frozen=True, slots=True)
|
|
233
|
+
class TaskBoardPrefix:
|
|
234
|
+
"""Where a resumed board starts in execution order and what its stored steps achieved."""
|
|
235
|
+
|
|
236
|
+
start_index: int
|
|
237
|
+
completed: int
|
|
238
|
+
failed: int
|
|
239
|
+
total_tokens: int
|
|
240
|
+
replay_indices: tuple[int, ...] = ()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass(frozen=True, slots=True)
|
|
244
|
+
class TaskBoardTurn:
|
|
245
|
+
"""One finished agent turn: the prompt it read, what it produced, what it cost."""
|
|
246
|
+
|
|
247
|
+
prompt: str
|
|
248
|
+
summary: str
|
|
249
|
+
result_text: str
|
|
250
|
+
thread_id: str
|
|
251
|
+
total_tokens: int | None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@dataclass(frozen=True, slots=True)
|
|
255
|
+
class TaskBoardTaskOutcome:
|
|
256
|
+
"""One task attempt result plus subtasks accepted by its native tool, if any."""
|
|
257
|
+
|
|
258
|
+
turn: TaskBoardTurn | None
|
|
259
|
+
note: str
|
|
260
|
+
subtasks: tuple[str, ...] = ()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class TaskBoardExecutionType(StrEnum):
|
|
264
|
+
"""Which loop structure a board run follows."""
|
|
265
|
+
|
|
266
|
+
LINEAR = "linear"
|
|
267
|
+
DAG = "dag"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class TaskBoardSettings(BaseModel):
|
|
271
|
+
"""Bounded, frozen task-board settings for one admitted local invocation."""
|
|
272
|
+
|
|
273
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
274
|
+
tasks: tuple[str, ...] = Field(
|
|
275
|
+
min_length=1,
|
|
276
|
+
max_length=500,
|
|
277
|
+
description=(
|
|
278
|
+
"The ordered board of work, where every entry is one complete task statement that "
|
|
279
|
+
"one Codex agent has to finish on its own. Position is meaningful: task N runs only "
|
|
280
|
+
"after tasks 0 through N-1 have been attempted, and its agent can be given the "
|
|
281
|
+
"results of the tasks immediately before it. Entries arrive either as literal task "
|
|
282
|
+
"strings or as the whole text of one Markdown task file each, and the board never "
|
|
283
|
+
"reorders, merges, or splits what it was given. A board holds between 1 and 500 "
|
|
284
|
+
"tasks, and no single task may exceed 20,000 characters."
|
|
285
|
+
),
|
|
286
|
+
)
|
|
287
|
+
window: int = Field(
|
|
288
|
+
ge=0,
|
|
289
|
+
le=25,
|
|
290
|
+
default=10,
|
|
291
|
+
description=(
|
|
292
|
+
"How many immediately preceding task results the next agent is allowed to read, "
|
|
293
|
+
"counted backwards from the task about to run. With a window of 10, the agent for "
|
|
294
|
+
"task 90 receives summaries of tasks 80 through 89 and nothing earlier, which is "
|
|
295
|
+
"what keeps prompt size flat as the board grows. A window of 0 gives an agent only "
|
|
296
|
+
"its own task while still telling it that earlier tasks ran, and a window larger "
|
|
297
|
+
"than the number of finished tasks simply clamps to what exists. This setting is "
|
|
298
|
+
"ignored entirely when the context mode is isolated."
|
|
299
|
+
),
|
|
300
|
+
)
|
|
301
|
+
context_mode: TaskBoardContextMode = Field(
|
|
302
|
+
default=TaskBoardContextMode.WINDOWED_SUMMARIES,
|
|
303
|
+
description=(
|
|
304
|
+
"Whether an agent is told anything about the tasks that ran before it. In "
|
|
305
|
+
"windowed-summaries mode each agent reads bounded summaries of the previous window "
|
|
306
|
+
"results, which suits a board whose tasks build on one another. In isolated mode no "
|
|
307
|
+
"prior result reaches any agent and the prompt carries no prior-results section at "
|
|
308
|
+
"all, which is the correct choice for a board of independently decomposed tasks "
|
|
309
|
+
"that must not inherit each other's assumptions. Isolated mode overrides the window "
|
|
310
|
+
"and summary settings rather than combining with them."
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
summary_mode: TaskBoardSummaryMode = Field(
|
|
314
|
+
default=TaskBoardSummaryMode.TRUNCATE_TAIL,
|
|
315
|
+
description=(
|
|
316
|
+
"The shape of each prior-result summary once that result is longer than the "
|
|
317
|
+
"character budget. In truncate-tail mode the summary keeps the opening of the "
|
|
318
|
+
"result and names how many characters were dropped, which favors setup and "
|
|
319
|
+
"reasoning. In head-tail mode the budget is split evenly between the opening and "
|
|
320
|
+
"the closing of the result, which is what preserves a concluding answer or a final "
|
|
321
|
+
"file listing. Neither mode calls a model, so summarization stays deterministic "
|
|
322
|
+
"and free, and this setting does nothing in isolated context mode."
|
|
323
|
+
),
|
|
324
|
+
)
|
|
325
|
+
summary_max_chars: int = Field(
|
|
326
|
+
ge=100,
|
|
327
|
+
le=8000,
|
|
328
|
+
default=1200,
|
|
329
|
+
description=(
|
|
330
|
+
"The character budget for one prior-result summary before the summary mode starts "
|
|
331
|
+
"dropping content. A result at or under this length is passed through untouched, "
|
|
332
|
+
"and anything longer is shortened and marked with the number of characters removed. "
|
|
333
|
+
"Multiply this budget by the window to predict the worst-case size of the "
|
|
334
|
+
"prior-results block a single agent will read. Accepted values run from 100 to "
|
|
335
|
+
"8000 characters, and this setting does nothing in isolated context mode."
|
|
336
|
+
),
|
|
337
|
+
)
|
|
338
|
+
stop_on_error: bool = Field(
|
|
339
|
+
default=True,
|
|
340
|
+
description=(
|
|
341
|
+
"What the board does the first time a task exhausts its retries. When enabled the "
|
|
342
|
+
"board halts immediately, keeps every result completed before the failure, and "
|
|
343
|
+
"returns that prefix rather than running work that depends on a task that never "
|
|
344
|
+
"finished. When disabled the failed task is recorded with a failed status, a short "
|
|
345
|
+
"placeholder stands in for its summary so board indices stay aligned, and the next "
|
|
346
|
+
"task starts anyway. Leave it enabled for a dependent board and disable it for a "
|
|
347
|
+
"board of independent tasks where one failure should not cancel the rest."
|
|
348
|
+
),
|
|
349
|
+
)
|
|
350
|
+
max_retries_per_task: int = Field(
|
|
351
|
+
ge=0,
|
|
352
|
+
le=3,
|
|
353
|
+
default=1,
|
|
354
|
+
description=(
|
|
355
|
+
"How many extra attempts one task receives after its first attempt fails, before "
|
|
356
|
+
"the board treats that task as failed. Every retry re-sends the identical prompt "
|
|
357
|
+
"to a brand-new agent, so a retry recovers from a crashed or timed-out host rather "
|
|
358
|
+
"than from a task the agent understood but could not do. Each attempt is charged "
|
|
359
|
+
"against the caller's own model account, so a high retry count on a large board "
|
|
360
|
+
"multiplies model cost. Accepted values run from 0 to 3, and a completed retry "
|
|
361
|
+
"produces exactly one result entry, never a duplicate."
|
|
362
|
+
),
|
|
363
|
+
)
|
|
364
|
+
handoff_mode: TaskBoardHandoffMode = Field(
|
|
365
|
+
default=TaskBoardHandoffMode.SUMMARY,
|
|
366
|
+
description=(
|
|
367
|
+
"What a finished task actually hands to the later tasks allowed to read it. In "
|
|
368
|
+
"summary mode, the default, a reader sees only the bounded summary of each prior "
|
|
369
|
+
"result, which is what keeps prompts flat on a long board. In task-and-summary mode "
|
|
370
|
+
"each entry is prefixed with the prior task's own statement, so a reader learns "
|
|
371
|
+
"what was asked as well as what came back. In full-result mode the untruncated "
|
|
372
|
+
"result text is forwarded instead, which is accurate but grows prompts without "
|
|
373
|
+
"bound and suits only short boards. This setting does nothing in isolated mode."
|
|
374
|
+
),
|
|
375
|
+
)
|
|
376
|
+
agent: TaskBoardAgentSettings = Field(
|
|
377
|
+
default_factory=TaskBoardAgentSettings,
|
|
378
|
+
description=(
|
|
379
|
+
"The Codex agent configuration each iteration of the board is constructed with, "
|
|
380
|
+
"covering model, sandbox authority, reasoning effort, and per-attempt timeout. One "
|
|
381
|
+
"configuration applies to every task, because a board whose agents differ task by "
|
|
382
|
+
"task would make its results incomparable and its cost unpredictable. The defaults "
|
|
383
|
+
"reproduce the behavior a board had before these controls existed. Every field here "
|
|
384
|
+
"affects the caller's own provider bill rather than the flat Vidbyte admission."
|
|
385
|
+
),
|
|
386
|
+
)
|
|
387
|
+
execution_type: TaskBoardExecutionType = Field(
|
|
388
|
+
default=TaskBoardExecutionType.LINEAR,
|
|
389
|
+
description=(
|
|
390
|
+
"Which loop structure the board run follows. In linear mode tasks run in board "
|
|
391
|
+
"order 0 through N-1 and each agent reads bounded summaries of the immediately "
|
|
392
|
+
"preceding window tasks, which preserves the original board behavior exactly. In "
|
|
393
|
+
"dag mode tasks run once each in deterministic topological order and each agent "
|
|
394
|
+
"reads only its direct dependencies' summaries, so an independent task never pays "
|
|
395
|
+
"for unrelated context. Execution stays sequential in both modes with one fresh "
|
|
396
|
+
"agent per task; dag mode ignores the window for context selection."
|
|
397
|
+
),
|
|
398
|
+
)
|
|
399
|
+
dependencies: tuple[tuple[int, int], ...] = Field(
|
|
400
|
+
default=(),
|
|
401
|
+
description=(
|
|
402
|
+
"The directed dependency links as sorted unique child-parent index pairs, where "
|
|
403
|
+
"each pair means the child task reads the parent task's summary and runs only "
|
|
404
|
+
"after it. Indices are 0-based board positions, so 8:2 means task 8 depends on "
|
|
405
|
+
"task 2. Links are accepted only in dag mode and must form an acyclic graph with "
|
|
406
|
+
"no self-links or duplicates; an empty set means every task is independent and "
|
|
407
|
+
"runs in board order with empty dependency context."
|
|
408
|
+
),
|
|
409
|
+
)
|
|
410
|
+
allow_decompose: bool = Field(
|
|
411
|
+
default=False,
|
|
412
|
+
description=(
|
|
413
|
+
"Whether a task's agent may call the native decompose_tool to replace its current "
|
|
414
|
+
"task with an ordered list of self-contained subtasks that run in its place. The "
|
|
415
|
+
"default is off and preserves the normal linear, DAG, windowed-context, and "
|
|
416
|
+
"checkpoint behavior exactly. When enabled, the board runs as a mutable linear "
|
|
417
|
+
"list: an accepted split is spliced in at the parent's position, every later task "
|
|
418
|
+
"shifts right, and the children run one at a time before anything after them. "
|
|
419
|
+
"Every parent and child runs isolated, so no agent reads prior summaries, and each "
|
|
420
|
+
"child sees only its own subtask string. Children are never offered the tool, "
|
|
421
|
+
"which keeps decomposition to exactly one level. Because a splice moves board "
|
|
422
|
+
"indices while the run is in progress, the command rejects this setting together "
|
|
423
|
+
"with checkpointing or DAG execution before any credential, payment, or agent."
|
|
424
|
+
),
|
|
425
|
+
)
|
|
426
|
+
max_subtasks: int = Field(
|
|
427
|
+
ge=2,
|
|
428
|
+
le=10,
|
|
429
|
+
default=5,
|
|
430
|
+
description=(
|
|
431
|
+
"The maximum number of child tasks one parent may create in a single "
|
|
432
|
+
"decompose_tool call, between 2 and 10, and it has no effect unless "
|
|
433
|
+
"allow_decompose is true. The default of 5 separates the distinct pieces of a "
|
|
434
|
+
"broad task without letting one parent fan out into many small billed turns. "
|
|
435
|
+
"Candidates are stripped, entries that are blank or longer than the board's "
|
|
436
|
+
"20,000-character task limit are dropped, and case-insensitive duplicates keep "
|
|
437
|
+
"only their first occurrence before this cap truncates the remainder. A call left "
|
|
438
|
+
"with fewer than two valid children is rejected, so the parent's task stays whole "
|
|
439
|
+
"and its own final text becomes the result. The board-wide ceiling of 500 live "
|
|
440
|
+
"tasks still applies, so a parent near that ceiling receives a smaller effective "
|
|
441
|
+
"cap, and one with room for fewer than two children is not offered the tool."
|
|
442
|
+
),
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
@model_validator(mode="after")
|
|
446
|
+
def validate_dependencies(self) -> TaskBoardSettings:
|
|
447
|
+
# Rejects dangling, reflexive, repeated, or cyclic links before paid admission.
|
|
448
|
+
count = len(self.tasks)
|
|
449
|
+
seen: set[tuple[int, int]] = set()
|
|
450
|
+
for child, parent in self.dependencies:
|
|
451
|
+
if child < 0 or parent < 0 or child >= count or parent >= count:
|
|
452
|
+
raise ValueError("dependency indices must reference board positions")
|
|
453
|
+
if child == parent:
|
|
454
|
+
raise ValueError("a task cannot depend on itself")
|
|
455
|
+
if (child, parent) in seen:
|
|
456
|
+
raise ValueError("duplicate dependency link")
|
|
457
|
+
seen.add((child, parent))
|
|
458
|
+
if seen and self.topological_order(count, self.dependencies) is None:
|
|
459
|
+
raise ValueError("dependency links must form an acyclic graph")
|
|
460
|
+
return self
|
|
461
|
+
|
|
462
|
+
def execution_order(self) -> tuple[int, ...]:
|
|
463
|
+
# The one order a board runs, resumes, replays, and forks in. A linear board runs in
|
|
464
|
+
# board order; a DAG board runs parents first. `--from N` and `fork --at N` count
|
|
465
|
+
# positions in this order, so they mean the same thing for both execution types.
|
|
466
|
+
if self.execution_type is TaskBoardExecutionType.LINEAR:
|
|
467
|
+
return tuple(range(len(self.tasks)))
|
|
468
|
+
return self.topological_order(len(self.tasks), self.dependencies) or ()
|
|
469
|
+
|
|
470
|
+
@staticmethod
|
|
471
|
+
def topological_order(count: int, edges: tuple[tuple[int, int], ...]) -> tuple[int, ...] | None:
|
|
472
|
+
# Kahn's algorithm with ties broken by the smallest index, so one board always yields
|
|
473
|
+
# one order. None means a cycle kept some task from ever becoming ready.
|
|
474
|
+
children: dict[int, list[int]] = {index: [] for index in range(count)}
|
|
475
|
+
pending: dict[int, int] = {index: 0 for index in range(count)}
|
|
476
|
+
for child, parent in edges:
|
|
477
|
+
children[parent].append(child)
|
|
478
|
+
pending[child] += 1
|
|
479
|
+
ready = sorted(index for index in range(count) if pending[index] == 0)
|
|
480
|
+
order: list[int] = []
|
|
481
|
+
while ready:
|
|
482
|
+
current = ready.pop(0)
|
|
483
|
+
order.append(current)
|
|
484
|
+
for child in children[current]:
|
|
485
|
+
pending[child] -= 1
|
|
486
|
+
if pending[child] == 0:
|
|
487
|
+
ready.append(child)
|
|
488
|
+
ready.sort()
|
|
489
|
+
return tuple(order) if len(order) == count else None
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class TaskBoardStepResult(BaseModel):
|
|
493
|
+
"""One task outcome with its bounded summary for downstream agents."""
|
|
494
|
+
|
|
495
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
496
|
+
index: int = Field(ge=0)
|
|
497
|
+
task: str = Field(min_length=1, max_length=20_000)
|
|
498
|
+
summary: str = Field(min_length=1, max_length=8000)
|
|
499
|
+
status: Literal["completed", "failed"]
|
|
500
|
+
thread_id: str = Field(min_length=1, max_length=128)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
class TaskBoardCheckpoint(BaseModel):
|
|
504
|
+
"""One durably stored step: prompt, outcome, usage, and its link to the step before."""
|
|
505
|
+
|
|
506
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
507
|
+
board_id: str = Field(min_length=1, max_length=64)
|
|
508
|
+
index: int = Field(ge=0)
|
|
509
|
+
# The link that makes stored steps a list rather than a bag: it names the step this one
|
|
510
|
+
# actually read, which after a fork is a step that lives in the parent board's directory.
|
|
511
|
+
parent_index: int | None = Field(default=None, ge=0)
|
|
512
|
+
task: str = Field(min_length=1, max_length=20_000)
|
|
513
|
+
prompt: str = Field(min_length=1, max_length=220_000)
|
|
514
|
+
summary: str = Field(min_length=1, max_length=8000)
|
|
515
|
+
result_text: str = Field(default="", max_length=220_000)
|
|
516
|
+
status: Literal["completed", "failed"]
|
|
517
|
+
thread_id: str = Field(min_length=1, max_length=128)
|
|
518
|
+
total_tokens: int | None = Field(default=None, ge=0)
|
|
519
|
+
estimated_cost_usd: float | None = Field(default=None, ge=0)
|
|
520
|
+
admission_id: str = Field(min_length=1, max_length=128)
|
|
521
|
+
created_at: datetime
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
class TaskBoardCheckpointChain(BaseModel):
|
|
525
|
+
"""One board's stored steps as a linked list, plus the fork edge to its parent board."""
|
|
526
|
+
|
|
527
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
528
|
+
board_id: str = Field(min_length=1, max_length=64)
|
|
529
|
+
board_dir: str = Field(min_length=1, max_length=4096)
|
|
530
|
+
parent_board_id: str | None = Field(default=None, min_length=1, max_length=64)
|
|
531
|
+
forked_at_index: int | None = Field(default=None, ge=0)
|
|
532
|
+
task_count: int = Field(ge=0)
|
|
533
|
+
nodes: tuple[TaskBoardCheckpoint, ...] = ()
|
|
534
|
+
|
|
535
|
+
def head(self) -> TaskBoardCheckpoint | None:
|
|
536
|
+
# The furthest step reached, which is where a resume continues from.
|
|
537
|
+
return max(self.nodes, key=lambda node: node.index, default=None)
|
|
538
|
+
|
|
539
|
+
def node_at(self, index: int) -> TaskBoardCheckpoint | None:
|
|
540
|
+
# Direct addressing, because stored indices can be sparse after a replay or repair.
|
|
541
|
+
return next((node for node in self.nodes if node.index == index), None)
|
|
542
|
+
|
|
543
|
+
def ancestors_of(self, index: int) -> tuple[TaskBoardCheckpoint, ...]:
|
|
544
|
+
# Walks parent_index backwards, so a caller sees the exact chain a step was built on
|
|
545
|
+
# rather than every lower index that happens to sit in the same directory.
|
|
546
|
+
walked: list[TaskBoardCheckpoint] = []
|
|
547
|
+
seen: set[int] = set()
|
|
548
|
+
cursor = self.node_at(index)
|
|
549
|
+
while cursor is not None and cursor.parent_index is not None:
|
|
550
|
+
if cursor.parent_index in seen:
|
|
551
|
+
break
|
|
552
|
+
seen.add(cursor.parent_index)
|
|
553
|
+
cursor = self.node_at(cursor.parent_index)
|
|
554
|
+
if cursor is not None:
|
|
555
|
+
walked.append(cursor)
|
|
556
|
+
return tuple(reversed(walked))
|
|
557
|
+
|
|
558
|
+
@property
|
|
559
|
+
def completed_indices(self) -> tuple[int, ...]:
|
|
560
|
+
return tuple(sorted(node.index for node in self.nodes if node.status == "completed"))
|
|
561
|
+
|
|
562
|
+
@property
|
|
563
|
+
def failed_indices(self) -> tuple[int, ...]:
|
|
564
|
+
return tuple(sorted(node.index for node in self.nodes if node.status == "failed"))
|
|
565
|
+
|
|
566
|
+
@property
|
|
567
|
+
def pending_indices(self) -> tuple[int, ...]:
|
|
568
|
+
stored = {node.index for node in self.nodes}
|
|
569
|
+
return tuple(index for index in range(self.task_count) if index not in stored)
|
|
570
|
+
|
|
571
|
+
@property
|
|
572
|
+
def total_tokens(self) -> int:
|
|
573
|
+
return sum(node.total_tokens or 0 for node in self.nodes)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
class TaskBoardResult(BaseModel):
|
|
577
|
+
"""Final local output: per-task summaries plus where the board lives and how to continue."""
|
|
578
|
+
|
|
579
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
580
|
+
admission_id: str = Field(min_length=1, max_length=128)
|
|
581
|
+
completed: int = Field(ge=0)
|
|
582
|
+
failed: int = Field(ge=0)
|
|
583
|
+
steps: tuple[TaskBoardStepResult, ...]
|
|
584
|
+
board_id: str | None = Field(default=None, min_length=1, max_length=64)
|
|
585
|
+
board_dir: str | None = Field(default=None, min_length=1, max_length=4096)
|
|
586
|
+
export_file: str | None = Field(default=None, min_length=1, max_length=4096)
|
|
587
|
+
report_file: str | None = Field(default=None, min_length=1, max_length=4096)
|
|
588
|
+
resume_command: str | None = Field(default=None, min_length=1, max_length=4096)
|
|
589
|
+
total_tokens: int | None = Field(default=None, ge=0)
|
|
590
|
+
estimated_cost_usd: float | None = Field(default=None, ge=0)
|
|
591
|
+
stopped_reason: str | None = Field(default=None, min_length=1, max_length=64)
|
|
592
|
+
text: str
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
class TaskBoardBoardSummary(BaseModel):
|
|
596
|
+
"""One row of the board index: identity, progress, and where the board lives."""
|
|
597
|
+
|
|
598
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
599
|
+
board_id: str = Field(min_length=1, max_length=64)
|
|
600
|
+
board_dir: str = Field(min_length=1, max_length=4096)
|
|
601
|
+
task_count: int = Field(ge=0)
|
|
602
|
+
completed: int = Field(ge=0)
|
|
603
|
+
failed: int = Field(ge=0)
|
|
604
|
+
pending: int = Field(ge=0)
|
|
605
|
+
parent_board_id: str | None = Field(default=None, min_length=1, max_length=64)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
class TaskBoardUnreadableBoard(BaseModel):
|
|
609
|
+
"""One board the index found but could not load, with the failure's own repair advice."""
|
|
610
|
+
|
|
611
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
612
|
+
board_id: str = Field(min_length=1, max_length=64)
|
|
613
|
+
board_dir: str = Field(min_length=1, max_length=4096)
|
|
614
|
+
problem: str = Field(min_length=1, max_length=1000)
|
|
615
|
+
hint: str = Field(default="", max_length=1000)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
@dataclass(frozen=True, slots=True)
|
|
619
|
+
class TaskBoardIndex:
|
|
620
|
+
"""Every board under one checkpoint root, split into loadable and unreadable."""
|
|
621
|
+
|
|
622
|
+
boards: tuple[TaskBoardBoardSummary, ...]
|
|
623
|
+
unreadable: tuple[TaskBoardUnreadableBoard, ...]
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
class TaskBoardListing(BaseModel):
|
|
627
|
+
"""Every board stored under one checkpoint root, newest progress first."""
|
|
628
|
+
|
|
629
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
630
|
+
checkpoint_root: str = Field(min_length=1, max_length=4096)
|
|
631
|
+
boards: tuple[TaskBoardBoardSummary, ...] = ()
|
|
632
|
+
unreadable: tuple[TaskBoardUnreadableBoard, ...] = ()
|
|
633
|
+
text: str
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
class TaskBoardStatusReport(BaseModel):
|
|
637
|
+
"""One board's offline progress, spend, and the command that continues it."""
|
|
638
|
+
|
|
639
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
640
|
+
board_id: str = Field(min_length=1, max_length=64)
|
|
641
|
+
board_dir: str = Field(min_length=1, max_length=4096)
|
|
642
|
+
task_count: int = Field(ge=0)
|
|
643
|
+
completed_indices: tuple[int, ...] = ()
|
|
644
|
+
failed_indices: tuple[int, ...] = ()
|
|
645
|
+
pending_indices: tuple[int, ...] = ()
|
|
646
|
+
total_tokens: int = Field(ge=0)
|
|
647
|
+
estimated_cost_usd: float = Field(ge=0)
|
|
648
|
+
parent_board_id: str | None = Field(default=None, min_length=1, max_length=64)
|
|
649
|
+
report_file: str | None = Field(default=None, min_length=1, max_length=4096)
|
|
650
|
+
resume_command: str = Field(min_length=1, max_length=4096)
|
|
651
|
+
text: str
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
class TaskBoardStepDetail(BaseModel):
|
|
655
|
+
"""Everything stored for one step, so inspecting it costs one call and no file reads."""
|
|
656
|
+
|
|
657
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
658
|
+
board_id: str = Field(min_length=1, max_length=64)
|
|
659
|
+
board_dir: str = Field(min_length=1, max_length=4096)
|
|
660
|
+
step_file: str = Field(min_length=1, max_length=4096)
|
|
661
|
+
step: TaskBoardCheckpoint
|
|
662
|
+
ancestors: tuple[int, ...] = ()
|
|
663
|
+
text: str
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
class TaskBoardForkResult(BaseModel):
|
|
667
|
+
"""A new board carrying a copy of another board's prefix, and how to continue it."""
|
|
668
|
+
|
|
669
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
670
|
+
board_id: str = Field(min_length=1, max_length=64)
|
|
671
|
+
board_dir: str = Field(min_length=1, max_length=4096)
|
|
672
|
+
parent_board_id: str = Field(min_length=1, max_length=64)
|
|
673
|
+
forked_at_index: int = Field(ge=0)
|
|
674
|
+
copied_steps: int = Field(ge=0)
|
|
675
|
+
resume_command: str = Field(min_length=1, max_length=4096)
|
|
676
|
+
text: str
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
class TaskBoardPromptPreview(BaseModel):
|
|
680
|
+
"""The exact prompt one step would receive, rebuilt without starting any agent."""
|
|
681
|
+
|
|
682
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
683
|
+
board_id: str = Field(min_length=1, max_length=64)
|
|
684
|
+
board_dir: str = Field(min_length=1, max_length=4096)
|
|
685
|
+
index: int = Field(ge=0)
|
|
686
|
+
prompt: str = Field(min_length=1, max_length=220_000)
|
|
687
|
+
text: str
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
class TaskBoardTaskExport(BaseModel):
|
|
691
|
+
"""A board's task list written back out as a reviewable file."""
|
|
692
|
+
|
|
693
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
694
|
+
board_id: str = Field(min_length=1, max_length=64)
|
|
695
|
+
path: str = Field(min_length=1, max_length=4096)
|
|
696
|
+
task_count: int = Field(ge=1)
|
|
697
|
+
text: str
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
class PersistenceStrength(IntEnum):
|
|
701
|
+
"""The six caller-facing persistence tiers, from lightest to most insistent."""
|
|
702
|
+
|
|
703
|
+
TIER_1 = 1
|
|
704
|
+
TIER_2 = 2
|
|
705
|
+
TIER_3 = 3
|
|
706
|
+
TIER_4 = 4
|
|
707
|
+
TIER_5 = 5
|
|
708
|
+
TIER_6 = 6
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
_PERSISTENCE_REPEAT_COUNTS: Mapping[PersistenceStrength, int] = {
|
|
712
|
+
PersistenceStrength.TIER_1: 6,
|
|
713
|
+
PersistenceStrength.TIER_2: 8,
|
|
714
|
+
PersistenceStrength.TIER_3: 20,
|
|
715
|
+
PersistenceStrength.TIER_4: 40,
|
|
716
|
+
PersistenceStrength.TIER_5: 70,
|
|
717
|
+
PersistenceStrength.TIER_6: 100,
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
class PersistenceSettings(BaseModel):
|
|
722
|
+
"""Bounded, frozen persistence-primitive settings for a future executor."""
|
|
723
|
+
|
|
724
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
725
|
+
strength: PersistenceStrength
|
|
726
|
+
|
|
727
|
+
@property
|
|
728
|
+
def repeat_count(self) -> int:
|
|
729
|
+
# Resolves the caller-facing tier to its fixed continuation-turn count.
|
|
730
|
+
return _PERSISTENCE_REPEAT_COUNTS[self.strength]
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
@dataclass(frozen=True, slots=True)
|
|
734
|
+
class RuntimeAdmissionCheck:
|
|
735
|
+
"""One deterministic check with a typed reason instead of string/None sentinels."""
|
|
736
|
+
|
|
737
|
+
reason: AdmissionReason
|
|
738
|
+
|
|
739
|
+
@property
|
|
740
|
+
def passed(self) -> bool:
|
|
741
|
+
return self.reason is AdmissionReason.PASSED
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
class RuntimeAdmissionVerdict(BaseModel):
|
|
745
|
+
"""Deterministic gate result that the executor requires before spawning."""
|
|
746
|
+
|
|
747
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
748
|
+
admitted: bool
|
|
749
|
+
admission_id: str = Field(min_length=1, max_length=128)
|
|
750
|
+
capability_id: str = Field(min_length=1, max_length=160)
|
|
751
|
+
reason: str | None = None
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
class RuntimeGrantVerificationRequest(BaseModel):
|
|
755
|
+
"""Proof sent only to the authenticated backend, never to a model."""
|
|
756
|
+
|
|
757
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
758
|
+
grant_token: str = Field(min_length=10, max_length=8192)
|
|
759
|
+
idempotency_key_hash: str = Field(pattern=r"^[0-9a-f]{64}$")
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
class PersistenceResult(BaseModel):
|
|
763
|
+
"""Final local output with deterministic completed-turn accounting."""
|
|
764
|
+
|
|
765
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
766
|
+
session_id: str
|
|
767
|
+
continuation_turns: int
|
|
768
|
+
text: str
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
class StageSandbox(StrEnum):
|
|
772
|
+
"""Filesystem and network reach one stage's agent runs under."""
|
|
773
|
+
|
|
774
|
+
READ_ONLY = "read-only"
|
|
775
|
+
WORKSPACE_WRITE = "workspace-write"
|
|
776
|
+
FULL_ACCESS = "full-access"
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
class StageEffort(StrEnum):
|
|
780
|
+
"""How much reasoning one stage's agent spends before it answers."""
|
|
781
|
+
|
|
782
|
+
NONE = "none"
|
|
783
|
+
MINIMAL = "minimal"
|
|
784
|
+
LOW = "low"
|
|
785
|
+
MEDIUM = "medium"
|
|
786
|
+
HIGH = "high"
|
|
787
|
+
XHIGH = "xhigh"
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
class StageSummary(StrEnum):
|
|
791
|
+
"""How much of its reasoning one stage's agent reports back."""
|
|
792
|
+
|
|
793
|
+
NONE = "none"
|
|
794
|
+
AUTO = "auto"
|
|
795
|
+
CONCISE = "concise"
|
|
796
|
+
DETAILED = "detailed"
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
class StageApproval(StrEnum):
|
|
800
|
+
"""What one stage's agent does when an action needs approval."""
|
|
801
|
+
|
|
802
|
+
AUTO_REVIEW = "auto_review"
|
|
803
|
+
DENY_ALL = "deny_all"
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
class StagePersonality(StrEnum):
|
|
807
|
+
"""Response personality one stage's agent writes in."""
|
|
808
|
+
|
|
809
|
+
NONE = "none"
|
|
810
|
+
FRIENDLY = "friendly"
|
|
811
|
+
PRAGMATIC = "pragmatic"
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
class StageSpec(BaseModel):
|
|
815
|
+
"""One caller-defined stage mapping to exactly one fresh Codex agent."""
|
|
816
|
+
|
|
817
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
818
|
+
name: str = Field(
|
|
819
|
+
min_length=1,
|
|
820
|
+
max_length=64,
|
|
821
|
+
description=(
|
|
822
|
+
"A short label for this stage, used as the agent's name and in progress output. It "
|
|
823
|
+
"identifies the stage in results and in any failure, so make it say what the stage "
|
|
824
|
+
"does rather than where it sits in the order. Names are not required to be unique "
|
|
825
|
+
"and are never sent to the backend with the admission request. Position in the "
|
|
826
|
+
"stage list, not this name, is what decides when a stage runs."
|
|
827
|
+
),
|
|
828
|
+
)
|
|
829
|
+
prompt: str = Field(
|
|
830
|
+
min_length=1,
|
|
831
|
+
max_length=20_000,
|
|
832
|
+
description=(
|
|
833
|
+
"The turn prompt this stage's agent is given, and the only instruction it receives "
|
|
834
|
+
"for the work itself. The token {{previous}} is replaced before the turn starts: in "
|
|
835
|
+
"sequential mode it becomes the previous stage's full output, and for the first "
|
|
836
|
+
"stage, or for every stage in parallel mode, it becomes the top-level task. Write "
|
|
837
|
+
"it as a complete instruction, because the agent has no thread history to fall back "
|
|
838
|
+
"on. A prompt that is empty or only whitespace is rejected before admission."
|
|
839
|
+
),
|
|
840
|
+
)
|
|
841
|
+
system_prompt: str = Field(
|
|
842
|
+
min_length=1,
|
|
843
|
+
max_length=20_000,
|
|
844
|
+
description=(
|
|
845
|
+
"The system prompt that shapes this stage's agent for its single turn. Each stage "
|
|
846
|
+
"gets its own, which is the whole point of staging: an auditing stage and an "
|
|
847
|
+
"implementing stage need different standing instructions, and one shared prompt "
|
|
848
|
+
"serves neither well. It is applied when the agent is built, before the turn runs, "
|
|
849
|
+
"and never carries over to another stage. Like the turn prompt, it may not be empty "
|
|
850
|
+
"or whitespace only."
|
|
851
|
+
),
|
|
852
|
+
)
|
|
853
|
+
model: str = Field(
|
|
854
|
+
default="",
|
|
855
|
+
max_length=128,
|
|
856
|
+
description=(
|
|
857
|
+
"The model id this stage's agent runs on, such as gpt-5.1-codex. Leave it empty to "
|
|
858
|
+
"accept whatever model the installed Codex is configured to use, which is the right "
|
|
859
|
+
"choice unless a stage genuinely needs a different one. This is the one stage "
|
|
860
|
+
"setting with an open value set, so an unrecognized id is not caught locally and "
|
|
861
|
+
"fails inside Codex after admission has already been charged. Set it per stage when "
|
|
862
|
+
"a cheap model can do the early stages and only the last stage needs a strong one."
|
|
863
|
+
),
|
|
864
|
+
)
|
|
865
|
+
effort: StageEffort = Field(
|
|
866
|
+
default=StageEffort.MEDIUM,
|
|
867
|
+
description=(
|
|
868
|
+
"How much reasoning this stage's agent spends before it answers, from none through "
|
|
869
|
+
"xhigh. Higher effort costs more tokens on your own OpenAI account and takes longer, "
|
|
870
|
+
"and it is the setting that most changes what a stage is capable of. Analysis and "
|
|
871
|
+
"planning stages usually justify high; mechanical stages rarely do. The value is one "
|
|
872
|
+
"of the closed set the CLI accepts, so a misspelling fails at parse time rather than "
|
|
873
|
+
"silently falling back to a default."
|
|
874
|
+
),
|
|
875
|
+
)
|
|
876
|
+
summary: StageSummary = Field(
|
|
877
|
+
default=StageSummary.AUTO,
|
|
878
|
+
description=(
|
|
879
|
+
"How much of its own reasoning this stage's agent reports alongside its answer. auto "
|
|
880
|
+
"lets Codex decide, none suppresses the summary entirely, and concise and detailed "
|
|
881
|
+
"ask for progressively more. This affects what you read, not what the agent does, so "
|
|
882
|
+
"it changes neither the result nor the price of the run. Choose detailed when the "
|
|
883
|
+
"point of a stage is to show its reasoning to a later stage. The value is drawn from "
|
|
884
|
+
"a closed set the CLI validates before anything is charged."
|
|
885
|
+
),
|
|
886
|
+
)
|
|
887
|
+
sandbox: StageSandbox = Field(
|
|
888
|
+
default=StageSandbox.WORKSPACE_WRITE,
|
|
889
|
+
description=(
|
|
890
|
+
"How far this stage's agent may reach into the filesystem: read-only, "
|
|
891
|
+
"workspace-write, or full-access. Sandboxing is enforced by Codex itself rather than "
|
|
892
|
+
"by this CLI, so it is a real boundary and not a hint. Give read-only to stages that "
|
|
893
|
+
"only inspect or plan, and keep workspace-write for the stages that actually change "
|
|
894
|
+
"files. full-access exists for the rare stage that must reach outside the working "
|
|
895
|
+
"directory, and it is never the right default for an unattended run."
|
|
896
|
+
),
|
|
897
|
+
)
|
|
898
|
+
approval: StageApproval = Field(
|
|
899
|
+
default=StageApproval.AUTO_REVIEW,
|
|
900
|
+
description=(
|
|
901
|
+
"What this stage's agent does when an action needs approval and no person is there "
|
|
902
|
+
"to give it. auto_review lets Codex review and proceed on its own, which is what "
|
|
903
|
+
"keeps an unattended staged run moving. deny_all refuses every such action instead, "
|
|
904
|
+
"which is the safe choice for a stage that should only ever read. Because a staged "
|
|
905
|
+
"run is not interactive, neither value ever prompts you. Both are validated against "
|
|
906
|
+
"a closed set before the wallet is touched."
|
|
907
|
+
),
|
|
908
|
+
)
|
|
909
|
+
personality: StagePersonality = Field(
|
|
910
|
+
default=StagePersonality.NONE,
|
|
911
|
+
description=(
|
|
912
|
+
"The response personality this stage's agent writes in: none, friendly, or "
|
|
913
|
+
"pragmatic. It changes tone and framing only, never capability, tool use, or "
|
|
914
|
+
"sandboxing. It is worth setting when a stage's output is read by a person and worth "
|
|
915
|
+
"leaving at none when the output is consumed by the next stage. Like the other "
|
|
916
|
+
"closed-set settings, an unrecognized value is rejected at parse time. Each stage "
|
|
917
|
+
"carries its own, so stages need not agree."
|
|
918
|
+
),
|
|
919
|
+
)
|
|
920
|
+
additional_context: str = Field(
|
|
921
|
+
default="",
|
|
922
|
+
max_length=20_000,
|
|
923
|
+
description=(
|
|
924
|
+
"Extra turn-scoped context handed to this stage's agent alongside its prompt, for "
|
|
925
|
+
"material that is reference rather than instruction. It is applied to this stage "
|
|
926
|
+
"only and never reaches another stage, so shared context has to be repeated on each "
|
|
927
|
+
"stage that needs it. Leave it empty when the prompt already says everything. It "
|
|
928
|
+
"counts against the same token budget the prompt does, billed to your own OpenAI "
|
|
929
|
+
"account."
|
|
930
|
+
),
|
|
931
|
+
)
|
|
932
|
+
image: str = Field(
|
|
933
|
+
default="",
|
|
934
|
+
max_length=4096,
|
|
935
|
+
description=(
|
|
936
|
+
"One image input handed to this stage's agent alongside its text prompt, either as "
|
|
937
|
+
"an https:// URL or a data: URL, or as a local image file path. A value starting "
|
|
938
|
+
"with https://, http://, or data: is sent as a remote image URL, and anything else "
|
|
939
|
+
"is sent as a local image path, which is the correct choice for a screenshot the "
|
|
940
|
+
"stage must review. Leave it empty when the stage needs no image, which is the "
|
|
941
|
+
"common case. A value that is only whitespace is rejected before admission."
|
|
942
|
+
),
|
|
943
|
+
)
|
|
944
|
+
skill: str = Field(
|
|
945
|
+
default="",
|
|
946
|
+
max_length=2048,
|
|
947
|
+
description=(
|
|
948
|
+
"One native Codex skill handed to this stage's agent, written as NAME=PATH where "
|
|
949
|
+
"NAME is the skill name and PATH is the skill directory the agent should load. "
|
|
950
|
+
"The separator is the first equals sign, so PATH itself may contain further equals "
|
|
951
|
+
"signs. Leave it empty when the stage needs no skill. A value without the separator, "
|
|
952
|
+
"or with a blank name or path, is rejected before admission."
|
|
953
|
+
),
|
|
954
|
+
)
|
|
955
|
+
mention: str = Field(
|
|
956
|
+
default="",
|
|
957
|
+
max_length=2048,
|
|
958
|
+
description=(
|
|
959
|
+
"One named resource mention handed to this stage's agent, written as NAME=PATH "
|
|
960
|
+
"where NAME is the resource name and PATH is the resource path the agent should "
|
|
961
|
+
"read. The separator is the first equals sign, so PATH itself may contain further "
|
|
962
|
+
"equals signs. Leave it empty when the stage needs no mention. A value without the "
|
|
963
|
+
"separator, or with a blank name or path, is rejected before admission."
|
|
964
|
+
),
|
|
965
|
+
)
|
|
966
|
+
output_schema: str = Field(
|
|
967
|
+
default="",
|
|
968
|
+
max_length=20_000,
|
|
969
|
+
description=(
|
|
970
|
+
"A JSON object schema string forcing this stage's agent to answer in a fixed shape, "
|
|
971
|
+
"so the next stage can check the handoff mechanically instead of re-reading prose. "
|
|
972
|
+
'For example a review stage can be forced to return {"files": [...], "summary": '
|
|
973
|
+
'"..."}. The value must parse as a JSON object when given, and an empty value '
|
|
974
|
+
"means the stage answers in free prose. A value that is not a JSON object is "
|
|
975
|
+
"rejected before admission."
|
|
976
|
+
),
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
@field_validator("name", "prompt", "system_prompt")
|
|
980
|
+
@classmethod
|
|
981
|
+
def _reject_blank(cls, value: str) -> str:
|
|
982
|
+
# Whitespace-only stage text would burn a paid turn on nothing.
|
|
983
|
+
if not value.strip():
|
|
984
|
+
raise ValueError("must contain non-whitespace characters")
|
|
985
|
+
return value
|
|
986
|
+
|
|
987
|
+
@field_validator("image")
|
|
988
|
+
@classmethod
|
|
989
|
+
def _reject_blank_image(cls, value: str) -> str:
|
|
990
|
+
# An explicit blank image would send an empty input item to the SDK.
|
|
991
|
+
if value != "" and not value.strip():
|
|
992
|
+
raise ValueError("must contain non-whitespace characters")
|
|
993
|
+
return value
|
|
994
|
+
|
|
995
|
+
@field_validator("skill", "mention")
|
|
996
|
+
@classmethod
|
|
997
|
+
def _reject_malformed_reference(cls, value: str) -> str:
|
|
998
|
+
# NAME=PATH is the only shape the adapter can split without guessing.
|
|
999
|
+
if value == "":
|
|
1000
|
+
return value
|
|
1001
|
+
name, separator, path = value.partition("=")
|
|
1002
|
+
if not separator or not name.strip() or not path.strip():
|
|
1003
|
+
raise ValueError("must be NAME=PATH with non-blank parts")
|
|
1004
|
+
return value
|
|
1005
|
+
|
|
1006
|
+
@field_validator("output_schema")
|
|
1007
|
+
@classmethod
|
|
1008
|
+
def _reject_malformed_schema(cls, value: str) -> str:
|
|
1009
|
+
# The adapter passes the parsed object to the SDK, so only an object parses.
|
|
1010
|
+
if value == "":
|
|
1011
|
+
return value
|
|
1012
|
+
try:
|
|
1013
|
+
parsed: object = json.loads(value)
|
|
1014
|
+
except ValueError as error:
|
|
1015
|
+
raise ValueError("must parse as a JSON object") from error
|
|
1016
|
+
if not isinstance(parsed, dict):
|
|
1017
|
+
raise ValueError("must parse as a JSON object")
|
|
1018
|
+
return value
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
class StagesSettings(BaseModel):
|
|
1022
|
+
"""Bounded, frozen stages-primitive settings for the stages executor."""
|
|
1023
|
+
|
|
1024
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
1025
|
+
stages: tuple[StageSpec, ...] = Field(
|
|
1026
|
+
min_length=1,
|
|
1027
|
+
max_length=StagesLimit.MAX_STAGES,
|
|
1028
|
+
description=(
|
|
1029
|
+
"The ordered stages of this run, where entry i is the complete configuration for "
|
|
1030
|
+
"the agent that runs stage i. Every stage gets a brand-new agent that is discarded "
|
|
1031
|
+
"when its turn ends, so no thread state, no tool state, and no history ever crosses "
|
|
1032
|
+
"a stage boundary. Order is meaningful in sequential mode and ignored in parallel "
|
|
1033
|
+
"mode. A run holds between 1 and 25 stages, and the one-cent admission covers all "
|
|
1034
|
+
"of them however many there are."
|
|
1035
|
+
),
|
|
1036
|
+
)
|
|
1037
|
+
parallel: bool = Field(
|
|
1038
|
+
default=False,
|
|
1039
|
+
description=(
|
|
1040
|
+
"Whether the stages run all at once instead of one after another. Sequential, the "
|
|
1041
|
+
"default, awaits each stage before starting the next and substitutes the finished "
|
|
1042
|
+
"output into the next stage's {{previous}} token, which is what lets stages build "
|
|
1043
|
+
"on each other. Parallel starts every stage together with asyncio.gather and gives "
|
|
1044
|
+
"each one the top-level task as {{previous}}, so stages must not depend on each "
|
|
1045
|
+
"other's results. Parallel finishes sooner but runs every stage's model calls "
|
|
1046
|
+
"concurrently against your own OpenAI account."
|
|
1047
|
+
),
|
|
1048
|
+
)
|
|
1049
|
+
stop_on_error: bool = Field(
|
|
1050
|
+
default=True,
|
|
1051
|
+
description=(
|
|
1052
|
+
"What the run does the first time a stage exhausts its retries. When enabled the "
|
|
1053
|
+
"run halts immediately, keeps every result completed before the failure, and "
|
|
1054
|
+
"returns that prefix rather than running work that depends on a stage that never "
|
|
1055
|
+
"finished. When disabled the failed stage is recorded with a failed status, a short "
|
|
1056
|
+
"placeholder stands in for its text so stage indices stay aligned, and the next "
|
|
1057
|
+
"stage starts anyway. Leave it enabled for a dependent run and disable it for "
|
|
1058
|
+
"independent stages where one failure should not cancel the rest."
|
|
1059
|
+
),
|
|
1060
|
+
)
|
|
1061
|
+
max_retries_per_stage: int = Field(
|
|
1062
|
+
ge=0,
|
|
1063
|
+
le=3,
|
|
1064
|
+
default=1,
|
|
1065
|
+
description=(
|
|
1066
|
+
"How many extra attempts one stage receives after its first attempt fails, before "
|
|
1067
|
+
"the run treats that stage as failed. Every retry re-sends the identical prompt "
|
|
1068
|
+
"to a brand-new agent, so a retry recovers from a crashed or timed-out host rather "
|
|
1069
|
+
"than from a stage the agent understood but could not do. Each attempt is charged "
|
|
1070
|
+
"against the caller's own model account, so a high retry count on a long run "
|
|
1071
|
+
"multiplies model cost. Accepted values run from 0 to 3, and a completed retry "
|
|
1072
|
+
"produces exactly one result entry, never a duplicate."
|
|
1073
|
+
),
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
class StageStepResult(BaseModel):
|
|
1078
|
+
"""One stage outcome with its identity and attempt accounting."""
|
|
1079
|
+
|
|
1080
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
1081
|
+
index: int = Field(ge=0)
|
|
1082
|
+
name: str = Field(min_length=1, max_length=64)
|
|
1083
|
+
status: Literal["completed", "failed"]
|
|
1084
|
+
thread_id: str = Field(min_length=1, max_length=128)
|
|
1085
|
+
attempts: int = Field(ge=1)
|
|
1086
|
+
duration_ms: int = Field(ge=0)
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
class StagesResult(BaseModel):
|
|
1090
|
+
"""Ordered per-stage outputs with per-stage accounting for partial runs."""
|
|
1091
|
+
|
|
1092
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
1093
|
+
admission_id: str = Field(min_length=1, max_length=128)
|
|
1094
|
+
completed: int = Field(ge=0)
|
|
1095
|
+
failed: int = Field(ge=0)
|
|
1096
|
+
steps: tuple[StageStepResult, ...]
|
|
1097
|
+
stage_texts: tuple[str, ...]
|
|
1098
|
+
text: str
|