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.
Files changed (269) hide show
  1. vidbyte_cli/__init__.py +11 -0
  2. vidbyte_cli/__main__.py +6 -0
  3. vidbyte_cli/cli.py +18 -0
  4. vidbyte_cli/commands/__init__.py +86 -0
  5. vidbyte_cli/commands/agent_options.py +53 -0
  6. vidbyte_cli/commands/agents/__init__.py +31 -0
  7. vidbyte_cli/commands/agents/suggestion/__init__.py +38 -0
  8. vidbyte_cli/commands/agents/suggestion/prompts/__init__.py +3 -0
  9. vidbyte_cli/commands/agents/suggestion/prompts/all_categories.md +5 -0
  10. vidbyte_cli/commands/agents/suggestion/prompts/approaches.md +100 -0
  11. vidbyte_cli/commands/agents/suggestion/prompts/avoid.md +100 -0
  12. vidbyte_cli/commands/agents/suggestion/prompts/blockers.md +100 -0
  13. vidbyte_cli/commands/agents/suggestion/prompts/capability.md +100 -0
  14. vidbyte_cli/commands/agents/suggestion/prompts/categories.md +5 -0
  15. vidbyte_cli/commands/agents/suggestion/prompts/category.md +5 -0
  16. vidbyte_cli/commands/agents/suggestion/prompts/completed.md +100 -0
  17. vidbyte_cli/commands/agents/suggestion/prompts/constraint.md +100 -0
  18. vidbyte_cli/commands/agents/suggestion/prompts/context.md +100 -0
  19. vidbyte_cli/commands/agents/suggestion/prompts/count.md +5 -0
  20. vidbyte_cli/commands/agents/suggestion/prompts/critic_model.md +100 -0
  21. vidbyte_cli/commands/agents/suggestion/prompts/decision.md +100 -0
  22. vidbyte_cli/commands/agents/suggestion/prompts/dry_run.md +5 -0
  23. vidbyte_cli/commands/agents/suggestion/prompts/extra_compute.md +5 -0
  24. vidbyte_cli/commands/agents/suggestion/prompts/feedback_accept.md +7 -0
  25. vidbyte_cli/commands/agents/suggestion/prompts/feedback_group.md +7 -0
  26. vidbyte_cli/commands/agents/suggestion/prompts/feedback_project.md +100 -0
  27. vidbyte_cli/commands/agents/suggestion/prompts/feedback_reason.md +98 -0
  28. vidbyte_cli/commands/agents/suggestion/prompts/feedback_reject.md +7 -0
  29. vidbyte_cli/commands/agents/suggestion/prompts/feedback_suggestion.md +101 -0
  30. vidbyte_cli/commands/agents/suggestion/prompts/files.md +100 -0
  31. vidbyte_cli/commands/agents/suggestion/prompts/forbidden.md +100 -0
  32. vidbyte_cli/commands/agents/suggestion/prompts/goal.md +100 -0
  33. vidbyte_cli/commands/agents/suggestion/prompts/handoff.md +7 -0
  34. vidbyte_cli/commands/agents/suggestion/prompts/horizon.md +5 -0
  35. vidbyte_cli/commands/agents/suggestion/prompts/hypotheses.md +100 -0
  36. vidbyte_cli/commands/agents/suggestion/prompts/idea.md +100 -0
  37. vidbyte_cli/commands/agents/suggestion/prompts/in_progress.md +100 -0
  38. vidbyte_cli/commands/agents/suggestion/prompts/input.md +100 -0
  39. vidbyte_cli/commands/agents/suggestion/prompts/library.py +29 -0
  40. vidbyte_cli/commands/agents/suggestion/prompts/max_messages.md +5 -0
  41. vidbyte_cli/commands/agents/suggestion/prompts/max_output_tokens.md +5 -0
  42. vidbyte_cli/commands/agents/suggestion/prompts/max_total_tokens.md +5 -0
  43. vidbyte_cli/commands/agents/suggestion/prompts/mistakes.md +100 -0
  44. vidbyte_cli/commands/agents/suggestion/prompts/outcomes.md +100 -0
  45. vidbyte_cli/commands/agents/suggestion/prompts/project.md +100 -0
  46. vidbyte_cli/commands/agents/suggestion/prompts/project_create.md +7 -0
  47. vidbyte_cli/commands/agents/suggestion/prompts/project_description.md +98 -0
  48. vidbyte_cli/commands/agents/suggestion/prompts/project_group.md +7 -0
  49. vidbyte_cli/commands/agents/suggestion/prompts/project_key.md +96 -0
  50. vidbyte_cli/commands/agents/suggestion/prompts/project_list.md +7 -0
  51. vidbyte_cli/commands/agents/suggestion/prompts/project_title.md +99 -0
  52. vidbyte_cli/commands/agents/suggestion/prompts/provider.md +5 -0
  53. vidbyte_cli/commands/agents/suggestion/prompts/question.md +100 -0
  54. vidbyte_cli/commands/agents/suggestion/prompts/risks.md +100 -0
  55. vidbyte_cli/commands/agents/suggestion/prompts/rounds.md +5 -0
  56. vidbyte_cli/commands/agents/suggestion/prompts/run.md +5 -0
  57. vidbyte_cli/commands/agents/suggestion/prompts/success.md +100 -0
  58. vidbyte_cli/commands/agents/suggestion/prompts/timeout.md +5 -0
  59. vidbyte_cli/commands/agents/suggestion/prompts/trajectory.md +100 -0
  60. vidbyte_cli/commands/agents/suggestion/prompts/view.md +5 -0
  61. vidbyte_cli/commands/agents/suggestion/prompts/view_all.md +5 -0
  62. vidbyte_cli/commands/agents/suggestion/render.py +77 -0
  63. vidbyte_cli/commands/agents/suggestion/request_builder.py +240 -0
  64. vidbyte_cli/commands/agents/suggestion/suggest.py +154 -0
  65. vidbyte_cli/commands/agents/suggestion/suggestion_categories.py +77 -0
  66. vidbyte_cli/commands/agents/suggestion/suggestion_feedback.py +85 -0
  67. vidbyte_cli/commands/agents/suggestion/suggestion_handoff.py +67 -0
  68. vidbyte_cli/commands/agents/suggestion/suggestion_projects.py +95 -0
  69. vidbyte_cli/commands/auth/__init__.py +0 -0
  70. vidbyte_cli/commands/auth/login.py +100 -0
  71. vidbyte_cli/commands/auth/logout.py +41 -0
  72. vidbyte_cli/commands/auth/whoami.py +51 -0
  73. vidbyte_cli/commands/billing/__init__.py +20 -0
  74. vidbyte_cli/commands/billing/prompts/__init__.py +1 -0
  75. vidbyte_cli/commands/billing/prompts/confirm.md +16 -0
  76. vidbyte_cli/commands/billing/prompts/group.md +13 -0
  77. vidbyte_cli/commands/billing/prompts/idempotency_key.md +16 -0
  78. vidbyte_cli/commands/billing/prompts/library.py +18 -0
  79. vidbyte_cli/commands/billing/prompts/top_up.md +16 -0
  80. vidbyte_cli/commands/billing/top_up.py +65 -0
  81. vidbyte_cli/commands/config/__init__.py +0 -0
  82. vidbyte_cli/commands/config/get.py +45 -0
  83. vidbyte_cli/commands/config/set.py +39 -0
  84. vidbyte_cli/commands/provider/__init__.py +9 -0
  85. vidbyte_cli/commands/provider/login.py +102 -0
  86. vidbyte_cli/commands/provider/logout.py +45 -0
  87. vidbyte_cli/commands/provider/whoami.py +49 -0
  88. vidbyte_cli/commands/research/__init__.py +5 -0
  89. vidbyte_cli/commands/research/add.py +41 -0
  90. vidbyte_cli/commands/research/options.py +134 -0
  91. vidbyte_cli/commands/research/render.py +157 -0
  92. vidbyte_cli/commands/research/resume.py +37 -0
  93. vidbyte_cli/commands/research/start.py +37 -0
  94. vidbyte_cli/commands/research/status.py +32 -0
  95. vidbyte_cli/commands/research/thread.py +30 -0
  96. vidbyte_cli/commands/research/threads.py +35 -0
  97. vidbyte_cli/commands/research/watch.py +93 -0
  98. vidbyte_cli/commands/runtime/__init__.py +23 -0
  99. vidbyte_cli/commands/runtime/adversarial_team.py +41 -0
  100. vidbyte_cli/commands/runtime/doctor.py +44 -0
  101. vidbyte_cli/commands/runtime/list.py +47 -0
  102. vidbyte_cli/commands/runtime/persistence.py +141 -0
  103. vidbyte_cli/commands/runtime/same_host_ensemble.py +221 -0
  104. vidbyte_cli/commands/runtime/stages.py +553 -0
  105. vidbyte_cli/commands/runtime/task_board.py +1342 -0
  106. vidbyte_cli/commands/setup/__init__.py +0 -0
  107. vidbyte_cli/commands/setup/doctor.py +64 -0
  108. vidbyte_cli/commands/setup/prompts/__init__.py +1 -0
  109. vidbyte_cli/commands/setup/prompts/browser.md +7 -0
  110. vidbyte_cli/commands/setup/prompts/library.py +18 -0
  111. vidbyte_cli/commands/setup/prompts/star.md +13 -0
  112. vidbyte_cli/commands/setup/star.py +132 -0
  113. vidbyte_cli/lib/__init__.py +0 -0
  114. vidbyte_cli/lib/api/__init__.py +0 -0
  115. vidbyte_cli/lib/api/client.py +236 -0
  116. vidbyte_cli/lib/api/endpoints/__init__.py +0 -0
  117. vidbyte_cli/lib/api/endpoints/auth.py +24 -0
  118. vidbyte_cli/lib/api/endpoints/billing.py +36 -0
  119. vidbyte_cli/lib/api/endpoints/research.py +103 -0
  120. vidbyte_cli/lib/api/endpoints/runtime.py +113 -0
  121. vidbyte_cli/lib/api/problem.py +86 -0
  122. vidbyte_cli/lib/api/response.py +101 -0
  123. vidbyte_cli/lib/api/retry.py +116 -0
  124. vidbyte_cli/lib/api/runtime_payment.py +89 -0
  125. vidbyte_cli/lib/auth/__init__.py +16 -0
  126. vidbyte_cli/lib/auth/credentials.py +223 -0
  127. vidbyte_cli/lib/auth/input.py +59 -0
  128. vidbyte_cli/lib/auth/keyring_store.py +126 -0
  129. vidbyte_cli/lib/auth/provider_credentials.py +157 -0
  130. vidbyte_cli/lib/auth/provider_input.py +54 -0
  131. vidbyte_cli/lib/auth/provider_resolver.py +89 -0
  132. vidbyte_cli/lib/auth/provider_store.py +149 -0
  133. vidbyte_cli/lib/auth/provider_verifier.py +219 -0
  134. vidbyte_cli/lib/auth/resolver.py +63 -0
  135. vidbyte_cli/lib/auth/verifier.py +38 -0
  136. vidbyte_cli/lib/config/__init__.py +25 -0
  137. vidbyte_cli/lib/config/atomic.py +47 -0
  138. vidbyte_cli/lib/config/config.py +141 -0
  139. vidbyte_cli/lib/config/migration.py +81 -0
  140. vidbyte_cli/lib/config/models.py +145 -0
  141. vidbyte_cli/lib/config/paths.py +66 -0
  142. vidbyte_cli/lib/config/resolver.py +137 -0
  143. vidbyte_cli/lib/constants/__init__.py +1 -0
  144. vidbyte_cli/lib/constants/runtime.py +186 -0
  145. vidbyte_cli/lib/errors/__init__.py +12 -0
  146. vidbyte_cli/lib/errors/cli_error.py +86 -0
  147. vidbyte_cli/lib/errors/codes.py +43 -0
  148. vidbyte_cli/lib/errors/failures.py +2910 -0
  149. vidbyte_cli/lib/errors/handler.py +72 -0
  150. vidbyte_cli/lib/files/__init__.py +6 -0
  151. vidbyte_cli/lib/files/documents.py +94 -0
  152. vidbyte_cli/lib/files/store.py +156 -0
  153. vidbyte_cli/lib/io/__init__.py +16 -0
  154. vidbyte_cli/lib/io/attachments.py +129 -0
  155. vidbyte_cli/lib/io/codex_attachments.py +47 -0
  156. vidbyte_cli/lib/io/prompt.py +71 -0
  157. vidbyte_cli/lib/io/streams.py +37 -0
  158. vidbyte_cli/lib/io/terminal.py +66 -0
  159. vidbyte_cli/lib/output/__init__.py +7 -0
  160. vidbyte_cli/lib/output/formats.py +29 -0
  161. vidbyte_cli/lib/output/manager.py +95 -0
  162. vidbyte_cli/lib/output/models.py +49 -0
  163. vidbyte_cli/lib/runtime/__init__.py +9 -0
  164. vidbyte_cli/lib/runtime/application.py +134 -0
  165. vidbyte_cli/lib/runtime/context.py +250 -0
  166. vidbyte_cli/lib/runtime/options.py +159 -0
  167. vidbyte_cli/lib/runtime/version.py +30 -0
  168. vidbyte_cli/lib/runtime_primitives/__init__.py +28 -0
  169. vidbyte_cli/lib/runtime_primitives/executor.py +54 -0
  170. vidbyte_cli/lib/runtime_primitives/gate.py +105 -0
  171. vidbyte_cli/lib/runtime_primitives/hosts.py +45 -0
  172. vidbyte_cli/lib/runtime_primitives/planner.py +79 -0
  173. vidbyte_cli/lib/runtime_primitives/stages.py +338 -0
  174. vidbyte_cli/lib/runtime_primitives/task_board.py +982 -0
  175. vidbyte_cli/lib/runtime_primitives/task_board_checkpoints.py +473 -0
  176. vidbyte_cli/lib/runtime_primitives/task_board_system.md +33 -0
  177. vidbyte_cli/lib/runtime_primitives/task_board_tasks.py +111 -0
  178. vidbyte_cli/lib/runtime_primitives/verification.py +68 -0
  179. vidbyte_cli/services/__init__.py +5 -0
  180. vidbyte_cli/services/ensemble/__init__.py +7 -0
  181. vidbyte_cli/services/ensemble/prompts/__init__.py +6 -0
  182. vidbyte_cli/services/ensemble/prompts/implementer_system.md +546 -0
  183. vidbyte_cli/services/ensemble/prompts/implementer_turn.md +49 -0
  184. vidbyte_cli/services/ensemble/prompts/library.py +188 -0
  185. vidbyte_cli/services/ensemble/prompts/planner_system.md +728 -0
  186. vidbyte_cli/services/ensemble/prompts/planner_turn.md +6 -0
  187. vidbyte_cli/services/ensemble/prompts/role_system.md +104 -0
  188. vidbyte_cli/services/ensemble/prompts/role_turn.md +19 -0
  189. vidbyte_cli/services/ensemble/prompts/selector_final_turn.md +19 -0
  190. vidbyte_cli/services/ensemble/prompts/selector_round_turn.md +17 -0
  191. vidbyte_cli/services/ensemble/prompts/selector_system.md +89 -0
  192. vidbyte_cli/services/ensemble/runner.py +30 -0
  193. vidbyte_cli/services/ensemble/sdk.py +134 -0
  194. vidbyte_cli/services/ensemble/service.py +233 -0
  195. vidbyte_cli/services/ensemble/settings.py +60 -0
  196. vidbyte_cli/services/persistence/__init__.py +6 -0
  197. vidbyte_cli/services/persistence/prompts/__init__.py +6 -0
  198. vidbyte_cli/services/persistence/prompts/library.py +46 -0
  199. vidbyte_cli/services/persistence/prompts/persistence_system.md +1 -0
  200. vidbyte_cli/services/persistence/prompts/persistence_turn.md +3 -0
  201. vidbyte_cli/services/persistence/runner.py +35 -0
  202. vidbyte_cli/services/persistence/session.py +116 -0
  203. vidbyte_cli/services/suggestions/__init__.py +7 -0
  204. vidbyte_cli/services/suggestions/categories.py +270 -0
  205. vidbyte_cli/services/suggestions/context.py +222 -0
  206. vidbyte_cli/services/suggestions/extra_compute.py +53 -0
  207. vidbyte_cli/services/suggestions/handoff.py +173 -0
  208. vidbyte_cli/services/suggestions/message_tools.py +47 -0
  209. vidbyte_cli/services/suggestions/project.py +253 -0
  210. vidbyte_cli/services/suggestions/prompts/__init__.py +1 -0
  211. vidbyte_cli/services/suggestions/prompts/categories/__init__.py +3 -0
  212. vidbyte_cli/services/suggestions/prompts/categories/adjacent_opportunity.md +37 -0
  213. vidbyte_cli/services/suggestions/prompts/categories/alternative.md +38 -0
  214. vidbyte_cli/services/suggestions/prompts/categories/big_bets.md +37 -0
  215. vidbyte_cli/services/suggestions/prompts/categories/bottleneck.md +38 -0
  216. vidbyte_cli/services/suggestions/prompts/categories/business_growth.md +38 -0
  217. vidbyte_cli/services/suggestions/prompts/categories/completion.md +37 -0
  218. vidbyte_cli/services/suggestions/prompts/categories/continuation.md +37 -0
  219. vidbyte_cli/services/suggestions/prompts/categories/coordination.md +37 -0
  220. vidbyte_cli/services/suggestions/prompts/categories/creative_exploration.md +37 -0
  221. vidbyte_cli/services/suggestions/prompts/categories/cross_domain.md +37 -0
  222. vidbyte_cli/services/suggestions/prompts/categories/delegation.md +37 -0
  223. vidbyte_cli/services/suggestions/prompts/categories/experiment.md +37 -0
  224. vidbyte_cli/services/suggestions/prompts/categories/feedback.md +37 -0
  225. vidbyte_cli/services/suggestions/prompts/categories/goal_clarification.md +37 -0
  226. vidbyte_cli/services/suggestions/prompts/categories/immediate_next_steps.md +37 -0
  227. vidbyte_cli/services/suggestions/prompts/categories/investigation.md +37 -0
  228. vidbyte_cli/services/suggestions/prompts/categories/learning.md +37 -0
  229. vidbyte_cli/services/suggestions/prompts/categories/leverage.md +37 -0
  230. vidbyte_cli/services/suggestions/prompts/categories/long_term_directions.md +37 -0
  231. vidbyte_cli/services/suggestions/prompts/categories/preparation.md +37 -0
  232. vidbyte_cli/services/suggestions/prompts/categories/prerequisite.md +37 -0
  233. vidbyte_cli/services/suggestions/prompts/categories/prioritization.md +37 -0
  234. vidbyte_cli/services/suggestions/prompts/categories/product_experience.md +37 -0
  235. vidbyte_cli/services/suggestions/prompts/categories/quick_wins.md +37 -0
  236. vidbyte_cli/services/suggestions/prompts/categories/reframing.md +37 -0
  237. vidbyte_cli/services/suggestions/prompts/categories/risk_prevention.md +37 -0
  238. vidbyte_cli/services/suggestions/prompts/categories/simplification.md +37 -0
  239. vidbyte_cli/services/suggestions/prompts/categories/stop_or_defer.md +37 -0
  240. vidbyte_cli/services/suggestions/prompts/categories/strategy.md +37 -0
  241. vidbyte_cli/services/suggestions/prompts/categories/technical_possibilities.md +37 -0
  242. vidbyte_cli/services/suggestions/prompts/categories/verification.md +37 -0
  243. vidbyte_cli/services/suggestions/prompts/critic.md +209 -0
  244. vidbyte_cli/services/suggestions/prompts/critic_message.md +8 -0
  245. vidbyte_cli/services/suggestions/prompts/generator.md +64 -0
  246. vidbyte_cli/services/suggestions/prompts/library.py +72 -0
  247. vidbyte_cli/services/suggestions/prompts/message_generator_argument.md +1 -0
  248. vidbyte_cli/services/suggestions/prompts/message_generator_tool.md +1 -0
  249. vidbyte_cli/services/suggestions/prompts/message_parent_argument.md +1 -0
  250. vidbyte_cli/services/suggestions/prompts/message_parent_tool.md +1 -0
  251. vidbyte_cli/services/suggestions/prompts/message_receipt.md +1 -0
  252. vidbyte_cli/services/suggestions/prompts/revision.md +8 -0
  253. vidbyte_cli/services/suggestions/sdk.py +228 -0
  254. vidbyte_cli/services/suggestions/selection.py +78 -0
  255. vidbyte_cli/services/suggestions/service.py +540 -0
  256. vidbyte_cli/types/__init__.py +0 -0
  257. vidbyte_cli/types/api.py +101 -0
  258. vidbyte_cli/types/attachments.py +79 -0
  259. vidbyte_cli/types/ensemble.py +189 -0
  260. vidbyte_cli/types/provider.py +62 -0
  261. vidbyte_cli/types/research.py +260 -0
  262. vidbyte_cli/types/runtime.py +1098 -0
  263. vidbyte_cli/types/suggestions.py +518 -0
  264. vidbyte_cli-0.1.0.dist-info/METADATA +335 -0
  265. vidbyte_cli-0.1.0.dist-info/RECORD +269 -0
  266. vidbyte_cli-0.1.0.dist-info/WHEEL +5 -0
  267. vidbyte_cli-0.1.0.dist-info/entry_points.txt +2 -0
  268. vidbyte_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  269. vidbyte_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,79 @@
