-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: Round-2 Gap Closure — Interrupts, Error Classifier, Path Overlap, Message Sanitization, Title Auto-Gen (5/12) #1489
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 all commits
9cd8dc8
7927ae6
a9cd02a
af66f59
76264e2
8f8f7f7
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 |
|---|---|---|
|
|
@@ -245,6 +245,7 @@ def _get_default_server_registry() -> ServerRegistry: | |
| from ..context.models import ContextConfig | ||
| from ..context.manager import ContextManager | ||
| from ..knowledge.knowledge import Knowledge | ||
| from .interrupt import InterruptController | ||
| from ..agent.autonomy import AutonomyConfig | ||
| from ..task.task import Task | ||
| from .handoff import Handoff, HandoffConfig, HandoffResult | ||
|
|
@@ -531,7 +532,7 @@ def __init__( | |
| # CONSOLIDATED FEATURE PARAMS (agent-centric API) | ||
| # Each follows: False=disabled, True=defaults, Config=custom | ||
| # ============================================================ | ||
| memory: Optional[Union[bool, str, 'MemoryConfig', 'MemoryManager']] = None, | ||
| memory: Optional[Union[bool, str, 'MemoryConfig', Any]] = None, | ||
| knowledge: Optional[Union[bool, str, List[str], 'KnowledgeConfig', 'Knowledge']] = None, | ||
| planning: Optional[Union[bool, str, 'PlanningConfig']] = False, | ||
| reflection: Optional[Union[bool, str, 'ReflectionConfig']] = None, | ||
|
|
@@ -551,6 +552,7 @@ def __init__( | |
| parallel_tool_calls: bool = False, # Gap 2: Enable parallel execution of batched LLM tool calls | ||
| learn: Optional[Union[bool, str, Dict[str, Any], 'LearnConfig']] = None, # Continuous learning (peer to memory) | ||
| backend: Optional[Any] = None, # External managed agent backend (e.g., ManagedAgentIntegration) | ||
| interrupt_controller: Optional['InterruptController'] = None, # G2: Cooperative cancellation | ||
| ): | ||
| """Initialize an Agent instance. | ||
|
|
||
|
|
@@ -575,7 +577,7 @@ def __init__( | |
| memory: Memory system configuration. Accepts: | ||
| - bool: True enables defaults, False disables | ||
| - MemoryConfig: Custom configuration | ||
| - MemoryManager: Pre-configured instance | ||
| - Any: Pre-configured memory instance | ||
| knowledge: Knowledge sources. Accepts: | ||
| - bool: True enables defaults | ||
| - List[str]: File paths, URLs, or text content | ||
|
|
@@ -1458,6 +1460,8 @@ def __init__( | |
| self.instructions = instructions | ||
| # Gap 2: Store parallel tool calls setting for ToolCallExecutor selection | ||
| self.parallel_tool_calls = parallel_tool_calls | ||
| # G2: Store interrupt controller for cooperative cancellation | ||
| self.interrupt_controller = interrupt_controller | ||
| # Check for model name in environment variable if not provided | ||
| self._using_custom_llm = False | ||
| # Flag to track if final result has been displayed to prevent duplicates | ||
|
|
@@ -2835,6 +2839,19 @@ def run_autonomous( | |
| started_at=started_at, | ||
| ) | ||
|
|
||
| # G2: Check for interrupt request (cooperative cancellation) - sync version | ||
| if self.interrupt_controller and self.interrupt_controller.is_set(): | ||
| reason = self.interrupt_controller.reason or "unknown" | ||
| return AutonomyResult( | ||
| success=False, | ||
| output=f"Task interrupted: {reason}", | ||
| completion_reason="interrupted", | ||
| iterations=iterations, | ||
| stage=stage, | ||
| actions=actions_taken, | ||
| duration_seconds=time_module.time() - start_time, | ||
| started_at=started_at, | ||
| ) | ||
|
|
||
| # Execute one turn using the agent's chat method | ||
| # Always use the original prompt (prompt re-injection) | ||
|
|
@@ -3232,6 +3249,20 @@ async def main(): | |
| started_at=started_at, | ||
| ) | ||
|
|
||
| # G2: Check for interrupt request (cooperative cancellation) | ||
| if self.interrupt_controller and self.interrupt_controller.is_set(): | ||
| reason = self.interrupt_controller.reason or "unknown" | ||
| return AutonomyResult( | ||
| success=False, | ||
| output=f"Task interrupted: {reason}", | ||
| completion_reason="interrupted", | ||
| iterations=iterations, | ||
| stage=stage, | ||
| actions=actions_taken, | ||
| duration_seconds=time_module.time() - start_time, | ||
| started_at=started_at, | ||
| ) | ||
|
Comment on lines
+3252
to
+3264
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. Mirror interruption in the sync autonomous loop.
Apply the same guard in `run_autonomous()` before `self.chat(prompt)` if timeout_seconds and (time_module.time() - start_time) > timeout_seconds:
return AutonomyResult(
success=False,
output="Task timed out",
completion_reason="timeout",
@@
started_at=started_at,
)
+
+ if self.interrupt_controller and self.interrupt_controller.is_set():
+ reason = self.interrupt_controller.reason or "unknown"
+ return AutonomyResult(
+ success=False,
+ output=f"Task interrupted: {reason}",
+ completion_reason="interrupted",
+ iterations=iterations,
+ stage=stage,
+ actions=actions_taken,
+ duration_seconds=time_module.time() - start_time,
+ started_at=started_at,
+ )As per coding guidelines, Agent parameters must support both sync and async entry points. 🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| # Execute one turn using the agent's async chat method | ||
| # Always use the original prompt (prompt re-injection) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| """ | ||
| Interrupt Controller - Cooperative cancellation for agent runs. | ||
|
|
||
| Provides thread-safe, cooperative cancellation mechanism for long-running agent | ||
| operations. Follows protocol-driven design with zero overhead when not used. | ||
| """ | ||
|
|
||
| import threading | ||
| from typing import Optional, Protocol | ||
| from dataclasses import dataclass, field | ||
|
|
||
| __all__ = ["InterruptControllerProtocol", "InterruptController"] | ||
|
|
||
|
|
||
| class InterruptControllerProtocol(Protocol): | ||
| """Protocol for interrupt controller extension point.""" | ||
|
|
||
| def request(self, reason: str = "user") -> None: | ||
| """Request cancellation with optional reason.""" | ||
| ... | ||
|
|
||
| def clear(self) -> None: | ||
| """Clear interrupt state.""" | ||
| ... | ||
|
|
||
| def is_set(self) -> bool: | ||
| """Check if interrupt was requested.""" | ||
| ... | ||
|
|
||
| @property | ||
| def reason(self) -> Optional[str]: | ||
| """Get interrupt reason if set.""" | ||
| ... | ||
|
|
||
| def check(self) -> None: | ||
| """Check for interrupt and raise if set.""" | ||
| ... | ||
|
|
||
|
|
||
| @dataclass | ||
| class InterruptController: | ||
|
Comment on lines
+40
to
+41
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. 🛠️ Refactor suggestion | 🟠 Major Add a protocol for the interrupt extension point.
Proposed protocol surface-from typing import Optional
+from typing import Optional, Protocol
from dataclasses import dataclass, field
-__all__ = ["InterruptController"]
+__all__ = ["InterruptControllerProtocol", "InterruptController"]
+
+
+class InterruptControllerProtocol(Protocol):
+ def request(self, reason: str = "user") -> None: ...
+ def clear(self) -> None: ...
+ def is_set(self) -> bool: ...
+
+ `@property`
+ def reason(self) -> Optional[str]: ...
+
+ def check(self) -> None: ...As per coding guidelines, Core SDK must use protocol-driven design with 🤖 Prompt for AI Agents |
||
| """Thread-safe cooperative cancellation for agent runs. | ||
|
|
||
| Provides a lightweight mechanism for requesting cancellation of agent | ||
| operations. Uses threading.Event for thread safety and cooperative | ||
| checking patterns. | ||
|
|
||
| Examples: | ||
| Basic usage: | ||
| >>> controller = InterruptController() | ||
| >>> # In another thread: | ||
| >>> controller.request("user_cancel") | ||
| >>> # In agent loop: | ||
| >>> if controller.is_set(): | ||
| >>> return f"Cancelled: {controller.reason}" | ||
| """ | ||
|
|
||
| _flag: threading.Event = field(default_factory=threading.Event, init=False, repr=False) | ||
| _reason: Optional[str] = field(default=None, init=False) | ||
| _lock: threading.Lock = field(default_factory=threading.Lock, init=False, repr=False) | ||
|
|
||
| def request(self, reason: str = "user") -> None: | ||
| """Request cancellation with a reason. | ||
|
|
||
| Args: | ||
| reason: Human-readable reason for cancellation | ||
| """ | ||
| with self._lock: | ||
| if not self._flag.is_set(): | ||
| self._reason = reason | ||
| self._flag.set() | ||
|
|
||
| def clear(self) -> None: | ||
| """Clear the cancellation request.""" | ||
| with self._lock: | ||
| self._reason = None | ||
| self._flag.clear() | ||
|
|
||
| def is_set(self) -> bool: | ||
| """Check if cancellation has been requested. | ||
|
|
||
| Returns: | ||
| True if cancellation was requested | ||
| """ | ||
| return self._flag.is_set() | ||
|
|
||
| @property | ||
| def reason(self) -> Optional[str]: | ||
| """Get the reason for cancellation. | ||
|
|
||
| Returns: | ||
| Reason string if cancelled, None otherwise | ||
| """ | ||
| with self._lock: | ||
| return self._reason | ||
|
|
||
| def check(self) -> None: | ||
| """Check for cancellation and raise if requested. | ||
|
|
||
| Raises: | ||
| InterruptedError: If cancellation was requested | ||
| """ | ||
| if self.is_set(): | ||
| reason = self.reason or "unknown" | ||
| raise InterruptedError(f"Operation cancelled: {reason}") | ||
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.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash python -m ruff check src/praisonai-agents/praisonaiagents/agent/agent.py --select F821Repository: MervinPraison/PraisonAI
Length of output: 1594
🏁 Script executed:
head -100 src/praisonai-agents/praisonaiagents/agent/agent.py | cat -nRepository: MervinPraison/PraisonAI
Length of output: 4467
🏁 Script executed:
rg "TYPE_CHECKING" src/praisonai-agents/praisonaiagents/agent/agent.py -A 5 -B 2Repository: MervinPraison/PraisonAI
Length of output: 734
🏁 Script executed:
rg "interrupt_controller" src/praisonai-agents/praisonaiagents/agent/agent.py -B 2 -A 2Repository: MervinPraison/PraisonAI
Length of output: 1076
🏁 Script executed:
rg "InterruptController" src/praisonai-agents/praisonaiagents/agent/ --type pyRepository: MervinPraison/PraisonAI
Length of output: 970
🏁 Script executed:
fd "interrupt" src/praisonai-agents/praisonaiagents/ --type fRepository: MervinPraison/PraisonAI
Length of output: 122
🏁 Script executed:
rg "MemoryManager" src/praisonai-agents/praisonaiagents/agent/agent.py -B 2 -A 2Repository: MervinPraison/PraisonAI
Length of output: 731
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 2551
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 7839
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 1484
🏁 Script executed:
rg "class MemoryManager" src/praisonai-agents/praisonaiagents/ --type pyRepository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
rg "from.*MemoryManager\|import.*MemoryManager" src/praisonai-agents/praisonaiagents/ --type pyRepository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 829
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 3612
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 1466
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 4714
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 1236
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 1732
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 1145
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 1594
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 49
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 151
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 3172
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 319
🏁 Script executed:
Repository: MervinPraison/PraisonAI
Length of output: 100
Add missing type imports for static analysis, and fix MemoryManager union type.
Ruff reports two F821 errors:
InterruptControllerandMemoryManager. AddInterruptControllerto the TYPE_CHECKING block; forMemoryManager, useAnyinstead since it's not a defined class—it's a placeholder for pre-configured instances.Proposed fix
if TYPE_CHECKING: from ..approval.protocols import ApprovalConfig, ApprovalProtocol from ..config.feature_configs import LearnConfig, MemoryConfig from ..context.models import ContextConfig from ..context.manager import ContextManager from ..knowledge.knowledge import Knowledge + from .interrupt import InterruptController from ..agent.autonomy import AutonomyConfig from ..task.task import Task from .handoff import Handoff, HandoffConfig, HandoffResult from ..rag.models import RAGResult, ContextPack from ..eval.results import EvaluationLoopResult🧰 Tools
🪛 Ruff (0.15.10)
[error] 554-554: Undefined name
InterruptController(F821)
🤖 Prompt for AI Agents