Skip to content

feat(agents): implement multi-agent orchestration system#47

Merged
jbdevprimary merged 4 commits into
mainfrom
feat/agent-orchestration-system
Jan 18, 2026
Merged

feat(agents): implement multi-agent orchestration system#47
jbdevprimary merged 4 commits into
mainfrom
feat/agent-orchestration-system

Conversation

@jbdevprimary
Copy link
Copy Markdown
Contributor

@jbdevprimary jbdevprimary commented Jan 18, 2026

Summary

Implements Issue #11 - Build multi-agent orchestration system for ThumbCode

This PR delivers a complete multi-agent AI system with:

  • AI Service Layer: Unified client interface supporting Anthropic Claude and OpenAI with streaming
  • Specialized Agents: Four agent types (Architect, Implementer, Reviewer, Tester) with tool calling
  • Orchestrator: Task management with dependency resolution and parallel execution
  • Comprehensive Tests: 30 tests covering all major functionality

Changes

AI Service Layer (packages/agent-intelligence/src/services/ai/)

  • Unified AIClient interface with complete() and completeStream() methods
  • Anthropic client with full streaming support and tool calling
  • OpenAI client with streaming and message format conversion
  • Factory function createAIClient() for provider selection

Specialized Agents (packages/agent-intelligence/src/services/agents/)

  • BaseAgent abstract class with tool calling loop and event system
  • ArchitectAgent for system design with tools: read_file, analyze_dependencies, create_spec
  • ImplementerAgent for code writing with tools: write_file, edit_file, search_code, run_command
  • ReviewerAgent for code review with tools: get_diff, add_comment, approve_changes
  • TesterAgent for test generation with tools: run_tests, get_coverage, create_mock

Orchestrator (packages/agent-intelligence/src/services/orchestrator/)

  • Task queue with priority and dependency support
  • Parallel execution with configurable concurrency
  • Agent lifecycle management (create, remove, status tracking)
  • Event system for real-time progress tracking
  • Execution planning with topological sort for dependencies

Test plan

  • All 30 unit tests passing
  • TypeScript typecheck passes
  • Biome lint passes
  • CodeRabbit review
  • Coveralls coverage check

Closes #11

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Introduced AI agent system with four agent types (Architect, Implementer, Reviewer, Tester)
    • Integrated Anthropic Claude and OpenAI model support
    • Added Agent Orchestrator for coordinating multi-agent workflows, task dependencies, parallel execution, and runtime metrics
    • Centralized package entrypoint and agent factory for easier consumption
  • Tests

    • Added comprehensive tests for AI clients and orchestrator behaviors
  • Chores

    • Added Jest/TypeScript configs, package exports, and test/type scripts

✏️ Tip: You can customize this high-level summary in your review settings.

Implements Issue #11 - Build multi-agent orchestration system

## Changes

### AI Service Layer
- Add unified AI client interface supporting multiple providers
- Implement Anthropic client with streaming support
- Implement OpenAI client with streaming support
- Add factory function for provider selection

### Specialized Agents
- Add base agent class with tool calling and streaming
- Implement Architect agent for system design and planning
- Implement Implementer agent for code writing
- Implement Reviewer agent for code review
- Implement Tester agent for test generation

### Orchestrator
- Add task management with dependency resolution
- Implement parallel task execution with concurrency control
- Add agent lifecycle management (create, remove, status)
- Implement event system for progress tracking
- Add execution planning with topological sort

### Testing
- Add comprehensive tests for AI clients (30 tests)
- Add orchestrator tests for all major features
- Configure Jest with ts-jest for TypeScript support

Closes #11

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 18, 2026

Warning

Rate limit exceeded

@jbdevprimary has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 24 minutes and 3 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 3da5cee and 7cc97fa.

📒 Files selected for processing (2)
  • packages/agent-intelligence/src/__tests__/ai-client.test.ts
  • packages/agent-intelligence/src/__tests__/orchestrator.test.ts
📝 Walkthrough

Walkthrough

Adds a full multi-agent subsystem to the agent-intelligence package: AI provider clients (Anthropic/OpenAI) with streaming, typed AI abstractions, a BaseAgent framework with four concrete agents, an AgentOrchestrator with scheduling/dependency handling and events, package/config/test setup, and comprehensive tests; removes legacy mock orchestrator.

Changes

