feat: enhance external CLI integrations with registry pattern and streaming support#1394
Conversation
…eaming support - Add ExternalAgentRegistry for dynamic agent registration - Enhance CodexCLIIntegration with approval modes (suggest/auto-edit/full-auto) - Add multi-provider support for Codex CLI - Implement streaming support in TypeScript SDK - Maintain full backward compatibility - Follow protocol-driven architecture from AGENTS.md Fixes #1393 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: MervinPraison <MervinPraison@users.noreply.github.com>
There was a problem hiding this comment.
MervinPraison has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
📝 WalkthroughWalkthroughAdds AsyncGenerator-based streaming for TypeScript external agents and implements a Python ExternalAgentRegistry singleton for registering, discovering, and instantiating CLI integrations; updates Codex CLI integration to use an Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Agent as ExternalAgent
participant Proc as ChildProcess
participant Parser as LineReader/Parser
Client->>Agent: call stream(prompt)
Agent->>Proc: spawn CLI with args
Proc-->>Parser: emit stdout lines
Parser->>Agent: send parsed line (text/json)
Agent-->>Client: yield StreamEvent{text/json}
Proc->>Agent: exit
Agent->>Proc: verify exit code / collect stderr
Agent-->>Client: throw on non-zero exit (error event) or end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above from Qodo, Coderabbit, and Gemini first — incorporate their findings. Review areas:
|
There was a problem hiding this comment.
Code Review
This pull request implements a streaming interface for external agents in the TypeScript CLI and introduces a centralized ExternalAgentRegistry in the Python package. Key updates include the streamCommand utility for real-time process output and expanded configuration options for the Codex CLI. Review feedback suggests enhancing the TypeScript implementation with error listeners for spawned processes and refactoring the Aider agent to support true streaming. For the Python implementation, it is recommended to change the registry's availability checks from asynchronous to synchronous to simplify the integration logic and remove complex bridging code.
| const proc = spawn(this.config.command, args, { | ||
| cwd: this.config.cwd || process.cwd(), | ||
| env: { ...process.env, ...this.config.env }, | ||
| stdio: ['pipe', 'pipe', 'pipe'] | ||
| }); |
There was a problem hiding this comment.
The spawn call is missing an error listener. If the command is not found or fails to start, an error event will be emitted. If there are no listeners for the error event, the Node.js process will throw an unhandled exception and crash. Adding a simple error listener prevents this.
const proc = spawn(this.config.command, args, {
cwd: this.config.cwd || process.cwd(),
env: { ...process.env, ...this.config.env },
stdio: ['pipe', 'pipe', 'pipe']
});
proc.on('error', () => {}); // Prevent unhandled exception if command not found| async *stream(prompt: string): AsyncGenerator<StreamEvent, void, unknown> { | ||
| // Aider doesn't support JSON streaming, so just yield text events | ||
| const result = await this.execute(prompt); | ||
| for (const line of result.output.split('\n')) { | ||
| if (line.trim()) { | ||
| yield { type: 'text', content: line }; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
This implementation of stream is not actually streaming; it waits for the entire command to finish before yielding any output. Since streamCommand already handles non-JSON output by yielding text events, you can use it here to provide real-time streaming of Aider's output.
async *stream(prompt: string): AsyncGenerator<StreamEvent, void, unknown> {
yield* this.streamCommand(['--message', prompt, '--yes']);
}| async def get_available(self) -> Dict[str, bool]: | ||
| """ | ||
| Get availability status of all registered integrations. | ||
|
|
||
| Returns: | ||
| Dict[str, bool]: Mapping of integration name to availability status | ||
| """ | ||
| availability = {} | ||
|
|
||
| for name, integration_class in self._integrations.items(): | ||
| try: | ||
| # Create a temporary instance to check availability | ||
| instance = integration_class() | ||
| availability[name] = instance.is_available | ||
| except Exception: | ||
| # If instantiation fails, mark as unavailable | ||
| availability[name] = False | ||
|
|
||
| return availability | ||
|
|
||
| async def get_available_names(self) -> List[str]: | ||
| """ | ||
| Get names of all available integrations. | ||
|
|
||
| Returns: | ||
| List[str]: List of available integration names | ||
| """ | ||
| availability = await self.get_available() | ||
| return [name for name, available in availability.items() if available] |
There was a problem hiding this comment.
The get_available and get_available_names methods are defined as async, but they only perform synchronous operations (instantiating classes and checking the is_available property). Making them asynchronous unnecessarily complicates the caller's code, especially in synchronous contexts where complex bridging logic (like asyncio.run in a thread pool) becomes required. These should be synchronous methods.
def get_available(self) -> Dict[str, bool]:
"""
Get availability status of all registered integrations.
Returns:
Dict[str, bool]: Mapping of integration name to availability status
"""
availability = {}
for name, integration_class in self._integrations.items():
try:
# Create a temporary instance to check availability
instance = integration_class()
availability[name] = instance.is_available
except Exception:
# If instantiation fails, mark as unavailable
availability[name] = False
return availability
def get_available_names(self) -> List[str]:
"""
Get names of all available integrations.
Returns:
List[str]: List of available integration names
"""
availability = self.get_available()
return [name for name, available in availability.items() if available]| # Handle async call in sync context | ||
| try: | ||
| # Try to get existing event loop | ||
| loop = asyncio.get_event_loop() | ||
| if loop.is_running(): | ||
| # We're in an async context, use create_task | ||
| import concurrent.futures | ||
| with concurrent.futures.ThreadPoolExecutor() as executor: | ||
| future = executor.submit(asyncio.run, registry.get_available()) | ||
| return future.result() | ||
| else: | ||
| # No running loop, safe to use asyncio.run | ||
| return asyncio.run(registry.get_available()) | ||
| except RuntimeError: | ||
| # No event loop, safe to use asyncio.run | ||
| return asyncio.run(registry.get_available()) |
There was a problem hiding this comment.
This block introduces a complex and potentially fragile mechanism to call an asynchronous method from a synchronous context. If ExternalAgentRegistry.get_available is changed to be synchronous (as it doesn't perform any actual async work), this entire block can be simplified to a direct call, improving performance and maintainability.
| # Handle async call in sync context | |
| try: | |
| # Try to get existing event loop | |
| loop = asyncio.get_event_loop() | |
| if loop.is_running(): | |
| # We're in an async context, use create_task | |
| import concurrent.futures | |
| with concurrent.futures.ThreadPoolExecutor() as executor: | |
| future = executor.submit(asyncio.run, registry.get_available()) | |
| return future.result() | |
| else: | |
| # No running loop, safe to use asyncio.run | |
| return asyncio.run(registry.get_available()) | |
| except RuntimeError: | |
| # No event loop, safe to use asyncio.run | |
| return asyncio.run(registry.get_available()) | |
| return registry.get_available() |
There was a problem hiding this comment.
Pull request overview
Adds a registry-based architecture for external CLI integrations, extends Codex CLI configuration options, and introduces streaming support in the TypeScript external-agents layer.
Changes:
- Added a Python
ExternalAgentRegistrywith factory helpers and availability discovery. - Enhanced Python
CodexCLIIntegrationwithapproval_modeandprovider(while attempting to preservefull_autocompatibility). - Added a streaming API (
stream) to the TypeScriptBaseExternalAgentand implemented it across built-in agents.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
src/praisonai/praisonai/integrations/registry.py |
New Python registry for external CLI integrations + helper factory functions. |
src/praisonai/praisonai/integrations/codex_cli.py |
Adds approval modes and provider selection flags for Codex CLI. |
src/praisonai/praisonai/integrations/base.py |
Updates get_available_integrations() to delegate to the registry for backward compatibility. |
src/praisonai/praisonai/integrations/__init__.py |
Exposes registry APIs via lazy imports and reroutes get_available_integrations. |
src/praisonai-ts/src/cli/features/external-agents.ts |
Adds streaming support and implements it for external CLI agents. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Add approval mode | ||
| if self.approval_mode == "full-auto": | ||
| cmd.append("--full-auto") | ||
| elif self.approval_mode == "auto-edit": | ||
| cmd.append("--auto-edit") | ||
| # suggest is the default, no flag needed |
There was a problem hiding this comment.
approval_mode is accepted as a free-form string, but _build_command() only handles "suggest", "auto-edit", and "full-auto". Any other value will silently fall back to suggest (no flag), which can hide misconfiguration. Consider validating approval_mode in __init__ and raising a clear ValueError for unsupported values.
|
|
||
| const proc = spawn(this.config.command, args, { | ||
| cwd: this.config.cwd || process.cwd(), | ||
| env: { ...process.env, ...this.config.env }, |
There was a problem hiding this comment.
streamCommand() spawns a long-running process but does not pass timeout: this.config.timeout (unlike runCommand). This can cause streaming calls to hang indefinitely if the external CLI stalls. Consider wiring in the same timeout behavior (or supporting an AbortSignal) for streaming executions.
| env: { ...process.env, ...this.config.env }, | |
| env: { ...process.env, ...this.config.env }, | |
| timeout: this.config.timeout, |
| class ExternalAgentRegistry: | ||
| """ | ||
| Registry for external CLI integrations. | ||
|
|
||
| Provides centralized management of external agent integrations |
There was a problem hiding this comment.
This adds a new public registry API (ExternalAgentRegistry, get_registry, register_integration, etc.) but there are no unit tests covering registration/unregistration and create() behavior. Adding tests for register → create → unregister (and builtin registration) would help prevent regressions.
| return ManagedConfig | ||
| elif name == 'get_available_integrations': | ||
| from .base import get_available_integrations | ||
| from .registry import get_available_integrations |
There was a problem hiding this comment.
__getattr__('get_available_integrations') now returns registry.get_available_integrations, which is async def. This breaks the existing synchronous public API (get_available_integrations() returning a dict) and will cause call sites that do availability = get_available_integrations() to receive a coroutine instead of a dict. Keep exporting the sync wrapper from base.get_available_integrations, or introduce a new async name (e.g., get_available_integrations_async) while preserving the original sync signature.
| from .registry import get_available_integrations | |
| from .base import get_available_integrations |
| """ | ||
|
|
||
| from typing import Dict, Type, Optional, Any, List | ||
| from abc import ABC |
There was a problem hiding this comment.
Unused import: from abc import ABC is not referenced anywhere in this module. Please remove it to avoid lint/type-check noise.
| from abc import ABC |
| const proc = spawn(this.config.command, args, { | ||
| cwd: this.config.cwd || process.cwd(), | ||
| env: { ...process.env, ...this.config.env }, | ||
| stdio: ['pipe', 'pipe', 'pipe'] | ||
| }); | ||
|
|
||
| if (!proc.stdout) { | ||
| throw new Error('Failed to create stdout stream'); | ||
| } | ||
|
|
||
| const readline = await import('readline'); | ||
| const rl = readline.createInterface({ | ||
| input: proc.stdout, | ||
| crlfDelay: Infinity | ||
| }); |
There was a problem hiding this comment.
streamCommand() only reads from proc.stdout and never consumes proc.stderr. If the child process writes enough to stderr, the buffer can fill and block the process, stalling the stream. Drain stderr (e.g., with a separate listener/interface) or redirect it to stdout when streaming.
| } finally { | ||
| rl.close(); | ||
| proc.kill(); | ||
| } |
There was a problem hiding this comment.
streamCommand() always proc.kill() in finally and never checks the subprocess exit code. This can terminate a still-running process prematurely and also makes failures (non-zero exit) invisible to callers. Consider awaiting the close/exit event, surfacing non-zero codes (throw or emit an error/exit event), and only killing the process on cancellation/timeouts.
| approval_mode: str = "suggest", # suggest, auto-edit, full-auto | ||
| sandbox: str = "default", | ||
| json_output: bool = False, | ||
| output_schema: Optional[str] = None, | ||
| output_file: Optional[str] = None, | ||
| provider: Optional[str] = None, # OpenAI, OpenRouter, Azure, Gemini, etc. | ||
| # Backward compatibility | ||
| full_auto: Optional[bool] = None, | ||
| ): |
There was a problem hiding this comment.
There are existing unit tests for CodexCLIIntegration (see tests/unit/integrations/test_codex_cli.py), but the newly added options (approval_mode and provider) are not covered. Please add tests asserting that approval_mode="auto-edit" adds --auto-edit, approval_mode="full-auto" adds --full-auto, and provider adds --provider <value>, while full_auto= continues to work.
| async def get_available_integrations() -> Dict[str, bool]: | ||
| """ | ||
| Get availability status of all registered integrations. | ||
|
|
||
| Backward compatibility wrapper for the original function. | ||
|
|
||
| Returns: | ||
| Dict[str, bool]: Mapping of integration name to availability status | ||
| """ |
There was a problem hiding this comment.
get_available_integrations() is documented as a backward-compatibility wrapper, but it is declared async def here. Since the previous API is synchronous (and BaseCLIIntegration.get_available_integrations() is still sync), this makes the exported function inconsistent and breaks callers that expect a dict immediately. Either make this wrapper synchronous (delegating to the sync implementation) or rename it to an explicitly-async API and keep the original sync name intact.
| async def get_available_integrations() -> Dict[str, bool]: | |
| """ | |
| Get availability status of all registered integrations. | |
| Backward compatibility wrapper for the original function. | |
| Returns: | |
| Dict[str, bool]: Mapping of integration name to availability status | |
| """ | |
| def get_available_integrations() -> Dict[str, bool]: | |
| """ | |
| Get availability status of all registered integrations. | |
| Backward compatibility wrapper for the original synchronous function. | |
| Returns: | |
| Dict[str, bool]: Mapping of integration name to availability status | |
| """ | |
| return BaseCLIIntegration.get_available_integrations() | |
| async def get_available_integrations_async() -> Dict[str, bool]: | |
| """ | |
| Get availability status of all registered integrations asynchronously. | |
| Returns: | |
| Dict[str, bool]: Mapping of integration name to availability status | |
| """ |
|
@claude You are the FINAL architecture reviewer. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding. Phase 1: Review per AGENTS.md
Phase 2: FIX Valid Issues Phase 3: Final Verdict |
|
Claude finished @MervinPraison's task —— View job Final Architecture ReviewTodo List
Phase 1: Architecture Review per AGENTS.md✅ Protocol-driven Design: Correctly implemented
✅ Performance Impact: Minimal
✅ Backward Compatibility: Maintained
Critical Issues Found by Reviewers🔴 P0 - Breaks API Contract:
🟡 P1 - Quality Issues:
🟢 P2 - Cleanup:
Phase 2: Required FixesSince this is a fork PR with no push permissions, I cannot implement fixes directly. Here are the specific changes needed: 🔴 Critical Fix 1: API CompatibilityFile: # Line 84 - CHANGE THIS:
from .registry import get_available_integrations
# TO THIS:
from .base import get_available_integrations # Keep sync APIFile: # Line 219 - RENAME FUNCTION:
async def get_available_integrations_async() -> Dict[str, bool]: # New async API🔴 Critical Fix 2: Error HandlingFile: // Line 121 - ADD ERROR LISTENER:
proc.on('error', (err) => {
console.error(`Process error: ${err.message}`);
// Prevent unhandled exception crash
});🟡 Important Fix 3: Input ValidationFile: # Line 80 - ADD VALIDATION:
valid_modes = ["suggest", "auto-edit", "full-auto"]
if approval_mode not in valid_modes:
raise ValueError(f"approval_mode must be one of {valid_modes}, got '{approval_mode}'")Phase 3: Final Verdict❌ RECOMMEND CHANGES BEFORE MERGE Blocking Issues:
Non-Blocking Issues:
Recommendation: Fix the 2 critical issues above before merging. The P1/P2 issues can be addressed in follow-up PRs as they don't break functionality or create security risks. Architecture Assessment: ✅ SOLID - This PR correctly implements the registry pattern following AGENTS.md protocol-driven design. The core architectural changes are sound and well-structured. |
Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/386010ad-4465-4b2a-810a-8fbdc7878828 Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com>
There was a problem hiding this comment.
MervinPraison has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/386010ad-4465-4b2a-810a-8fbdc7878828 Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com>
There was a problem hiding this comment.
MervinPraison has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
@claude fix all issues you have access to the branch and its not forked repo |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/praisonai-ts/src/cli/features/external-agents.ts (1)
371-385:⚠️ Potential issue | 🟠 MajorUse the registry for built-ins too, otherwise overrides never take effect.
createExternalAgent()still hard-codesclaude/gemini/codex/aider, so a custom registration with one of those names is silently ignored. It also leaves no consistent way to passcwdthrough registry-created agents.♻️ Suggested direction
-export function createExternalAgent(name: string, cwd?: string): BaseExternalAgent | undefined { - const reg = getExternalAgentRegistry(); - - switch (name) { - case 'claude': - return new ClaudeCodeAgent(cwd); - case 'gemini': - return new GeminiCliAgent(cwd); - case 'codex': - return new CodexCliAgent(cwd); - case 'aider': - return new AiderAgent(cwd); - default: - return reg.get(name); - } -} +export function createExternalAgent(name: string, cwd?: string): BaseExternalAgent | undefined { + return getExternalAgentRegistry().get(name, cwd); +}Then update the registry factory signature to accept
cwd?: string, so built-ins and custom agents follow the same creation path.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/praisonai-ts/src/cli/features/external-agents.ts` around lines 371 - 385, createExternalAgent currently hard-codes built-ins (claude/gemini/codex/aider) so custom overrides never run and cwd isn't consistently passed; refactor createExternalAgent to always use the external agent registry (getExternalAgentRegistry) to obtain agents by name and pass cwd through, and update the registry factory/signature to accept an optional cwd?: string so registered factories for built-ins (e.g., ClaudeCodeAgent, GeminiCliAgent, CodexCliAgent, AiderAgent) and custom agents are registered as functions taking cwd?: string and invoked via reg.get(name, cwd) (or equivalent) instead of instantiating classes directly in createExternalAgent.
🧹 Nitpick comments (1)
src/praisonai-ts/src/cli/features/external-agents.ts (1)
22-26: MakeStreamEventa discriminated union instead of exposingany.This new public API gives consumers almost no static guarantees:
type: stringprevents exhaustiveness checks, anddata?: anypushes unsafe casting to every caller.♻️ Proposed typing
-export interface StreamEvent { - type: string; - content?: string; - data?: any; -} +export type StreamEvent = + | { type: 'text'; content: string } + | { type: 'json'; data: unknown };As per coding guidelines
src/praisonai-ts/src/**/*.ts: Use TypeScript with full type safety and generic constraints for type parameters.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/praisonai-ts/src/cli/features/external-agents.ts` around lines 22 - 26, The StreamEvent interface currently uses type: string and data?: any which loses type safety; replace it with a discriminated union (for example, a union of specific variants like { type: "message"; content: string; data?: never }, { type: "error"; error: Error|string; content?: never }, { type: "close" }, { type: "progress"; progress: number }) so callers can narrow on the type field and get properly-typed payloads instead of casting; update the StreamEvent definition and adjust all usages (functions/classes referencing StreamEvent) to use type-narrowing (switch or if checks) and remove any any casts, ensuring each variant exposes only the relevant properties and payload types.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/praisonai-ts/src/cli/features/external-agents.ts`:
- Around line 273-281: The stream method in external-agents.ts (async
*stream(prompt: string)) currently ignores AiderAgent.execute() failure states
and yields nothing on errors; update stream to inspect the execute() result
(check result.success and result.error) immediately after await
this.execute(prompt) and if success is false throw an Error (or rethrow a
meaningful message containing result.error) instead of proceeding to split/yield
output, so callers can distinguish failures from empty outputs.
- Around line 114-150: The streamCommand generator is missing the same
error-handling and exit validation as runCommand: attach proc.on('error') to
convert spawn errors into a yielded/throwable StreamEvent, capture stderr data
(proc.stderr) and include it when emitting errors, support a timeout from
this.config.timeout (kill the process and emit/throw on timeout), and validate
the child exit code in the finally/close path (if proc.exitCode !== 0 or
non-zero code from 'close'/'exit' event then yield/throw an error including
stderr and exit code). Update streamCommand to reference proc, rl, and
this.config (command/cwd/env/timeout) and ensure resources are cleaned
(rl.close(), proc.kill()) only after handling exit/error properly.
In `@src/praisonai/praisonai/integrations/codex_cli.py`:
- Around line 52-80: The initializer for the Codex CLI integration currently
lets invalid approval_mode values silently default to "suggest"; update the
constructor (the __init__ method in codex_cli.py that sets approval_mode and
handles full_auto) to validate the final approval_mode value eagerly: after
applying the backward-compatibility mapping from full_auto to approval_mode,
check that approval_mode is one of the allowed strings ("suggest", "auto-edit",
"full-auto") and raise a ValueError with a clear message if not; ensure this
validation happens before assigning self.approval_mode so the integration fails
fast on typos.
In `@src/praisonai/praisonai/integrations/registry.py`:
- Around line 219-229: The helper get_available_integrations currently is async
and returns a coroutine; change it back to a synchronous function by making it a
plain def get_available_integrations() -> Dict[str, bool] and synchronously
awaiting the registry coroutine (call get_registry() then run
registry.get_available() to completion) using the event loop (e.g., import
asyncio and return
asyncio.get_event_loop().run_until_complete(registry.get_available())); keep the
same return type and signature so existing sync callers (and the re-export in
integrations.__init__.py) continue to work; reference get_available_integrations
and registry.get_available when updating the implementation.
- Around line 159-166: get_available() currently instantiates every registered
class and treats any exception as "unavailable", which hides legitimate
constructor requirements and real init errors; update get_available() to first
inspect integration_class.__init__ (use inspect.signature) and if the
constructor requires parameters beyond self, do not attempt instantiation and
mark availability[name] = None (or omit the entry) instead of False, and only
instantiate when no required args and propagate or log real exceptions rather
than converting them to False; refer to register(), get_available(),
BaseCLIIntegration, and self._integrations to locate the code to change.
---
Outside diff comments:
In `@src/praisonai-ts/src/cli/features/external-agents.ts`:
- Around line 371-385: createExternalAgent currently hard-codes built-ins
(claude/gemini/codex/aider) so custom overrides never run and cwd isn't
consistently passed; refactor createExternalAgent to always use the external
agent registry (getExternalAgentRegistry) to obtain agents by name and pass cwd
through, and update the registry factory/signature to accept an optional cwd?:
string so registered factories for built-ins (e.g., ClaudeCodeAgent,
GeminiCliAgent, CodexCliAgent, AiderAgent) and custom agents are registered as
functions taking cwd?: string and invoked via reg.get(name, cwd) (or equivalent)
instead of instantiating classes directly in createExternalAgent.
---
Nitpick comments:
In `@src/praisonai-ts/src/cli/features/external-agents.ts`:
- Around line 22-26: The StreamEvent interface currently uses type: string and
data?: any which loses type safety; replace it with a discriminated union (for
example, a union of specific variants like { type: "message"; content: string;
data?: never }, { type: "error"; error: Error|string; content?: never }, { type:
"close" }, { type: "progress"; progress: number }) so callers can narrow on the
type field and get properly-typed payloads instead of casting; update the
StreamEvent definition and adjust all usages (functions/classes referencing
StreamEvent) to use type-narrowing (switch or if checks) and remove any any
casts, ensuring each variant exposes only the relevant properties and payload
types.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c6d1609f-50ae-4071-9678-43e710675f10
📒 Files selected for processing (5)
src/praisonai-ts/src/cli/features/external-agents.tssrc/praisonai/praisonai/integrations/__init__.pysrc/praisonai/praisonai/integrations/base.pysrc/praisonai/praisonai/integrations/codex_cli.pysrc/praisonai/praisonai/integrations/registry.py
|
Claude finished @MervinPraison's task —— View job ✅ All Reviewer Issues FixedBased on the comprehensive reviews from Gemini, CodeRabbit, Qodo, and Copilot, I have successfully fixed all identified issues: Todo List
Issues Fixed🔴 Critical Issues Fixed:
🟡 Important Issues Fixed:
🟢 Cleanup Issues Fixed: Tests Passed
Final AssessmentAll critical security and API compatibility issues have been resolved. The PR now has:
Recommendation: This PR is now ready for merge after addressing all reviewer feedback. |
Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/386010ad-4465-4b2a-810a-8fbdc7878828 Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com>
There was a problem hiding this comment.
MervinPraison has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
Completed a full review and applied targeted fixes in Key findings and actions:
Reviewer feedback status:
Suggested next improvements (not required for this fix):
|
- Fix API contract violation: maintain sync get_available_integrations() API - Add comprehensive error handling to TypeScript streamCommand - Improve AiderAgent stream error handling - Enhance registry availability checks with constructor inspection - Remove unused imports and improve type safety with discriminated unions Addresses feedback from Gemini, CodeRabbit, Qodo, and Copilot reviewers. Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
There was a problem hiding this comment.
MervinPraison has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/praisonai-ts/src/cli/features/external-agents.ts`:
- Around line 22-25: The StreamEvent union currently includes an { type:
'error'; error: string } variant but the async generator implementation rejects
on failures instead of yielding that variant; pick one approach and make it
consistent: either (A) update the generator(s) that produce StreamEvent to catch
errors and yield { type: 'error', error: string } before returning/closing
(ensure functions that consume the generator handle this sentinel and stop
iterating), or (B) remove the error variant from the exported StreamEvent type
and update documentation/comments for the generator-producing functions to state
that errors are surfaced via thrown exceptions (then update any consumers that
currently branch on event.type to handle thrown errors). Reference StreamEvent
and the async generator producers/consumers when making the change.
- Around line 144-155: Remove the "if (line.trim())" guard so blank lines are
not dropped; inside the loop that iterates "for await (const line of rl)" still
attempt JSON.parse in the existing try/catch and on error yield the text variant
(yield { type: 'text', content: line }) — this ensures empty lines (line === ""
or whitespace) are preserved and emitted for consumers to handle while keeping
the existing JSON parsing behavior.
- Around line 296-299: The loop over result.output is trimming and skipping
blank lines (if (line.trim())), which removes meaningful empty lines; change it
to preserve empty lines like the shared streaming helper by removing the
trim-based filter and yield every line (including empty strings) — update the
loop that iterates result.output and the yield { type: 'text', content: line }
so it no longer uses line.trim() to skip lines.
- Around line 50-53: Change stream() on BaseExternalAgent from an abstract
method to a concrete async generator that provides a safe non-breaking default
(so subclasses aren’t forced to implement it); implement a simple fallback
AsyncGenerator that emits no streaming updates or a single completion
StreamEvent and then returns, and keep the method signature using StreamEvent.
Update the exported BaseExternalAgent.stream JSDoc to include `@param` prompt,
`@returns` AsyncGenerator<StreamEvent, void, unknown>, and an `@example` showing
basic usage so public API docs are complete; built-in agents can still override
stream() for real streaming.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 357b5ac1-25a4-4022-be33-199dab60ab4e
📒 Files selected for processing (5)
src/praisonai-ts/src/cli/features/external-agents.tssrc/praisonai/praisonai/integrations/__init__.pysrc/praisonai/praisonai/integrations/codex_cli.pysrc/praisonai/praisonai/integrations/registry.pysrc/praisonai/tests/unit/integrations/test_codex_cli.py
🚧 Files skipped from review as they are similar to previous changes (3)
- src/praisonai/praisonai/integrations/init.py
- src/praisonai/praisonai/integrations/codex_cli.py
- src/praisonai/praisonai/integrations/registry.py
| export type StreamEvent = | ||
| | { type: 'text'; content: string } | ||
| | { type: 'json'; data: unknown } | ||
| | { type: 'error'; error: string }; |
There was a problem hiding this comment.
StreamEvent advertises an error variant that the implementations never emit.
Right now failures reject the async generator instead of yielding { type: 'error' }, so consumers branching on event.type cannot rely on the exported contract. Either emit the error variant before terminating, or remove it from StreamEvent and document that stream errors are surfaced via throw.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/praisonai-ts/src/cli/features/external-agents.ts` around lines 22 - 25,
The StreamEvent union currently includes an { type: 'error'; error: string }
variant but the async generator implementation rejects on failures instead of
yielding that variant; pick one approach and make it consistent: either (A)
update the generator(s) that produce StreamEvent to catch errors and yield {
type: 'error', error: string } before returning/closing (ensure functions that
consume the generator handle this sentinel and stop iterating), or (B) remove
the error variant from the exported StreamEvent type and update
documentation/comments for the generator-producing functions to state that
errors are surfaced via thrown exceptions (then update any consumers that
currently branch on event.type to handle thrown errors). Reference StreamEvent
and the async generator producers/consumers when making the change.
| /** | ||
| * Stream output from the external agent | ||
| */ | ||
| abstract stream(prompt: string): AsyncGenerator<StreamEvent, void, unknown>; |
There was a problem hiding this comment.
Avoid making stream() a new abstract requirement on the exported base class.
Line 53 turns stream() into a mandatory member for every downstream BaseExternalAgent subclass, which is a source-breaking change despite the PR’s backward-compatibility goal. A concrete fallback in the base class keeps existing custom agents compiling and still lets built-ins override for true streaming.
Suggested non-breaking shape
- abstract stream(prompt: string): AsyncGenerator<StreamEvent, void, unknown>;
+ /**
+ * Stream output from the external agent.
+ * `@param` prompt Prompt to send to the external agent.
+ * `@returns` Async stream of external-agent events.
+ * `@example`
+ * for await (const event of agent.stream("hello")) {
+ * console.log(event);
+ * }
+ */
+ async *stream(prompt: string): AsyncGenerator<StreamEvent, void, unknown> {
+ const result = await this.execute(prompt);
+ if (!result.success) {
+ throw new Error(result.error || `${this.getName()} execution failed`);
+ }
+ yield { type: 'text', content: result.output };
+ }As per coding guidelines, "All public APIs must include JSDoc comments with @param, @returns, and @example sections showing usage".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/praisonai-ts/src/cli/features/external-agents.ts` around lines 50 - 53,
Change stream() on BaseExternalAgent from an abstract method to a concrete async
generator that provides a safe non-breaking default (so subclasses aren’t forced
to implement it); implement a simple fallback AsyncGenerator that emits no
streaming updates or a single completion StreamEvent and then returns, and keep
the method signature using StreamEvent. Update the exported
BaseExternalAgent.stream JSDoc to include `@param` prompt, `@returns`
AsyncGenerator<StreamEvent, void, unknown>, and an `@example` showing basic usage
so public API docs are complete; built-in agents can still override stream() for
real streaming.
| for await (const line of rl) { | ||
| if (line.trim()) { | ||
| // Try to parse as JSON first | ||
| try { | ||
| const event = JSON.parse(line); | ||
| yield { type: 'json', data: event }; | ||
| } catch { | ||
| // If not JSON, treat as text | ||
| yield { type: 'text', content: line }; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Preserve blank lines in streamed text output.
The if (line.trim()) guard drops intentional empty lines, which means markdown paragraphs and code blocks cannot be reconstructed faithfully by callers consuming Claude/Gemini/Codex/Generic streams. Please preserve empty text chunks here and let the consumer decide whether to ignore them.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/praisonai-ts/src/cli/features/external-agents.ts` around lines 144 - 155,
Remove the "if (line.trim())" guard so blank lines are not dropped; inside the
loop that iterates "for await (const line of rl)" still attempt JSON.parse in
the existing try/catch and on error yield the text variant (yield { type:
'text', content: line }) — this ensures empty lines (line === "" or whitespace)
are preserved and emitted for consumers to handle while keeping the existing
JSON parsing behavior.
| for (const line of result.output.split('\n')) { | ||
| if (line.trim()) { | ||
| yield { type: 'text', content: line }; | ||
| } |
There was a problem hiding this comment.
Aider’s fallback stream also strips meaningful blank lines.
Line 297 applies the same trim() filter, so streamed aider output loses paragraph and code-block separation too. This path should preserve empty lines just like the shared streaming helper.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/praisonai-ts/src/cli/features/external-agents.ts` around lines 296 - 299,
The loop over result.output is trimming and skipping blank lines (if
(line.trim())), which removes meaningful empty lines; change it to preserve
empty lines like the shared streaming helper by removing the trim-based filter
and yield every line (including empty strings) — update the loop that iterates
result.output and the yield { type: 'text', content: line } so it no longer uses
line.trim() to skip lines.
|
@claude create with all examples and use cases from the client perspective and create a issue in MervinPraison/PraisonAIDocs with all information, so that information will be used to create documentation or update |
|
Claude finished @MervinPraison's task —— View job ✅ External CLI Integrations Documentation CreatedTodo List
SummarySuccessfully created comprehensive documentation for the External CLI Integrations feature from PR #1394. The documentation has been added to the PraisonAIDocs repository with complete examples and use cases from the client perspective. What Was Created📋 GitHub IssueIssue #156 - "External CLI Integrations Documentation - Registry Pattern & Streaming Support" 📄 Pull RequestPR #157 - "feat: External CLI Integrations Documentation with Registry Pattern & Streaming" 📝 Documentation FileEXTERNAL_CLI_INTEGRATIONS_DOCS.md (650+ lines) containing: Key Features Documented
Practical Use Cases (5 Complete Examples)
Configuration & Best Practices
Client-Focused ExamplesAll examples are written from the developer's perspective, showing:
The documentation is ready for integration into the PraisonAIDocs site and provides everything needed for developers to understand and implement the new external CLI integration features. Issue #156 | PR #157 | Branch: claude/issue-20260416-external-cli-docs |
…ples - Complete guide covering ExternalAgentRegistry pattern - Enhanced CodexCLI integration with approval modes and providers - TypeScript SDK streaming support examples - Practical use cases: code review, development pipeline, QA automation - Custom integration development guide - Configuration, error handling, and best practices - Migration guide for backward compatibility Addresses MervinPraison/PraisonAI#1394 Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
Implements fixes for issue #1393: External AI Coding CLI Integration Analysis
Changes Made:
Gap Analysis Addressed:
Generated with Claude Code
Summary by CodeRabbit