agents-library 0.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.
Files changed (89) hide show
  1. package/dist/base-agent.d.ts +172 -0
  2. package/dist/base-agent.d.ts.map +1 -0
  3. package/dist/base-agent.js +255 -0
  4. package/dist/base-agent.js.map +1 -0
  5. package/dist/base-bot.d.ts +282 -0
  6. package/dist/base-bot.d.ts.map +1 -0
  7. package/dist/base-bot.js +375 -0
  8. package/dist/base-bot.js.map +1 -0
  9. package/dist/common/result.d.ts +51 -0
  10. package/dist/common/result.d.ts.map +1 -0
  11. package/dist/common/result.js +45 -0
  12. package/dist/common/result.js.map +1 -0
  13. package/dist/common/types.d.ts +57 -0
  14. package/dist/common/types.d.ts.map +1 -0
  15. package/dist/common/types.js +42 -0
  16. package/dist/common/types.js.map +1 -0
  17. package/dist/index.d.ts +94 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +108 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/kadi-event-publisher.d.ts +163 -0
  22. package/dist/kadi-event-publisher.d.ts.map +1 -0
  23. package/dist/kadi-event-publisher.js +286 -0
  24. package/dist/kadi-event-publisher.js.map +1 -0
  25. package/dist/memory/arcadedb-adapter.d.ts +159 -0
  26. package/dist/memory/arcadedb-adapter.d.ts.map +1 -0
  27. package/dist/memory/arcadedb-adapter.js +314 -0
  28. package/dist/memory/arcadedb-adapter.js.map +1 -0
  29. package/dist/memory/file-storage-adapter.d.ts +122 -0
  30. package/dist/memory/file-storage-adapter.d.ts.map +1 -0
  31. package/dist/memory/file-storage-adapter.js +352 -0
  32. package/dist/memory/file-storage-adapter.js.map +1 -0
  33. package/dist/memory/memory-service.d.ts +208 -0
  34. package/dist/memory/memory-service.d.ts.map +1 -0
  35. package/dist/memory/memory-service.js +410 -0
  36. package/dist/memory/memory-service.js.map +1 -0
  37. package/dist/memory/types.d.ts +126 -0
  38. package/dist/memory/types.d.ts.map +1 -0
  39. package/dist/memory/types.js +41 -0
  40. package/dist/memory/types.js.map +1 -0
  41. package/dist/producer-tool-utils.d.ts +474 -0
  42. package/dist/producer-tool-utils.d.ts.map +1 -0
  43. package/dist/producer-tool-utils.js +664 -0
  44. package/dist/producer-tool-utils.js.map +1 -0
  45. package/dist/providers/anthropic-provider.d.ts +160 -0
  46. package/dist/providers/anthropic-provider.d.ts.map +1 -0
  47. package/dist/providers/anthropic-provider.js +527 -0
  48. package/dist/providers/anthropic-provider.js.map +1 -0
  49. package/dist/providers/model-manager-provider.d.ts +91 -0
  50. package/dist/providers/model-manager-provider.d.ts.map +1 -0
  51. package/dist/providers/model-manager-provider.js +355 -0
  52. package/dist/providers/model-manager-provider.js.map +1 -0
  53. package/dist/providers/provider-manager.d.ts +111 -0
  54. package/dist/providers/provider-manager.d.ts.map +1 -0
  55. package/dist/providers/provider-manager.js +337 -0
  56. package/dist/providers/provider-manager.js.map +1 -0
  57. package/dist/providers/types.d.ts +145 -0
  58. package/dist/providers/types.d.ts.map +1 -0
  59. package/dist/providers/types.js +23 -0
  60. package/dist/providers/types.js.map +1 -0
  61. package/dist/shadow-agent-factory.d.ts +623 -0
  62. package/dist/shadow-agent-factory.d.ts.map +1 -0
  63. package/dist/shadow-agent-factory.js +1117 -0
  64. package/dist/shadow-agent-factory.js.map +1 -0
  65. package/dist/types/agent-config.d.ts +307 -0
  66. package/dist/types/agent-config.d.ts.map +1 -0
  67. package/dist/types/agent-config.js +15 -0
  68. package/dist/types/agent-config.js.map +1 -0
  69. package/dist/types/event-schemas.d.ts +358 -0
  70. package/dist/types/event-schemas.d.ts.map +1 -0
  71. package/dist/types/event-schemas.js +188 -0
  72. package/dist/types/event-schemas.js.map +1 -0
  73. package/dist/types/tool-schemas.d.ts +498 -0
  74. package/dist/types/tool-schemas.d.ts.map +1 -0
  75. package/dist/types/tool-schemas.js +457 -0
  76. package/dist/types/tool-schemas.js.map +1 -0
  77. package/dist/utils/logger.d.ts +135 -0
  78. package/dist/utils/logger.d.ts.map +1 -0
  79. package/dist/utils/logger.js +205 -0
  80. package/dist/utils/logger.js.map +1 -0
  81. package/dist/utils/timer.d.ts +186 -0
  82. package/dist/utils/timer.d.ts.map +1 -0
  83. package/dist/utils/timer.js +211 -0
  84. package/dist/utils/timer.js.map +1 -0
  85. package/dist/worker-agent-factory.d.ts +688 -0
  86. package/dist/worker-agent-factory.d.ts.map +1 -0
  87. package/dist/worker-agent-factory.js +1517 -0
  88. package/dist/worker-agent-factory.js.map +1 -0
  89. 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