Skip to content

Commit e720759

Browse files
committed
fix: address review findings -- error handling, security, docs
1 parent a585d1f commit e720759

6 files changed

Lines changed: 79 additions & 24 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ See `web/CLAUDE.md` for the full component inventory, design token rules, and po
9090
- **Every module** with business logic MUST have: `from synthorg.observability import get_logger` then `logger = get_logger(__name__)`
9191
- **Never** use `import logging` / `logging.getLogger()` / `print()` in application code (exception: `observability/setup.py`, `observability/sinks.py`, `observability/syslog_handler.py`, `observability/http_handler.py`, and `observability/otlp_handler.py` may use stdlib `logging` and `print(..., file=sys.stderr)` for handler construction, bootstrap, and error reporting code that runs before or during logging system configuration)
9292
- **Variable name**: always `logger` (not `_logger`, not `log`)
93-
- **Event names**: always use constants from the domain-specific module under `synthorg.observability.events` (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`, `GIT_COMMAND_START` from `events.git`, `CONTEXT_BUDGET_FILL_UPDATED`, `CONTEXT_BUDGET_COMPACTION_STARTED`, `CONTEXT_BUDGET_COMPACTION_COMPLETED`, `CONTEXT_BUDGET_COMPACTION_FAILED`, `CONTEXT_BUDGET_COMPACTION_SKIPPED`, `CONTEXT_BUDGET_COMPACTION_FALLBACK`, `CONTEXT_BUDGET_INDICATOR_INJECTED`, `CONTEXT_BUDGET_AGENT_COMPACTION_REQUESTED`, `CONTEXT_BUDGET_EPISTEMIC_MARKERS_PRESERVED` from `events.context_budget`, `BACKUP_STARTED` from `events.backup`, `SETUP_COMPLETED` from `events.setup`, `ROUTING_CANDIDATE_SELECTED` from `events.routing`, `SHIPPING_HTTP_BATCH_SENT` from `events.shipping`, `EVAL_REPORT_COMPUTED` from `events.evaluation`, `PROMPT_PROFILE_SELECTED` from `events.prompt`, `PROCEDURAL_MEMORY_START` from `events.procedural_memory`, `PERF_LLM_JUDGE_STARTED` from `events.performance`, `TASK_ENGINE_OBSERVER_FAILED` from `events.task_engine`, `WORKFLOW_EXEC_COMPLETED` from `events.workflow_execution`, `BLUEPRINT_INSTANTIATE_START` from `events.blueprint`, `WORKFLOW_DEF_ROLLED_BACK` from `events.workflow_definition`, `WORKFLOW_VERSION_SAVED` from `events.workflow_version`, `MEMORY_FINE_TUNE_STARTED`, `MEMORY_SELF_EDIT_TOOL_EXECUTE`, `MEMORY_SELF_EDIT_CORE_READ`, `MEMORY_SELF_EDIT_CORE_WRITE`, `MEMORY_SELF_EDIT_CORE_WRITE_REJECTED`, `MEMORY_SELF_EDIT_ARCHIVAL_SEARCH`, `MEMORY_SELF_EDIT_ARCHIVAL_WRITE`, `MEMORY_SELF_EDIT_RECALL_READ`, `MEMORY_SELF_EDIT_RECALL_WRITE`, `MEMORY_SELF_EDIT_WRITE_FAILED` from `events.memory`, `REPORTING_GENERATION_STARTED` from `events.reporting`, `RISK_BUDGET_SCORE_COMPUTED` from `events.risk_budget`, `LLM_STRATEGY_SYNTHESIZED` and `DISTILLATION_CAPTURED` from `events.consolidation`, `MEMORY_DIVERSITY_RERANKED`, `MEMORY_DIVERSITY_RERANK_FAILED`, and `MEMORY_REFORMULATION_ROUND` from `events.memory`, `NOTIFICATION_DISPATCHED` and `NOTIFICATION_DISPATCH_FAILED` from `events.notification`, `QUALITY_STEP_CLASSIFIED` from `events.quality`, `HEALTH_TICKET_EMITTED` from `events.health`, `TRAJECTORY_SCORING_START` from `events.trajectory`, `COORD_METRICS_AMDAHL_COMPUTED` from `events.coordination_metrics`, `COORDINATION_STARTED`, `COORDINATION_COMPLETED`, `COORDINATION_FAILED`, `COORDINATION_PHASE_STARTED`, `COORDINATION_PHASE_COMPLETED`, `COORDINATION_PHASE_FAILED`, `COORDINATION_WAVE_STARTED`, `COORDINATION_WAVE_COMPLETED`, `COORDINATION_TOPOLOGY_RESOLVED`, `COORDINATION_CLEANUP_STARTED`, `COORDINATION_CLEANUP_COMPLETED`, `COORDINATION_CLEANUP_FAILED`, `COORDINATION_WAVE_BUILT`, `COORDINATION_FACTORY_BUILT`, and `COORDINATION_ATTRIBUTION_BUILT` from `events.coordination`, `WEB_REQUEST_START` and `WEB_SSRF_BLOCKED` from `events.web`, `DB_QUERY_START` and `DB_WRITE_BLOCKED` from `events.database`, `TERMINAL_COMMAND_START` and `TERMINAL_COMMAND_BLOCKED` from `events.terminal`, `SUB_CONSTRAINT_RESOLVED` and `SUB_CONSTRAINT_DENIED` from `events.sub_constraint`, `VERSION_SAVED` and `VERSION_SNAPSHOT_FAILED` from `events.versioning`, `ANALYTICS_AGGREGATION_COMPUTED` and `ANALYTICS_RETRY_RATE_ALERT` from `events.analytics`, `CALL_CLASSIFICATION_COMPUTED` from `events.call_classification`, `QUOTA_THRESHOLD_ALERT` and `QUOTA_POLL_FAILED` from `events.quota`, `CONFLICT_DEBATE_EVALUATOR_FAILED` from `events.conflict`, `DELEGATION_LOOP_CIRCUIT_BACKOFF` and `DELEGATION_LOOP_CIRCUIT_PERSIST_FAILED` from `events.delegation`, `MEETING_EVENT_COOLDOWN_SKIPPED` and `MEETING_TASKS_CAPPED` from `events.meeting`, `PERSISTENCE_CIRCUIT_BREAKER_SAVED`, `PERSISTENCE_CIRCUIT_BREAKER_SAVE_FAILED`, `PERSISTENCE_CIRCUIT_BREAKER_LOADED`, `PERSISTENCE_CIRCUIT_BREAKER_LOAD_FAILED`, `PERSISTENCE_CIRCUIT_BREAKER_DELETED`, and `PERSISTENCE_CIRCUIT_BREAKER_DELETE_FAILED` from `events.persistence`, `METRICS_SCRAPE_COMPLETED`, `METRICS_SCRAPE_FAILED`, `METRICS_COLLECTOR_INITIALIZED`, `METRICS_COORDINATION_RECORDED`, `METRICS_OTLP_EXPORT_COMPLETED` and `METRICS_OTLP_FLUSHER_STOPPED` from `events.metrics`, `ORG_MEMORY_QUERY_START`, `ORG_MEMORY_QUERY_COMPLETE`, `ORG_MEMORY_QUERY_FAILED`, `ORG_MEMORY_WRITE_START`, `ORG_MEMORY_WRITE_COMPLETE`, `ORG_MEMORY_WRITE_DENIED`, `ORG_MEMORY_WRITE_FAILED`, `ORG_MEMORY_POLICIES_LISTED`, `ORG_MEMORY_BACKEND_CREATED`, `ORG_MEMORY_CONNECT_FAILED`, `ORG_MEMORY_DISCONNECT_FAILED`, `ORG_MEMORY_NOT_CONNECTED`, `ORG_MEMORY_ROW_PARSE_FAILED`, `ORG_MEMORY_CONFIG_INVALID`, `ORG_MEMORY_MODEL_INVALID`, `ORG_MEMORY_MVCC_PUBLISH_APPENDED`, `ORG_MEMORY_MVCC_RETRACT_APPENDED`, `ORG_MEMORY_MVCC_SNAPSHOT_AT_QUERIED`, and `ORG_MEMORY_MVCC_LOG_QUERIED` from `events.org_memory`). Each domain has its own module -- see `src/synthorg/observability/events/` for the full inventory of constants. Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`
93+
- **Event names**: always use constants from the domain-specific module under `synthorg.observability.events` (e.g., `API_REQUEST_STARTED` from `events.api`, `TOOL_INVOKE_START` from `events.tool`, `GIT_COMMAND_START` from `events.git`, `CONTEXT_BUDGET_FILL_UPDATED`, `CONTEXT_BUDGET_COMPACTION_STARTED`, `CONTEXT_BUDGET_COMPACTION_COMPLETED`, `CONTEXT_BUDGET_COMPACTION_FAILED`, `CONTEXT_BUDGET_COMPACTION_SKIPPED`, `CONTEXT_BUDGET_COMPACTION_FALLBACK`, `CONTEXT_BUDGET_INDICATOR_INJECTED`, `CONTEXT_BUDGET_AGENT_COMPACTION_REQUESTED`, `CONTEXT_BUDGET_EPISTEMIC_MARKERS_PRESERVED` from `events.context_budget`, `BACKUP_STARTED` from `events.backup`, `SETUP_COMPLETED` from `events.setup`, `ROUTING_CANDIDATE_SELECTED` from `events.routing`, `SHIPPING_HTTP_BATCH_SENT` from `events.shipping`, `EVAL_REPORT_COMPUTED` from `events.evaluation`, `PROMPT_PROFILE_SELECTED` from `events.prompt`, `PROCEDURAL_MEMORY_START` from `events.procedural_memory`, `PERF_LLM_JUDGE_STARTED` from `events.performance`, `TASK_ENGINE_OBSERVER_FAILED` from `events.task_engine`, `TASK_ASSIGNMENT_PROJECT_FILTERED` and `TASK_ASSIGNMENT_PROJECT_NO_ELIGIBLE` from `events.task_assignment`, `WORKFLOW_EXEC_COMPLETED` from `events.workflow_execution`, `BLUEPRINT_INSTANTIATE_START` from `events.blueprint`, `WORKFLOW_DEF_ROLLED_BACK` from `events.workflow_definition`, `WORKFLOW_VERSION_SAVED` from `events.workflow_version`, `MEMORY_FINE_TUNE_STARTED`, `MEMORY_SELF_EDIT_TOOL_EXECUTE`, `MEMORY_SELF_EDIT_CORE_READ`, `MEMORY_SELF_EDIT_CORE_WRITE`, `MEMORY_SELF_EDIT_CORE_WRITE_REJECTED`, `MEMORY_SELF_EDIT_ARCHIVAL_SEARCH`, `MEMORY_SELF_EDIT_ARCHIVAL_WRITE`, `MEMORY_SELF_EDIT_RECALL_READ`, `MEMORY_SELF_EDIT_RECALL_WRITE`, `MEMORY_SELF_EDIT_WRITE_FAILED` from `events.memory`, `REPORTING_GENERATION_STARTED` from `events.reporting`, `RISK_BUDGET_SCORE_COMPUTED` from `events.risk_budget`, `BUDGET_PROJECT_COST_QUERIED`, `BUDGET_PROJECT_BUDGET_EXCEEDED`, and `BUDGET_PROJECT_ENFORCEMENT_CHECK` from `events.budget`, `LLM_STRATEGY_SYNTHESIZED` and `DISTILLATION_CAPTURED` from `events.consolidation`, `MEMORY_DIVERSITY_RERANKED`, `MEMORY_DIVERSITY_RERANK_FAILED`, and `MEMORY_REFORMULATION_ROUND` from `events.memory`, `NOTIFICATION_DISPATCHED` and `NOTIFICATION_DISPATCH_FAILED` from `events.notification`, `QUALITY_STEP_CLASSIFIED` from `events.quality`, `HEALTH_TICKET_EMITTED` from `events.health`, `TRAJECTORY_SCORING_START` from `events.trajectory`, `COORD_METRICS_AMDAHL_COMPUTED` from `events.coordination_metrics`, `COORDINATION_STARTED`, `COORDINATION_COMPLETED`, `COORDINATION_FAILED`, `COORDINATION_PHASE_STARTED`, `COORDINATION_PHASE_COMPLETED`, `COORDINATION_PHASE_FAILED`, `COORDINATION_WAVE_STARTED`, `COORDINATION_WAVE_COMPLETED`, `COORDINATION_TOPOLOGY_RESOLVED`, `COORDINATION_CLEANUP_STARTED`, `COORDINATION_CLEANUP_COMPLETED`, `COORDINATION_CLEANUP_FAILED`, `COORDINATION_WAVE_BUILT`, `COORDINATION_FACTORY_BUILT`, and `COORDINATION_ATTRIBUTION_BUILT` from `events.coordination`, `WEB_REQUEST_START` and `WEB_SSRF_BLOCKED` from `events.web`, `DB_QUERY_START` and `DB_WRITE_BLOCKED` from `events.database`, `TERMINAL_COMMAND_START` and `TERMINAL_COMMAND_BLOCKED` from `events.terminal`, `SUB_CONSTRAINT_RESOLVED` and `SUB_CONSTRAINT_DENIED` from `events.sub_constraint`, `VERSION_SAVED` and `VERSION_SNAPSHOT_FAILED` from `events.versioning`, `ANALYTICS_AGGREGATION_COMPUTED` and `ANALYTICS_RETRY_RATE_ALERT` from `events.analytics`, `CALL_CLASSIFICATION_COMPUTED` from `events.call_classification`, `QUOTA_THRESHOLD_ALERT` and `QUOTA_POLL_FAILED` from `events.quota`, `CONFLICT_DEBATE_EVALUATOR_FAILED` from `events.conflict`, `DELEGATION_LOOP_CIRCUIT_BACKOFF` and `DELEGATION_LOOP_CIRCUIT_PERSIST_FAILED` from `events.delegation`, `MEETING_EVENT_COOLDOWN_SKIPPED` and `MEETING_TASKS_CAPPED` from `events.meeting`, `PERSISTENCE_CIRCUIT_BREAKER_SAVED`, `PERSISTENCE_CIRCUIT_BREAKER_SAVE_FAILED`, `PERSISTENCE_CIRCUIT_BREAKER_LOADED`, `PERSISTENCE_CIRCUIT_BREAKER_LOAD_FAILED`, `PERSISTENCE_CIRCUIT_BREAKER_DELETED`, and `PERSISTENCE_CIRCUIT_BREAKER_DELETE_FAILED` from `events.persistence`, `METRICS_SCRAPE_COMPLETED`, `METRICS_SCRAPE_FAILED`, `METRICS_COLLECTOR_INITIALIZED`, `METRICS_COORDINATION_RECORDED`, `METRICS_OTLP_EXPORT_COMPLETED` and `METRICS_OTLP_FLUSHER_STOPPED` from `events.metrics`, `EXECUTION_PROJECT_VALIDATION_FAILED` and `EXECUTION_PROJECT_COST_RECORDED` from `events.execution`, `ORG_MEMORY_QUERY_START`, `ORG_MEMORY_QUERY_COMPLETE`, `ORG_MEMORY_QUERY_FAILED`, `ORG_MEMORY_WRITE_START`, `ORG_MEMORY_WRITE_COMPLETE`, `ORG_MEMORY_WRITE_DENIED`, `ORG_MEMORY_WRITE_FAILED`, `ORG_MEMORY_POLICIES_LISTED`, `ORG_MEMORY_BACKEND_CREATED`, `ORG_MEMORY_CONNECT_FAILED`, `ORG_MEMORY_DISCONNECT_FAILED`, `ORG_MEMORY_NOT_CONNECTED`, `ORG_MEMORY_ROW_PARSE_FAILED`, `ORG_MEMORY_CONFIG_INVALID`, `ORG_MEMORY_MODEL_INVALID`, `ORG_MEMORY_MVCC_PUBLISH_APPENDED`, `ORG_MEMORY_MVCC_RETRACT_APPENDED`, `ORG_MEMORY_MVCC_SNAPSHOT_AT_QUERIED`, and `ORG_MEMORY_MVCC_LOG_QUERIED` from `events.org_memory`). Each domain has its own module -- see `src/synthorg/observability/events/` for the full inventory of constants. Import directly: `from synthorg.observability.events.<domain> import EVENT_CONSTANT`
9494
- **Structured kwargs**: always `logger.info(EVENT, key=value)` -- never `logger.info("msg %s", val)`
9595
- **All error paths** must log at WARNING or ERROR with context before raising
9696
- **All state transitions** must log at INFO

docs/design/engine.md

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,15 @@ async run(
650650
monthly hard stop and daily limit via `check_can_execute()`, then apply
651651
auto-downgrade via `resolve_model()`. Raises `BudgetExhaustedError` or
652652
`DailyLimitExceededError` on violation.
653-
3. **Build system prompt** -- calls `build_system_prompt()` with agent identity,
653+
3. **Project validation** -- if `ProjectRepository` is provided, validate that the
654+
task's project exists (`ProjectNotFoundError` if not) and that the agent is a
655+
member of the project team (`ProjectAgentNotMemberError` if not; empty teams
656+
allow any agent). When the project has a non-zero budget and `BudgetEnforcer`
657+
is available, check project-level budget via `check_project_budget()`. Raises
658+
`ProjectBudgetExhaustedError` when the project's accumulated cost has reached
659+
its budget. Pre-flight project budget checks are best-effort under concurrency
660+
(TOCTOU); the in-flight `BudgetChecker` closure provides the true safety net.
661+
4. **Build system prompt** -- calls `build_system_prompt()` with agent identity,
654662
task, and resolved model tier. The tier determines a `PromptProfile` that
655663
controls prompt verbosity (see [Prompt Profiles](#prompt-profiles) below),
656664
including personality token trimming when the section exceeds the profile's
@@ -661,17 +669,17 @@ async run(
661669
Follows the **non-inferable-only principle**: system prompts include only
662670
information the agent cannot discover by reading the codebase or environment
663671
(role constraints, custom conventions, organizational policies).
664-
4. **Create context** -- `AgentContext.from_identity()` with the configured
672+
5. **Create context** -- `AgentContext.from_identity()` with the configured
665673
`max_turns`.
666-
5. **Seed conversation** -- injects system prompt, optional memory messages, and
674+
6. **Seed conversation** -- injects system prompt, optional memory messages, and
667675
formatted task instruction as initial messages.
668-
6. **Transition task** -- `ASSIGNED` -> `IN_PROGRESS` (pass-through if already
676+
7. **Transition task** -- `ASSIGNED` -> `IN_PROGRESS` (pass-through if already
669677
`IN_PROGRESS`).
670-
7. **Prepare tools and budget** -- creates `ToolInvoker` from registry and
671-
`BudgetChecker` from `BudgetEnforcer` (task + monthly + daily limits with
672-
pre-computed baselines and alert deduplication) or from task budget limit
678+
8. **Prepare tools and budget** -- creates `ToolInvoker` from registry and
679+
`BudgetChecker` from `BudgetEnforcer` (task + monthly + daily + project limits
680+
with pre-computed baselines and alert deduplication) or from task budget limit
673681
alone when no enforcer is configured.
674-
8. **Resolve execution loop** -- if `auto_loop_config` is set, calls
682+
9. **Resolve execution loop** -- if `auto_loop_config` is set, calls
675683
`select_loop_type()` with the task's `estimated_complexity` and current
676684
budget utilization (via `BudgetEnforcer.get_budget_utilization_pct()`).
677685
Budget-aware downgrade: hybrid is downgraded to plan_execute when
@@ -681,7 +689,7 @@ async run(
681689
engine's `compaction_callback`, `plan_execute_config` (for
682690
plan-execute), and `hybrid_loop_config` (for hybrid), along with the
683691
approval gate and stagnation detector.
684-
9. **Delegate to loop** -- calls `ExecutionLoop.execute()` with context,
692+
10. **Delegate to loop** -- calls `ExecutionLoop.execute()` with context,
685693
provider, tool invoker, budget checker, and completion config. If
686694
`timeout_seconds` is set, wraps the call in `asyncio.wait`; on expiry
687695
the run returns with `TerminationReason.ERROR` but cost recording and
@@ -691,9 +699,10 @@ async run(
691699
parking is needed. If so, the context is serialized via `ParkService`
692700
and persisted when a `ParkedContextRepository` is configured; the loop
693701
then returns a `PARKED` result.
694-
10. **Record costs** -- records accumulated `TokenUsage` to `CostTracker` (if
695-
available). Cost recording failures are logged but do not affect the result.
696-
11. **Apply post-execution transitions:**
702+
11. **Record costs** -- records accumulated `TokenUsage` to `CostTracker` (if
703+
available), tagged with `project_id` for project-level cost aggregation.
704+
Cost recording failures are logged but do not affect the result.
705+
12. **Apply post-execution transitions:**
697706
- `COMPLETED` termination: IN_PROGRESS -> IN_REVIEW (review gate).
698707
The task parks at IN_REVIEW until resolved by one of two paths:
699708
(a) a human approves (-> COMPLETED) or rejects (-> IN_PROGRESS
@@ -757,13 +766,13 @@ async run(
757766
[AgentEngine ↔ TaskEngine Incremental Sync](#agentengine--taskengine-incremental-sync)).
758767
- Transition failures are logged but do not discard the successful execution
759768
result.
760-
12. **Procedural memory generation** (non-critical) -- when
769+
13. **Procedural memory generation** (non-critical) -- when
761770
`ProceduralMemoryConfig` is enabled and the execution failed
762771
(recovery_result exists), a separate proposer LLM call analyses the
763772
failure and stores a `PROCEDURAL` memory entry for future retrieval.
764773
Optionally materializes a SKILL.md file. Failures are logged but do
765774
not affect the result (see [Memory > Procedural Memory Auto-Generation](memory.md#procedural-memory-auto-generation)).
766-
13. **Return result** -- wraps `ExecutionResult` in `AgentRunResult` with
775+
14. **Return result** -- wraps `ExecutionResult` in `AgentRunResult` with
767776
engine-level metadata.
768777

769778
**Error handling:** `MemoryError` and `RecursionError` propagate

src/synthorg/budget/enforcer.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,19 @@ async def check_project_budget(
263263
if project_budget <= 0:
264264
return
265265

266-
project_cost = await self._cost_tracker.get_project_cost(
267-
project_id,
268-
)
266+
try:
267+
project_cost = await self._cost_tracker.get_project_cost(
268+
project_id,
269+
)
270+
except MemoryError, RecursionError:
271+
raise
272+
except Exception:
273+
logger.exception(
274+
BUDGET_PREFLIGHT_ERROR,
275+
project_id=project_id,
276+
reason="project_cost_query_failed",
277+
)
278+
return
269279

270280
logger.debug(
271281
BUDGET_PROJECT_ENFORCEMENT_CHECK,
@@ -626,6 +636,7 @@ async def make_budget_checker(
626636
logger.exception(
627637
BUDGET_BASELINE_ERROR,
628638
agent_id=agent_id,
639+
project_id=project_id,
629640
reason="project_baseline_query_failed",
630641
)
631642

src/synthorg/engine/agent_engine.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,9 @@ async def run( # noqa: PLR0913
603603
)
604604
raise
605605
except ProjectNotFoundError, ProjectAgentNotMemberError:
606+
# ProjectBudgetExhaustedError (from _validate_project)
607+
# is a BudgetExhaustedError subclass -- intentionally
608+
# caught by the handler below, not here.
606609
raise
607610
except BudgetExhaustedError as exc:
608611
return self._handle_budget_error(
@@ -1289,8 +1292,7 @@ async def _validate_project(
12891292
project=task.project,
12901293
reason="project_not_found",
12911294
)
1292-
msg = f"Project {task.project!r} not found"
1293-
raise ProjectNotFoundError(msg)
1295+
raise ProjectNotFoundError(project_id=task.project)
12941296
if project.team and agent_id not in project.team:
12951297
logger.warning(
12961298
EXECUTION_PROJECT_VALIDATION_FAILED,
@@ -1299,8 +1301,10 @@ async def _validate_project(
12991301
project=task.project,
13001302
reason="agent_not_in_team",
13011303
)
1302-
msg = f"Agent {agent_id!r} not in project {task.project!r} team"
1303-
raise ProjectAgentNotMemberError(msg)
1304+
raise ProjectAgentNotMemberError(
1305+
project_id=task.project,
1306+
agent_id=agent_id,
1307+
)
13041308
if self._budget_enforcer is not None and project.budget > 0:
13051309
await self._budget_enforcer.check_project_budget(
13061310
project_id=project.id,

src/synthorg/engine/assignment/service.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ def assign(self, request: AssignmentRequest) -> AssignmentResult:
9292
logger.warning(
9393
TASK_ASSIGNMENT_PROJECT_NO_ELIGIBLE,
9494
task_id=task.id,
95+
available_agents=len(request.available_agents),
9596
project_team_size=len(request.project_team),
9697
)
9798
return AssignmentResult(

0 commit comments

Comments
 (0)