agents-library 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of agents-library might be problematic. Click here for more details.
- package/dist/base-agent.d.ts +172 -0
- package/dist/base-agent.d.ts.map +1 -0
- package/dist/base-agent.js +255 -0
- package/dist/base-agent.js.map +1 -0
- package/dist/base-bot.d.ts +282 -0
- package/dist/base-bot.d.ts.map +1 -0
- package/dist/base-bot.js +375 -0
- package/dist/base-bot.js.map +1 -0
- package/dist/common/result.d.ts +51 -0
- package/dist/common/result.d.ts.map +1 -0
- package/dist/common/result.js +45 -0
- package/dist/common/result.js.map +1 -0
- package/dist/common/types.d.ts +57 -0
- package/dist/common/types.d.ts.map +1 -0
- package/dist/common/types.js +42 -0
- package/dist/common/types.js.map +1 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +108 -0
- package/dist/index.js.map +1 -0
- package/dist/kadi-event-publisher.d.ts +163 -0
- package/dist/kadi-event-publisher.d.ts.map +1 -0
- package/dist/kadi-event-publisher.js +286 -0
- package/dist/kadi-event-publisher.js.map +1 -0
- package/dist/memory/arcadedb-adapter.d.ts +159 -0
- package/dist/memory/arcadedb-adapter.d.ts.map +1 -0
- package/dist/memory/arcadedb-adapter.js +314 -0
- package/dist/memory/arcadedb-adapter.js.map +1 -0
- package/dist/memory/file-storage-adapter.d.ts +122 -0
- package/dist/memory/file-storage-adapter.d.ts.map +1 -0
- package/dist/memory/file-storage-adapter.js +352 -0
- package/dist/memory/file-storage-adapter.js.map +1 -0
- package/dist/memory/memory-service.d.ts +208 -0
- package/dist/memory/memory-service.d.ts.map +1 -0
- package/dist/memory/memory-service.js +410 -0
- package/dist/memory/memory-service.js.map +1 -0
- package/dist/memory/types.d.ts +126 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +41 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/producer-tool-utils.d.ts +474 -0
- package/dist/producer-tool-utils.d.ts.map +1 -0
- package/dist/producer-tool-utils.js +664 -0
- package/dist/producer-tool-utils.js.map +1 -0
- package/dist/providers/anthropic-provider.d.ts +160 -0
- package/dist/providers/anthropic-provider.d.ts.map +1 -0
- package/dist/providers/anthropic-provider.js +527 -0
- package/dist/providers/anthropic-provider.js.map +1 -0
- package/dist/providers/model-manager-provider.d.ts +91 -0
- package/dist/providers/model-manager-provider.d.ts.map +1 -0
- package/dist/providers/model-manager-provider.js +355 -0
- package/dist/providers/model-manager-provider.js.map +1 -0
- package/dist/providers/provider-manager.d.ts +111 -0
- package/dist/providers/provider-manager.d.ts.map +1 -0
- package/dist/providers/provider-manager.js +337 -0
- package/dist/providers/provider-manager.js.map +1 -0
- package/dist/providers/types.d.ts +145 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +23 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/shadow-agent-factory.d.ts +623 -0
- package/dist/shadow-agent-factory.d.ts.map +1 -0
- package/dist/shadow-agent-factory.js +1117 -0
- package/dist/shadow-agent-factory.js.map +1 -0
- package/dist/types/agent-config.d.ts +307 -0
- package/dist/types/agent-config.d.ts.map +1 -0
- package/dist/types/agent-config.js +15 -0
- package/dist/types/agent-config.js.map +1 -0
- package/dist/types/event-schemas.d.ts +358 -0
- package/dist/types/event-schemas.d.ts.map +1 -0
- package/dist/types/event-schemas.js +188 -0
- package/dist/types/event-schemas.js.map +1 -0
- package/dist/types/tool-schemas.d.ts +498 -0
- package/dist/types/tool-schemas.d.ts.map +1 -0
- package/dist/types/tool-schemas.js +457 -0
- package/dist/types/tool-schemas.js.map +1 -0
- package/dist/utils/logger.d.ts +135 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +205 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/timer.d.ts +186 -0
- package/dist/utils/timer.d.ts.map +1 -0
- package/dist/utils/timer.js +211 -0
- package/dist/utils/timer.js.map +1 -0
- package/dist/worker-agent-factory.d.ts +688 -0
- package/dist/worker-agent-factory.d.ts.map +1 -0
- package/dist/worker-agent-factory.js +1517 -0
- package/dist/worker-agent-factory.js.map +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1,1517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Agent Factory
|
|
3
|
+
* =====================
|
|
4
|
+
*
|
|
5
|
+
* Factory for creating worker agents (artist, designer, programmer) with
|
|
6
|
+
* configuration-driven instantiation and shared infrastructure.
|
|
7
|
+
*
|
|
8
|
+
* Architecture Pattern: **Composition over Inheritance**
|
|
9
|
+
* - BaseWorkerAgent COMPOSES with BaseBot (does NOT extend)
|
|
10
|
+
* - Uses delegation pattern to access BaseBot's circuit breaker and retry logic
|
|
11
|
+
* - This avoids tight coupling and allows flexible behavior customization
|
|
12
|
+
*
|
|
13
|
+
* Design Principles:
|
|
14
|
+
* - Factory pattern for consistent agent creation
|
|
15
|
+
* - Composition over inheritance for flexibility
|
|
16
|
+
* - Strategy pattern for role-specific customization via WorkerBehaviors
|
|
17
|
+
* - Template method pattern for lifecycle management (start/stop)
|
|
18
|
+
*
|
|
19
|
+
* Tool-Calling Agent Loop (Task 3.15):
|
|
20
|
+
* - executeTask sends task + available tools to LLM via ProviderManager
|
|
21
|
+
* - LLM calls tools iteratively (file ops, git ops via MCP)
|
|
22
|
+
* - Loop continues until LLM returns final text response
|
|
23
|
+
* - Git operations use client.invokeRemote() MCP tools, NOT child_process
|
|
24
|
+
*
|
|
25
|
+
* @module worker-agent-factory
|
|
26
|
+
*/
|
|
27
|
+
import { KadiClient, z } from '@kadi.build/core';
|
|
28
|
+
import { BaseBot } from './base-bot.js';
|
|
29
|
+
import { TaskAssignedEventSchema } from './types/event-schemas.js';
|
|
30
|
+
import { logger, MODULE_AGENT } from './utils/logger.js';
|
|
31
|
+
import { timer } from './utils/timer.js';
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Configuration Validation Schema
|
|
34
|
+
// ============================================================================
|
|
35
|
+
/**
|
|
36
|
+
* Zod schema for WorkerAgentConfig runtime validation
|
|
37
|
+
*
|
|
38
|
+
* Validates all required fields for worker agent configuration:
|
|
39
|
+
* - role: Must be 'artist', 'designer', or 'programmer'
|
|
40
|
+
* - worktreePath: Must be non-empty string (absolute path)
|
|
41
|
+
* - brokerUrl: Must be valid WebSocket URL (ws:// or wss://)
|
|
42
|
+
* - networks: Must be non-empty array of network names
|
|
43
|
+
* - anthropicApiKey: Must be non-empty string
|
|
44
|
+
* - claudeModel: Optional string with default value
|
|
45
|
+
* - customBehaviors: Optional object with behavior overrides
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const config = WorkerAgentConfigSchema.parse({
|
|
50
|
+
* role: 'artist',
|
|
51
|
+
* worktreePath: 'C:/p4/Personal/SD/agent-playground-artist',
|
|
52
|
+
* brokerUrl: 'ws://localhost:8080/kadi',
|
|
53
|
+
* networks: ['kadi'],
|
|
54
|
+
* anthropicApiKey: process.env.ANTHROPIC_API_KEY!
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
const WorkerAgentConfigSchema = z.object({
|
|
59
|
+
role: z.enum(['artist', 'designer', 'programmer']),
|
|
60
|
+
worktreePath: z.string().min(1, 'Worktree path is required'),
|
|
61
|
+
brokerUrl: z.string()
|
|
62
|
+
.min(1, 'Broker URL is required')
|
|
63
|
+
.regex(/^wss?:\/\//, 'Broker URL must start with ws:// or wss://'),
|
|
64
|
+
networks: z.array(z.string().min(1, 'Network name cannot be empty'))
|
|
65
|
+
.min(1, 'At least one network is required'),
|
|
66
|
+
anthropicApiKey: z.string().min(1, 'Anthropic API key is required'),
|
|
67
|
+
claudeModel: z.string().optional(),
|
|
68
|
+
capabilities: z.array(z.string()).optional(),
|
|
69
|
+
customBehaviors: z.any().optional() // Use any() for customBehaviors to avoid complex function type inference
|
|
70
|
+
});
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// BaseWorkerAgent Class (Skeleton)
|
|
73
|
+
// ============================================================================
|
|
74
|
+
/**
|
|
75
|
+
* Base class for worker agents (artist, designer, programmer)
|
|
76
|
+
*
|
|
77
|
+
* **COMPOSITION PATTERN**: This class COMPOSES with BaseBot instead of extending it.
|
|
78
|
+
* - Maintains a private `baseBot` instance for circuit breaker and retry logic
|
|
79
|
+
* - Delegates tool invocation to `baseBot.invokeToolWithRetry()`
|
|
80
|
+
* - Keeps agent-specific logic separate from bot resilience patterns
|
|
81
|
+
*
|
|
82
|
+
* Why Composition over Inheritance?
|
|
83
|
+
* 1. **Flexibility**: Can compose multiple utilities (BaseBot, etc.)
|
|
84
|
+
* 2. **Decoupling**: Changes to BaseBot don't force changes to worker agent interface
|
|
85
|
+
* 3. **Single Responsibility**: BaseBot handles resilience, BaseWorkerAgent handles workflow
|
|
86
|
+
* 4. **Testability**: Can mock BaseBot behavior independently
|
|
87
|
+
*
|
|
88
|
+
* Lifecycle:
|
|
89
|
+
* 1. Constructor: Initialize configuration and compose utilities
|
|
90
|
+
* 2. start(): Connect to broker, subscribe to task events, initialize protocol
|
|
91
|
+
* 3. [Task execution happens asynchronously via event handlers]
|
|
92
|
+
* 4. stop(): Cleanup subscriptions, disconnect from broker
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* const config: WorkerAgentConfig = {
|
|
97
|
+
* role: 'artist',
|
|
98
|
+
* worktreePath: 'C:/p4/Personal/SD/agent-playground-artist',
|
|
99
|
+
* brokerUrl: 'ws://localhost:8080/kadi',
|
|
100
|
+
* networks: ['kadi'],
|
|
101
|
+
* anthropicApiKey: process.env.ANTHROPIC_API_KEY!,
|
|
102
|
+
* claudeModel: 'claude-sonnet-4-20250514'
|
|
103
|
+
* };
|
|
104
|
+
*
|
|
105
|
+
* const agent = new BaseWorkerAgent(config);
|
|
106
|
+
* await agent.start();
|
|
107
|
+
* // Agent now listens for {role}.task.assigned events and executes tasks
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export class BaseWorkerAgent {
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// Protected Properties (accessible to subclasses/extensions)
|
|
113
|
+
// ============================================================================
|
|
114
|
+
/**
|
|
115
|
+
* KĀDI client for broker communication
|
|
116
|
+
*
|
|
117
|
+
* Used for:
|
|
118
|
+
* - Subscribing to events
|
|
119
|
+
* - Publishing completion/failure events
|
|
120
|
+
* - Invoking remote MCP tools (git, file management)
|
|
121
|
+
*/
|
|
122
|
+
client;
|
|
123
|
+
/**
|
|
124
|
+
* LLM provider manager for model selection and chat
|
|
125
|
+
*
|
|
126
|
+
* Replaces direct Anthropic SDK usage. Provides:
|
|
127
|
+
* - Model-based routing (claude→Anthropic, gpt→Model Manager)
|
|
128
|
+
* - Automatic fallback on provider failure
|
|
129
|
+
* - Tool-calling via ChatOptions.tools
|
|
130
|
+
*
|
|
131
|
+
* Optional — if null, agent cannot execute tasks requiring LLM.
|
|
132
|
+
*/
|
|
133
|
+
providerManager = null;
|
|
134
|
+
/**
|
|
135
|
+
* Agent role (artist, designer, programmer)
|
|
136
|
+
*
|
|
137
|
+
* Used for:
|
|
138
|
+
* - Event topic filtering (generic task.assigned, role in payload)
|
|
139
|
+
* - Default file extensions
|
|
140
|
+
* - Commit message prefixes
|
|
141
|
+
*/
|
|
142
|
+
role;
|
|
143
|
+
/**
|
|
144
|
+
* Absolute path to git worktree for this agent
|
|
145
|
+
*
|
|
146
|
+
* Agent creates files and commits in this directory.
|
|
147
|
+
* Must be a valid git worktree with initialized repository.
|
|
148
|
+
*
|
|
149
|
+
* @example 'C:/GitHub/agent-playground-artist'
|
|
150
|
+
*/
|
|
151
|
+
worktreePath;
|
|
152
|
+
/**
|
|
153
|
+
* Network(s) this agent belongs to
|
|
154
|
+
*
|
|
155
|
+
* Used for network-based event routing in KĀDI broker.
|
|
156
|
+
* Agent only receives events published to these networks.
|
|
157
|
+
*
|
|
158
|
+
* @example ['kadi']
|
|
159
|
+
* @example ['production', 'team-alpha']
|
|
160
|
+
*/
|
|
161
|
+
networks;
|
|
162
|
+
/**
|
|
163
|
+
* Claude model to use for task execution (from role config or WorkerAgentConfig)
|
|
164
|
+
*
|
|
165
|
+
* @default 'claude-sonnet-4-20250514'
|
|
166
|
+
*/
|
|
167
|
+
claudeModel;
|
|
168
|
+
/**
|
|
169
|
+
* Temperature for LLM requests (from role config)
|
|
170
|
+
*
|
|
171
|
+
* @default undefined (uses provider default)
|
|
172
|
+
*/
|
|
173
|
+
temperature;
|
|
174
|
+
/**
|
|
175
|
+
* Max tokens for LLM responses (from role config)
|
|
176
|
+
*
|
|
177
|
+
* @default undefined (uses provider default)
|
|
178
|
+
*/
|
|
179
|
+
maxTokens;
|
|
180
|
+
/**
|
|
181
|
+
* Commit message format template from role config
|
|
182
|
+
*
|
|
183
|
+
* Supports `{taskId}` placeholder. Used in the system prompt to guide
|
|
184
|
+
* the LLM on commit message formatting.
|
|
185
|
+
*
|
|
186
|
+
* @default 'feat({role}): <description> [{taskId}]'
|
|
187
|
+
*/
|
|
188
|
+
commitFormat;
|
|
189
|
+
/**
|
|
190
|
+
* Agent capabilities for task validation
|
|
191
|
+
*
|
|
192
|
+
* Used to validate incoming tasks before execution.
|
|
193
|
+
* If a task description doesn't match any capabilities, the agent rejects the task.
|
|
194
|
+
*/
|
|
195
|
+
capabilities;
|
|
196
|
+
/**
|
|
197
|
+
* MCP tool prefixes this agent is allowed to invoke (from role config)
|
|
198
|
+
*
|
|
199
|
+
* Controls which remote tools the agent can call via client.invokeRemote().
|
|
200
|
+
* Examples: ['git_git_', 'ability_file_']
|
|
201
|
+
*
|
|
202
|
+
* If empty, no remote tools are available to the agent.
|
|
203
|
+
*/
|
|
204
|
+
toolPrefixes;
|
|
205
|
+
/**
|
|
206
|
+
* Maximum iterations for the tool-calling loop.
|
|
207
|
+
* Prevents infinite loops if Claude keeps calling tools.
|
|
208
|
+
*/
|
|
209
|
+
static MAX_TOOL_LOOP_ITERATIONS = 25;
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// Private Properties (internal use only)
|
|
212
|
+
// ============================================================================
|
|
213
|
+
/**
|
|
214
|
+
* BaseBot instance for circuit breaker and retry logic (COMPOSITION)
|
|
215
|
+
*
|
|
216
|
+
* Used for resilient tool invocation with exponential backoff.
|
|
217
|
+
* Created lazily when anthropicApiKey is available.
|
|
218
|
+
*/
|
|
219
|
+
baseBot = null;
|
|
220
|
+
/**
|
|
221
|
+
* Full agent configuration
|
|
222
|
+
*
|
|
223
|
+
* Stored for reference and potential reconfiguration.
|
|
224
|
+
*/
|
|
225
|
+
config;
|
|
226
|
+
/**
|
|
227
|
+
* Set of task IDs that have been processed or are currently in-flight.
|
|
228
|
+
* Prevents duplicate execution when the same task.assigned event arrives
|
|
229
|
+
* multiple times (e.g., retry re-publish while first execution is still running).
|
|
230
|
+
*/
|
|
231
|
+
processedTaskIds = new Set();
|
|
232
|
+
// ============================================================================
|
|
233
|
+
// Constructor
|
|
234
|
+
// ============================================================================
|
|
235
|
+
/**
|
|
236
|
+
* Create a new BaseWorkerAgent instance
|
|
237
|
+
*
|
|
238
|
+
* Initializes all configuration properties and composes utility classes
|
|
239
|
+
* (BaseBot). Does NOT connect to broker yet - call start()
|
|
240
|
+
* to establish connection.
|
|
241
|
+
*
|
|
242
|
+
* @param config - Worker agent configuration with all required fields
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```typescript
|
|
246
|
+
* const agent = new BaseWorkerAgent({
|
|
247
|
+
* role: 'artist',
|
|
248
|
+
* worktreePath: 'C:/p4/Personal/SD/agent-playground-artist',
|
|
249
|
+
* brokerUrl: 'ws://localhost:8080/kadi',
|
|
250
|
+
* networks: ['kadi'],
|
|
251
|
+
* anthropicApiKey: process.env.ANTHROPIC_API_KEY!,
|
|
252
|
+
* claudeModel: 'claude-sonnet-4-20250514',
|
|
253
|
+
* customBehaviors: {
|
|
254
|
+
* determineFilename: (taskId) => `artwork-${taskId}.png`
|
|
255
|
+
* }
|
|
256
|
+
* });
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
constructor(config) {
|
|
260
|
+
// Start factory timer for lifetime tracking
|
|
261
|
+
timer.start('factory');
|
|
262
|
+
// Store full configuration
|
|
263
|
+
this.config = config;
|
|
264
|
+
// Extract and store individual config properties
|
|
265
|
+
this.role = config.role;
|
|
266
|
+
this.worktreePath = config.worktreePath;
|
|
267
|
+
this.networks = config.networks;
|
|
268
|
+
this.claudeModel = config.claudeModel || 'claude-sonnet-4-20250514';
|
|
269
|
+
this.capabilities = config.capabilities || [];
|
|
270
|
+
this.toolPrefixes = [];
|
|
271
|
+
// Initialize KĀDI client
|
|
272
|
+
this.client = new KadiClient({
|
|
273
|
+
name: `agent-${config.role}`,
|
|
274
|
+
version: '1.0.0',
|
|
275
|
+
brokers: {
|
|
276
|
+
default: { url: config.brokerUrl, networks: config.networks }
|
|
277
|
+
},
|
|
278
|
+
defaultBroker: 'default',
|
|
279
|
+
});
|
|
280
|
+
// COMPOSITION: Create BaseBot instance for circuit breaker and retry logic
|
|
281
|
+
// Only created when anthropicApiKey is available (backward compatibility)
|
|
282
|
+
if (config.anthropicApiKey) {
|
|
283
|
+
const baseBotConfig = {
|
|
284
|
+
client: this.client,
|
|
285
|
+
anthropicApiKey: config.anthropicApiKey,
|
|
286
|
+
botUserId: `agent-${config.role}`
|
|
287
|
+
};
|
|
288
|
+
this.baseBot = new (class extends BaseBot {
|
|
289
|
+
async handleMention(_event) { }
|
|
290
|
+
async start() { }
|
|
291
|
+
stop() { }
|
|
292
|
+
})(baseBotConfig);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// ============================================================================
|
|
296
|
+
// Protected Initialization Methods
|
|
297
|
+
// ============================================================================
|
|
298
|
+
/**
|
|
299
|
+
* Initialize KĀDI client and connect to broker
|
|
300
|
+
*
|
|
301
|
+
* Performs connection sequence with retry logic:
|
|
302
|
+
* 1. Connect KĀDI client to broker (client.serve blocks, so we use setTimeout)
|
|
303
|
+
* 2. Wait for connection to establish (1 second delay)
|
|
304
|
+
* 3. Initialize broker protocol for tool invocation
|
|
305
|
+
* 4. Connect event publisher with retry logic
|
|
306
|
+
*
|
|
307
|
+
* Connection is performed with exponential backoff retry logic inherited from
|
|
308
|
+
* Uses client.publish() for event publishing. If broker is unavailable, events will be queued.
|
|
309
|
+
*
|
|
310
|
+
* @throws {Error} If broker connection fails after all retries
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```typescript
|
|
314
|
+
* await this.initializeClient();
|
|
315
|
+
* // Client is now connected, protocol is initialized, publisher is ready
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
318
|
+
async initializeClient() {
|
|
319
|
+
logger.info(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
320
|
+
logger.info(MODULE_AGENT, '🔌 Initializing KĀDI client...', timer.elapsed('factory'));
|
|
321
|
+
try {
|
|
322
|
+
// Step 1: Start client connection to broker
|
|
323
|
+
logger.info(MODULE_AGENT, ' → Connecting to broker...', timer.elapsed('factory'));
|
|
324
|
+
try {
|
|
325
|
+
await this.client.connect();
|
|
326
|
+
logger.info(MODULE_AGENT, ' ✅ Connected to broker', timer.elapsed('factory'));
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
logger.error(MODULE_AGENT, `Client connection error: ${error.message || String(error)}`, timer.elapsed('factory'), error);
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
// Step 2: Initialize ability response subscription (if BaseBot available)
|
|
333
|
+
if (this.baseBot) {
|
|
334
|
+
logger.info(MODULE_AGENT, ' → Initializing ability response subscription...', timer.elapsed('factory'));
|
|
335
|
+
await this.baseBot['initializeAbilityResponseSubscription']();
|
|
336
|
+
logger.info(MODULE_AGENT, ' ✅ Ability response subscription initialized', timer.elapsed('factory'));
|
|
337
|
+
}
|
|
338
|
+
logger.info(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
339
|
+
logger.info(MODULE_AGENT, '✅ KĀDI client initialized successfully', timer.elapsed('factory'));
|
|
340
|
+
logger.info(MODULE_AGENT, ` Networks: ${this.networks.join(', ')}`, timer.elapsed('factory'));
|
|
341
|
+
logger.info(MODULE_AGENT, ` Protocol: Ready`, timer.elapsed('factory'));
|
|
342
|
+
logger.info(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
343
|
+
// Note: client.serve() continues running in background to handle incoming requests
|
|
344
|
+
// We don't await it because it never resolves (blocks indefinitely)
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
logger.error(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
348
|
+
logger.error(MODULE_AGENT, 'Failed to initialize KĀDI client', timer.elapsed('factory'), error);
|
|
349
|
+
logger.error(MODULE_AGENT, ` Error: ${error.message || String(error)}`, timer.elapsed('factory'));
|
|
350
|
+
logger.error(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Subscribe to task assignment events
|
|
356
|
+
*
|
|
357
|
+
* Subscribes to {role}.task.assigned topic pattern to receive task assignments
|
|
358
|
+
* from agent-producer. Topic is constructed dynamically from config.role to avoid
|
|
359
|
+
* hardcoding and ensure flexibility.
|
|
360
|
+
*
|
|
361
|
+
* Event Flow:
|
|
362
|
+
* 1. agent-producer publishes {role}.task.assigned event
|
|
363
|
+
* 2. Broker routes event to this agent based on network membership
|
|
364
|
+
* 3. handleTaskAssignment callback is invoked with event data
|
|
365
|
+
* 4. Event is validated with Zod schema
|
|
366
|
+
* 5. If valid, task execution begins
|
|
367
|
+
* 6. If invalid, error is logged and event is rejected
|
|
368
|
+
*
|
|
369
|
+
* @throws {Error} If subscription fails (e.g., client not connected)
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* ```typescript
|
|
373
|
+
* await this.subscribeToTaskAssignments();
|
|
374
|
+
* // Agent now listens for task assignments on {role}.task.assigned
|
|
375
|
+
* ```
|
|
376
|
+
*/
|
|
377
|
+
async subscribeToTaskAssignments() {
|
|
378
|
+
// Subscribe to generic task.assigned topic (role filtering done in handler)
|
|
379
|
+
const topic = `task.assigned`;
|
|
380
|
+
logger.info(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
381
|
+
logger.info(MODULE_AGENT, `📡 Subscribing to task assignments...`, timer.elapsed('factory'));
|
|
382
|
+
logger.info(MODULE_AGENT, ` Topic: ${topic}`, timer.elapsed('factory'));
|
|
383
|
+
try {
|
|
384
|
+
// Subscribe to event topic with bound callback
|
|
385
|
+
// Using .bind(this) to preserve instance context in callback
|
|
386
|
+
await this.client.subscribe(topic, this.handleTaskAssignment.bind(this), { broker: 'default' });
|
|
387
|
+
logger.info(MODULE_AGENT, ` ✅ Subscribed successfully`, timer.elapsed('factory'));
|
|
388
|
+
logger.info(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
logger.error(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
392
|
+
logger.error(MODULE_AGENT, `Failed to subscribe to task assignments`, timer.elapsed('factory'), error);
|
|
393
|
+
logger.error(MODULE_AGENT, ` Topic: ${topic}`, timer.elapsed('factory'));
|
|
394
|
+
logger.error(MODULE_AGENT, ` Error: ${error.message || String(error)}`, timer.elapsed('factory'));
|
|
395
|
+
logger.error(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Handle task assignment event
|
|
401
|
+
*
|
|
402
|
+
* Callback invoked when {role}.task.assigned event is received.
|
|
403
|
+
* Performs validation and delegates to task execution.
|
|
404
|
+
*
|
|
405
|
+
* Validation Strategy:
|
|
406
|
+
* - Uses Zod schema for runtime validation (TaskAssignedEventSchema)
|
|
407
|
+
* - Rejects invalid events gracefully without crashing
|
|
408
|
+
* - Logs detailed validation errors for debugging
|
|
409
|
+
* - Maintains backward compatibility with existing event format
|
|
410
|
+
*
|
|
411
|
+
* Error Handling:
|
|
412
|
+
* - Invalid events: Log error, reject event, continue processing
|
|
413
|
+
* - Execution errors: Logged by executeTask method (implemented in next task)
|
|
414
|
+
*
|
|
415
|
+
* @param event - Raw event data from KĀDI broker (may include envelope wrapper)
|
|
416
|
+
*
|
|
417
|
+
* @example
|
|
418
|
+
* ```typescript
|
|
419
|
+
* // Event structure from broker:
|
|
420
|
+
* {
|
|
421
|
+
* data: {
|
|
422
|
+
* taskId: 'task-123',
|
|
423
|
+
* role: 'artist',
|
|
424
|
+
* description: 'Create hero banner',
|
|
425
|
+
* requirements: 'Size: 1920x1080',
|
|
426
|
+
* timestamp: '2025-12-04T10:30:00.000Z'
|
|
427
|
+
* }
|
|
428
|
+
* }
|
|
429
|
+
* ```
|
|
430
|
+
*/
|
|
431
|
+
async handleTaskAssignment(event) {
|
|
432
|
+
try {
|
|
433
|
+
// Extract event data from KĀDI envelope if present
|
|
434
|
+
// Broker may wrap events in { data: {...} } envelope
|
|
435
|
+
const eventData = event?.data || event;
|
|
436
|
+
logger.info(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
437
|
+
logger.info(MODULE_AGENT, '📬 Task assignment received', timer.elapsed('factory'));
|
|
438
|
+
logger.info(MODULE_AGENT, ` Raw event: ${JSON.stringify(eventData).substring(0, 200)}...`, timer.elapsed('factory'));
|
|
439
|
+
// Validate event with Zod schema
|
|
440
|
+
const validatedEvent = TaskAssignedEventSchema.parse(eventData);
|
|
441
|
+
logger.info(MODULE_AGENT, ` ✅ Event validated`, timer.elapsed('factory'));
|
|
442
|
+
logger.info(MODULE_AGENT, ` Task ID: ${validatedEvent.taskId}`, timer.elapsed('factory'));
|
|
443
|
+
logger.info(MODULE_AGENT, ` Role: ${validatedEvent.role}`, timer.elapsed('factory'));
|
|
444
|
+
logger.info(MODULE_AGENT, ` Description: ${validatedEvent.description.substring(0, 80)}${validatedEvent.description.length > 80 ? '...' : ''}`, timer.elapsed('factory'));
|
|
445
|
+
// Check if task is for this agent's role
|
|
446
|
+
if (validatedEvent.role !== this.role) {
|
|
447
|
+
logger.warn(MODULE_AGENT, ` ⚠️ Task role mismatch: expected ${this.role}, got ${validatedEvent.role}`, timer.elapsed('factory'));
|
|
448
|
+
logger.warn(MODULE_AGENT, ` Rejecting task (wrong role)`, timer.elapsed('factory'));
|
|
449
|
+
logger.info(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
// Deduplication: skip tasks already processed or in-flight
|
|
453
|
+
// Allow retries (events with feedback) by clearing the previous entry
|
|
454
|
+
if (this.processedTaskIds.has(validatedEvent.taskId)) {
|
|
455
|
+
if (validatedEvent.feedback) {
|
|
456
|
+
// Retry with feedback — allow re-processing
|
|
457
|
+
logger.info(MODULE_AGENT, ` 🔄 Retry detected for task ${validatedEvent.taskId}, allowing re-execution`, timer.elapsed('factory'));
|
|
458
|
+
this.processedTaskIds.delete(validatedEvent.taskId);
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
// Duplicate without feedback — skip
|
|
462
|
+
logger.warn(MODULE_AGENT, ` ⚠️ Duplicate task.assigned for ${validatedEvent.taskId}, skipping (already processed/in-flight)`, timer.elapsed('factory'));
|
|
463
|
+
logger.info(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// Mark task as in-flight before execution
|
|
468
|
+
this.processedTaskIds.add(validatedEvent.taskId);
|
|
469
|
+
// Capability validation: check if task matches this agent's capabilities
|
|
470
|
+
if (this.capabilities.length > 0) {
|
|
471
|
+
const rejectionReason = this.validateTaskCapability(validatedEvent);
|
|
472
|
+
if (rejectionReason) {
|
|
473
|
+
logger.warn(MODULE_AGENT, ` ⚠️ Task capability mismatch`, timer.elapsed('factory'));
|
|
474
|
+
logger.warn(MODULE_AGENT, ` Reason: ${rejectionReason}`, timer.elapsed('factory'));
|
|
475
|
+
logger.warn(MODULE_AGENT, ` Publishing task.rejected event`, timer.elapsed('factory'));
|
|
476
|
+
await this.publishRejection(validatedEvent.taskId, validatedEvent.questId, rejectionReason);
|
|
477
|
+
logger.info(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
logger.info(MODULE_AGENT, ` ✅ Capability check passed`, timer.elapsed('factory'));
|
|
481
|
+
}
|
|
482
|
+
// Worktree scope validation: soft warning only (worker always operates within its worktree directory)
|
|
483
|
+
const outOfScopeReason = this.validateTaskScope(validatedEvent);
|
|
484
|
+
if (outOfScopeReason) {
|
|
485
|
+
logger.warn(MODULE_AGENT, ` ⚠️ Path reference outside worktree detected (non-blocking)`, timer.elapsed('factory'));
|
|
486
|
+
logger.warn(MODULE_AGENT, ` Note: ${outOfScopeReason}`, timer.elapsed('factory'));
|
|
487
|
+
logger.warn(MODULE_AGENT, ` Proceeding — worker operates within worktree "${this.worktreePath}"`, timer.elapsed('factory'));
|
|
488
|
+
}
|
|
489
|
+
// Log retry context if present
|
|
490
|
+
if (validatedEvent.feedback) {
|
|
491
|
+
logger.info(MODULE_AGENT, ` 🔄 RETRY attempt #${validatedEvent.retryAttempt || 1}`, timer.elapsed('factory'));
|
|
492
|
+
logger.info(MODULE_AGENT, ` Feedback: ${validatedEvent.feedback.substring(0, 120)}${validatedEvent.feedback.length > 120 ? '...' : ''}`, timer.elapsed('factory'));
|
|
493
|
+
}
|
|
494
|
+
// Execute task
|
|
495
|
+
await this.executeTask(validatedEvent);
|
|
496
|
+
logger.info(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
// Validation error or other error - log and reject gracefully
|
|
500
|
+
if (error.name === 'ZodError') {
|
|
501
|
+
// Zod validation error - detailed error logging
|
|
502
|
+
logger.error(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
503
|
+
logger.error(MODULE_AGENT, 'Invalid task assignment event (Zod validation failed)', timer.elapsed('factory'), error);
|
|
504
|
+
logger.error(MODULE_AGENT, ` Validation errors:`, timer.elapsed('factory'));
|
|
505
|
+
// Log each validation issue
|
|
506
|
+
for (const issue of error.issues || []) {
|
|
507
|
+
logger.error(MODULE_AGENT, ` - ${issue.path.join('.')}: ${issue.message}`, timer.elapsed('factory'));
|
|
508
|
+
}
|
|
509
|
+
logger.error(MODULE_AGENT, ` Raw event: ${JSON.stringify(event?.data || event).substring(0, 300)}...`, timer.elapsed('factory'));
|
|
510
|
+
logger.error(MODULE_AGENT, ' Event rejected (invalid format)', timer.elapsed('factory'));
|
|
511
|
+
logger.error(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
// Other error (execution error, etc.)
|
|
515
|
+
logger.error(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
516
|
+
logger.error(MODULE_AGENT, 'Error handling task assignment', timer.elapsed('factory'), error);
|
|
517
|
+
logger.error(MODULE_AGENT, ` Error: ${error.message || String(error)}`, timer.elapsed('factory'));
|
|
518
|
+
logger.error(MODULE_AGENT, ` Stack: ${error.stack || 'No stack trace'}`, timer.elapsed('factory'));
|
|
519
|
+
logger.error(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
520
|
+
}
|
|
521
|
+
// Don't throw - reject event gracefully and continue processing
|
|
522
|
+
// Agent should remain operational even if one event fails
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Execute task using tool-calling agent loop
|
|
527
|
+
*
|
|
528
|
+
* Replaces the old linear pipeline (generate content → write file → git commit)
|
|
529
|
+
* with an iterative tool-calling loop where the LLM decides what tools to use.
|
|
530
|
+
*
|
|
531
|
+
* Flow:
|
|
532
|
+
* 1. Fetch full task details from quest (if questId provided)
|
|
533
|
+
* 2. Set git working directory to worktree via MCP
|
|
534
|
+
* 3. Build system prompt with task context and available tools
|
|
535
|
+
* 4. Enter tool-calling loop:
|
|
536
|
+
* a. Send messages + tools to ProviderManager.chat()
|
|
537
|
+
* b. If response contains __TOOL_CALLS__, execute each tool via invokeRemote
|
|
538
|
+
* c. Feed tool results back as messages
|
|
539
|
+
* d. Repeat until LLM returns plain text (done) or max iterations reached
|
|
540
|
+
* 5. Extract files created/modified and commit SHA from tool call history
|
|
541
|
+
* 6. Publish task.completed event
|
|
542
|
+
*
|
|
543
|
+
* @param task - Validated task assignment event
|
|
544
|
+
*/
|
|
545
|
+
async executeTask(task) {
|
|
546
|
+
logger.info(MODULE_AGENT, `🎨 Processing ${this.role} task: ${task.taskId}`, timer.elapsed('factory'));
|
|
547
|
+
logger.info(MODULE_AGENT, ` Description: ${task.description}`, timer.elapsed('factory'));
|
|
548
|
+
try {
|
|
549
|
+
// Step 1: Get full task details if questId is provided
|
|
550
|
+
let implementationGuide = task.requirements;
|
|
551
|
+
let verificationCriteria = '';
|
|
552
|
+
if (task.questId) {
|
|
553
|
+
logger.info(MODULE_AGENT, `📋 Fetching full task details from quest ${task.questId}...`, timer.elapsed('factory'));
|
|
554
|
+
try {
|
|
555
|
+
const taskDetails = await this.client.invokeRemote('quest_quest_query_task', { taskId: task.taskId });
|
|
556
|
+
const detailsText = taskDetails.content[0].text;
|
|
557
|
+
const details = JSON.parse(detailsText);
|
|
558
|
+
implementationGuide = details.task?.implementationGuide || task.requirements;
|
|
559
|
+
verificationCriteria = details.task?.verificationCriteria || '';
|
|
560
|
+
logger.info(MODULE_AGENT, ` ✅ Task details fetched`, timer.elapsed('factory'));
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
logger.warn(MODULE_AGENT, ` ⚠️ Failed to fetch task details: ${error.message}`, timer.elapsed('factory'));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// Step 2: Set git working directory via MCP tool
|
|
567
|
+
logger.info(MODULE_AGENT, `📂 Setting git working directory: ${this.worktreePath}`, timer.elapsed('factory'));
|
|
568
|
+
try {
|
|
569
|
+
await this.client.invokeRemote('git_git_set_working_dir', { path: this.worktreePath });
|
|
570
|
+
logger.info(MODULE_AGENT, ` ✅ Git working directory set`, timer.elapsed('factory'));
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
logger.warn(MODULE_AGENT, ` ⚠️ Failed to set git working dir: ${error.message}`, timer.elapsed('factory'));
|
|
574
|
+
// Non-fatal — tools can still pass explicit path
|
|
575
|
+
}
|
|
576
|
+
// Step 3: Check if ProviderManager is available
|
|
577
|
+
if (!this.providerManager) {
|
|
578
|
+
throw new Error('ProviderManager not initialized — cannot execute task without LLM');
|
|
579
|
+
}
|
|
580
|
+
// Step 4: Build tool definitions — local tools + dynamic discovery from broker
|
|
581
|
+
const tools = await this.buildToolDefinitionsAsync();
|
|
582
|
+
logger.info(MODULE_AGENT, `🔧 Available tools: ${tools.length} (prefixes: ${this.toolPrefixes.join(', ') || 'none'})`, timer.elapsed('factory'));
|
|
583
|
+
// Step 5: Build initial system prompt
|
|
584
|
+
const systemPrompt = this.buildTaskSystemPrompt(task, implementationGuide, verificationCriteria);
|
|
585
|
+
// Step 6: Enter tool-calling agent loop
|
|
586
|
+
const messages = [
|
|
587
|
+
{ role: 'user', content: systemPrompt }
|
|
588
|
+
];
|
|
589
|
+
const chatOptions = {
|
|
590
|
+
model: this.claudeModel,
|
|
591
|
+
maxTokens: this.maxTokens || 8192,
|
|
592
|
+
temperature: this.temperature,
|
|
593
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
594
|
+
tool_choice: tools.length > 0 ? 'auto' : undefined
|
|
595
|
+
};
|
|
596
|
+
// Track files created/modified and commit SHA across tool calls
|
|
597
|
+
const filesCreated = [];
|
|
598
|
+
const filesModified = [];
|
|
599
|
+
let commitSha = 'unknown';
|
|
600
|
+
let finalResponse = '';
|
|
601
|
+
for (let iteration = 0; iteration < BaseWorkerAgent.MAX_TOOL_LOOP_ITERATIONS; iteration++) {
|
|
602
|
+
logger.info(MODULE_AGENT, `🔄 Agent loop iteration ${iteration + 1}/${BaseWorkerAgent.MAX_TOOL_LOOP_ITERATIONS}`, timer.elapsed('factory'));
|
|
603
|
+
const result = await this.providerManager.chat(messages, chatOptions);
|
|
604
|
+
if (!result.success) {
|
|
605
|
+
throw new Error(`LLM chat failed: ${result.error?.message || 'Unknown error'}`);
|
|
606
|
+
}
|
|
607
|
+
const responseText = result.data;
|
|
608
|
+
// Check if response contains tool calls
|
|
609
|
+
if (responseText.startsWith('__TOOL_CALLS__')) {
|
|
610
|
+
const toolCallsData = JSON.parse(responseText.substring('__TOOL_CALLS__'.length));
|
|
611
|
+
const toolCalls = toolCallsData.tool_calls;
|
|
612
|
+
const assistantMessage = toolCallsData.message || '';
|
|
613
|
+
logger.info(MODULE_AGENT, ` 🤖 LLM requested ${toolCalls.length} tool call(s)`, timer.elapsed('factory'));
|
|
614
|
+
if (assistantMessage) {
|
|
615
|
+
logger.info(MODULE_AGENT, ` 💬 ${assistantMessage.substring(0, 120)}${assistantMessage.length > 120 ? '...' : ''}`, timer.elapsed('factory'));
|
|
616
|
+
}
|
|
617
|
+
// Add assistant message with tool_calls to conversation
|
|
618
|
+
messages.push({
|
|
619
|
+
role: 'assistant',
|
|
620
|
+
content: assistantMessage || null,
|
|
621
|
+
tool_calls: toolCalls
|
|
622
|
+
});
|
|
623
|
+
// Execute each tool call and collect results
|
|
624
|
+
for (const toolCall of toolCalls) {
|
|
625
|
+
const toolName = toolCall.function.name;
|
|
626
|
+
const toolArgs = JSON.parse(toolCall.function.arguments);
|
|
627
|
+
logger.info(MODULE_AGENT, ` 🔧 Executing tool: ${toolName}`, timer.elapsed('factory'));
|
|
628
|
+
try {
|
|
629
|
+
const toolResult = await this.executeRemoteTool(toolName, toolArgs);
|
|
630
|
+
const resultText = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult);
|
|
631
|
+
// Track file operations from tool results
|
|
632
|
+
this.trackFileOperations(toolName, toolArgs, toolResult, filesCreated, filesModified);
|
|
633
|
+
// Track commit SHA from git_commit results
|
|
634
|
+
if (toolName === 'git_git_commit' || toolName === 'git_commit') {
|
|
635
|
+
const sha = this.extractCommitSha(toolResult);
|
|
636
|
+
if (sha)
|
|
637
|
+
commitSha = sha;
|
|
638
|
+
}
|
|
639
|
+
messages.push({
|
|
640
|
+
role: 'tool',
|
|
641
|
+
content: resultText,
|
|
642
|
+
tool_call_id: toolCall.id
|
|
643
|
+
});
|
|
644
|
+
logger.info(MODULE_AGENT, ` ✅ Tool ${toolName} succeeded`, timer.elapsed('factory'));
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
const errorMsg = `Tool ${toolName} failed: ${error.message || String(error)}`;
|
|
648
|
+
logger.error(MODULE_AGENT, ` ❌ ${errorMsg}`, timer.elapsed('factory'));
|
|
649
|
+
messages.push({
|
|
650
|
+
role: 'tool',
|
|
651
|
+
content: JSON.stringify({ error: errorMsg }),
|
|
652
|
+
tool_call_id: toolCall.id
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
// Continue loop — LLM will process tool results
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
// Plain text response — agent is done
|
|
660
|
+
finalResponse = responseText;
|
|
661
|
+
logger.info(MODULE_AGENT, ` ✅ Agent completed (${finalResponse.length} chars response)`, timer.elapsed('factory'));
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// Step 7: Publish completion event
|
|
666
|
+
const contentSummary = finalResponse.length > 500
|
|
667
|
+
? finalResponse.substring(0, 500) + `... (${finalResponse.length} chars total)`
|
|
668
|
+
: finalResponse;
|
|
669
|
+
await this.publishCompletion(task.taskId, task.questId, filesCreated, filesModified, commitSha, contentSummary);
|
|
670
|
+
logger.info(MODULE_AGENT, `✅ Task ${task.taskId} execution completed`, timer.elapsed('factory'));
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
logger.error(MODULE_AGENT, `Failed to execute task ${task.taskId}`, timer.elapsed('factory'), error);
|
|
674
|
+
logger.error(MODULE_AGENT, ` Error: ${error.message || String(error)}`, timer.elapsed('factory'));
|
|
675
|
+
logger.error(MODULE_AGENT, ` Stack: ${error.stack || 'No stack trace'}`, timer.elapsed('factory'));
|
|
676
|
+
await this.publishFailure(task.taskId, error, task.questId);
|
|
677
|
+
throw error;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// ============================================================================
|
|
681
|
+
// Tool-Calling Loop Helpers
|
|
682
|
+
// ============================================================================
|
|
683
|
+
/**
|
|
684
|
+
* Build the system prompt for the tool-calling agent loop
|
|
685
|
+
*
|
|
686
|
+
* Includes task context, role identity, worktree path, and instructions
|
|
687
|
+
* for using available tools (git, file operations).
|
|
688
|
+
*/
|
|
689
|
+
buildTaskSystemPrompt(task, implementationGuide, verificationCriteria) {
|
|
690
|
+
const retryContext = task.feedback
|
|
691
|
+
? `\n⚠️ REVISION REQUIRED (Attempt #${task.retryAttempt || 1})\nPrevious attempt was rejected. Feedback:\n${task.feedback}\nPlease carefully address the feedback above.\n`
|
|
692
|
+
: '';
|
|
693
|
+
return `You are a ${this.role} agent working in the KĀDI multi-agent system.
|
|
694
|
+
Your worktree directory is: ${this.worktreePath}
|
|
695
|
+
|
|
696
|
+
Task ID: ${task.taskId}
|
|
697
|
+
Description: ${task.description}
|
|
698
|
+
Implementation Guide: ${implementationGuide}
|
|
699
|
+
${verificationCriteria ? `Verification Criteria: ${verificationCriteria}` : ''}
|
|
700
|
+
${retryContext}
|
|
701
|
+
|
|
702
|
+
Instructions:
|
|
703
|
+
1. Analyze the task requirements carefully
|
|
704
|
+
2. Create the necessary files in the worktree using available tools
|
|
705
|
+
3. Stage and commit your changes using git tools
|
|
706
|
+
4. When done, provide a brief summary of what you created
|
|
707
|
+
|
|
708
|
+
Important:
|
|
709
|
+
- All file operations must be within the worktree: ${this.worktreePath}
|
|
710
|
+
- Use git_git_add to stage files and git_git_commit to commit
|
|
711
|
+
- If the task description specifies an exact commit message, you MUST use that exact message
|
|
712
|
+
- Default commit message format (use ONLY when no commit message is specified in the task): "${this.commitFormat ? this.commitFormat.replace('{taskId}', task.taskId) : `feat(${this.role}): <description> [${task.taskId}]`}"
|
|
713
|
+
- Focus on ${this.role === 'artist' ? 'creative and artistic elements' : this.role === 'designer' ? 'design principles and aesthetics' : 'code quality and best practices'}`;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Build tool definitions: local tools + dynamic discovery from KĀDI broker
|
|
717
|
+
*
|
|
718
|
+
* 1. Always includes local tools (write_file, read_file)
|
|
719
|
+
* 2. Discovers network tools from broker via kadi.ability.list
|
|
720
|
+
* 3. Filters network tools by toolPrefixes from role config
|
|
721
|
+
* 4. Converts to OpenAI-compatible ToolDefinition format
|
|
722
|
+
*/
|
|
723
|
+
async buildToolDefinitionsAsync() {
|
|
724
|
+
const tools = [];
|
|
725
|
+
// Always include local file tools (not MCP — handled directly in executeRemoteTool)
|
|
726
|
+
tools.push({
|
|
727
|
+
type: 'function',
|
|
728
|
+
function: {
|
|
729
|
+
name: 'write_file',
|
|
730
|
+
description: 'Write content to a file in the worktree. Creates parent directories if needed.',
|
|
731
|
+
parameters: {
|
|
732
|
+
type: 'object',
|
|
733
|
+
properties: {
|
|
734
|
+
path: { type: 'string', description: 'Absolute file path within the worktree' },
|
|
735
|
+
content: { type: 'string', description: 'File content to write' }
|
|
736
|
+
},
|
|
737
|
+
required: ['path', 'content']
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
tools.push({
|
|
742
|
+
type: 'function',
|
|
743
|
+
function: {
|
|
744
|
+
name: 'read_file',
|
|
745
|
+
description: 'Read content from a file in the worktree.',
|
|
746
|
+
parameters: {
|
|
747
|
+
type: 'object',
|
|
748
|
+
properties: {
|
|
749
|
+
path: { type: 'string', description: 'Absolute file path within the worktree' }
|
|
750
|
+
},
|
|
751
|
+
required: ['path']
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
// Discover network tools from broker, filtered by toolPrefixes
|
|
756
|
+
if (this.toolPrefixes.length > 0 && this.client.isConnected()) {
|
|
757
|
+
try {
|
|
758
|
+
const response = await this.client.invokeRemote('kadi.ability.list', { includeProviders: false });
|
|
759
|
+
if (response?.tools && Array.isArray(response.tools)) {
|
|
760
|
+
for (const tool of response.tools) {
|
|
761
|
+
// Only include tools matching one of the configured prefixes
|
|
762
|
+
if (this.toolPrefixes.some(prefix => tool.name.startsWith(prefix))) {
|
|
763
|
+
tools.push({
|
|
764
|
+
type: 'function',
|
|
765
|
+
function: {
|
|
766
|
+
name: tool.name,
|
|
767
|
+
description: tool.description || '',
|
|
768
|
+
parameters: tool.inputSchema || {
|
|
769
|
+
type: 'object',
|
|
770
|
+
properties: {},
|
|
771
|
+
required: []
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
logger.info(MODULE_AGENT, `Discovered ${tools.length - 2} network tools from broker (filtered by prefixes: ${this.toolPrefixes.join(', ')})`, timer.elapsed('factory'));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
catch (error) {
|
|
781
|
+
logger.warn(MODULE_AGENT, `Failed to discover network tools from broker: ${error.message} — falling back to hardcoded definitions`, timer.elapsed('factory'));
|
|
782
|
+
// Fallback: use hardcoded git tools if discovery fails
|
|
783
|
+
this.appendHardcodedGitTools(tools);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
else if (this.toolPrefixes.length > 0) {
|
|
787
|
+
// Not connected to broker — use hardcoded fallback
|
|
788
|
+
logger.warn(MODULE_AGENT, 'Not connected to broker — using hardcoded tool definitions', timer.elapsed('factory'));
|
|
789
|
+
this.appendHardcodedGitTools(tools);
|
|
790
|
+
}
|
|
791
|
+
return tools;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Fallback: append hardcoded git tool definitions when broker discovery fails
|
|
795
|
+
*/
|
|
796
|
+
appendHardcodedGitTools(tools) {
|
|
797
|
+
if (!this.toolPrefixes.some(p => p.startsWith('git_git_')))
|
|
798
|
+
return;
|
|
799
|
+
tools.push({
|
|
800
|
+
type: 'function',
|
|
801
|
+
function: {
|
|
802
|
+
name: 'git_git_status',
|
|
803
|
+
description: 'Show git working tree status',
|
|
804
|
+
parameters: { type: 'object', properties: { path: { type: 'string', description: 'Repository path (defaults to working dir)' } } }
|
|
805
|
+
}
|
|
806
|
+
}, {
|
|
807
|
+
type: 'function',
|
|
808
|
+
function: {
|
|
809
|
+
name: 'git_git_add',
|
|
810
|
+
description: 'Stage files for commit',
|
|
811
|
+
parameters: { type: 'object', properties: { files: { type: 'array', items: { type: 'string' }, description: 'File paths to stage' }, all: { type: 'boolean', description: 'Stage all changes' }, path: { type: 'string', description: 'Repository path' } }, required: ['files'] }
|
|
812
|
+
}
|
|
813
|
+
}, {
|
|
814
|
+
type: 'function',
|
|
815
|
+
function: {
|
|
816
|
+
name: 'git_git_commit',
|
|
817
|
+
description: 'Create a new git commit with staged changes',
|
|
818
|
+
parameters: { type: 'object', properties: { message: { type: 'string', description: 'Commit message' }, path: { type: 'string', description: 'Repository path' } }, required: ['message'] }
|
|
819
|
+
}
|
|
820
|
+
}, {
|
|
821
|
+
type: 'function',
|
|
822
|
+
function: {
|
|
823
|
+
name: 'git_git_log',
|
|
824
|
+
description: 'View recent commit history',
|
|
825
|
+
parameters: { type: 'object', properties: { maxCount: { type: 'number', description: 'Max commits to show' }, path: { type: 'string', description: 'Repository path' } } }
|
|
826
|
+
}
|
|
827
|
+
}, {
|
|
828
|
+
type: 'function',
|
|
829
|
+
function: {
|
|
830
|
+
name: 'git_git_diff',
|
|
831
|
+
description: 'Show changes between commits or working tree',
|
|
832
|
+
parameters: { type: 'object', properties: { path: { type: 'string', description: 'Repository path' } } }
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* @deprecated Use buildToolDefinitionsAsync() instead. Kept for backward compatibility.
|
|
838
|
+
*/
|
|
839
|
+
buildToolDefinitions() {
|
|
840
|
+
const tools = [];
|
|
841
|
+
tools.push({
|
|
842
|
+
type: 'function',
|
|
843
|
+
function: {
|
|
844
|
+
name: 'write_file',
|
|
845
|
+
description: 'Write content to a file in the worktree. Creates parent directories if needed.',
|
|
846
|
+
parameters: {
|
|
847
|
+
type: 'object',
|
|
848
|
+
properties: {
|
|
849
|
+
path: { type: 'string', description: 'Absolute file path within the worktree' },
|
|
850
|
+
content: { type: 'string', description: 'File content to write' }
|
|
851
|
+
},
|
|
852
|
+
required: ['path', 'content']
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
tools.push({
|
|
857
|
+
type: 'function',
|
|
858
|
+
function: {
|
|
859
|
+
name: 'read_file',
|
|
860
|
+
description: 'Read content from a file in the worktree.',
|
|
861
|
+
parameters: {
|
|
862
|
+
type: 'object',
|
|
863
|
+
properties: {
|
|
864
|
+
path: { type: 'string', description: 'Absolute file path within the worktree' }
|
|
865
|
+
},
|
|
866
|
+
required: ['path']
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
this.appendHardcodedGitTools(tools);
|
|
871
|
+
return tools;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Execute a remote MCP tool or local file operation via KĀDI broker
|
|
875
|
+
*
|
|
876
|
+
* Routes tool calls to either local file operations (write_file, read_file)
|
|
877
|
+
* or remote MCP tools via client.invokeRemote().
|
|
878
|
+
*
|
|
879
|
+
* @param toolName - Tool name (e.g., 'git_git_add', 'write_file')
|
|
880
|
+
* @param toolArgs - Tool arguments object
|
|
881
|
+
* @returns Tool execution result
|
|
882
|
+
*/
|
|
883
|
+
async executeRemoteTool(toolName, toolArgs) {
|
|
884
|
+
// Handle local file operations (not MCP)
|
|
885
|
+
if (toolName === 'write_file') {
|
|
886
|
+
const filePath = toolArgs.path;
|
|
887
|
+
// Validate path is within worktree
|
|
888
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
889
|
+
const normalizedWorktree = this.worktreePath.replace(/\\/g, '/');
|
|
890
|
+
if (!normalizedPath.startsWith(normalizedWorktree)) {
|
|
891
|
+
throw new Error(`Path ${filePath} is outside worktree ${this.worktreePath}`);
|
|
892
|
+
}
|
|
893
|
+
const fs = await import('fs/promises');
|
|
894
|
+
const path = await import('path');
|
|
895
|
+
// Ensure parent directory exists
|
|
896
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
897
|
+
await fs.writeFile(filePath, toolArgs.content, 'utf-8');
|
|
898
|
+
return { success: true, path: filePath, bytesWritten: toolArgs.content.length };
|
|
899
|
+
}
|
|
900
|
+
if (toolName === 'read_file') {
|
|
901
|
+
const filePath = toolArgs.path;
|
|
902
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
903
|
+
const normalizedWorktree = this.worktreePath.replace(/\\/g, '/');
|
|
904
|
+
if (!normalizedPath.startsWith(normalizedWorktree)) {
|
|
905
|
+
throw new Error(`Path ${filePath} is outside worktree ${this.worktreePath}`);
|
|
906
|
+
}
|
|
907
|
+
const fs = await import('fs/promises');
|
|
908
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
909
|
+
return { success: true, content };
|
|
910
|
+
}
|
|
911
|
+
// Route to MCP tool via KĀDI broker
|
|
912
|
+
const result = await this.client.invokeRemote(toolName, toolArgs);
|
|
913
|
+
// invokeRemote returns { content: [{ type, text }] } — extract text
|
|
914
|
+
if (result?.content?.[0]?.text) {
|
|
915
|
+
try {
|
|
916
|
+
return JSON.parse(result.content[0].text);
|
|
917
|
+
}
|
|
918
|
+
catch {
|
|
919
|
+
return result.content[0].text;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return result;
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Track file operations from tool call results
|
|
926
|
+
*
|
|
927
|
+
* Inspects tool name and arguments to determine which files were
|
|
928
|
+
* created or modified during the agent loop.
|
|
929
|
+
*/
|
|
930
|
+
trackFileOperations(toolName, toolArgs, _toolResult, filesCreated, filesModified) {
|
|
931
|
+
if (toolName === 'write_file') {
|
|
932
|
+
const filePath = toolArgs.path;
|
|
933
|
+
// Extract relative path from worktree
|
|
934
|
+
const relativePath = filePath.replace(/\\/g, '/').replace(this.worktreePath.replace(/\\/g, '/') + '/', '');
|
|
935
|
+
if (!filesCreated.includes(relativePath) && !filesModified.includes(relativePath)) {
|
|
936
|
+
filesCreated.push(relativePath);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Extract commit SHA from git_commit tool result
|
|
942
|
+
*/
|
|
943
|
+
extractCommitSha(toolResult) {
|
|
944
|
+
if (typeof toolResult === 'string') {
|
|
945
|
+
const match = toolResult.match(/\b([a-f0-9]{7,40})\b/);
|
|
946
|
+
return match ? match[1] : null;
|
|
947
|
+
}
|
|
948
|
+
// Check common property names for commit SHA (including mcp-server-git's "commitHash")
|
|
949
|
+
if (toolResult?.commitHash)
|
|
950
|
+
return toolResult.commitHash;
|
|
951
|
+
if (toolResult?.sha)
|
|
952
|
+
return toolResult.sha;
|
|
953
|
+
if (toolResult?.commit)
|
|
954
|
+
return toolResult.commit;
|
|
955
|
+
if (toolResult?.hash)
|
|
956
|
+
return toolResult.hash;
|
|
957
|
+
// Try to find SHA in stringified result
|
|
958
|
+
const str = JSON.stringify(toolResult);
|
|
959
|
+
const match = str.match(/"(?:commitHash|sha|hash|commit)":\s*"([a-f0-9]{7,40})"/);
|
|
960
|
+
return match ? match[1] : null;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Sanitize filename to remove unsafe characters
|
|
964
|
+
*
|
|
965
|
+
* Utility method kept for subclass use and backward compatibility.
|
|
966
|
+
*
|
|
967
|
+
* @param filename - Raw filename
|
|
968
|
+
* @returns Sanitized filename safe for filesystem
|
|
969
|
+
*/
|
|
970
|
+
sanitizeFilename(filename) {
|
|
971
|
+
return filename.replace(/[^a-zA-Z0-9._\-/]/g, '_');
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Format commit message for git commit
|
|
975
|
+
*
|
|
976
|
+
* Utility method kept for subclass use. The tool-calling loop
|
|
977
|
+
* instructs the LLM to use this format in the system prompt.
|
|
978
|
+
*
|
|
979
|
+
* @param taskId - Task ID to include in commit message
|
|
980
|
+
* @param files - Array of file paths that were modified/created
|
|
981
|
+
* @param taskDescription - Optional task description for context
|
|
982
|
+
* @returns Formatted commit message
|
|
983
|
+
*/
|
|
984
|
+
formatCommitMessage(taskId, files, taskDescription) {
|
|
985
|
+
if (this.config.customBehaviors?.formatCommitMessage) {
|
|
986
|
+
return this.config.customBehaviors.formatCommitMessage(taskId, files);
|
|
987
|
+
}
|
|
988
|
+
if (taskDescription) {
|
|
989
|
+
const commitMsgMatch = taskDescription.match(/(?:commit.*?(?:with\s+)?message|commit\s+message)[\s:]*['"`]([^'"`]+)['"`]/i);
|
|
990
|
+
if (commitMsgMatch) {
|
|
991
|
+
return commitMsgMatch[1];
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return `feat: create ${this.role} for task ${taskId}`;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Publish task completion event
|
|
998
|
+
*
|
|
999
|
+
* Publishes TaskCompletedEvent to KĀDI broker with topic pattern: {role}.task.completed
|
|
1000
|
+
* Event payload matches TaskCompletedEvent schema exactly for backward compatibility.
|
|
1001
|
+
*
|
|
1002
|
+
* Publishing failures are handled gracefully - errors are logged but do not throw.
|
|
1003
|
+
* This ensures task execution completes even if event publishing fails.
|
|
1004
|
+
*
|
|
1005
|
+
* @param taskId - Task ID that completed
|
|
1006
|
+
* @param filesCreated - Array of file paths created during task execution
|
|
1007
|
+
* @param filesModified - Array of file paths modified during task execution
|
|
1008
|
+
* @param commitSha - Git commit SHA of the commit containing task output
|
|
1009
|
+
*
|
|
1010
|
+
* @example
|
|
1011
|
+
* ```typescript
|
|
1012
|
+
* await this.publishCompletion(
|
|
1013
|
+
* 'task-123',
|
|
1014
|
+
* ['artwork.png'],
|
|
1015
|
+
* [],
|
|
1016
|
+
* 'a1b2c3d4e5f6g7h8i9j0'
|
|
1017
|
+
* );
|
|
1018
|
+
* // Publishes to: task.completed
|
|
1019
|
+
* ```
|
|
1020
|
+
*/
|
|
1021
|
+
async publishCompletion(taskId, questId, filesCreated, filesModified, commitSha, contentSummary) {
|
|
1022
|
+
// Include worktree path for independent verification by agent-producer
|
|
1023
|
+
const worktreePath = this.worktreePath;
|
|
1024
|
+
// Generic topic — agent identity is in the payload (agent, role fields)
|
|
1025
|
+
const topic = `task.completed`;
|
|
1026
|
+
// Create payload matching TaskCompletedEvent schema
|
|
1027
|
+
const payload = {
|
|
1028
|
+
taskId,
|
|
1029
|
+
questId,
|
|
1030
|
+
role: this.role,
|
|
1031
|
+
status: 'completed',
|
|
1032
|
+
filesCreated,
|
|
1033
|
+
filesModified,
|
|
1034
|
+
commitSha,
|
|
1035
|
+
timestamp: new Date().toISOString(),
|
|
1036
|
+
agent: `agent-${this.role}`,
|
|
1037
|
+
...(contentSummary ? { contentSummary } : {}),
|
|
1038
|
+
...(worktreePath ? { worktreePath } : {})
|
|
1039
|
+
};
|
|
1040
|
+
try {
|
|
1041
|
+
logger.info(MODULE_AGENT, `📢 Publishing completion event`, timer.elapsed('factory'));
|
|
1042
|
+
logger.info(MODULE_AGENT, ` Topic: ${topic}`, timer.elapsed('factory'));
|
|
1043
|
+
logger.info(MODULE_AGENT, ` Task ID: ${taskId}`, timer.elapsed('factory'));
|
|
1044
|
+
if (questId) {
|
|
1045
|
+
logger.info(MODULE_AGENT, ` Quest ID: ${questId}`, timer.elapsed('factory'));
|
|
1046
|
+
}
|
|
1047
|
+
logger.info(MODULE_AGENT, ` Commit SHA: ${commitSha.substring(0, 7)}`, timer.elapsed('factory'));
|
|
1048
|
+
await this.client.publish(topic, payload, { broker: 'default', network: 'global' });
|
|
1049
|
+
logger.info(MODULE_AGENT, ` ✅ Completion event published`, timer.elapsed('factory'));
|
|
1050
|
+
}
|
|
1051
|
+
catch (error) {
|
|
1052
|
+
// Handle publishing failures gracefully - don't throw
|
|
1053
|
+
logger.error(MODULE_AGENT, `Failed to publish completion event (non-fatal)`, timer.elapsed('factory'), error);
|
|
1054
|
+
logger.error(MODULE_AGENT, ` Error: ${error.message || String(error)}`, timer.elapsed('factory'));
|
|
1055
|
+
logger.error(MODULE_AGENT, ` Task execution succeeded despite event publishing failure`, timer.elapsed('factory'));
|
|
1056
|
+
// Don't throw - event publishing failure should not fail the task
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Publish task failure event
|
|
1061
|
+
*
|
|
1062
|
+
* Publishes TaskFailedEvent to KĀDI broker with topic pattern: {role}.task.failed
|
|
1063
|
+
* Event payload matches TaskFailedEvent schema exactly for backward compatibility.
|
|
1064
|
+
*
|
|
1065
|
+
* Publishing failures are handled gracefully - errors are logged but do not throw.
|
|
1066
|
+
* This prevents cascading failures when event publishing is unavailable.
|
|
1067
|
+
*
|
|
1068
|
+
* @param taskId - Task ID that failed
|
|
1069
|
+
* @param error - Error that caused task failure
|
|
1070
|
+
*
|
|
1071
|
+
* @example
|
|
1072
|
+
* ```typescript
|
|
1073
|
+
* try {
|
|
1074
|
+
* await this.executeTask(task);
|
|
1075
|
+
* } catch (error) {
|
|
1076
|
+
* await this.publishFailure('task-123', error as Error);
|
|
1077
|
+
* // Publishes to: task.failed
|
|
1078
|
+
* }
|
|
1079
|
+
* ```
|
|
1080
|
+
*/
|
|
1081
|
+
async publishFailure(taskId, error, questId) {
|
|
1082
|
+
// Generic topic — agent identity is in the payload (agent, role fields)
|
|
1083
|
+
const topic = `task.failed`;
|
|
1084
|
+
// Create payload matching TaskFailedEvent schema
|
|
1085
|
+
const payload = {
|
|
1086
|
+
taskId,
|
|
1087
|
+
questId: questId || '',
|
|
1088
|
+
role: this.role,
|
|
1089
|
+
error: error.message || String(error),
|
|
1090
|
+
timestamp: new Date().toISOString(),
|
|
1091
|
+
agent: `agent-${this.role}`
|
|
1092
|
+
};
|
|
1093
|
+
try {
|
|
1094
|
+
logger.info(MODULE_AGENT, `📢 Publishing failure event`, timer.elapsed('factory'));
|
|
1095
|
+
logger.info(MODULE_AGENT, ` Topic: ${topic}`, timer.elapsed('factory'));
|
|
1096
|
+
logger.info(MODULE_AGENT, ` Task ID: ${taskId}`, timer.elapsed('factory'));
|
|
1097
|
+
logger.info(MODULE_AGENT, ` Error: ${error.message.substring(0, 100)}${error.message.length > 100 ? '...' : ''}`, timer.elapsed('factory'));
|
|
1098
|
+
await this.client.publish(topic, payload, { broker: 'default', network: 'global' });
|
|
1099
|
+
logger.info(MODULE_AGENT, ` ✅ Failure event published`, timer.elapsed('factory'));
|
|
1100
|
+
}
|
|
1101
|
+
catch (publishError) {
|
|
1102
|
+
// Handle publishing failures gracefully - don't throw
|
|
1103
|
+
logger.error(MODULE_AGENT, `Failed to publish failure event (non-fatal)`, timer.elapsed('factory'), publishError);
|
|
1104
|
+
logger.error(MODULE_AGENT, ` Error: ${publishError.message || String(publishError)}`, timer.elapsed('factory'));
|
|
1105
|
+
// Don't throw - cascading event publishing failure is worse than no event
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Validate whether a task matches this agent's capabilities
|
|
1110
|
+
*
|
|
1111
|
+
* Performs keyword matching between the task description and the agent's
|
|
1112
|
+
* capability list. If no overlap is found, returns a rejection reason.
|
|
1113
|
+
*
|
|
1114
|
+
* @param task - Task assignment event to validate
|
|
1115
|
+
* @returns Rejection reason string if task doesn't match, null if it does
|
|
1116
|
+
*/
|
|
1117
|
+
validateTaskCapability(task) {
|
|
1118
|
+
const taskText = `${task.description} ${task.requirements}`.toLowerCase();
|
|
1119
|
+
// Check if any capability keyword appears in the task description
|
|
1120
|
+
const matchedCapabilities = [];
|
|
1121
|
+
for (const capability of this.capabilities) {
|
|
1122
|
+
const capWords = capability.toLowerCase().split('-');
|
|
1123
|
+
for (const word of capWords) {
|
|
1124
|
+
if (word.length > 2 && taskText.includes(word)) {
|
|
1125
|
+
matchedCapabilities.push(capability);
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
if (matchedCapabilities.length > 0) {
|
|
1131
|
+
logger.info(MODULE_AGENT, ` Matched capabilities: ${matchedCapabilities.join(', ')}`, timer.elapsed('factory'));
|
|
1132
|
+
return null; // Task matches capabilities
|
|
1133
|
+
}
|
|
1134
|
+
// No capability match found — reject
|
|
1135
|
+
return `Task "${task.description.substring(0, 80)}" does not match agent capabilities [${this.capabilities.join(', ')}]. This ${this.role} agent cannot handle this type of work.`;
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Validate that task targets paths within this agent's worktree.
|
|
1139
|
+
*
|
|
1140
|
+
* Extracts absolute file paths from task description and requirements,
|
|
1141
|
+
* then checks if any fall outside the agent's worktree directory.
|
|
1142
|
+
*
|
|
1143
|
+
* @param task - Task assignment event to validate
|
|
1144
|
+
* @returns Rejection reason string if task targets out-of-scope paths, null if OK
|
|
1145
|
+
*/
|
|
1146
|
+
validateTaskScope(task) {
|
|
1147
|
+
const combined = `${task.description || ''} ${task.requirements || ''}`;
|
|
1148
|
+
// Extract absolute file paths (Windows C:\... and Unix /foo/bar with 2+ segments)
|
|
1149
|
+
const pathPattern = /[A-Za-z]:\\[\w\\.\-\s]+|\/[\w.\-]+(?:\/[\w.\-]+)+/g;
|
|
1150
|
+
const paths = combined.match(pathPattern) || [];
|
|
1151
|
+
if (paths.length === 0) {
|
|
1152
|
+
return null; // No absolute paths found — allow execution
|
|
1153
|
+
}
|
|
1154
|
+
const worktreeNormalized = this.worktreePath.replace(/\\/g, '/').replace(/\/+/g, '/').toLowerCase();
|
|
1155
|
+
for (const rawPath of paths) {
|
|
1156
|
+
const normalized = rawPath.replace(/\\/g, '/').replace(/\/+/g, '/').toLowerCase().trimEnd();
|
|
1157
|
+
// Only check absolute paths
|
|
1158
|
+
const isAbsolute = /^[a-z]:\//.test(normalized) || normalized.startsWith('/');
|
|
1159
|
+
if (isAbsolute && !normalized.startsWith(worktreeNormalized)) {
|
|
1160
|
+
return `Task references path "${rawPath.trim()}" which is outside this agent's worktree "${this.worktreePath}". Agent cannot operate on files outside its designated directory.`;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Publish task rejection event
|
|
1167
|
+
*
|
|
1168
|
+
* Publishes TaskRejectedEvent to KĀDI broker with topic: task.rejected
|
|
1169
|
+
* This notifies agent-producer that the task was rejected due to capability mismatch,
|
|
1170
|
+
* allowing it to reassign or escalate to the human.
|
|
1171
|
+
*
|
|
1172
|
+
* @param taskId - Task ID that was rejected
|
|
1173
|
+
* @param questId - Quest ID (optional)
|
|
1174
|
+
* @param reason - Reason for rejection
|
|
1175
|
+
*/
|
|
1176
|
+
async publishRejection(taskId, questId, reason) {
|
|
1177
|
+
const topic = `task.rejected`;
|
|
1178
|
+
const payload = {
|
|
1179
|
+
taskId,
|
|
1180
|
+
questId,
|
|
1181
|
+
role: this.role,
|
|
1182
|
+
reason,
|
|
1183
|
+
timestamp: new Date().toISOString(),
|
|
1184
|
+
agent: `agent-${this.role}`
|
|
1185
|
+
};
|
|
1186
|
+
try {
|
|
1187
|
+
logger.info(MODULE_AGENT, `📢 Publishing rejection event`, timer.elapsed('factory'));
|
|
1188
|
+
logger.info(MODULE_AGENT, ` Topic: ${topic}`, timer.elapsed('factory'));
|
|
1189
|
+
logger.info(MODULE_AGENT, ` Task ID: ${taskId}`, timer.elapsed('factory'));
|
|
1190
|
+
logger.info(MODULE_AGENT, ` Reason: ${reason.substring(0, 100)}${reason.length > 100 ? '...' : ''}`, timer.elapsed('factory'));
|
|
1191
|
+
await this.client.publish(topic, payload, { broker: 'default', network: 'global' });
|
|
1192
|
+
logger.info(MODULE_AGENT, ` ✅ Rejection event published`, timer.elapsed('factory'));
|
|
1193
|
+
}
|
|
1194
|
+
catch (error) {
|
|
1195
|
+
// Handle publishing failures gracefully — don't throw
|
|
1196
|
+
logger.error(MODULE_AGENT, `Failed to publish rejection event (non-fatal)`, timer.elapsed('factory'), error);
|
|
1197
|
+
logger.error(MODULE_AGENT, ` Error: ${error.message || String(error)}`, timer.elapsed('factory'));
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
// ============================================================================
|
|
1201
|
+
// Lifecycle Methods (Public API)
|
|
1202
|
+
// ============================================================================
|
|
1203
|
+
/**
|
|
1204
|
+
* Start the worker agent
|
|
1205
|
+
*
|
|
1206
|
+
* Performs initialization sequence:
|
|
1207
|
+
* 1. Connect to KĀDI broker
|
|
1208
|
+
* 2. Initialize broker protocol
|
|
1209
|
+
* 3. Connect event publisher
|
|
1210
|
+
* 4. Subscribe to {role}.task.assigned events
|
|
1211
|
+
* 5. Enter event loop (non-blocking)
|
|
1212
|
+
*
|
|
1213
|
+
* After start() completes, the agent is ready to receive task assignments.
|
|
1214
|
+
*
|
|
1215
|
+
* @throws {Error} If broker connection fails after all retries
|
|
1216
|
+
*
|
|
1217
|
+
* @example
|
|
1218
|
+
* ```typescript
|
|
1219
|
+
* const agent = new BaseWorkerAgent(config);
|
|
1220
|
+
* await agent.start();
|
|
1221
|
+
* console.log('Agent is now listening for task assignments');
|
|
1222
|
+
* ```
|
|
1223
|
+
*/
|
|
1224
|
+
async start() {
|
|
1225
|
+
logger.info(MODULE_AGENT, '='.repeat(60), timer.elapsed('factory'));
|
|
1226
|
+
logger.info(MODULE_AGENT, `Starting Worker Agent: ${this.role}`, timer.elapsed('factory'));
|
|
1227
|
+
logger.info(MODULE_AGENT, '='.repeat(60), timer.elapsed('factory'));
|
|
1228
|
+
logger.info(MODULE_AGENT, `Broker URL: ${this.config.brokerUrl}`, timer.elapsed('factory'));
|
|
1229
|
+
logger.info(MODULE_AGENT, `Networks: ${this.networks.join(', ')}`, timer.elapsed('factory'));
|
|
1230
|
+
logger.info(MODULE_AGENT, `Worktree Path: ${this.worktreePath}`, timer.elapsed('factory'));
|
|
1231
|
+
logger.info(MODULE_AGENT, `LLM Model: ${this.claudeModel}`, timer.elapsed('factory'));
|
|
1232
|
+
logger.info(MODULE_AGENT, '='.repeat(60), timer.elapsed('factory'));
|
|
1233
|
+
// Initialize KĀDI client and connect to broker
|
|
1234
|
+
await this.initializeClient();
|
|
1235
|
+
// Subscribe to task assignment events
|
|
1236
|
+
await this.subscribeToTaskAssignments();
|
|
1237
|
+
// TODO: Implement remaining start logic in next tasks
|
|
1238
|
+
// 1. Register tools (if any)
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Stop the worker agent
|
|
1242
|
+
*
|
|
1243
|
+
* Performs cleanup sequence:
|
|
1244
|
+
* 1. Unsubscribe from all events (TODO: next task)
|
|
1245
|
+
* 2. Disconnect event publisher
|
|
1246
|
+
* 3. Disconnect KĀDI client
|
|
1247
|
+
* 4. Clear protocol reference
|
|
1248
|
+
*
|
|
1249
|
+
* After stop() completes, the agent is fully shut down and can be safely destroyed.
|
|
1250
|
+
*
|
|
1251
|
+
* @example
|
|
1252
|
+
* ```typescript
|
|
1253
|
+
* await agent.stop();
|
|
1254
|
+
* console.log('Agent has been stopped');
|
|
1255
|
+
* ```
|
|
1256
|
+
*/
|
|
1257
|
+
async stop() {
|
|
1258
|
+
logger.info(MODULE_AGENT, '='.repeat(60), timer.elapsed('factory'));
|
|
1259
|
+
logger.info(MODULE_AGENT, `Stopping Worker Agent: ${this.role}`, timer.elapsed('factory'));
|
|
1260
|
+
logger.info(MODULE_AGENT, '='.repeat(60), timer.elapsed('factory'));
|
|
1261
|
+
try {
|
|
1262
|
+
// TODO: Unsubscribe from events in next task
|
|
1263
|
+
// Disconnect KĀDI client
|
|
1264
|
+
logger.info(MODULE_AGENT, ' → Disconnecting KĀDI client...', timer.elapsed('factory'));
|
|
1265
|
+
await this.client.disconnect();
|
|
1266
|
+
logger.info(MODULE_AGENT, ' ✅ KĀDI client disconnected', timer.elapsed('factory'));
|
|
1267
|
+
// Clear references
|
|
1268
|
+
this.providerManager = null;
|
|
1269
|
+
logger.info(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
1270
|
+
logger.info(MODULE_AGENT, '✅ Worker agent stopped successfully', timer.elapsed('factory'));
|
|
1271
|
+
logger.info(MODULE_AGENT, '='.repeat(60), timer.elapsed('factory'));
|
|
1272
|
+
}
|
|
1273
|
+
catch (error) {
|
|
1274
|
+
logger.error(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
1275
|
+
logger.error(MODULE_AGENT, 'Error during shutdown', timer.elapsed('factory'), error);
|
|
1276
|
+
logger.error(MODULE_AGENT, ` Error: ${error.message || String(error)}`, timer.elapsed('factory'));
|
|
1277
|
+
logger.error(MODULE_AGENT, '', timer.elapsed('factory'));
|
|
1278
|
+
// Don't throw - best effort cleanup
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
// ============================================================================
|
|
1282
|
+
// Protected Helper Methods (for subclass/extension use)
|
|
1283
|
+
// ============================================================================
|
|
1284
|
+
/**
|
|
1285
|
+
* Invoke tool with retry logic using composed BaseBot
|
|
1286
|
+
*
|
|
1287
|
+
* Delegates to BaseBot's invokeToolWithRetry() method for resilient tool invocation.
|
|
1288
|
+
* This demonstrates the composition pattern - we use BaseBot's functionality
|
|
1289
|
+
* without inheriting from it.
|
|
1290
|
+
*
|
|
1291
|
+
* @param params - Tool invocation parameters
|
|
1292
|
+
* @returns Tool invocation result
|
|
1293
|
+
*
|
|
1294
|
+
* @example
|
|
1295
|
+
* ```typescript
|
|
1296
|
+
* const result = await this.invokeToolWithRetry({
|
|
1297
|
+
* targetAgent: 'mcp-server-git',
|
|
1298
|
+
* toolName: 'git_commit',
|
|
1299
|
+
* toolInput: { message: 'feat: create artwork', files: ['artwork.png'] },
|
|
1300
|
+
* timeout: 30000
|
|
1301
|
+
* });
|
|
1302
|
+
* ```
|
|
1303
|
+
*/
|
|
1304
|
+
async invokeToolWithRetry(params) {
|
|
1305
|
+
if (!this.baseBot) {
|
|
1306
|
+
// Fallback: direct invocation without retry
|
|
1307
|
+
return this.client.invokeRemote(params.toolName, params.toolInput);
|
|
1308
|
+
}
|
|
1309
|
+
return this.baseBot['invokeToolWithRetry'](params);
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Check circuit breaker state using composed BaseBot
|
|
1313
|
+
*
|
|
1314
|
+
* @returns True if circuit is open, false if closed (or no BaseBot)
|
|
1315
|
+
*/
|
|
1316
|
+
checkCircuitBreaker() {
|
|
1317
|
+
if (!this.baseBot)
|
|
1318
|
+
return false;
|
|
1319
|
+
return this.baseBot['checkCircuitBreaker']();
|
|
1320
|
+
}
|
|
1321
|
+
// ============================================================================
|
|
1322
|
+
// Public Setters (for dependency injection)
|
|
1323
|
+
// ============================================================================
|
|
1324
|
+
/**
|
|
1325
|
+
* Set the ProviderManager for LLM chat
|
|
1326
|
+
*
|
|
1327
|
+
* Called by the agent entry point after constructing BaseAgent
|
|
1328
|
+
* (which creates the ProviderManager).
|
|
1329
|
+
*
|
|
1330
|
+
* @param pm - ProviderManager instance from BaseAgent
|
|
1331
|
+
*/
|
|
1332
|
+
setProviderManager(pm) {
|
|
1333
|
+
this.providerManager = pm;
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Configure role-specific settings from a loaded RoleConfig
|
|
1337
|
+
*
|
|
1338
|
+
* @param roleConfig - Parsed role configuration
|
|
1339
|
+
*/
|
|
1340
|
+
applyRoleConfig(roleConfig) {
|
|
1341
|
+
if (roleConfig.capabilities)
|
|
1342
|
+
this.capabilities = roleConfig.capabilities;
|
|
1343
|
+
if (roleConfig.tools)
|
|
1344
|
+
this.toolPrefixes = roleConfig.tools;
|
|
1345
|
+
if (roleConfig.provider?.model)
|
|
1346
|
+
this.claudeModel = roleConfig.provider.model;
|
|
1347
|
+
if (roleConfig.provider?.temperature !== undefined)
|
|
1348
|
+
this.temperature = roleConfig.provider.temperature;
|
|
1349
|
+
if (roleConfig.provider?.maxTokens !== undefined)
|
|
1350
|
+
this.maxTokens = roleConfig.provider.maxTokens;
|
|
1351
|
+
if (roleConfig.commitFormat)
|
|
1352
|
+
this.commitFormat = roleConfig.commitFormat;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
// ============================================================================
|
|
1356
|
+
// Factory Function (to be implemented)
|
|
1357
|
+
// ============================================================================
|
|
1358
|
+
// ============================================================================
|
|
1359
|
+
// WorkerAgentFactory (Public API)
|
|
1360
|
+
// ============================================================================
|
|
1361
|
+
/**
|
|
1362
|
+
* Factory class for creating worker agents with validated configuration
|
|
1363
|
+
*
|
|
1364
|
+
* Provides static factory method for instantiating worker agents with:
|
|
1365
|
+
* - Runtime configuration validation using Zod schemas
|
|
1366
|
+
* - Descriptive error messages for invalid configurations
|
|
1367
|
+
* - Type-safe agent creation with compile-time checks
|
|
1368
|
+
* - Clean separation between validation and instantiation
|
|
1369
|
+
*
|
|
1370
|
+
* The factory validates all required configuration fields before creating
|
|
1371
|
+
* the agent instance, ensuring early failure with clear error messages
|
|
1372
|
+
* rather than runtime errors during agent execution.
|
|
1373
|
+
*
|
|
1374
|
+
* @example
|
|
1375
|
+
* ```typescript
|
|
1376
|
+
* // Minimal agent creation (10-15 lines)
|
|
1377
|
+
* import { WorkerAgentFactory } from 'agents-library';
|
|
1378
|
+
*
|
|
1379
|
+
* const agent = WorkerAgentFactory.createAgent({
|
|
1380
|
+
* role: 'artist',
|
|
1381
|
+
* worktreePath: 'C:/p4/Personal/SD/agent-playground-artist',
|
|
1382
|
+
* brokerUrl: 'ws://localhost:8080/kadi',
|
|
1383
|
+
* networks: ['kadi'],
|
|
1384
|
+
* anthropicApiKey: process.env.ANTHROPIC_API_KEY!
|
|
1385
|
+
* });
|
|
1386
|
+
*
|
|
1387
|
+
* await agent.start();
|
|
1388
|
+
* // Agent now listens for artist.task.assigned events
|
|
1389
|
+
* ```
|
|
1390
|
+
*
|
|
1391
|
+
* @example
|
|
1392
|
+
* ```typescript
|
|
1393
|
+
* // With custom behaviors (under 50 lines)
|
|
1394
|
+
* import { WorkerAgentFactory } from 'agents-library';
|
|
1395
|
+
*
|
|
1396
|
+
* const agent = WorkerAgentFactory.createAgent({
|
|
1397
|
+
* role: 'artist',
|
|
1398
|
+
* worktreePath: 'C:/p4/Personal/SD/agent-playground-artist',
|
|
1399
|
+
* brokerUrl: 'ws://localhost:8080/kadi',
|
|
1400
|
+
* networks: ['kadi'],
|
|
1401
|
+
* anthropicApiKey: process.env.ANTHROPIC_API_KEY!,
|
|
1402
|
+
* claudeModel: 'claude-sonnet-4-20250514',
|
|
1403
|
+
* customBehaviors: {
|
|
1404
|
+
* determineFilename: (taskId, description) => {
|
|
1405
|
+
* // Custom filename logic
|
|
1406
|
+
* return `artwork-${taskId}.png`;
|
|
1407
|
+
* },
|
|
1408
|
+
* formatCommitMessage: (taskId, files) => {
|
|
1409
|
+
* // Custom commit message format
|
|
1410
|
+
* return `feat: create artwork for ${taskId}`;
|
|
1411
|
+
* }
|
|
1412
|
+
* }
|
|
1413
|
+
* });
|
|
1414
|
+
*
|
|
1415
|
+
* await agent.start();
|
|
1416
|
+
* console.log('Agent started successfully');
|
|
1417
|
+
* ```
|
|
1418
|
+
*/
|
|
1419
|
+
export class WorkerAgentFactory {
|
|
1420
|
+
/**
|
|
1421
|
+
* Create a worker agent with validated configuration
|
|
1422
|
+
*
|
|
1423
|
+
* Static factory method that validates configuration and creates a
|
|
1424
|
+
* BaseWorkerAgent instance ready for start().
|
|
1425
|
+
*
|
|
1426
|
+
* Validation performed:
|
|
1427
|
+
* - Role must be 'artist', 'designer', or 'programmer'
|
|
1428
|
+
* - Worktree path must be non-empty string
|
|
1429
|
+
* - Broker URL must start with ws:// or wss://
|
|
1430
|
+
* - Networks array must contain at least one network name
|
|
1431
|
+
* - Anthropic API key must be provided
|
|
1432
|
+
* - Custom behaviors (if provided) must match expected signatures
|
|
1433
|
+
*
|
|
1434
|
+
* @param config - Worker agent configuration object
|
|
1435
|
+
* @returns Configured BaseWorkerAgent instance (not started)
|
|
1436
|
+
*
|
|
1437
|
+
* @throws {z.ZodError} If configuration validation fails with detailed error messages
|
|
1438
|
+
*
|
|
1439
|
+
* @example
|
|
1440
|
+
* ```typescript
|
|
1441
|
+
* // Basic usage
|
|
1442
|
+
* const agent = WorkerAgentFactory.createAgent({
|
|
1443
|
+
* role: 'artist',
|
|
1444
|
+
* worktreePath: 'C:/p4/Personal/SD/agent-playground-artist',
|
|
1445
|
+
* brokerUrl: 'ws://localhost:8080/kadi',
|
|
1446
|
+
* networks: ['kadi'],
|
|
1447
|
+
* anthropicApiKey: process.env.ANTHROPIC_API_KEY!
|
|
1448
|
+
* });
|
|
1449
|
+
*
|
|
1450
|
+
* // Agent is created but not started - caller must call start()
|
|
1451
|
+
* await agent.start();
|
|
1452
|
+
* ```
|
|
1453
|
+
*
|
|
1454
|
+
* @example
|
|
1455
|
+
* ```typescript
|
|
1456
|
+
* // Error handling
|
|
1457
|
+
* try {
|
|
1458
|
+
* const agent = WorkerAgentFactory.createAgent(config);
|
|
1459
|
+
* await agent.start();
|
|
1460
|
+
* } catch (error) {
|
|
1461
|
+
* if (error instanceof z.ZodError) {
|
|
1462
|
+
* console.error('Configuration validation failed:');
|
|
1463
|
+
* error.issues.forEach(issue => {
|
|
1464
|
+
* console.error(` - ${issue.path.join('.')}: ${issue.message}`);
|
|
1465
|
+
* });
|
|
1466
|
+
* } else {
|
|
1467
|
+
* console.error('Agent startup failed:', error);
|
|
1468
|
+
* }
|
|
1469
|
+
* }
|
|
1470
|
+
* ```
|
|
1471
|
+
*/
|
|
1472
|
+
static createAgent(config) {
|
|
1473
|
+
try {
|
|
1474
|
+
// Validate configuration with Zod schema
|
|
1475
|
+
const validatedConfig = WorkerAgentConfigSchema.parse(config);
|
|
1476
|
+
// Create and return BaseWorkerAgent instance
|
|
1477
|
+
// Agent is not started automatically - caller must call start()
|
|
1478
|
+
return new BaseWorkerAgent(validatedConfig);
|
|
1479
|
+
}
|
|
1480
|
+
catch (error) {
|
|
1481
|
+
// Re-throw Zod validation errors with context
|
|
1482
|
+
if (error.name === 'ZodError') {
|
|
1483
|
+
logger.error(MODULE_AGENT, 'Worker agent configuration validation failed', timer.elapsed('factory'), error);
|
|
1484
|
+
logger.error(MODULE_AGENT, ' Validation errors:', timer.elapsed('factory'));
|
|
1485
|
+
for (const issue of error.issues || []) {
|
|
1486
|
+
logger.error(MODULE_AGENT, ` - ${issue.path.join('.')}: ${issue.message}`, timer.elapsed('factory'));
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
throw error;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Create a worker agent with configuration
|
|
1495
|
+
*
|
|
1496
|
+
* Convenience function that delegates to WorkerAgentFactory.createAgent().
|
|
1497
|
+
* Provides backward compatibility with existing code.
|
|
1498
|
+
*
|
|
1499
|
+
* @param config - Worker agent configuration
|
|
1500
|
+
* @returns Configured BaseWorkerAgent instance
|
|
1501
|
+
*
|
|
1502
|
+
* @example
|
|
1503
|
+
* ```typescript
|
|
1504
|
+
* const agent = createWorkerAgent({
|
|
1505
|
+
* role: 'artist',
|
|
1506
|
+
* worktreePath: 'C:/p4/Personal/SD/agent-playground-artist',
|
|
1507
|
+
* brokerUrl: 'ws://localhost:8080/kadi',
|
|
1508
|
+
* networks: ['kadi'],
|
|
1509
|
+
* anthropicApiKey: process.env.ANTHROPIC_API_KEY!
|
|
1510
|
+
* });
|
|
1511
|
+
* await agent.start();
|
|
1512
|
+
* ```
|
|
1513
|
+
*/
|
|
1514
|
+
export function createWorkerAgent(config) {
|
|
1515
|
+
return WorkerAgentFactory.createAgent(config);
|
|
1516
|
+
}
|
|
1517
|
+
//# sourceMappingURL=worker-agent-factory.js.map
|