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,540 @@
|
|
|
1
|
+
"""Runs the SDK-backed suggestion generation, critique, and revision workflow.
|
|
2
|
+
|
|
3
|
+
One generator agent owns the whole run: its first turn drafts the slate, and each
|
|
4
|
+
later turn carries one general critic handoff and returns the complete revised
|
|
5
|
+
slate on the same native thread, so every critic handoff stays in its history.
|
|
6
|
+
Message tools let the generator stop the run to ask its parent for input, and let
|
|
7
|
+
a critic stop its review early to message the generator directly.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import time
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any, Literal, cast
|
|
17
|
+
from uuid import uuid4
|
|
18
|
+
|
|
19
|
+
from ...lib.errors.failures import (
|
|
20
|
+
SuggestionInputInvalid,
|
|
21
|
+
SuggestionProviderFailed,
|
|
22
|
+
SuggestionSdkUnavailable,
|
|
23
|
+
)
|
|
24
|
+
from ...types.suggestions import (
|
|
25
|
+
RunStatus,
|
|
26
|
+
StopReason,
|
|
27
|
+
SuggestionAgentContext,
|
|
28
|
+
SuggestionCandidateBatch,
|
|
29
|
+
SuggestionCriticHandoff,
|
|
30
|
+
SuggestionDraft,
|
|
31
|
+
SuggestionFeedback,
|
|
32
|
+
SuggestionHorizon,
|
|
33
|
+
SuggestionIdea,
|
|
34
|
+
SuggestionRequest,
|
|
35
|
+
SuggestionResult,
|
|
36
|
+
)
|
|
37
|
+
from .categories import SuggestionCategories
|
|
38
|
+
from .extra_compute import ExtraComputeService
|
|
39
|
+
from .handoff import SuggestionHandoffBuilder
|
|
40
|
+
from .message_tools import SuggestionMessageTool
|
|
41
|
+
from .project import REJECTED_FEEDBACK_KIND, SuggestionProject, SuggestionProjectKey
|
|
42
|
+
from .prompts.library import SuggestionPrompts
|
|
43
|
+
from .sdk import (
|
|
44
|
+
SuggestionAgent,
|
|
45
|
+
SuggestionAgentSettingsInput,
|
|
46
|
+
SuggestionSdk,
|
|
47
|
+
SuggestionTextInput,
|
|
48
|
+
)
|
|
49
|
+
from .selection import SuggestionSelection
|
|
50
|
+
|
|
51
|
+
# The first draft is wider than the request so revision has material to cut.
|
|
52
|
+
_POOL_MULTIPLE = 2
|
|
53
|
+
_POOL_CAP = 40
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _WorkflowLimit(Exception):
|
|
57
|
+
"""Internal control flow for a caller-owned time or token boundary."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, reason: StopReason) -> None:
|
|
60
|
+
self.reason = reason
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class _AgentMessage(Exception):
|
|
64
|
+
"""Internal control flow for an agent that stopped its turn by sending a message."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, message: str) -> None:
|
|
67
|
+
self.message = message
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True, slots=True)
|
|
71
|
+
class _WorkflowOutcome:
|
|
72
|
+
"""Final ideas plus accounting and the reason the loop stopped."""
|
|
73
|
+
|
|
74
|
+
ideas: tuple[SuggestionIdea, ...]
|
|
75
|
+
usage: dict[str, int]
|
|
76
|
+
stop_reason: StopReason
|
|
77
|
+
warnings: tuple[str, ...] = ()
|
|
78
|
+
parent_message: str | None = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class SuggestionService:
|
|
82
|
+
"""Coordinates independent SDK agents and deterministic selection boundaries."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, sdk: Any | None = None) -> None:
|
|
85
|
+
# Injection keeps offline verification at the same typed agent boundary as production.
|
|
86
|
+
self._sdk = sdk
|
|
87
|
+
self._categories = SuggestionCategories()
|
|
88
|
+
self._selection = SuggestionSelection()
|
|
89
|
+
self._handoffs = SuggestionHandoffBuilder()
|
|
90
|
+
self._prompts = SuggestionPrompts()
|
|
91
|
+
|
|
92
|
+
def run(self, request: SuggestionRequest) -> SuggestionResult:
|
|
93
|
+
"""Validate one request, run the loop, and return a versioned result."""
|
|
94
|
+
if not isinstance(request, SuggestionRequest):
|
|
95
|
+
raise SuggestionInputInvalid()
|
|
96
|
+
# A dry run validates inputs and reports settings without loading the SDK.
|
|
97
|
+
if request.settings.dry_run:
|
|
98
|
+
return self._result(request, _WorkflowOutcome((), {}, StopReason.DRY_RUN))
|
|
99
|
+
sdk = self._sdk or SuggestionSdk.load()
|
|
100
|
+
# Every unexpected failure inside the loop surfaces as one typed provider failure.
|
|
101
|
+
try:
|
|
102
|
+
outcome = asyncio.run(self._run(request, sdk))
|
|
103
|
+
except (SuggestionSdkUnavailable, SuggestionProviderFailed):
|
|
104
|
+
raise
|
|
105
|
+
except Exception as error:
|
|
106
|
+
raise SuggestionProviderFailed(error) from error
|
|
107
|
+
return self._result(request, outcome)
|
|
108
|
+
|
|
109
|
+
async def _run(self, request: SuggestionRequest, sdk: Any) -> _WorkflowOutcome:
|
|
110
|
+
# Accounting and caller thresholds shared by every agent turn in this run.
|
|
111
|
+
started = time.monotonic()
|
|
112
|
+
usage = {"tokens": 0, "agent_calls": 0, "generation_calls": 0, "critique_calls": 0}
|
|
113
|
+
categories = request.settings.categories or self._categories.ids()
|
|
114
|
+
pool_size = min(request.settings.requested_count * _POOL_MULTIPLE, _POOL_CAP)
|
|
115
|
+
warnings: list[str] = list(request.context_warnings)
|
|
116
|
+
current: tuple[SuggestionIdea, ...] = ()
|
|
117
|
+
# Only the persistent generator can reach the parent; its message ends the run.
|
|
118
|
+
parent = SuggestionMessageTool("message_parent", self._prompts)
|
|
119
|
+
try:
|
|
120
|
+
if request.settings.extra_compute:
|
|
121
|
+
# Extra compute drafts in fresh per-category agents, then seeds the one
|
|
122
|
+
# persistent generator with the merged pool so revision keeps one thread.
|
|
123
|
+
drafts = await ExtraComputeService(self._categories, self._prompts).generate(
|
|
124
|
+
request,
|
|
125
|
+
categories,
|
|
126
|
+
pool_size,
|
|
127
|
+
lambda role, prompt, context, model: self._call_agent(
|
|
128
|
+
sdk,
|
|
129
|
+
role,
|
|
130
|
+
self._prompts.generator_system(),
|
|
131
|
+
prompt,
|
|
132
|
+
context,
|
|
133
|
+
model,
|
|
134
|
+
SuggestionCandidateBatch,
|
|
135
|
+
request,
|
|
136
|
+
started,
|
|
137
|
+
usage,
|
|
138
|
+
"generation",
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
current = self._ideas_from_drafts(drafts, request, 1)
|
|
142
|
+
seed = tuple(idea.to_draft() for idea in current)
|
|
143
|
+
generator = self._generator(request, sdk, categories, parent, seed)
|
|
144
|
+
else:
|
|
145
|
+
# The generator drafts the first slate on the thread it keeps for the run.
|
|
146
|
+
generator = self._generator(request, sdk, categories, parent)
|
|
147
|
+
batch = await self._ask(
|
|
148
|
+
sdk,
|
|
149
|
+
generator,
|
|
150
|
+
self._prompts.generator_turn(request.goal, pool_size),
|
|
151
|
+
SuggestionCandidateBatch,
|
|
152
|
+
request,
|
|
153
|
+
started,
|
|
154
|
+
usage,
|
|
155
|
+
"generation",
|
|
156
|
+
parent,
|
|
157
|
+
)
|
|
158
|
+
current = self._ideas_from_drafts(batch.ideas, request, 1)
|
|
159
|
+
if not current:
|
|
160
|
+
return _WorkflowOutcome((), usage, StopReason.COUNT_SHORTFALL, tuple(warnings))
|
|
161
|
+
|
|
162
|
+
messages = 0
|
|
163
|
+
for round_index in range(request.settings.rounds):
|
|
164
|
+
# A fresh critic grades the latest slate and returns one general handoff.
|
|
165
|
+
# It gets the message tool only while the caller's --max-messages remains.
|
|
166
|
+
critic_tool = (
|
|
167
|
+
SuggestionMessageTool("message_generator", self._prompts)
|
|
168
|
+
if messages < request.settings.max_messages
|
|
169
|
+
else None
|
|
170
|
+
)
|
|
171
|
+
try:
|
|
172
|
+
handoff = await self._critique(
|
|
173
|
+
request, sdk, categories, current, started, usage, critic_tool
|
|
174
|
+
)
|
|
175
|
+
prompt = self._prompts.revision_turn(
|
|
176
|
+
request.goal, request.settings.requested_count, handoff.handoff
|
|
177
|
+
)
|
|
178
|
+
except _AgentMessage as sent:
|
|
179
|
+
# A critic that stopped early sends its message in place of the review.
|
|
180
|
+
messages += 1
|
|
181
|
+
prompt = self._prompts.critic_message_turn(
|
|
182
|
+
request.goal, request.settings.requested_count, sent.message
|
|
183
|
+
)
|
|
184
|
+
# The handoff is the generator's next turn, so its thread accumulates
|
|
185
|
+
# generation, critique, revision, critique, ... across every round.
|
|
186
|
+
batch = await self._ask(
|
|
187
|
+
sdk,
|
|
188
|
+
generator,
|
|
189
|
+
prompt,
|
|
190
|
+
SuggestionCandidateBatch,
|
|
191
|
+
request,
|
|
192
|
+
started,
|
|
193
|
+
usage,
|
|
194
|
+
"revision",
|
|
195
|
+
parent,
|
|
196
|
+
)
|
|
197
|
+
# The generator owns the slate, so its complete reply replaces the last one.
|
|
198
|
+
current = self._ideas_from_drafts(batch.ideas, request, round_index + 2)
|
|
199
|
+
if not current:
|
|
200
|
+
break
|
|
201
|
+
except _AgentMessage as sent:
|
|
202
|
+
# The generator asked its parent for input, so the run stops without a slate.
|
|
203
|
+
return _WorkflowOutcome(
|
|
204
|
+
(), usage, StopReason.PARENT_MESSAGE, tuple(warnings), sent.message
|
|
205
|
+
)
|
|
206
|
+
except _WorkflowLimit as limit:
|
|
207
|
+
# A caller threshold ends the run with the latest generator-owned slate.
|
|
208
|
+
return _WorkflowOutcome(
|
|
209
|
+
self._finalize(current, request), usage, limit.reason, tuple(warnings)
|
|
210
|
+
)
|
|
211
|
+
ideas = self._finalize(current, request)
|
|
212
|
+
reason = (
|
|
213
|
+
StopReason.COMPLETED
|
|
214
|
+
if len(ideas) >= request.settings.requested_count
|
|
215
|
+
else StopReason.COUNT_SHORTFALL
|
|
216
|
+
)
|
|
217
|
+
return _WorkflowOutcome(ideas, usage, reason, tuple(warnings))
|
|
218
|
+
|
|
219
|
+
def _generator(
|
|
220
|
+
self,
|
|
221
|
+
request: SuggestionRequest,
|
|
222
|
+
sdk: Any,
|
|
223
|
+
categories: tuple[str, ...],
|
|
224
|
+
parent: SuggestionMessageTool,
|
|
225
|
+
candidates: tuple[SuggestionDraft, ...] = (),
|
|
226
|
+
) -> SuggestionAgent:
|
|
227
|
+
# Builds the run's single generator; reusing the object resumes its native thread.
|
|
228
|
+
return self._agent(
|
|
229
|
+
sdk,
|
|
230
|
+
"generator",
|
|
231
|
+
self._prompts.generator_system(),
|
|
232
|
+
self._stage_context(request, categories, candidates),
|
|
233
|
+
None,
|
|
234
|
+
SuggestionCandidateBatch,
|
|
235
|
+
request,
|
|
236
|
+
(parent,),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
async def _critique(
|
|
240
|
+
self,
|
|
241
|
+
request: SuggestionRequest,
|
|
242
|
+
sdk: Any,
|
|
243
|
+
categories: tuple[str, ...],
|
|
244
|
+
ideas: tuple[SuggestionIdea, ...],
|
|
245
|
+
started: float,
|
|
246
|
+
usage: dict[str, int],
|
|
247
|
+
tool: SuggestionMessageTool | None,
|
|
248
|
+
) -> SuggestionCriticHandoff:
|
|
249
|
+
# Each round gets an independent critic that sees the slate but no generator history.
|
|
250
|
+
context = self._stage_context(request, categories, tuple(idea.to_draft() for idea in ideas))
|
|
251
|
+
ids = ", ".join(idea.id for idea in ideas)
|
|
252
|
+
prompt = self._prompts.critic_turn(request.goal, ids)
|
|
253
|
+
return cast(
|
|
254
|
+
SuggestionCriticHandoff,
|
|
255
|
+
await self._call_agent(
|
|
256
|
+
sdk,
|
|
257
|
+
"critic",
|
|
258
|
+
self._prompts.critic_system(),
|
|
259
|
+
prompt,
|
|
260
|
+
context,
|
|
261
|
+
request.settings.critic_model,
|
|
262
|
+
SuggestionCriticHandoff,
|
|
263
|
+
request,
|
|
264
|
+
started,
|
|
265
|
+
usage,
|
|
266
|
+
"critique",
|
|
267
|
+
tool,
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
async def _call_agent(
|
|
272
|
+
self,
|
|
273
|
+
sdk: Any,
|
|
274
|
+
role: Literal["generator", "critic"],
|
|
275
|
+
system_prompt: str,
|
|
276
|
+
prompt: str,
|
|
277
|
+
context: SuggestionAgentContext,
|
|
278
|
+
model: str | None,
|
|
279
|
+
schema: type[Any],
|
|
280
|
+
request: SuggestionRequest,
|
|
281
|
+
started: float,
|
|
282
|
+
usage: dict[str, int],
|
|
283
|
+
phase: str,
|
|
284
|
+
tool: SuggestionMessageTool | None = None,
|
|
285
|
+
) -> Any:
|
|
286
|
+
# One fresh agent answering one turn: the critic and extra-compute fan-out shape.
|
|
287
|
+
self._check_limit(request, started, usage)
|
|
288
|
+
tools = () if tool is None else (tool,)
|
|
289
|
+
agent = self._agent(sdk, role, system_prompt, context, model, schema, request, tools)
|
|
290
|
+
return await self._ask(sdk, agent, prompt, schema, request, started, usage, phase, tool)
|
|
291
|
+
|
|
292
|
+
def _agent(
|
|
293
|
+
self,
|
|
294
|
+
sdk: Any,
|
|
295
|
+
role: Literal["generator", "critic"],
|
|
296
|
+
system_prompt: str,
|
|
297
|
+
context: SuggestionAgentContext,
|
|
298
|
+
model: str | None,
|
|
299
|
+
schema: type[Any],
|
|
300
|
+
request: SuggestionRequest,
|
|
301
|
+
tools: tuple[SuggestionMessageTool, ...] = (),
|
|
302
|
+
) -> SuggestionAgent:
|
|
303
|
+
# Every agent is read-only and bound to one output schema for all of its turns.
|
|
304
|
+
settings = sdk.agent_settings(
|
|
305
|
+
SuggestionAgentSettingsInput(
|
|
306
|
+
role=role,
|
|
307
|
+
system_prompt=system_prompt,
|
|
308
|
+
context=context,
|
|
309
|
+
output_schema=schema,
|
|
310
|
+
provider=request.settings.provider,
|
|
311
|
+
model=model,
|
|
312
|
+
tools=tools,
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
return cast(SuggestionAgent, sdk.agent(settings))
|
|
316
|
+
|
|
317
|
+
async def _ask(
|
|
318
|
+
self,
|
|
319
|
+
sdk: Any,
|
|
320
|
+
agent: SuggestionAgent,
|
|
321
|
+
prompt: str,
|
|
322
|
+
schema: type[Any],
|
|
323
|
+
request: SuggestionRequest,
|
|
324
|
+
started: float,
|
|
325
|
+
usage: dict[str, int],
|
|
326
|
+
phase: str,
|
|
327
|
+
tool: SuggestionMessageTool | None = None,
|
|
328
|
+
) -> Any:
|
|
329
|
+
# Runs one turn inside the caller's time and token limits and parses its artifact.
|
|
330
|
+
self._check_limit(request, started, usage)
|
|
331
|
+
run_input = sdk.run_input(
|
|
332
|
+
SuggestionTextInput(self._with_output_budget(prompt, request), request.attachments)
|
|
333
|
+
)
|
|
334
|
+
remaining = self._remaining_seconds(request, started)
|
|
335
|
+
try:
|
|
336
|
+
if remaining is None:
|
|
337
|
+
reply = await self._arun(agent, run_input, tool)
|
|
338
|
+
else:
|
|
339
|
+
async with asyncio.timeout(remaining):
|
|
340
|
+
reply = await self._arun(agent, run_input, tool)
|
|
341
|
+
except TimeoutError as error:
|
|
342
|
+
raise _WorkflowLimit(StopReason.TIME_LIMIT) from error
|
|
343
|
+
except _AgentMessage:
|
|
344
|
+
# A stopped turn still counts as a call, but it returns no provider usage.
|
|
345
|
+
self._record_usage(None, usage, phase)
|
|
346
|
+
raise
|
|
347
|
+
self._record_usage(reply, usage, phase)
|
|
348
|
+
structured = getattr(reply, "structured", None)
|
|
349
|
+
if isinstance(structured, schema):
|
|
350
|
+
return structured
|
|
351
|
+
if isinstance(structured, Mapping):
|
|
352
|
+
return schema.model_validate(structured)
|
|
353
|
+
raise ValueError(f"{phase} agent returned no structured artifact")
|
|
354
|
+
|
|
355
|
+
async def _arun(
|
|
356
|
+
self, agent: SuggestionAgent, run_input: Any, tool: SuggestionMessageTool | None
|
|
357
|
+
) -> Any:
|
|
358
|
+
# A message tool stops the turn: the run is cancelled as soon as its message lands,
|
|
359
|
+
# so nothing the model does after the call can reach the workflow.
|
|
360
|
+
if tool is None:
|
|
361
|
+
return await agent.arun(run_input)
|
|
362
|
+
run = asyncio.ensure_future(agent.arun(run_input))
|
|
363
|
+
stop = asyncio.ensure_future(tool.stopped.wait())
|
|
364
|
+
try:
|
|
365
|
+
await asyncio.wait((run, stop), return_when=asyncio.FIRST_COMPLETED)
|
|
366
|
+
finally:
|
|
367
|
+
for task in (run, stop):
|
|
368
|
+
task.cancel()
|
|
369
|
+
# The message wins even when the turn also finished, because the call means stop.
|
|
370
|
+
if tool.message is not None:
|
|
371
|
+
await asyncio.gather(run, return_exceptions=True)
|
|
372
|
+
raise _AgentMessage(tool.message)
|
|
373
|
+
return run.result()
|
|
374
|
+
|
|
375
|
+
def _ideas_from_drafts(
|
|
376
|
+
self, drafts: tuple[SuggestionDraft, ...], request: SuggestionRequest, revision: int
|
|
377
|
+
) -> tuple[SuggestionIdea, ...]:
|
|
378
|
+
# Identity is positional, which is how the critic and the revision turn name candidates.
|
|
379
|
+
return tuple(
|
|
380
|
+
self._idea_from_draft(draft, request, f"idea-{index:03d}", revision, index)
|
|
381
|
+
for index, draft in enumerate(drafts, 1)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def _idea_from_draft(
|
|
385
|
+
self,
|
|
386
|
+
draft: SuggestionDraft,
|
|
387
|
+
request: SuggestionRequest,
|
|
388
|
+
idea_id: str,
|
|
389
|
+
revision: int,
|
|
390
|
+
rank: int,
|
|
391
|
+
) -> SuggestionIdea:
|
|
392
|
+
# Adds CLI-owned identity and a provisional handoff to one model draft.
|
|
393
|
+
values = draft.model_dump(exclude={"idea_id"})
|
|
394
|
+
handoff = self._handoffs.build_draft(draft, request, idea_id, revision)
|
|
395
|
+
return SuggestionIdea(**values, id=idea_id, revision=revision, rank=rank, handoff=handoff)
|
|
396
|
+
|
|
397
|
+
def _finalize(
|
|
398
|
+
self, ideas: tuple[SuggestionIdea, ...], request: SuggestionRequest
|
|
399
|
+
) -> tuple[SuggestionIdea, ...]:
|
|
400
|
+
# Deterministic gates the model cannot talk its way past, applied to the final slate.
|
|
401
|
+
allowed = set(self._categories.ids())
|
|
402
|
+
manifest_refs = {
|
|
403
|
+
entry.ref for entry in request.context_manifest if entry.status != "omitted"
|
|
404
|
+
}
|
|
405
|
+
# Without a manifest, the supplied context items are the only citable evidence.
|
|
406
|
+
if not manifest_refs:
|
|
407
|
+
manifest_refs = {item.ref for item in request.context_items}
|
|
408
|
+
selected = self._selection.validate_categories(ideas, allowed)
|
|
409
|
+
selected = self._selection.validate_evidence(selected, manifest_refs)
|
|
410
|
+
selected = self._selection.deduplicate(selected)
|
|
411
|
+
selected = self._selection.suppress_rejected(selected, self._rejected_terms(request))
|
|
412
|
+
if request.settings.horizon is not SuggestionHorizon.ANY:
|
|
413
|
+
selected = tuple(
|
|
414
|
+
idea for idea in selected if idea.horizon.value == request.settings.horizon.value
|
|
415
|
+
)
|
|
416
|
+
# Handoffs are rebuilt last so they embed only evidence that survived the gates.
|
|
417
|
+
selected = tuple(self._handoff_refresh(idea, request) for idea in selected)
|
|
418
|
+
return self._selection.rank(selected, request.settings.requested_count)
|
|
419
|
+
|
|
420
|
+
def _handoff_refresh(self, idea: SuggestionIdea, request: SuggestionRequest) -> SuggestionIdea:
|
|
421
|
+
# Replaces the provisional draft handoff with the final evidence-bearing one.
|
|
422
|
+
return idea.model_copy(update={"handoff": self._handoffs.build(idea, request)})
|
|
423
|
+
|
|
424
|
+
def _stage_context(
|
|
425
|
+
self,
|
|
426
|
+
request: SuggestionRequest,
|
|
427
|
+
categories: tuple[str, ...],
|
|
428
|
+
candidates: tuple[SuggestionDraft, ...] = (),
|
|
429
|
+
) -> SuggestionAgentContext:
|
|
430
|
+
# One window type per stage: what a stage omits, it simply never passes here.
|
|
431
|
+
return SuggestionAgentContext.for_stage(
|
|
432
|
+
request.context,
|
|
433
|
+
self._categories.prompt_section(categories),
|
|
434
|
+
candidates,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
def _check_limit(
|
|
438
|
+
self, request: SuggestionRequest, started: float, usage: dict[str, int]
|
|
439
|
+
) -> None:
|
|
440
|
+
# Raised before a turn starts, so a limit never discards a finished reply.
|
|
441
|
+
if (
|
|
442
|
+
request.settings.max_total_tokens is not None
|
|
443
|
+
and usage["tokens"] >= request.settings.max_total_tokens
|
|
444
|
+
):
|
|
445
|
+
raise _WorkflowLimit(StopReason.TOKEN_LIMIT)
|
|
446
|
+
if self._remaining_seconds(request, started) == 0:
|
|
447
|
+
raise _WorkflowLimit(StopReason.TIME_LIMIT)
|
|
448
|
+
|
|
449
|
+
def _with_output_budget(self, prompt: str, request: SuggestionRequest) -> str:
|
|
450
|
+
# Carries the caller's generous output ceiling into every typed model turn.
|
|
451
|
+
limit = request.settings.max_output_tokens
|
|
452
|
+
if limit is None:
|
|
453
|
+
return prompt
|
|
454
|
+
return f"{prompt}\n\nKeep the structured response within {limit} output tokens."
|
|
455
|
+
|
|
456
|
+
def _remaining_seconds(self, request: SuggestionRequest, started: float) -> float | None:
|
|
457
|
+
# None means no deadline; zero means the deadline has already passed.
|
|
458
|
+
limit = request.settings.timeout_seconds
|
|
459
|
+
if limit is None:
|
|
460
|
+
return None
|
|
461
|
+
remaining = limit - (time.monotonic() - started)
|
|
462
|
+
return max(remaining, 0.0)
|
|
463
|
+
|
|
464
|
+
def _record_usage(self, reply: Any, usage: dict[str, int], phase: str) -> None:
|
|
465
|
+
# Counts calls per phase and adds the provider's token total when it reports one.
|
|
466
|
+
usage["agent_calls"] += 1
|
|
467
|
+
usage[f"{phase}_calls"] = usage.get(f"{phase}_calls", 0) + 1
|
|
468
|
+
codex = getattr(reply, "codex", None)
|
|
469
|
+
snapshot = getattr(codex, "last_usage", None) or getattr(codex, "usage", None)
|
|
470
|
+
tokens = int(getattr(snapshot, "total_tokens", 0) or 0)
|
|
471
|
+
usage["tokens"] += max(tokens, 0)
|
|
472
|
+
|
|
473
|
+
def _rejected_terms(self, request: SuggestionRequest) -> tuple[str, ...]:
|
|
474
|
+
# Rejected project feedback carries a label and optional reason, so only its verbatim
|
|
475
|
+
# suggestion text is used as a suppression needle.
|
|
476
|
+
terms: list[str] = []
|
|
477
|
+
for item in request.context_items:
|
|
478
|
+
if item.kind == REJECTED_FEEDBACK_KIND:
|
|
479
|
+
terms.append(SuggestionFeedback.suggestion_from_context(item.content))
|
|
480
|
+
elif item.kind in {"completed", "in_progress", "avoid", "mistakes", "forbidden"}:
|
|
481
|
+
terms.append(item.content)
|
|
482
|
+
return tuple(terms)
|
|
483
|
+
|
|
484
|
+
def _result(self, request: SuggestionRequest, outcome: _WorkflowOutcome) -> SuggestionResult:
|
|
485
|
+
# Converts the loop outcome into the versioned envelope the command renders.
|
|
486
|
+
ideas = outcome.ideas
|
|
487
|
+
warnings = list(outcome.warnings)
|
|
488
|
+
if outcome.stop_reason is StopReason.COUNT_SHORTFALL and ideas:
|
|
489
|
+
warnings.append(
|
|
490
|
+
f"Only {len(ideas)} worthwhile suggestions survived review; no filler was added."
|
|
491
|
+
)
|
|
492
|
+
status = (
|
|
493
|
+
RunStatus.NEEDS_INPUT
|
|
494
|
+
if outcome.stop_reason is StopReason.PARENT_MESSAGE
|
|
495
|
+
else RunStatus.NO_SUGGESTIONS
|
|
496
|
+
if not ideas
|
|
497
|
+
else RunStatus.COMPLETE
|
|
498
|
+
if outcome.stop_reason is StopReason.COMPLETED
|
|
499
|
+
else RunStatus.PARTIAL
|
|
500
|
+
)
|
|
501
|
+
# Names the context kinds the caller never supplied, so gaps are visible, not guessed.
|
|
502
|
+
present_kinds = {item.kind for item in request.context_items}
|
|
503
|
+
missing = tuple(
|
|
504
|
+
kind
|
|
505
|
+
for kind in (
|
|
506
|
+
"completed",
|
|
507
|
+
"in_progress",
|
|
508
|
+
"decision",
|
|
509
|
+
"constraint",
|
|
510
|
+
"risks",
|
|
511
|
+
"trajectory",
|
|
512
|
+
)
|
|
513
|
+
if kind not in present_kinds
|
|
514
|
+
)
|
|
515
|
+
return SuggestionResult(
|
|
516
|
+
run_id=f"sug-{uuid4().hex[:12]}",
|
|
517
|
+
status=status,
|
|
518
|
+
goal=request.goal,
|
|
519
|
+
project_key=request.project_key,
|
|
520
|
+
requested_count=request.settings.requested_count,
|
|
521
|
+
returned_count=len(ideas),
|
|
522
|
+
settings=request.settings,
|
|
523
|
+
context_manifest=request.context_manifest,
|
|
524
|
+
ideas=ideas,
|
|
525
|
+
category_coverage=self._selection.coverage(ideas),
|
|
526
|
+
missing_context=missing,
|
|
527
|
+
warnings=tuple(dict.fromkeys(warnings)),
|
|
528
|
+
usage=outcome.usage,
|
|
529
|
+
stop_reason=outcome.stop_reason,
|
|
530
|
+
parent_message=outcome.parent_message,
|
|
531
|
+
feedback_capture=(
|
|
532
|
+
SuggestionProject.feedback_capture(SuggestionProjectKey(request.project_key))
|
|
533
|
+
if request.project_key
|
|
534
|
+
else None
|
|
535
|
+
),
|
|
536
|
+
prompt_version=request.prompt_version,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
__all__ = ["SuggestionService"]
|
|
File without changes
|
vidbyte_cli/types/api.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""The transport envelope and the identity behind a validated API key.
|
|
2
|
+
|
|
3
|
+
This is *how* the CLI talks to the backend, independent of any one domain — a product's own
|
|
4
|
+
wire shapes belong beside that product, the way types/research.py owns the research surface.
|
|
5
|
+
Keep the envelope field names in sync with the backend response wrapper; the ApiClient
|
|
6
|
+
unwraps `data` so callers only ever see the typed payload.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Generic, Literal, TypeVar
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ApiError(BaseModel):
|
|
19
|
+
# Structured backend error; mapped to a CliError before it ever reaches the user.
|
|
20
|
+
code: str
|
|
21
|
+
title: str
|
|
22
|
+
detail: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ApiUsageRemediation(BaseModel):
|
|
26
|
+
"""Safe, bounded instructions for restoring a Vidbyte API balance."""
|
|
27
|
+
|
|
28
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
29
|
+
|
|
30
|
+
action: Literal["top_up_api_balance"]
|
|
31
|
+
requires_user_approval: bool
|
|
32
|
+
topup_method: Literal["POST"]
|
|
33
|
+
topup_path: str = Field(min_length=1, max_length=128, pattern=r"^/[A-Za-z0-9_./{}-]+$")
|
|
34
|
+
cli_command: Literal["vidbyte-cli billing top-up --confirm"]
|
|
35
|
+
minimum_topup_cents: int = Field(ge=1, le=1_000_000)
|
|
36
|
+
supported_payment_methods: tuple[Literal["x402", "mpp"], ...] = Field(
|
|
37
|
+
min_length=1, max_length=4
|
|
38
|
+
)
|
|
39
|
+
browser_url: str = Field(min_length=1, max_length=512, pattern=r"^https://")
|
|
40
|
+
retry_original_operation: bool
|
|
41
|
+
steps: tuple[str, ...] = Field(min_length=1, max_length=20)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ApiUsageExhaustedProblem(BaseModel):
|
|
45
|
+
"""The backend's allowlisted HTTP 402 account-balance problem."""
|
|
46
|
+
|
|
47
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
48
|
+
|
|
49
|
+
error: Literal[True]
|
|
50
|
+
title: str = Field(min_length=1, max_length=256)
|
|
51
|
+
subtitle: str = Field(min_length=1, max_length=512)
|
|
52
|
+
description: str = Field(min_length=1, max_length=8192)
|
|
53
|
+
code: Literal["api_usage_exhausted", "usage_credit_exhausted"]
|
|
54
|
+
incident_id: str = Field(min_length=1, max_length=64)
|
|
55
|
+
remediation: ApiUsageRemediation
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class BillingTopUpResult(BaseModel):
|
|
59
|
+
"""The safe success fields returned after an API-balance top-up."""
|
|
60
|
+
|
|
61
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
62
|
+
|
|
63
|
+
credited_cents: int = Field(ge=1)
|
|
64
|
+
rail: str = Field(min_length=1, max_length=32)
|
|
65
|
+
payment_ref: str | None = Field(default=None, max_length=256)
|
|
66
|
+
available_balance_cents: int = Field(ge=0)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ApiPagination(BaseModel):
|
|
70
|
+
limit: int
|
|
71
|
+
page: int
|
|
72
|
+
total: int | None = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ApiEnvelope(BaseModel, Generic[T]):
|
|
76
|
+
# Standard response wrapper the ApiClient unwraps so callers see only `data`.
|
|
77
|
+
success: bool
|
|
78
|
+
message: str | None = None
|
|
79
|
+
data: T | None = None
|
|
80
|
+
error: ApiError | None = None
|
|
81
|
+
pagination: ApiPagination | None = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class KeyIdentity(BaseModel):
|
|
85
|
+
"""Non-secret identity behind a validated API key.
|
|
86
|
+
|
|
87
|
+
`extra="ignore"` is the one deliberate departure from this repo's `extra="forbid"` default.
|
|
88
|
+
The success body also carries a live, long-lived session token this CLI has no use for, and
|
|
89
|
+
ignoring it keeps that token out of every modelled field, output document, and log line.
|
|
90
|
+
The body's `email` is dropped for a different reason: the backend sets it to the user id,
|
|
91
|
+
so presenting it as an email address would be a lie.
|
|
92
|
+
|
|
93
|
+
`success` is modelled rather than ignored so a response that declares its own failure is a
|
|
94
|
+
detectable protocol violation instead of a silent approval.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
model_config = ConfigDict(extra="ignore", frozen=True)
|
|
98
|
+
|
|
99
|
+
success: bool
|
|
100
|
+
username: str = Field(min_length=1, max_length=256)
|
|
101
|
+
account_tier: str = Field(min_length=1, max_length=64)
|