Cohort / File(s) Summary
Config & Packaging
packages/agent-intelligence/jest.config.js, packages/agent-intelligence/tsconfig.json, packages/agent-intelligence/package.json
Adds Jest/ts-jest config, TypeScript project config, package exports map, test/typecheck scripts, and runtime/dev dependencies.
AI Types & Index
packages/agent-intelligence/src/services/ai/types.ts, .../ai/index.ts
New AI type system (Message, ContentBlock, ToolDefinition, CompletionResponse, StreamEvent, AIClient) and unified ai index with createAIClient/getDefaultModel/getAvailableModels.
AI Provider Clients
packages/agent-intelligence/src/services/ai/anthropic-client.ts, .../ai/openai-client.ts
Implements Anthropic and OpenAI client wrappers with complete and completeStream flows, mapping provider responses to internal CompletionResponse and emitting stream events.
Agent Framework
packages/agent-intelligence/src/services/agents/base-agent.ts, .../agents/index.ts, src/index.ts
Adds abstract BaseAgent (execution, streaming, tool handling, events), agent factory/exports, and package entry re-exports.
Concrete Agents
packages/agent-intelligence/src/services/agents/architect-agent.ts, implementer-agent.ts, reviewer-agent.ts, tester-agent.ts
New Architect, Implementer, Reviewer, Tester agent classes extending BaseAgent, each with system prompts, capabilities, tool definitions, and executeTool implementations.
Orchestrator Types & Impl
packages/agent-intelligence/src/services/orchestrator/types.ts, .../orchestrator/orchestrator.ts, .../orchestrator/index.ts
New orchestrator types and AgentOrchestrator class: agent lifecycle, task creation/assignment, dependency-aware scheduling, concurrency controls, event system, metrics, and state accessors.
Tests
packages/agent-intelligence/src/__tests__/ai-client.test.ts, .../__tests__/orchestrator.test.ts
Adds comprehensive tests for AI clients (streaming and non-streaming) and orchestrator behavior (agent lifecycle, tasks, events, dependencies).
Legacy Removal
packages/agent-intelligence/src/services/agent-orchestrator/*
Removes prior mock agent-orchestrator index, orchestrator, and types files.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Orchestrator as AgentOrchestrator
    participant TaskQueue as Task Queue
    participant Agent as BaseAgent
    participant AIClient as AIClient
    participant Tool as Tool Executor

    Client->>Orchestrator: createTask(...)
    Orchestrator->>TaskQueue: enqueue(task)
    Orchestrator->>Orchestrator: processTaskQueue()
    TaskQueue->>Orchestrator: select ready task
    Orchestrator->>Agent: assignTask(taskId, agentId)
    Orchestrator->>Orchestrator: emitEvent('task_started')
    Orchestrator->>Agent: execute(task, context)
    Agent->>AIClient: complete/completeStream(messages, options)
    AIClient-->>Agent: stream/content blocks/events
    alt tool_use encountered
        Agent->>Tool: executeTool(name, input, context)
        Tool-->>Agent: tool_result
        Agent->>AIClient: resume with tool_result
    end
    AIClient-->>Agent: final CompletionResponse
    Agent->>Orchestrator: AgentExecutionResult
    Orchestrator->>Orchestrator: update task/agent state
    Orchestrator->>Orchestrator: emitEvent('task_completed')
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 Hopping through code, I found a plan,

Four agents ready, by my paw and hand.
Architect dreams, Implementer writes,
Reviewer checks, Tester runs the lights.
Together we orchestrate—hip-hop, hooray!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(agents): implement multi-agent orchestration system' accurately and directly summarizes the primary change in the changeset.
Linked Issues check ✅ Passed The PR successfully implements all core requirements from Issue #11: AI service layer with Anthropic/OpenAI clients [#11], BaseAgent and four specialized agents (Architect, Implementer, Reviewer, Tester) [#11], AgentOrchestrator with task queues and dependency resolution [#11], streaming support and event systems [#11], and comprehensive tests [#11]. Token usage tracking is integrated throughout the AI clients and agent execution pipeline.
Out of Scope Changes check ✅ Passed All changes are directly aligned with Issue #11 objectives. The PR adds AI service abstractions, specialized agent implementations, orchestrator with task management, comprehensive test coverage, TypeScript configuration, and package.json updates for dependencies and exports—all required for the multi-agent system.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread packages/agent-intelligence/src/__tests__/orchestrator.test.ts Fixed
The CI uses babel-jest which doesn't allow async generators inside jest.mock()
due to scope restrictions. Replaced with simple array-based iterators.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Comment thread packages/agent-intelligence/src/__tests__/orchestrator.test.ts Fixed
jbdevprimary and others added 2 commits January 18, 2026 14:25
…bility

Variables inside jest.mock() must be prefixed with 'mock' (case insensitive)
to be allowed by babel-jest's scope restrictions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Addresses code quality review comment.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 Fix all issues with AI agents
In `@packages/agent-intelligence/src/__tests__/ai-client.test.ts`:
- Around line 8-50: The mock factory for '@anthropic-ai/sdk' creates an inline
async generator (used for messages.stream) which gets hoisted by Jest and causes
out-of-scope errors; fix it by extracting the async generator to a top-level
named async function* (e.g., anthropicStreamGenerator) outside the jest.mock
factory and have messages.stream.mockImplementation return an object whose
[Symbol.asyncIterator] is that top-level generator; do the same extraction for
the OpenAI mock's stream iterator (create e.g., openAIStreamGenerator) and
reference those top-level generators from the respective mock implementations
(keep existing shape: message_start/content_block_delta/message_stop and
finalMessage mocks).

In `@packages/agent-intelligence/src/__tests__/orchestrator.test.ts`:
- Around line 9-37: The mock's inline async generator inside the jest.mock
factory (the async function* used for messages.stream with Symbol.asyncIterator)
gets hoisted and references helper transforms that are out of scope; fix this by
pulling the async iterator out of the mock factory into a top-level helper
(e.g., createMockStreamAsyncIterator or anthropicStreamGenerator) defined before
the jest.mock call and have the messages.stream implementation return that
helper's iterator, keeping finalMessage and messages.create mocks intact;
alternatively implement the async iterable manually (an object with a
Symbol.asyncIterator method returning an object with an async next) as a
top-level helper and reference it from the jest.mock factory.

In `@packages/agent-intelligence/src/services/agents/architect-agent.ts`:
- Around line 69-74: The capability with id 'interface-design' references a
non-existent tool 'write_file'; update the mapping so it's consistent by either
adding a 'write_file' tool definition to getTools() (ensuring the tool name and
any handler/export match exactly) or removing 'write_file' from the tools array
in the 'interface-design' capability; locate these in the architect-agent.ts
capability object (id: 'interface-design', name: 'Interface Design') and in the
getTools() implementation and make the change so both lists reference the same
set of tool names.

In `@packages/agent-intelligence/src/services/agents/base-agent.ts`:
- Around line 393-408: The catch block in executeStream() currently sets status
and returns an error result but doesn't emit the error event like execute()
does; update executeStream() to call this.emit('error', error) (or
this.emit('error', errorMessage) matching execute()'s behavior) before
returning, ensuring subscribers receive the error; locate executeStream() and
mirror the error emission used in execute() while preserving setStatus('error'),
the returned object shape
(filesCreated/filesModified/filesDeleted/summary/messages/tokensUsed/error), and
use the same error value (error or errorMessage) to keep behavior consistent
with conversationHistory and totalTokens reporting.
- Around line 94-118: getInfo() currently sets createdAt on every call; add a
class field (e.g., private readonly createdAt: string) and initialize it in the
class constructor (this.createdAt = new Date().toISOString()) and then change
getInfo() to return that stored this.createdAt instead of calling new
Date().toISOString() there; keep lastActiveAt behavior as-is or update
separately if needed.
- Around line 360-382: The streaming executor (executeStream) is missing the
same "tool_use" event emissions that execute emits; update the executeStream
flow where it handles response.stopReason === 'tool_use' and toolUseBlocks to
emit a "tool_use" event for each block (including block.id, block.name,
block.input and any context) before calling this.executeTool, and then emit
"tool_result" events (with tool_use_id and content) after the tool completes;
ensure you still call this.setStatus('coding'), call this.executeTool(block.name
|| '', block.input || {}, context) for each block, push the toolResultMessage
into this.conversationHistory exactly as in execute, and keep the same payload
shapes so subscribers receive identical events between execute and
executeStream.

In `@packages/agent-intelligence/src/services/agents/implementer-agent.ts`:
- Around line 70-84: The capabilities list includes undefined tools: add
definitions for "create_component" and "move_file" in getTools() or change the
capabilities to reference existing tools; specifically update the
'component-creation' capability (id 'component-creation') to use an available
tool instead of create_component or implement a create_component tool in
getTools(), and update the 'refactoring' capability (id 'refactoring') to either
reference an existing move/rename tool or add a move_file implementation in
getTools(); ensure the tool names in the capabilities exactly match the keys
returned by getTools().
- Around line 256-257: In the 'run_command' branch in implementer-agent.ts the
code unsafely casts input.args to string[]; replace that cast with a
runtime-safe check and normalization: verify input.args is an array
(Array.isArray), map each element to String (or filter non-primitives) and then
join, falling back to an empty string if not an array or empty; update the case
'run_command' return expression to use this validated/normalized args handling
rather than (input.args as string[])?.join(' ') || '' so join cannot throw at
runtime.

In `@packages/agent-intelligence/src/services/ai/anthropic-client.ts`:
- Around line 46-48: Replace the unsafe cast of m.role in
conversationMessages.map with an explicit mapper function (e.g.,
mapAnthropicRole) that accepts Message['role'] and returns only 'user' |
'assistant' or throws for unsupported roles; update both occurrences (the
mapping at the messages creation and the similar code around lines 84-86) to
call mapAnthropicRole(m.role) instead of casting, and ensure formatContentBlocks
usage remains unchanged; confirm the mapper covers all valid MessageRole values
(adjust mapping if roles like 'tool' or 'system' should be allowed) so Anthropic
only receives permitted roles.

In `@packages/agent-intelligence/src/services/ai/openai-client.ts`:
- Around line 335-347: The mapping in mapOpenAIStopReason incorrectly converts
OpenAI's 'content_filter' stop reason to 'stop_sequence', which loses the
safety/policy signal; update the implementation so 'content_filter' is preserved
as a distinct stop reason (either by adding 'content_filter' to the
CompletionResponse['stopReason'] union in types.ts and returning
'content_filter' from mapOpenAIStopReason, or by mapping it to another distinct
value that conveys a policy block), and adjust any consumers/types that rely on
CompletionResponse['stopReason'] accordingly (refer to mapOpenAIStopReason and
CompletionResponse['stopReason'] for the exact places to change).
- Around line 20-24: The OPENAI_MODELS constant uses the preview alias for GPT-4
Turbo; update the GPT_4_TURBO entry in OPENAI_MODELS to use the current primary
identifier 'gpt-4-turbo' (replace the value for the GPT_4_TURBO key in the
OPENAI_MODELS object) so other code referencing OPENAI_MODELS.GPT_4_TURBO uses
the recommended model name.

In `@packages/agent-intelligence/src/services/orchestrator/orchestrator.ts`:
- Around line 378-383: Replace the non-null assertion on task.startedAt (used
when constructing the task result object with keys like
success/result/startedAt/completedAt/duration) with a defensive fallback value
instead of task.startedAt!; for example use a safe fallback (e.g.,
task.startedAt ?? a sensible default timestamp or null) so the code handles
undefined startedAt, and apply the same change to the other occurrence that
builds a similar object around lines 440–445.
- Around line 452-456: getState() currently returns a shallow copy via the
spread operator so internal collections (agents, taskQueue, activeTasks,
completedTasks) can still be mutated by callers; change getState to return a
deep copy of the OrchestratorState by constructing new containers for each
collection (e.g., new Map(this.state.agents), new Map(this.state.activeTasks),
new Map(this.state.completedTasks), and cloned arrays for taskQueue or other
arrays) and shallow-clone the values inside those containers as needed so
callers cannot mutate the orchestrator's internal Maps or arrays.
🧹 Nitpick comments (15)
packages/agent-intelligence/tsconfig.json (1)

12-15: Clarify declaration emit vs. noEmit.

noEmit: true prevents .d.ts generation even though declaration/declarationMap are enabled. If you plan to ship types, consider removing noEmit here (keeping tsc --noEmit only in the typecheck script) or splitting a build tsconfig. If you don’t need declarations, drop the declaration flags.

♻️ Option: allow declarations to emit in a build config
-    "noEmit": true,
packages/agent-intelligence/package.json (1)

5-11: Exports point to TS sources—confirm runtime can load TS.

main/exports target .ts files. This only works if every consumer runs through a TS-aware bundler/loader. If this package is ever consumed by plain Node/Jest or published, consider emitting JS to a dist/ folder and pointing main/exports (and types) there. Please verify consumer toolchains.

packages/agent-intelligence/jest.config.js (1)

3-23: Align ts-jest ESM settings with module output.

useESM: true while the embedded tsconfig sets module: 'commonjs' can create ESM/CJS mismatches in Jest. Either disable ESM here or switch to the ESM preset and an ESM module target. Please verify which mode you intend.

♻️ Option: keep CommonJS and disable ESM in ts-jest
-      useESM: true,
packages/agent-intelligence/src/__tests__/orchestrator.test.ts (1)

315-317: Unused variable state.

The variable is declared but never used in the assertion.

♻️ Suggested fix
-      const state = orchestrator.getState();
       const tasks = orchestrator.getTasks();
packages/agent-intelligence/src/services/ai/index.ts (1)

46-55: Inconsistent error handling for unsupported providers.

createAIClient and getDefaultModel throw an error for unsupported providers, but getAvailableModels silently returns an empty array. This inconsistency could mask bugs when adding new providers.

♻️ Suggested fix for consistency
 export function getAvailableModels(provider: AIProvider): string[] {
   switch (provider) {
     case 'anthropic':
       return Object.values(ANTHROPIC_MODELS);
     case 'openai':
       return Object.values(OPENAI_MODELS);
     default:
-      return [];
+      throw new Error(`Unsupported AI provider: ${provider}`);
   }
 }

Alternatively, if returning an empty array is intentional for graceful degradation, document this behavior and update the test to cover it.

packages/agent-intelligence/src/services/agents/reviewer-agent.ts (2)

249-251: Variable declaration in switch case without block scope.

Declaring const location directly in a case clause without braces can cause issues in some JavaScript environments and reduces readability.

♻️ Suggested fix
-      case 'add_comment':
-        const location = input.line ? `:${input.line}` : '';
-        return `Added ${input.severity} comment on ${input.path}${location}: ${input.message}`;
+      case 'add_comment': {
+        const location = input.line ? `:${input.line}` : '';
+        return `Added ${input.severity} comment on ${input.path}${location}: ${input.message}`;
+      }

256-257: Unsafe type assertion on input.blocking_issues.

The assertion (input.blocking_issues as string[]) will fail at runtime with ?.join returning undefined if the property is missing or not an array. Consider defensive handling.

♻️ Suggested fix
-      case 'request_changes':
-        return `Requested changes: ${input.summary}\nBlocking issues: ${(input.blocking_issues as string[])?.join(', ')}`;
+      case 'request_changes': {
+        const issues = Array.isArray(input.blocking_issues)
+          ? input.blocking_issues.join(', ')
+          : 'none specified';
+        return `Requested changes: ${input.summary}\nBlocking issues: ${issues}`;
+      }
packages/agent-intelligence/src/services/agents/implementer-agent.ts (1)

207-225: Security: run_command tool requires safeguards before real implementation.

This tool accepts arbitrary shell commands. When implementing the real execution logic:

  • Use an allowlist of permitted commands
  • Sanitize arguments to prevent injection
  • Consider sandboxing or restricting the execution environment
  • Add timeouts to prevent runaway processes

The current mock is safe, but document these requirements for future implementation.

packages/agent-intelligence/src/services/ai/openai-client.ts (2)

98-100: Prefer const for collectedToolCalls.

The Map is mutated via set() but never reassigned. Using const communicates intent more clearly.

♻️ Suggested fix
-      let collectedToolCalls: Map<number, { id: string; name: string; arguments: string }> =
-        new Map();
+      const collectedToolCalls = new Map<number, { id: string; name: string; arguments: string }>();

194-200: Consider logging when tool argument JSON parsing fails.

Silently returning {} on parse failure can make debugging difficult. Adding a warning log would help identify malformed responses from the API.

packages/agent-intelligence/src/services/ai/types.ts (1)

30-41: Consider using discriminated unions for ContentBlock.

The current design uses optional fields for all content types. A discriminated union would provide better type safety and exhaustiveness checking:

♻️ Example discriminated union approach
export type ContentBlock =
  | { type: 'text'; text: string }
  | { type: 'tool_use'; id: string; name: string; input: Record<string, unknown> }
  | { type: 'tool_result'; tool_use_id: string; content: string };

This ensures required fields are present for each type and enables exhaustive pattern matching.

packages/agent-intelligence/src/services/orchestrator/orchestrator.ts (2)

261-264: Polling loop could be replaced with event-driven coordination.

The 1-second setTimeout creates a polling loop that may delay task starts or waste cycles. Consider using a Promise that resolves when any active task completes, or an event-based mechanism.

This is acceptable for initial implementation but may impact responsiveness at scale.


320-343: No circular dependency detection in dependency graph.

buildDependencyGraph and buildExecutionPlan don't detect circular dependencies. Tasks with circular deps would remain in waiting indefinitely without feedback.

Consider adding cycle detection (e.g., during createTask) or marking cyclic tasks as blocked in the execution plan.

packages/agent-intelligence/src/services/agents/base-agent.ts (2)

133-142: Consider wrapping callback invocations in try/catch.

If an event callback throws an exception, it will prevent subsequent callbacks from being notified and could propagate the error up to the caller unexpectedly. For event systems, it's typically safer to isolate callback failures.

♻️ Proposed improvement
 protected emitEvent(event: Omit<AgentEvent, 'agentId' | 'timestamp'>): void {
   const fullEvent: AgentEvent = {
     ...event,
     agentId: this.id,
     timestamp: new Date().toISOString(),
   };
   for (const callback of this.eventCallbacks) {
-    callback(fullEvent);
+    try {
+      callback(fullEvent);
+    } catch (error) {
+      console.error('Agent event callback error:', error);
+    }
   }
 }

444-492: String-based parsing is functional but fragile.

The regex-based extraction of file operations (Created file:, Modified file:, Deleted file:) depends on tool implementations producing these exact strings. If tool output formats change, this parsing will silently fail to capture file operations.

Consider having executeTool implementations return structured data (or update agent state directly) rather than relying on string parsing. This is acceptable for now but worth tracking as a future improvement.

Comment thread packages/agent-intelligence/src/__tests__/ai-client.test.ts
Comment thread packages/agent-intelligence/src/__tests__/orchestrator.test.ts
Comment on lines +69 to +74
id: 'interface-design',
name: 'Interface Design',
description: 'Define TypeScript interfaces and data models',
requiredCredentials: [],
tools: ['read_file', 'write_file'],
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Capability references undefined tool write_file.

The interface-design capability lists write_file in its tools array, but getTools() does not define this tool. This inconsistency may cause issues if the capability-tool mapping is validated or used to filter available tools.

Either add the write_file tool definition to getTools() or remove it from this capability's tools list.

🤖 Prompt for AI Agents
In `@packages/agent-intelligence/src/services/agents/architect-agent.ts` around
lines 69 - 74, The capability with id 'interface-design' references a
non-existent tool 'write_file'; update the mapping so it's consistent by either
adding a 'write_file' tool definition to getTools() (ensuring the tool name and
any handler/export match exactly) or removing 'write_file' from the tools array
in the 'interface-design' capability; locate these in the architect-agent.ts
capability object (id: 'interface-design', name: 'Interface Design') and in the
getTools() implementation and make the change so both lists reference the same
set of tool names.

Comment on lines +94 to +118
getInfo(): Agent {
return {
id: this.id,
name: this.name,
role: this.role,
status: this.status,
capabilities: this.getCapabilities(),
metrics: {
tasksCompleted: 0,
linesWritten: 0,
reviewsPerformed: 0,
averageTaskTime: 0,
successRate: 0,
tokensUsed: 0,
},
config: {
model: this.model,
temperature: this.temperature,
maxTokens: this.maxTokens,
tools: this.getTools().map((t) => t.name),
},
createdAt: new Date().toISOString(),
lastActiveAt: new Date().toISOString(),
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

createdAt timestamp should be set once at construction, not on every call.

Currently createdAt is set to new Date().toISOString() each time getInfo() is called, which means the agent's "creation time" changes on every invocation. This is semantically incorrect and could cause issues in monitoring/dashboard scenarios that track agent lifecycle.

🐛 Proposed fix: Store createdAt in constructor

Add a field in the class and initialize it in the constructor:

 protected eventCallbacks: AgentEventCallback[] = [];
 protected conversationHistory: Message[] = [];
+protected createdAt: string;

 constructor(
   config: {
     // ...
   }
 ) {
   // ...
   this.temperature = config.temperature || 0.7;
+  this.createdAt = new Date().toISOString();
 }

Then update getInfo():

     config: {
       model: this.model,
       temperature: this.temperature,
       maxTokens: this.maxTokens,
       tools: this.getTools().map((t) => t.name),
     },
-    createdAt: new Date().toISOString(),
+    createdAt: this.createdAt,
     lastActiveAt: new Date().toISOString(),
   };
🤖 Prompt for AI Agents
In `@packages/agent-intelligence/src/services/agents/base-agent.ts` around lines
94 - 118, getInfo() currently sets createdAt on every call; add a class field
(e.g., private readonly createdAt: string) and initialize it in the class
constructor (this.createdAt = new Date().toISOString()) and then change
getInfo() to return that stored this.createdAt instead of calling new
Date().toISOString() there; keep lastActiveAt behavior as-is or update
separately if needed.

Comment on lines +360 to +382
if (response.stopReason === 'tool_use' && toolUseBlocks.length > 0) {
this.setStatus('coding');

const toolResults = await Promise.all(
toolUseBlocks.map(async (block) => {
const output = await this.executeTool(block.name || '', block.input || {}, context);
return {
type: 'tool_result' as const,
tool_use_id: block.id,
content: output,
};
})
);

const toolResultMessage: Message = {
role: 'user',
content: toolResults,
};
this.conversationHistory.push(toolResultMessage);
} else {
continueExecution = false;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inconsistent event emission between execute() and executeStream().

The streaming version omits tool_use events that the non-streaming version emits (lines 229-245 in execute()). This inconsistency means event subscribers won't receive tool usage notifications during streaming execution, breaking observability.

🐛 Proposed fix: Add tool_use events to streaming execution
         const toolResults = await Promise.all(
           toolUseBlocks.map(async (block) => {
+            this.emitEvent({
+              type: 'tool_use',
+              data: {
+                tool: block.name,
+                toolInput: block.input,
+              },
+            });
+
             const output = await this.executeTool(block.name || '', block.input || {}, context);
+
+            this.emitEvent({
+              type: 'tool_use',
+              data: {
+                tool: block.name,
+                toolOutput: output,
+              },
+            });
+
             return {
               type: 'tool_result' as const,
               tool_use_id: block.id,
               content: output,
             };
           })
         );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (response.stopReason === 'tool_use' && toolUseBlocks.length > 0) {
this.setStatus('coding');
const toolResults = await Promise.all(
toolUseBlocks.map(async (block) => {
const output = await this.executeTool(block.name || '', block.input || {}, context);
return {
type: 'tool_result' as const,
tool_use_id: block.id,
content: output,
};
})
);
const toolResultMessage: Message = {
role: 'user',
content: toolResults,
};
this.conversationHistory.push(toolResultMessage);
} else {
continueExecution = false;
}
}
if (response.stopReason === 'tool_use' && toolUseBlocks.length > 0) {
this.setStatus('coding');
const toolResults = await Promise.all(
toolUseBlocks.map(async (block) => {
this.emitEvent({
type: 'tool_use',
data: {
tool: block.name,
toolInput: block.input,
},
});
const output = await this.executeTool(block.name || '', block.input || {}, context);
this.emitEvent({
type: 'tool_use',
data: {
tool: block.name,
toolOutput: output,
},
});
return {
type: 'tool_result' as const,
tool_use_id: block.id,
content: output,
};
})
);
const toolResultMessage: Message = {
role: 'user',
content: toolResults,
};
this.conversationHistory.push(toolResultMessage);
} else {
continueExecution = false;
}
}
🤖 Prompt for AI Agents
In `@packages/agent-intelligence/src/services/agents/base-agent.ts` around lines
360 - 382, The streaming executor (executeStream) is missing the same "tool_use"
event emissions that execute emits; update the executeStream flow where it
handles response.stopReason === 'tool_use' and toolUseBlocks to emit a
"tool_use" event for each block (including block.id, block.name, block.input and
any context) before calling this.executeTool, and then emit "tool_result" events
(with tool_use_id and content) after the tool completes; ensure you still call
this.setStatus('coding'), call this.executeTool(block.name || '', block.input ||
{}, context) for each block, push the toolResultMessage into
this.conversationHistory exactly as in execute, and keep the same payload shapes
so subscribers receive identical events between execute and executeStream.

Comment on lines +46 to +48
messages: conversationMessages.map((m) => ({
role: m.role as 'user' | 'assistant',
content: typeof m.content === 'string' ? m.content : formatContentBlocks(m.content),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Map/guard message roles instead of casting.

Casting m.role to 'user' | 'assistant' bypasses validation. If any other role slips through, Anthropic will receive an invalid role. Prefer an explicit mapping or throw on unsupported roles.

🔧 Add an explicit role mapper
-          role: m.role as 'user' | 'assistant',
+          role: mapAnthropicRole(m.role),
-          role: m.role as 'user' | 'assistant',
+          role: mapAnthropicRole(m.role),

If you don’t already have a helper, you can add one like:

function mapAnthropicRole(role: Message['role']): 'user' | 'assistant' {
  if (role === 'user' || role === 'assistant') return role;
  throw new Error(`Unsupported Anthropic role: ${role}`);
}

Please verify the actual MessageRole values and adjust the mapping if tool or other roles are permitted.

#!/bin/bash
# Verify MessageRole values and where it is defined.
# Expect: only 'system' | 'user' | 'assistant' (or update mapping accordingly).
rg -n "MessageRole" --type=ts

Also applies to: 84-86

🤖 Prompt for AI Agents
In `@packages/agent-intelligence/src/services/ai/anthropic-client.ts` around lines
46 - 48, Replace the unsafe cast of m.role in conversationMessages.map with an
explicit mapper function (e.g., mapAnthropicRole) that accepts Message['role']
and returns only 'user' | 'assistant' or throws for unsupported roles; update
both occurrences (the mapping at the messages creation and the similar code
around lines 84-86) to call mapAnthropicRole(m.role) instead of casting, and
ensure formatContentBlocks usage remains unchanged; confirm the mapper covers
all valid MessageRole values (adjust mapping if roles like 'tool' or 'system'
should be allowed) so Anthropic only receives permitted roles.

Comment on lines +20 to +24
export const OPENAI_MODELS = {
GPT_4_TURBO: 'gpt-4-turbo-preview',
GPT_4O: 'gpt-4o',
GPT_4O_MINI: 'gpt-4o-mini',
} as const;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

OpenAI GPT-4 Turbo model identifier 2025 2026 current version

💡 Result:

Current OpenAI GPT-4 Turbo model identifier: "gpt-4-turbo" (with available snapshot aliases such as "gpt-4-turbo-2024-04-09" and preview aliases like "gpt-4-turbo-preview"/"gpt-4-0125-preview"). See OpenAI model docs. [1][2]

Sources:
[1] OpenAI API — GPT-4 Turbo model page.
[2] Azure / Microsoft documentation summarizing OpenAI model versions.


Update GPT-4 Turbo model identifier to current version.

GPT_4_TURBO: 'gpt-4-turbo-preview' uses a preview alias. The current recommended identifier is 'gpt-4-turbo'. While the preview variant remains functional, update to the primary identifier for consistency with OpenAI's current API.

🤖 Prompt for AI Agents
In `@packages/agent-intelligence/src/services/ai/openai-client.ts` around lines 20
- 24, The OPENAI_MODELS constant uses the preview alias for GPT-4 Turbo; update
the GPT_4_TURBO entry in OPENAI_MODELS to use the current primary identifier
'gpt-4-turbo' (replace the value for the GPT_4_TURBO key in the OPENAI_MODELS
object) so other code referencing OPENAI_MODELS.GPT_4_TURBO uses the recommended
model name.

Comment on lines +335 to +347
function mapOpenAIStopReason(reason: string | null): CompletionResponse['stopReason'] {
switch (reason) {
case 'stop':
return 'end_turn';
case 'length':
return 'max_tokens';
case 'tool_calls':
return 'tool_use';
case 'content_filter':
return 'stop_sequence';
default:
return 'end_turn';
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Incorrect mapping: content_filterstop_sequence loses safety signal.

OpenAI's content_filter indicates content was blocked for policy reasons, which is semantically different from stop_sequence. Mapping it to stop_sequence obscures important safety information from consumers.

Consider either:

  1. Adding 'content_filter' to the CompletionResponse['stopReason'] union type
  2. Mapping to a distinct value that preserves the safety signal
🔧 Option: Add content_filter to union (requires types.ts change)

In types.ts:

 export interface CompletionResponse {
   id: string;
   content: ContentBlock[];
   model: string;
-  stopReason: 'end_turn' | 'max_tokens' | 'tool_use' | 'stop_sequence';
+  stopReason: 'end_turn' | 'max_tokens' | 'tool_use' | 'stop_sequence' | 'content_filter';
   usage: TokenUsage;
 }

Then in openai-client.ts:

     case 'content_filter':
-      return 'stop_sequence';
+      return 'content_filter';
🤖 Prompt for AI Agents
In `@packages/agent-intelligence/src/services/ai/openai-client.ts` around lines
335 - 347, The mapping in mapOpenAIStopReason incorrectly converts OpenAI's
'content_filter' stop reason to 'stop_sequence', which loses the safety/policy
signal; update the implementation so 'content_filter' is preserved as a distinct
stop reason (either by adding 'content_filter' to the
CompletionResponse['stopReason'] union in types.ts and returning
'content_filter' from mapOpenAIStopReason, or by mapping it to another distinct
value that conveys a policy block), and adjust any consumers/types that rely on
CompletionResponse['stopReason'] accordingly (refer to mapOpenAIStopReason and
CompletionResponse['stopReason'] for the exact places to change).

Comment on lines +378 to +383
success: result.success,
result,
startedAt: task.startedAt!,
completedAt: new Date().toISOString(),
duration: Date.now() - startTime,
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Replace non-null assertions with defensive defaults.

task.startedAt! assumes the field is always set, but the type allows undefined. Use a fallback for robustness.

🔧 Proposed fix
       const taskResult: TaskResult = {
         taskId,
         agentId: task.assignee,
         success: result.success,
         result,
-        startedAt: task.startedAt!,
+        startedAt: task.startedAt ?? new Date().toISOString(),
         completedAt: new Date().toISOString(),
         duration: Date.now() - startTime,
       };

Apply similarly at line 442.

Also applies to: 440-445

🤖 Prompt for AI Agents
In `@packages/agent-intelligence/src/services/orchestrator/orchestrator.ts` around
lines 378 - 383, Replace the non-null assertion on task.startedAt (used when
constructing the task result object with keys like
success/result/startedAt/completedAt/duration) with a defensive fallback value
instead of task.startedAt!; for example use a safe fallback (e.g.,
task.startedAt ?? a sensible default timestamp or null) so the code handles
undefined startedAt, and apply the same change to the other occurrence that
builds a similar object around lines 440–445.

Comment on lines +452 to +456
* Get current state
*/
getState(): OrchestratorState {
return { ...this.state };
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

getState() shallow copy allows mutation of internal Maps.

The spread operator creates a shallow copy, but agents, taskQueue, activeTasks, and completedTasks are references to the internal collections. Callers can mutate the orchestrator's internal state.

🔧 Proposed fix with deep copy
   getState(): OrchestratorState {
-    return { ...this.state };
+    return {
+      ...this.state,
+      agents: new Map(this.state.agents),
+      taskQueue: [...this.state.taskQueue],
+      activeTasks: new Map(this.state.activeTasks),
+      completedTasks: [...this.state.completedTasks],
+    };
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* Get current state
*/
getState(): OrchestratorState {
return { ...this.state };
}
/**
* Get current state
*/
getState(): OrchestratorState {
return {
...this.state,
agents: new Map(this.state.agents),
taskQueue: [...this.state.taskQueue],
activeTasks: new Map(this.state.activeTasks),
completedTasks: [...this.state.completedTasks],
};
}
🤖 Prompt for AI Agents
In `@packages/agent-intelligence/src/services/orchestrator/orchestrator.ts` around
lines 452 - 456, getState() currently returns a shallow copy via the spread
operator so internal collections (agents, taskQueue, activeTasks,
completedTasks) can still be mutated by callers; change getState to return a
deep copy of the OrchestratorState by constructing new containers for each
collection (e.g., new Map(this.state.agents), new Map(this.state.activeTasks),
new Map(this.state.completedTasks), and cloned arrays for taskQueue or other
arrays) and shallow-clone the values inside those containers as needed so
callers cannot mutate the orchestrator's internal Maps or arrays.

@sonarqubecloud
Copy link
Copy Markdown

@jbdevprimary jbdevprimary merged commit 2299d37 into main Jan 18, 2026
37 checks passed
@jbdevprimary jbdevprimary deleted the feat/agent-orchestration-system branch January 18, 2026 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Agent] Build multi-agent orchestration system

1 participant