-
Notifications
You must be signed in to change notification settings - Fork 1
feat: implement single-task execution lifecycle (#21) #144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |||||
| tool invocation, and budget tracking into a single ``run()`` entry point. | ||||||
| """ | ||||||
|
|
||||||
| import asyncio | ||||||
| import time | ||||||
| from datetime import UTC, datetime | ||||||
| from typing import TYPE_CHECKING | ||||||
|
|
@@ -17,6 +18,7 @@ | |||||
| TerminationReason, | ||||||
| TurnRecord, | ||||||
| ) | ||||||
| from ai_company.engine.metrics import TaskCompletionMetrics | ||||||
| from ai_company.engine.prompt import SystemPrompt, build_system_prompt | ||||||
| from ai_company.engine.react_loop import ReactLoop | ||||||
| from ai_company.engine.run_result import AgentRunResult | ||||||
|
|
@@ -31,7 +33,9 @@ | |||||
| EXECUTION_ENGINE_INVALID_INPUT, | ||||||
| EXECUTION_ENGINE_PROMPT_BUILT, | ||||||
| EXECUTION_ENGINE_START, | ||||||
| EXECUTION_ENGINE_TASK_METRICS, | ||||||
| EXECUTION_ENGINE_TASK_TRANSITION, | ||||||
| EXECUTION_ENGINE_TIMEOUT, | ||||||
| ) | ||||||
| from ai_company.providers.enums import MessageRole | ||||||
| from ai_company.providers.models import ChatMessage | ||||||
|
|
@@ -90,14 +94,15 @@ def __init__( | |||||
| has_cost_tracker=self._cost_tracker is not None, | ||||||
| ) | ||||||
|
|
||||||
| async def run( | ||||||
| async def run( # noqa: PLR0913 | ||||||
| self, | ||||||
| *, | ||||||
| identity: AgentIdentity, | ||||||
| task: Task, | ||||||
| completion_config: CompletionConfig | None = None, | ||||||
| max_turns: int = DEFAULT_MAX_TURNS, | ||||||
| memory_messages: tuple[ChatMessage, ...] = (), | ||||||
| timeout_seconds: float | None = None, | ||||||
| ) -> AgentRunResult: | ||||||
|
Comment on lines
+98
to
107
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||||||
| """Execute an agent on a task. | ||||||
|
|
||||||
|
|
@@ -108,6 +113,10 @@ async def run( | |||||
| max_turns: Maximum LLM turns allowed (must be >= 1). | ||||||
| memory_messages: Optional working memory messages to inject | ||||||
| between the system prompt and task instruction. | ||||||
| timeout_seconds: Optional wall-clock timeout in seconds. | ||||||
| When exceeded, the execution loop is cancelled and the | ||||||
| run returns with ``TerminationReason.ERROR``. Cost | ||||||
| recording and post-execution processing still occur. | ||||||
|
|
||||||
| Returns: | ||||||
| ``AgentRunResult`` with execution outcome and metadata. | ||||||
|
|
@@ -118,7 +127,8 @@ async def run( | |||||
| Raises: | ||||||
| ExecutionStateError: If pre-flight validation fails (agent | ||||||
| not ACTIVE or task not ASSIGNED/IN_PROGRESS). | ||||||
| ValueError: If ``max_turns`` is less than 1. | ||||||
| ValueError: If ``max_turns`` is less than 1, or if | ||||||
| ``timeout_seconds`` is not positive. | ||||||
| MemoryError: Re-raised unconditionally (non-recoverable). | ||||||
| RecursionError: Re-raised unconditionally (non-recoverable). | ||||||
| """ | ||||||
|
|
@@ -135,6 +145,16 @@ async def run( | |||||
| ) | ||||||
| raise ValueError(msg) | ||||||
|
|
||||||
| if timeout_seconds is not None and timeout_seconds <= 0: | ||||||
| msg = f"timeout_seconds must be > 0, got {timeout_seconds}" | ||||||
| logger.warning( | ||||||
| EXECUTION_ENGINE_INVALID_INPUT, | ||||||
| agent_id=agent_id, | ||||||
| task_id=task_id, | ||||||
| reason=msg, | ||||||
| ) | ||||||
| raise ValueError(msg) | ||||||
|
|
||||||
| self._validate_agent(identity, agent_id) | ||||||
| self._validate_task(task, agent_id, task_id) | ||||||
|
|
||||||
|
|
@@ -167,6 +187,7 @@ async def run( | |||||
| ctx=ctx, | ||||||
| system_prompt=system_prompt, | ||||||
| start=start, | ||||||
| timeout_seconds=timeout_seconds, | ||||||
| ) | ||||||
| except MemoryError, RecursionError: | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Python 2
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/ai_company/engine/agent_engine.py
Line: 192
Comment:
**Python 2 `except` comma syntax — not valid Python 3**
`except MemoryError, RecursionError:` is Python 2 syntax and is a `SyntaxError` in Python 3. Python 2 parsed this as "catch `MemoryError`, bind it to the name `RecursionError`" — it does NOT catch both exception types. The Python 3 form to catch multiple exceptions requires parentheses. This same pattern appears on lines **584** and **669** as well.
```suggestion
except (MemoryError, RecursionError):
```
How can I resolve this? If you propose a fix, please make it concise.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
| logger.error( | ||||||
|
|
@@ -200,6 +221,7 @@ async def _execute( # noqa: PLR0913 | |||||
| ctx: AgentContext, | ||||||
| system_prompt: SystemPrompt, | ||||||
| start: float, | ||||||
| timeout_seconds: float | None = None, | ||||||
| ) -> AgentRunResult: | ||||||
| """Run the execution loop, record costs, and build result.""" | ||||||
| budget_checker = _make_budget_checker(task) | ||||||
|
|
@@ -212,16 +234,49 @@ async def _execute( # noqa: PLR0913 | |||||
| estimated_tokens=system_prompt.estimated_tokens, | ||||||
| ) | ||||||
|
|
||||||
| execution_result = await self._loop.execute( | ||||||
| context=ctx, | ||||||
| provider=self._provider, | ||||||
| tool_invoker=tool_invoker, | ||||||
| budget_checker=budget_checker, | ||||||
| completion_config=completion_config, | ||||||
| ) | ||||||
| duration = time.monotonic() - start | ||||||
| try: | ||||||
| coro = self._loop.execute( | ||||||
| context=ctx, | ||||||
| provider=self._provider, | ||||||
| tool_invoker=tool_invoker, | ||||||
| budget_checker=budget_checker, | ||||||
| completion_config=completion_config, | ||||||
| ) | ||||||
| if timeout_seconds is not None: | ||||||
| execution_result = await asyncio.wait_for( | ||||||
| coro, | ||||||
| timeout=timeout_seconds, | ||||||
| ) | ||||||
| else: | ||||||
| execution_result = await coro | ||||||
| except TimeoutError: | ||||||
|
||||||
| except TimeoutError: | |
| except asyncio.TimeoutError: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
asyncio.wait_for raises asyncio.TimeoutError. While this is an alias for the built-in TimeoutError in Python 3.11+, using except TimeoutError: will not catch the exception on older Python 3 versions. For better portability, it's recommended to explicitly catch asyncio.TimeoutError.
except asyncio.TimeoutError:
Copilot
AI
Mar 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EXECUTION_ENGINE_TASK_METRICS is logged unconditionally, even when the run ends with TerminationReason.ERROR / MAX_TURNS / BUDGET_EXHAUSTED. Given the naming (TaskCompletionMetrics) and spec wording (“logged at task completion”), this will emit misleading “completion” metrics for incomplete tasks. Consider either (a) only logging this event when result.is_success/termination_reason == COMPLETED, or (b) include termination_reason in the metrics event (and update naming/docs) to make it clear these are per-run metrics.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DESIGN_SPEC references
asyncio.wait_forbut implementation usesasyncio.waitThe spec description (line 928) states:
However, the actual implementation in
_run_loop_with_timeout(lines 278–300) deliberately usesasyncio.waitinstead, with clear rationale: this prevents conflating an internalTimeoutErrorfrom the loop with the engine's wall-clock deadline (see the method's docstring at lines 280–282).The implementation choice is correct and well-reasoned; the spec just needs to match:
Prompt To Fix With AI