1
+ """Provider-neutral contracts for explicit files supplied to future agent invocations.
2
+
3
+ These models describe one immutable snapshot and its body-free output manifest. Filesystem
4
+ access and provider-specific conversion belong to the I/O layer, not to these contracts.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import StrEnum
10
+ from pathlib import Path
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
13
+
14
+
15
+ class AttachmentKind(StrEnum):
16
+ """The two file forms the first shared attachment contract supports."""
17
+
18
+ TEXT = "text"
19
+ IMAGE = "image"
20
+
21
+
22
+ class AgentAttachment(BaseModel):
23
+ """One validated file snapshot with provenance and integrity metadata."""
24
+
25
+ model_config = ConfigDict(extra="forbid", frozen=True)
26
+
27
+ supplied_path: Path
28
+ resolved_path: Path
29
+ name: str = Field(min_length=1, max_length=256)
30
+ kind: AttachmentKind
31
+ size_bytes: int = Field(gt=0)
32
+ sha256: str = Field(pattern=r"^[0-9a-f]{64}$")
33
+ content: str | None = None
34
+
35
+ @model_validator(mode="after")
36
+ def validate_content_shape(self) -> AgentAttachment:
37
+ # Images use the native path input; text attachments carry the captured body.
38
+ if self.kind is AttachmentKind.TEXT and not self.content:
39
+ raise ValueError("text attachments require content")
40
+ if self.kind is AttachmentKind.IMAGE and self.content is not None:
41
+ raise ValueError("image attachments cannot carry text content")
42
+ return self
43
+
44
+
45
+ class AttachmentBundle(BaseModel):
46
+ """An ordered immutable attachment collection resolved for one invocation."""
47
+
48
+ model_config = ConfigDict(extra="forbid", frozen=True)
49
+
50
+ items: tuple[AgentAttachment, ...] = ()
51
+ total_bytes: int = Field(default=0, ge=0)
52
+
53
+ @model_validator(mode="after")
54
+ def validate_totals_and_paths(self) -> AttachmentBundle:
55
+ # The bundle owns the aggregate invariants so programmatic callers cannot bypass limits.
56
+ sizes = sum(item.size_bytes for item in self.items)
57
+ if sizes != self.total_bytes:
58
+ raise ValueError("attachment total_bytes must equal the sum of item sizes")
59
+ paths = tuple(item.resolved_path for item in self.items)
60
+ if len(set(paths)) != len(paths):
61
+ raise ValueError("attachment resolved paths must be unique")
62
+ return self
63
+
64
+ def manifest(self) -> tuple[dict[str, object], ...]:
65
+ # Emits safe metadata only; attachment bodies never enter machine-facing manifests.
66
+ return tuple(
67
+ {
68
+ "supplied_path": str(item.supplied_path),
69
+ "resolved_path": str(item.resolved_path),
70
+ "name": item.name,
71
+ "kind": item.kind.value,
72
+ "size_bytes": item.size_bytes,
73
+ "sha256": item.sha256,
74
+ }
75
+ for item in self.items
76
+ )
77
+
78
+
79
+ __all__ = ["AgentAttachment", "AttachmentBundle", "AttachmentKind"]
@@ -0,0 +1,189 @@
1
+ """Typed contracts for the same-host ensemble runtime primitive.
2
+
3
+ `EnsembleInputs` is the whole command surface as one validated value, so a bound holds
4
+ whether the value came from Click or from a programmatic caller. `RolePlan`, `RoleProposal`,
5
+ and `SelectionRound` are the three structured-output schemas the agents themselves fill in;
6
+ each is the only shape its stage can return, which is what makes the handoffs deterministic.
7
+
8
+ Nothing here crosses the API boundary: the backend sees only admission metadata.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from enum import StrEnum
14
+ from typing import Literal
15
+
16
+ from pydantic import BaseModel, ConfigDict, Field
17
+
18
+ # The proposal band every role is held to, named once so the schema and the authored mandate
19
+ # that tells the role about it cannot drift apart.
20
+ APPROACHES_PER_ROLE_MIN = 5
21
+ APPROACHES_PER_ROLE_MAX = 10
22
+
23
+ # The largest roster the planner may be asked for, which also bounds RolePlan's own schema.
24
+ ROLES_MIN = 3
25
+ ROLES_MAX = 100
26
+
27
+
28
+ class EnsembleHost(StrEnum):
29
+ """Native hosts the ensemble can actually run on today."""
30
+
31
+ CODEX = "codex"
32
+
33
+
34
+ class EnsembleReasoningEffort(StrEnum):
35
+ """Reasoning-effort values the Codex integration accepts."""
36
+
37
+ NONE = "none"
38
+ MINIMAL = "minimal"
39
+ LOW = "low"
40
+ MEDIUM = "medium"
41
+ HIGH = "high"
42
+ XHIGH = "xhigh"
43
+
44
+
45
+ class EnsembleConfidence(StrEnum):
46
+ """How strongly a role stands behind one approach it proposed."""
47
+
48
+ LOW = "low"
49
+ MEDIUM = "medium"
50
+ HIGH = "high"
51
+
52
+
53
+ class EnsembleInputs(BaseModel):
54
+ """Every caller-settable ensemble option, validated once as a single value."""
55
+
56
+ model_config = ConfigDict(extra="forbid", frozen=True)
57
+ task: str = Field(min_length=1, max_length=20_000)
58
+ host: EnsembleHost = EnsembleHost.CODEX
59
+ roles: int = Field(default=3, ge=ROLES_MIN, le=ROLES_MAX)
60
+ model: str | None = Field(default=None, max_length=128)
61
+ reasoning_effort: EnsembleReasoningEffort | None = None
62
+ role_timeout_seconds: int = Field(default=300, ge=1, le=3600)
63
+
64
+
65
+ class GeneratedRole(BaseModel):
66
+ """One role the planner invented, as the six sections its prompt is built from."""
67
+
68
+ model_config = ConfigDict(extra="forbid", frozen=True)
69
+ name: str = Field(min_length=1, max_length=64)
70
+ identity: str = Field(min_length=1, max_length=4_000)
71
+ personality: str = Field(min_length=1, max_length=4_000)
72
+ expertise: str = Field(min_length=1, max_length=8_000)
73
+ knowledge: str = Field(min_length=1, max_length=8_000)
74
+ skills: str = Field(min_length=1, max_length=8_000)
75
+ goal: str = Field(min_length=1, max_length=4_000)
76
+
77
+
78
+ class RolePlan(BaseModel):
79
+ """The planner turn's structured output: the ensemble's whole roster."""
80
+
81
+ model_config = ConfigDict(extra="forbid", frozen=True)
82
+ task_analysis: str = Field(min_length=1, max_length=8_000)
83
+ roster_strategy: str = Field(min_length=1, max_length=8_000)
84
+ coverage_summary: str = Field(min_length=1, max_length=8_000)
85
+ roles: tuple[GeneratedRole, ...] = Field(min_length=1, max_length=ROLES_MAX)
86
+
87
+
88
+ class ProposedApproach(BaseModel):
89
+ """One candidate way to do the task, weighed by the role that thought of it."""
90
+
91
+ model_config = ConfigDict(extra="forbid", frozen=True)
92
+ title: str = Field(min_length=1, max_length=120)
93
+ approach: str = Field(min_length=1, max_length=8_000)
94
+ pros: tuple[str, ...] = Field(min_length=1, max_length=10)
95
+ cons: tuple[str, ...] = Field(min_length=1, max_length=10)
96
+ risks: tuple[str, ...] = Field(default=(), max_length=10)
97
+ files: tuple[str, ...] = Field(default=(), max_length=50)
98
+ confidence: EnsembleConfidence
99
+ assumptions: tuple[str, ...] = Field(min_length=1, max_length=20)
100
+ tradeoffs: tuple[str, ...] = Field(min_length=1, max_length=20)
101
+ validation_steps: tuple[str, ...] = Field(min_length=1, max_length=20)
102
+
103
+
104
+ class RoleProposal(BaseModel):
105
+ """One read-only role's whole slate of candidate approaches, never an edit."""
106
+
107
+ model_config = ConfigDict(extra="forbid", frozen=True)
108
+ role: str = Field(min_length=1, max_length=64)
109
+ task_analysis: str = Field(min_length=1, max_length=8_000)
110
+ role_strategy: str = Field(min_length=1, max_length=8_000)
111
+ approaches: tuple[ProposedApproach, ...] = Field(
112
+ min_length=APPROACHES_PER_ROLE_MIN, max_length=APPROACHES_PER_ROLE_MAX
113
+ )
114
+
115
+
116
+ class CandidateVerdict(BaseModel):
117
+ """The selector's weighing of one candidate it chose to keep in a round."""
118
+
119
+ model_config = ConfigDict(extra="forbid", frozen=True)
120
+ candidate_id: str = Field(min_length=1, max_length=32)
121
+ pros: tuple[str, ...] = Field(min_length=1, max_length=10)
122
+ cons: tuple[str, ...] = Field(min_length=1, max_length=10)
123
+ score: int = Field(ge=1, le=100)
124
+ rationale: str = Field(min_length=1, max_length=4_000)
125
+ evidence: tuple[str, ...] = Field(min_length=1, max_length=20)
126
+ uncertainties: tuple[str, ...] = Field(default=(), max_length=20)
127
+ implementation_guidance: str = Field(min_length=1, max_length=8_000)
128
+
129
+
130
+ class SelectionRound(BaseModel):
131
+ """One narrowing round's structured output: who survived it, and who did not."""
132
+
133
+ model_config = ConfigDict(extra="forbid", frozen=True)
134
+ round_summary: str = Field(min_length=1, max_length=8_000)
135
+ comparison_basis: tuple[str, ...] = Field(min_length=1, max_length=20)
136
+ kept: tuple[CandidateVerdict, ...] = Field(min_length=1, max_length=1_000)
137
+ eliminated: tuple[str, ...] = Field(default=(), max_length=1_000)
138
+
139
+
140
+ class ApproachCandidate(BaseModel):
141
+ """One numbered approach on the selector's slate, joined to the role that proposed it.
142
+
143
+ The id is assigned by the service rather than by any agent, so the identifiers the
144
+ selector echoes back can be checked against the slate it was actually given.
145
+ """
146
+
147
+ model_config = ConfigDict(extra="forbid", frozen=True)
148
+ candidate_id: str
149
+ role: str
150
+ task_analysis: str
151
+ role_strategy: str
152
+ approach: ProposedApproach
153
+
154
+
155
+ class SelectedApproach(BaseModel):
156
+ """The single approach the selector ended on, with the verdict that chose it."""
157
+
158
+ model_config = ConfigDict(extra="forbid", frozen=True)
159
+ candidate: ApproachCandidate
160
+ verdict: CandidateVerdict
161
+ round_summary: str
162
+ comparison_basis: tuple[str, ...]
163
+
164
+
165
+ class EnsembleRoleFailure(BaseModel):
166
+ """Why one role produced no proposal, in a closed machine-branchable vocabulary."""
167
+
168
+ model_config = ConfigDict(extra="forbid", frozen=True)
169
+ role: str = Field(min_length=1, max_length=64)
170
+ reason: Literal["timeout", "host_error", "invalid_output"]
171
+
172
+
173
+ class EnsembleResult(BaseModel):
174
+ """The complete outcome of one ensemble run, including its partial failures."""
175
+
176
+ model_config = ConfigDict(extra="forbid", frozen=True)
177
+ task: str
178
+ host: EnsembleHost
179
+ roles: tuple[GeneratedRole, ...]
180
+ proposals: tuple[RoleProposal, ...]
181
+ failures: tuple[EnsembleRoleFailure, ...]
182
+ candidates: int
183
+ rounds: tuple[SelectionRound, ...]
184
+ selected: SelectedApproach
185
+ implementation: str
186
+ root_thread_id: str
187
+ selector_thread_id: str
188
+ implementer_thread_id: str
189
+ charged_cents: int
@@ -0,0 +1,62 @@
1
+ """Provider identity for BYOK runtime execution.
2
+
3
+ Defines the closed set of providers a user can bring their own key for,
4
+ plus probe and environment mappings. No networking or storage here.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import StrEnum
10
+
11
+
12
+ class Provider(StrEnum):
13
+ """Every external provider this CLI can store a key for."""
14
+
15
+ OPENAI = "openai"
16
+ CLAUDE = "claude"
17
+ GROK = "grok"
18
+ DEEPSEEK = "deepseek"
19
+ GLM = "glm"
20
+ MUSE = "muse"
21
+ GEMINI = "gemini"
22
+
23
+
24
+ PROVIDER_KEY_PREFIXES: dict[Provider, tuple[str, ...]] = {
25
+ Provider.OPENAI: ("sk-",),
26
+ Provider.CLAUDE: ("sk-ant-",),
27
+ Provider.GROK: (),
28
+ Provider.DEEPSEEK: ("sk-",),
29
+ Provider.GLM: (),
30
+ Provider.MUSE: ("LLM|",),
31
+ Provider.GEMINI: ("AIza",),
32
+ }
33
+
34
+ PROVIDER_ENV_VARS: dict[Provider, str] = {
35
+ Provider.OPENAI: "OPENAI_API_KEY",
36
+ Provider.CLAUDE: "ANTHROPIC_API_KEY",
37
+ Provider.GROK: "XAI_API_KEY",
38
+ Provider.DEEPSEEK: "DEEPSEEK_API_KEY",
39
+ Provider.GLM: "ZAI_API_KEY",
40
+ Provider.MUSE: "MODEL_API_KEY",
41
+ Provider.GEMINI: "GEMINI_API_KEY",
42
+ }
43
+
44
+ PROVIDER_DISPLAY: dict[Provider, str] = {
45
+ Provider.OPENAI: "OpenAI",
46
+ Provider.CLAUDE: "Claude",
47
+ Provider.GROK: "Grok",
48
+ Provider.DEEPSEEK: "DeepSeek",
49
+ Provider.GLM: "GLM",
50
+ Provider.MUSE: "Muse",
51
+ Provider.GEMINI: "Gemini",
52
+ }
53
+
54
+ PROVIDER_PROBE_URLS: dict[Provider, str] = {
55
+ Provider.OPENAI: "https://api.openai.com/v1/models",
56
+ Provider.CLAUDE: "https://api.anthropic.com/v1/models",
57
+ Provider.GROK: "https://api.x.ai/v1/models",
58
+ Provider.DEEPSEEK: "https://api.deepseek.com/v1/models",
59
+ Provider.GLM: "https://api.z.ai/api/paas/v4/models",
60
+ Provider.MUSE: "https://api.meta.ai/v1/models",
61
+ Provider.GEMINI: "https://generativelanguage.googleapis.com/v1beta/models",
62
+ }
@@ -0,0 +1,260 @@
1
+ """The research surface's wire shapes, its status vocabulary, and its two identifier guards.
2
+
3
+ A thread has two identifiers and the API publishes only one of them. `encrypted_id` is the
4
+ public share token every route accepts in a path; `thread_id` is internal and is rejected by
5
+ path validation before any lookup. So the models here bind the CLI's `thread_id` field to the
6
+ wire key `encrypted_id` by alias, deliberately leave `populate_by_name` off so the internal
7
+ key cannot populate it, and set `extra="ignore"` so it is discarded on arrival instead.
8
+
9
+ `ResearchRunStatus` declares no thread field at all, because the status route carries only
10
+ the internal identifier — a field the CLI must never print, since a user would paste it back.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ from datetime import date, datetime
17
+ from enum import StrEnum
18
+ from typing import Annotated, Literal
19
+ from uuid import uuid4
20
+
21
+ from pydantic import AfterValidator, BaseModel, ConfigDict, Field, field_validator, model_validator
22
+
23
+ from ..lib.errors.failures import ResearchIdempotencyKeyInvalid, ResearchThreadIdInvalid
24
+
25
+ # The backend pins a thread share token to a UUIDv4 and matches path segments against it in
26
+ # transport, so the same shape is checked locally before a request is ever spent.
27
+ _THREAD_ID_PATTERN = re.compile(
28
+ r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"
29
+ )
30
+ _IDEMPOTENCY_KEY_PATTERN = re.compile(r"^[A-Za-z0-9._:-]{8,128}$")
31
+ _MAX_PROMPT_CHARACTERS = 20_000
32
+
33
+
34
+ class ResearchStatus(StrEnum):
35
+ """Every durable state a research run can report."""
36
+
37
+ ADMITTING = "admitting"
38
+ ACCEPTED = "accepted"
39
+ RUNNING = "running"
40
+ COMPLETED = "completed"
41
+ PARTIAL = "partial"
42
+ FAILED = "failed"
43
+ CANCELLED = "cancelled"
44
+ CREDIT_EXHAUSTED = "credit_exhausted"
45
+
46
+ def is_terminal(self) -> bool:
47
+ # True once the backend will not move this run again without a continuation.
48
+ return self in _TERMINAL_STATUSES
49
+
50
+ def is_resumable(self) -> bool:
51
+ # The three states the backend accepts a continuation from; anything else is a 409.
52
+ return self in _RESUMABLE_STATUSES
53
+
54
+
55
+ _TERMINAL_STATUSES = frozenset(
56
+ {
57
+ ResearchStatus.COMPLETED,
58
+ ResearchStatus.PARTIAL,
59
+ ResearchStatus.FAILED,
60
+ ResearchStatus.CANCELLED,
61
+ ResearchStatus.CREDIT_EXHAUSTED,
62
+ }
63
+ )
64
+ _RESUMABLE_STATUSES = frozenset(
65
+ {ResearchStatus.PARTIAL, ResearchStatus.FAILED, ResearchStatus.CREDIT_EXHAUSTED}
66
+ )
67
+
68
+
69
+ class ResearchSize(StrEnum):
70
+ """Preset breadth for a run, when the caller does not size it by hand."""
71
+
72
+ SMALL = "small"
73
+ MEDIUM = "medium"
74
+ LARGE = "large"
75
+
76
+
77
+ class ResearchKind(StrEnum):
78
+ """The resource kinds discovery is allowed to return."""
79
+
80
+ PAPER = "paper"
81
+ WEB = "web"
82
+
83
+
84
+ class ThreadId:
85
+ """One public research thread share token, validated on construction."""
86
+
87
+ def __init__(self, value: str) -> None:
88
+ self.value = value
89
+
90
+ @classmethod
91
+ def parse(cls, value: str) -> ThreadId:
92
+ # Raises the usage failure locally rather than spending a request on a 422.
93
+ candidate = value.strip()
94
+ if not _THREAD_ID_PATTERN.fullmatch(candidate):
95
+ raise ResearchThreadIdInvalid()
96
+ return cls(candidate)
97
+
98
+ @classmethod
99
+ def normalize(cls, value: str) -> str:
100
+ # The pydantic entry point: validating and canonicalizing are one operation.
101
+ return str(cls.parse(value))
102
+
103
+ def __str__(self) -> str:
104
+ return self.value
105
+
106
+
107
+ class IdempotencyKey:
108
+ """The key that lets one priced mutation be retried without being charged twice."""
109
+
110
+ def __init__(self, value: str) -> None:
111
+ self.value = value
112
+
113
+ @classmethod
114
+ def create(cls, explicit: str | None) -> IdempotencyKey:
115
+ # A fresh key per invocation, unless the caller supplies one to replay a mutation.
116
+ if explicit is None:
117
+ return cls(str(uuid4()))
118
+ candidate = explicit.strip()
119
+ if not _IDEMPOTENCY_KEY_PATTERN.fullmatch(candidate):
120
+ raise ResearchIdempotencyKeyInvalid()
121
+ return cls(candidate)
122
+
123
+ def __str__(self) -> str:
124
+ return self.value
125
+
126
+
127
+ NormalizedThreadId = Annotated[str, AfterValidator(ThreadId.normalize)]
128
+
129
+
130
+ class ResearchRunCreateRequest(BaseModel):
131
+ """The body both `research start` and `research add` submit.
132
+
133
+ Mirrors the backend request DTO field for field. Every optional field defaults to None so
134
+ `exclude_none` drops it from the encoded body: the backend forbids unknown keys and
135
+ requires a non-empty `resource_kinds` when the key is present, so an unset option has to
136
+ be absent rather than null or empty.
137
+ """
138
+
139
+ model_config = ConfigDict(extra="forbid", frozen=True)
140
+
141
+ prompt: str = Field(min_length=1, max_length=_MAX_PROMPT_CHARACTERS)
142
+ # Version 2 is the backend default and the only version that carries reference artifacts.
143
+ request_schema_version: Literal[2] = 2
144
+ size: ResearchSize | None = None
145
+ target_sources: int | None = Field(default=None, ge=1, le=1_000)
146
+ search_calls: int | None = Field(default=None, ge=1, le=100)
147
+ resource_kinds: list[ResearchKind] | None = Field(default=None, min_length=1, max_length=2)
148
+ include_domains: list[str] | None = Field(default=None, max_length=50)
149
+ exclude_domains: list[str] | None = Field(default=None, max_length=50)
150
+ published_after: str | None = Field(default=None, pattern=r"^\d{4}-\d{2}-\d{2}$")
151
+ language: str | None = Field(default=None, min_length=2, max_length=12)
152
+ max_run_cost_cents: int | None = Field(default=None, ge=1, le=100_000)
153
+
154
+ @field_validator("prompt")
155
+ @classmethod
156
+ def normalize_prompt(cls, value: str) -> str:
157
+ normalized = value.strip()
158
+ if not normalized:
159
+ raise ValueError("prompt cannot be blank")
160
+ return normalized
161
+
162
+ @field_validator("published_after")
163
+ @classmethod
164
+ def validate_published_after(cls, value: str | None) -> str | None:
165
+ # The pattern accepts 2025-13-45; the backend does not. Catching it here keeps a
166
+ # calendar typo from costing a request against a priced route.
167
+ if value is not None:
168
+ date.fromisoformat(value)
169
+ return value
170
+
171
+ @field_validator("include_domains", "exclude_domains", mode="before")
172
+ @classmethod
173
+ def normalize_domains(cls, value: object) -> object:
174
+ # The backend accepts hostnames only, so a pasted URL is corrected here, not there.
175
+ # Before the length bound, so a list that only exceeds 50 through duplicates passes.
176
+ if not isinstance(value, list):
177
+ return value
178
+ normalized: list[str] = []
179
+ for item in value:
180
+ host = str(item).strip().lower().rstrip(".")
181
+ if not host or "/" in host or ":" in host:
182
+ raise ValueError("domain filters must be hostname-only values")
183
+ if host not in normalized:
184
+ normalized.append(host)
185
+ return normalized
186
+
187
+ @field_validator("resource_kinds", mode="before")
188
+ @classmethod
189
+ def deduplicate_kinds(cls, value: object) -> object:
190
+ # Runs before the length bound: `--kind paper --kind paper --kind web` is three
191
+ # values and two kinds, and rejecting it for length would be a lie about the input.
192
+ if not isinstance(value, list):
193
+ return value
194
+ return list(dict.fromkeys(value))
195
+
196
+ @model_validator(mode="after")
197
+ def reject_contradictory_filters(self) -> ResearchRunCreateRequest:
198
+ # A domain in both lists can never match, so catch it before spending a request.
199
+ if set(self.include_domains or ()) & set(self.exclude_domains or ()):
200
+ raise ValueError("a domain cannot be both included and excluded")
201
+ return self
202
+
203
+
204
+ class ResearchRunAccepted(BaseModel):
205
+ """The 202 identity returned by start, add, and resume."""
206
+
207
+ model_config = ConfigDict(extra="ignore", frozen=True)
208
+
209
+ thread_id: str = Field(alias="encrypted_id")
210
+ run_id: str
211
+ status: ResearchStatus
212
+
213
+
214
+ class ResearchRunStatus(BaseModel):
215
+ """One run's durable progress.
216
+
217
+ The backend also sends the thread's internal identifier here. It is deliberately not
218
+ declared, so `extra="ignore"` drops it and no renderer can print an ID that would be
219
+ rejected the moment a user pasted it into `research add`.
220
+ """
221
+
222
+ model_config = ConfigDict(extra="ignore", frozen=True)
223
+
224
+ run_id: str
225
+ status: ResearchStatus
226
+ phase: str
227
+ continuation_count: int = Field(ge=0)
228
+ updated_at: datetime
229
+
230
+
231
+ class ResearchThreadSummary(BaseModel):
232
+ """One row of the caller's thread portfolio."""
233
+
234
+ model_config = ConfigDict(extra="ignore", frozen=True)
235
+
236
+ thread_id: str = Field(alias="encrypted_id")
237
+ title: str
238
+ run_count: int = Field(ge=0)
239
+ source_count: int = Field(ge=0)
240
+ artifact_count: int = Field(ge=0)
241
+ latest_run_id: str | None = None
242
+ latest_run_status: ResearchStatus | None = None
243
+ created_at: datetime
244
+ updated_at: datetime
245
+
246
+
247
+ class ResearchThreadDetail(ResearchThreadSummary):
248
+ """One thread read on its own, which carries two fields a portfolio row omits."""
249
+
250
+ latest_run_phase: str | None = None
251
+ favorite: bool = False
252
+
253
+
254
+ class ResearchPortfolioPage(BaseModel):
255
+ """One bounded page of threads and the cursor that continues it."""
256
+
257
+ model_config = ConfigDict(extra="ignore", frozen=True)
258
+
259
+ threads: list[ResearchThreadSummary]
260
+ next_cursor: str | None = None