@flomatai/core 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 (127) hide show
  1. package/dist/agent.d.ts +92 -0
  2. package/dist/agent.d.ts.map +1 -0
  3. package/dist/agent.js +137 -0
  4. package/dist/agent.js.map +1 -0
  5. package/dist/cli-utils.d.ts +41 -0
  6. package/dist/cli-utils.d.ts.map +1 -0
  7. package/dist/cli-utils.js +64 -0
  8. package/dist/cli-utils.js.map +1 -0
  9. package/dist/errors.d.ts +52 -0
  10. package/dist/errors.d.ts.map +1 -0
  11. package/dist/errors.js +105 -0
  12. package/dist/errors.js.map +1 -0
  13. package/dist/index.d.ts +29 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +35 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/llm-provider.d.ts +29 -0
  18. package/dist/llm-provider.d.ts.map +1 -0
  19. package/dist/llm-provider.js +44 -0
  20. package/dist/llm-provider.js.map +1 -0
  21. package/dist/logger.d.ts +32 -0
  22. package/dist/logger.d.ts.map +1 -0
  23. package/dist/logger.js +75 -0
  24. package/dist/logger.js.map +1 -0
  25. package/dist/mock-llm.d.ts +70 -0
  26. package/dist/mock-llm.d.ts.map +1 -0
  27. package/dist/mock-llm.js +385 -0
  28. package/dist/mock-llm.js.map +1 -0
  29. package/dist/orchestrator-helpers.d.ts +20 -0
  30. package/dist/orchestrator-helpers.d.ts.map +1 -0
  31. package/dist/orchestrator-helpers.js +38 -0
  32. package/dist/orchestrator-helpers.js.map +1 -0
  33. package/dist/orchestrator.d.ts +124 -0
  34. package/dist/orchestrator.d.ts.map +1 -0
  35. package/dist/orchestrator.js +349 -0
  36. package/dist/orchestrator.js.map +1 -0
  37. package/dist/pipeline-registry.d.ts +120 -0
  38. package/dist/pipeline-registry.d.ts.map +1 -0
  39. package/dist/pipeline-registry.js +171 -0
  40. package/dist/pipeline-registry.js.map +1 -0
  41. package/dist/pipeline.d.ts +122 -0
  42. package/dist/pipeline.d.ts.map +1 -0
  43. package/dist/pipeline.js +152 -0
  44. package/dist/pipeline.js.map +1 -0
  45. package/dist/skill.d.ts +112 -0
  46. package/dist/skill.d.ts.map +1 -0
  47. package/dist/skill.js +12 -0
  48. package/dist/skill.js.map +1 -0
  49. package/dist/skills/io-skill.d.ts +49 -0
  50. package/dist/skills/io-skill.d.ts.map +1 -0
  51. package/dist/skills/io-skill.js +103 -0
  52. package/dist/skills/io-skill.js.map +1 -0
  53. package/dist/skills/llm-skill.d.ts +64 -0
  54. package/dist/skills/llm-skill.d.ts.map +1 -0
  55. package/dist/skills/llm-skill.js +112 -0
  56. package/dist/skills/llm-skill.js.map +1 -0
  57. package/dist/skills/transform-skill.d.ts +27 -0
  58. package/dist/skills/transform-skill.d.ts.map +1 -0
  59. package/dist/skills/transform-skill.js +32 -0
  60. package/dist/skills/transform-skill.js.map +1 -0
  61. package/dist/state/file-store.d.ts +25 -0
  62. package/dist/state/file-store.d.ts.map +1 -0
  63. package/dist/state/file-store.js +92 -0
  64. package/dist/state/file-store.js.map +1 -0
  65. package/dist/state/memory-store.d.ts +24 -0
  66. package/dist/state/memory-store.d.ts.map +1 -0
  67. package/dist/state/memory-store.js +65 -0
  68. package/dist/state/memory-store.js.map +1 -0
  69. package/dist/state/types.d.ts +40 -0
  70. package/dist/state/types.d.ts.map +1 -0
  71. package/dist/state/types.js +8 -0
  72. package/dist/state/types.js.map +1 -0
  73. package/dist/strategies/custom.d.ts +12 -0
  74. package/dist/strategies/custom.d.ts.map +1 -0
  75. package/dist/strategies/custom.js +14 -0
  76. package/dist/strategies/custom.js.map +1 -0
  77. package/dist/strategies/plan-and-execute.d.ts +27 -0
  78. package/dist/strategies/plan-and-execute.d.ts.map +1 -0
  79. package/dist/strategies/plan-and-execute.js +195 -0
  80. package/dist/strategies/plan-and-execute.js.map +1 -0
  81. package/dist/strategies/react.d.ts +27 -0
  82. package/dist/strategies/react.d.ts.map +1 -0
  83. package/dist/strategies/react.js +172 -0
  84. package/dist/strategies/react.js.map +1 -0
  85. package/dist/strategies/router.d.ts +11 -0
  86. package/dist/strategies/router.d.ts.map +1 -0
  87. package/dist/strategies/router.js +70 -0
  88. package/dist/strategies/router.js.map +1 -0
  89. package/dist/strategies/sequential.d.ts +12 -0
  90. package/dist/strategies/sequential.d.ts.map +1 -0
  91. package/dist/strategies/sequential.js +39 -0
  92. package/dist/strategies/sequential.js.map +1 -0
  93. package/dist/strategies/types.d.ts +62 -0
  94. package/dist/strategies/types.d.ts.map +1 -0
  95. package/dist/strategies/types.js +5 -0
  96. package/dist/strategies/types.js.map +1 -0
  97. package/dist/types.d.ts +83 -0
  98. package/dist/types.d.ts.map +1 -0
  99. package/dist/types.js +5 -0
  100. package/dist/types.js.map +1 -0
  101. package/package.json +28 -0
  102. package/src/agent.ts +243 -0
  103. package/src/cli-utils.ts +73 -0
  104. package/src/errors.ts +146 -0
  105. package/src/index.ts +124 -0
  106. package/src/llm-provider.ts +88 -0
  107. package/src/logger.ts +97 -0
  108. package/src/mock-llm.ts +433 -0
  109. package/src/orchestrator-helpers.ts +40 -0
  110. package/src/orchestrator.ts +522 -0
  111. package/src/pipeline-registry.ts +253 -0
  112. package/src/pipeline.ts +265 -0
  113. package/src/skill.ts +127 -0
  114. package/src/skills/io-skill.ts +133 -0
  115. package/src/skills/llm-skill.ts +207 -0
  116. package/src/skills/transform-skill.ts +61 -0
  117. package/src/state/file-store.ts +119 -0
  118. package/src/state/memory-store.ts +82 -0
  119. package/src/state/types.ts +53 -0
  120. package/src/strategies/custom.ts +24 -0
  121. package/src/strategies/plan-and-execute.ts +268 -0
  122. package/src/strategies/react.ts +239 -0
  123. package/src/strategies/router.ts +101 -0
  124. package/src/strategies/sequential.ts +55 -0
  125. package/src/strategies/types.ts +97 -0
  126. package/src/types.ts +102 -0
  127. package/tsconfig.json +9 -0
@@ -0,0 +1,522 @@
1
+ /**
2
+ * Orchestrator — the runtime engine that executes pipelines.
3
+ *
4
+ * Responsibilities:
5
+ * - Validate pipeline input/output
6
+ * - Execute steps in order, passing outputs to inputs
7
+ * - Handle map/filter/reduce/parallel/branch steps
8
+ * - Retry failed steps with exponential backoff
9
+ * - Save checkpoints for resume capability
10
+ * - Record full run history
11
+ * - Fire lifecycle hooks
12
+ * - Manage LLM registry and state store
13
+ */
14
+
15
+ import { randomUUID } from 'crypto';
16
+ import type { LLMRegistry } from './llm-provider.js';
17
+ import { resolveLLM } from './llm-provider.js';
18
+ import type { MCPClientLike } from './skill.js';
19
+ import type { StateStore } from './state/types.js';
20
+ import { MemoryStore } from './state/memory-store.js';
21
+ import type { Logger } from './logger.js';
22
+ import { logger as defaultLogger } from './logger.js';
23
+ import type { BuiltPipeline, PipelineStep, StepInputContext } from './pipeline.js';
24
+ import { resolvePath } from './pipeline.js';
25
+ import type { SkillContext } from './skill.js';
26
+ import type { PipelineRun, StepRecord } from './types.js';
27
+ import { PipelineError, StepMaxRetriesError } from './errors.js';
28
+ import { buildAgentContext } from './agent.js';
29
+ import type { Agent } from './agent.js';
30
+
31
+ // ── Orchestrator Config ───────────────────────────────────────────────────────
32
+
33
+ export interface OrchestratorConfig {
34
+ /**
35
+ * LLM registry. 'default' key is used when no specific LLM is requested.
36
+ * @example { default: anthropic({ model: 'claude-3-5-sonnet' }) }
37
+ */
38
+ llm: LLMRegistry;
39
+ /**
40
+ * MCP client registry. Named MCP clients that skills and agents can access
41
+ * via SkillContext.getMCPClient(key).
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * import { MCPClient } from '@flomatai/mcp-client';
46
+ * {
47
+ * mcp: {
48
+ * filesystem: new MCPClient({ command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem', '/tmp'] })
49
+ * }
50
+ * }
51
+ * ```
52
+ */
53
+ mcp?: Record<string, MCPClientLike>;
54
+ /**
55
+ * State store for persistence, caching, and run history.
56
+ * @default MemoryStore
57
+ */
58
+ state?: StateStore;
59
+ /**
60
+ * Root logger. Defaults to console logger.
61
+ */
62
+ logger?: Logger;
63
+ /**
64
+ * Lifecycle hooks.
65
+ */
66
+ hooks?: OrchestratorHooks;
67
+ /**
68
+ * Global configuration passed to all skill/agent contexts.
69
+ */
70
+ config?: Record<string, unknown>;
71
+ /**
72
+ * Maximum sub-agent depth for hierarchical agents.
73
+ * @default 5
74
+ */
75
+ maxAgentDepth?: number;
76
+ }
77
+
78
+ export interface OrchestratorHooks {
79
+ /** Called before the pipeline starts. */
80
+ beforePipeline?: (pipeline: BuiltPipeline, input: unknown, runId: string) => void | Promise<void>;
81
+ /** Called after the pipeline completes (success or failure). */
82
+ afterPipeline?: (pipeline: BuiltPipeline, run: PipelineRun) => void | Promise<void>;
83
+ /** Called before each step executes. */
84
+ beforeStep?: (step: PipelineStep, input: unknown, runId: string) => void | Promise<void>;
85
+ /** Called after each step completes (success or failure). */
86
+ afterStep?: (step: PipelineStep, record: StepRecord) => void | Promise<void>;
87
+ /** Called on any error. */
88
+ onError?: (step: PipelineStep | null, error: Error, runId: string) => void | Promise<void>;
89
+ }
90
+
91
+ // ── Run Options ───────────────────────────────────────────────────────────────
92
+
93
+ export interface RunOptions {
94
+ /** External run ID (auto-generated if not provided). */
95
+ runId?: string;
96
+ /** Override the LLM for this run. */
97
+ llm?: string;
98
+ /** Abort controller for cancellation. */
99
+ abortController?: AbortController;
100
+ /**
101
+ * Resume from a previous run's checkpoints.
102
+ * Provide the runId of the run to resume.
103
+ */
104
+ resumeFromRunId?: string;
105
+ }
106
+
107
+ // ── Orchestrator ──────────────────────────────────────────────────────────────
108
+
109
+ export class Orchestrator {
110
+ private readonly llmRegistry: LLMRegistry;
111
+ private readonly mcpRegistry: Record<string, MCPClientLike>;
112
+ private readonly state: StateStore;
113
+ private readonly log: Logger;
114
+ private readonly hooks: OrchestratorHooks;
115
+ private readonly globalConfig: Record<string, unknown>;
116
+ private readonly maxAgentDepth: number;
117
+ private initialized = false;
118
+
119
+ constructor(config: OrchestratorConfig) {
120
+ this.llmRegistry = config.llm;
121
+ this.mcpRegistry = config.mcp ?? {};
122
+ this.state = config.state ?? new MemoryStore();
123
+ this.log = config.logger ?? defaultLogger;
124
+ this.hooks = config.hooks ?? {};
125
+ this.globalConfig = config.config ?? {};
126
+ this.maxAgentDepth = config.maxAgentDepth ?? 5;
127
+ }
128
+
129
+ private async ensureInit(): Promise<void> {
130
+ if (!this.initialized) {
131
+ await this.state.init();
132
+ this.initialized = true;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Execute a pipeline with the given input.
138
+ */
139
+ async run<TOutput = unknown>(
140
+ pipeline: BuiltPipeline,
141
+ input: unknown,
142
+ options: RunOptions = {},
143
+ ): Promise<{ output: TOutput; run: PipelineRun }> {
144
+ await this.ensureInit();
145
+
146
+ const runId = options.runId ?? randomUUID();
147
+ const abortController = options.abortController ?? new AbortController();
148
+ const events: Array<{ event: string; data: unknown }> = [];
149
+
150
+ const emit = (event: string, data: unknown): void => {
151
+ events.push({ event, data });
152
+ this.log.debug(`event:${event}`, data);
153
+ // Accumulate token usage from LLM skill responses
154
+ if (event === 'llm:response') {
155
+ const d = data as { tokens?: number };
156
+ if (typeof d.tokens === 'number') totalTokens += d.tokens;
157
+ }
158
+ };
159
+
160
+ const run: PipelineRun = {
161
+ id: runId,
162
+ pipelineName: pipeline.name,
163
+ status: 'running',
164
+ input,
165
+ startedAt: new Date().toISOString(),
166
+ tokensUsed: 0,
167
+ steps: [],
168
+ };
169
+
170
+ await this.state.saveRun(run);
171
+ await this.hooks.beforePipeline?.(pipeline, input, runId);
172
+
173
+ this.log.info(`Pipeline "${pipeline.name}" starting [${runId}]`);
174
+
175
+ // Validate pipeline input
176
+ if (pipeline.inputSchema) {
177
+ const result = pipeline.inputSchema.safeParse(input);
178
+ if (!result.success) {
179
+ throw new PipelineError(
180
+ pipeline.name,
181
+ `Input validation failed: ${result.error.message}`,
182
+ );
183
+ }
184
+ }
185
+
186
+ // Build skill context factory
187
+ const mcpRegistry = this.mcpRegistry;
188
+ const makeSkillCtx = (): SkillContext => ({
189
+ llm: resolveLLM(this.llmRegistry, options.llm ?? 'default'),
190
+ logger: this.log,
191
+ state: this.state,
192
+ emit,
193
+ config: this.globalConfig,
194
+ abortSignal: abortController.signal,
195
+ runId,
196
+ getLLM: (key: string) => resolveLLM(this.llmRegistry, key),
197
+ getMCPClient: Object.keys(mcpRegistry).length > 0
198
+ ? (key: string) => {
199
+ const client = mcpRegistry[key];
200
+ if (!client) {
201
+ throw new Error(
202
+ `MCP client "${key}" not found in registry. ` +
203
+ `Available: ${Object.keys(mcpRegistry).join(', ')}`,
204
+ );
205
+ }
206
+ return client;
207
+ }
208
+ : undefined,
209
+ });
210
+
211
+ // Execute steps
212
+ const stepOutputs: Record<string, unknown> = {};
213
+ let previousOutput: unknown = input;
214
+ let totalTokens = 0; // NOTE: also incremented by emit('llm:response') above
215
+
216
+ try {
217
+ for (const step of pipeline.steps) {
218
+ if (abortController.signal.aborted) {
219
+ throw new PipelineError(pipeline.name, 'Pipeline was cancelled', step.name);
220
+ }
221
+
222
+ // Resolve step input
223
+ const stepInputCtx: StepInputContext = {
224
+ pipelineInput: input,
225
+ stepOutputs,
226
+ previousOutput,
227
+ };
228
+
229
+ // Check skip condition
230
+ if (step.skipIf?.(stepInputCtx)) {
231
+ this.log.debug(`Step "${step.name}" skipped`);
232
+ continue;
233
+ }
234
+
235
+ const stepInput = step.inputMapper
236
+ ? step.inputMapper(stepInputCtx)
237
+ : previousOutput;
238
+
239
+ await this.hooks.beforeStep?.(step, stepInput, runId);
240
+
241
+ const stepRecord: StepRecord = {
242
+ name: step.name,
243
+ status: 'running',
244
+ input: stepInput,
245
+ startedAt: new Date().toISOString(),
246
+ tokensUsed: 0,
247
+ attempt: 1,
248
+ };
249
+ run.steps.push(stepRecord);
250
+
251
+ // Check for checkpoint (resume)
252
+ if (options.resumeFromRunId && pipeline.checkpointing) {
253
+ const checkpoint = await this.state.getCheckpoint(
254
+ options.resumeFromRunId,
255
+ step.name,
256
+ );
257
+ if (checkpoint !== null) {
258
+ this.log.info(`Step "${step.name}" resumed from checkpoint`);
259
+ stepOutputs[step.name] = checkpoint;
260
+ previousOutput = checkpoint;
261
+ stepRecord.status = 'completed';
262
+ stepRecord.output = checkpoint;
263
+ continue;
264
+ }
265
+ }
266
+
267
+ // Execute step
268
+ const t0 = Date.now();
269
+ let stepOutput: unknown;
270
+ const maxAttempts = (step.skill?.meta.retries ?? 0) + 1;
271
+
272
+ let lastError: Error | null = null;
273
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
274
+ stepRecord.attempt = attempt;
275
+ try {
276
+ stepOutput = await this.executeStep(
277
+ step,
278
+ stepInput,
279
+ makeSkillCtx,
280
+ runId,
281
+ abortController.signal,
282
+ emit,
283
+ );
284
+ lastError = null;
285
+ break;
286
+ } catch (err) {
287
+ lastError = err instanceof Error ? err : new Error(String(err));
288
+ if (attempt < maxAttempts) {
289
+ const delay = Math.min(500 * Math.pow(2, attempt - 1), 10_000);
290
+ this.log.warn(
291
+ `Step "${step.name}" attempt ${attempt}/${maxAttempts} failed: ${lastError.message}. Retrying in ${delay}ms`,
292
+ );
293
+ await this.hooks.onError?.(step, lastError, runId);
294
+ await new Promise((r) => setTimeout(r, delay));
295
+ }
296
+ }
297
+ }
298
+
299
+ if (lastError) {
300
+ throw new StepMaxRetriesError(pipeline.name, step.name, maxAttempts, lastError);
301
+ }
302
+
303
+ const durationMs = Date.now() - t0;
304
+ stepRecord.status = 'completed';
305
+ stepRecord.output = stepOutput;
306
+ stepRecord.completedAt = new Date().toISOString();
307
+ stepRecord.durationMs = durationMs;
308
+
309
+ // Save checkpoint if enabled
310
+ if (pipeline.checkpointing) {
311
+ await this.state.saveCheckpoint(runId, step.name, stepOutput);
312
+ }
313
+
314
+ stepOutputs[step.name] = stepOutput;
315
+ previousOutput = stepOutput;
316
+
317
+ await this.state.saveRun(run);
318
+ await this.hooks.afterStep?.(step, stepRecord);
319
+
320
+ this.log.debug(`Step "${step.name}" completed in ${durationMs}ms`);
321
+ }
322
+
323
+ // Validate pipeline output
324
+ let finalOutput: TOutput = previousOutput as TOutput;
325
+ if (pipeline.outputSchema) {
326
+ const result = pipeline.outputSchema.safeParse(finalOutput);
327
+ if (!result.success) {
328
+ this.log.warn(`Pipeline "${pipeline.name}" output failed schema validation`, result.error.issues);
329
+ }
330
+ }
331
+
332
+ run.status = 'completed';
333
+ run.output = finalOutput;
334
+ run.completedAt = new Date().toISOString();
335
+ run.durationMs = Date.now() - new Date(run.startedAt).getTime();
336
+ run.tokensUsed = totalTokens;
337
+
338
+ await this.state.saveRun(run);
339
+ if (pipeline.checkpointing) await this.state.clearCheckpoints(runId);
340
+ await this.hooks.afterPipeline?.(pipeline, run);
341
+
342
+ this.log.info(
343
+ `Pipeline "${pipeline.name}" completed in ${run.durationMs}ms [${runId}]`,
344
+ );
345
+
346
+ return { output: finalOutput, run };
347
+ } catch (err) {
348
+ const error = err instanceof Error ? err : new Error(String(err));
349
+ run.status = 'failed';
350
+ run.error = error.message;
351
+ run.completedAt = new Date().toISOString();
352
+ run.durationMs = Date.now() - new Date(run.startedAt).getTime();
353
+
354
+ await this.state.saveRun(run);
355
+ await this.hooks.onError?.(null, error, runId);
356
+ await this.hooks.afterPipeline?.(pipeline, run);
357
+
358
+ this.log.error(`Pipeline "${pipeline.name}" failed: ${error.message}`);
359
+ throw error;
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Execute a single pipeline step.
365
+ */
366
+ private async executeStep(
367
+ step: PipelineStep,
368
+ input: unknown,
369
+ makeSkillCtx: () => SkillContext,
370
+ runId: string,
371
+ abortSignal: AbortSignal,
372
+ emit: (event: string, data: unknown) => void,
373
+ ): Promise<unknown> {
374
+ switch (step.kind) {
375
+ case 'skill': {
376
+ if (!step.skill) throw new PipelineError('?', `Step "${step.name}" has no skill`);
377
+ return step.skill.execute(
378
+ input as Parameters<typeof step.skill.execute>[0],
379
+ makeSkillCtx(),
380
+ );
381
+ }
382
+
383
+ case 'map': {
384
+ const source = this.resolveSource(step.sourceField!, input);
385
+ if (!Array.isArray(source)) {
386
+ throw new PipelineError('?', `Step "${step.name}" map source "${step.sourceField}" is not an array`);
387
+ }
388
+ return this.runMap(step, source, makeSkillCtx, runId, abortSignal, emit);
389
+ }
390
+
391
+ case 'filter': {
392
+ const source = this.resolveSource(step.sourceField!, input);
393
+ if (!Array.isArray(source)) return [];
394
+ return source.filter((item, i) => step.predicate?.(item, i) ?? true);
395
+ }
396
+
397
+ case 'reduce': {
398
+ const source = this.resolveSource(step.sourceField!, input);
399
+ if (!Array.isArray(source)) return step.initialValue;
400
+ return source.reduce(
401
+ (acc, item, i) => step.reducer?.(acc, item, i) ?? acc,
402
+ step.initialValue,
403
+ );
404
+ }
405
+
406
+ case 'parallel': {
407
+ if (!step.branches) return [];
408
+ const results = await Promise.all(
409
+ step.branches.map((branch) =>
410
+ this.run(branch, input, { runId: `${runId}:${branch.name}`, abortController: { signal: abortSignal } as AbortController }),
411
+ ),
412
+ );
413
+ return results.map((r) => r.output);
414
+ }
415
+
416
+ case 'branch': {
417
+ // inputMapper stores the condition function
418
+ const branchKey = (step.inputMapper as (ctx: unknown) => string)(input);
419
+ const selectedBranch = step.branches?.find((b) => b.name === branchKey);
420
+ if (!selectedBranch) {
421
+ this.log.warn(`Branch "${step.name}": no branch matched key "${branchKey}"`);
422
+ return input;
423
+ }
424
+ const result = await this.run(selectedBranch, input, {
425
+ runId: `${runId}:${selectedBranch.name}`,
426
+ abortController: { signal: abortSignal } as AbortController,
427
+ });
428
+ return result.output;
429
+ }
430
+
431
+ default:
432
+ throw new PipelineError('?', `Unknown step kind: "${(step as PipelineStep).kind}"`);
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Run a map step: execute sub-pipeline for each item.
438
+ */
439
+ private async runMap(
440
+ step: PipelineStep,
441
+ items: unknown[],
442
+ makeSkillCtx: () => SkillContext,
443
+ runId: string,
444
+ abortSignal: AbortSignal,
445
+ emit: (event: string, data: unknown) => void,
446
+ ): Promise<unknown[]> {
447
+ const concurrency = step.concurrency ?? 1;
448
+ const results: unknown[] = new Array(items.length);
449
+
450
+ // Process in batches of `concurrency`
451
+ for (let i = 0; i < items.length; i += concurrency) {
452
+ const batch = items.slice(i, i + concurrency);
453
+ const batchResults = await Promise.all(
454
+ batch.map(async (item, batchIdx) => {
455
+ const idx = i + batchIdx;
456
+ const maxAttempts = (step.maxItemRetries ?? 0) + 1;
457
+
458
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
459
+ try {
460
+ const result = await this.run(step.subPipeline!, item, {
461
+ runId: `${runId}:${step.name}[${idx}]`,
462
+ abortController: { signal: abortSignal } as AbortController,
463
+ });
464
+ return { idx, output: result.output, error: null };
465
+ } catch (err) {
466
+ const error = err instanceof Error ? err : new Error(String(err));
467
+ if (attempt < maxAttempts) {
468
+ const delay = Math.min(500 * Math.pow(2, attempt - 1), 5_000);
469
+ this.log.warn(`Map item ${idx} attempt ${attempt}/${maxAttempts} failed. Retrying in ${delay}ms`);
470
+ await new Promise((r) => setTimeout(r, delay));
471
+ } else {
472
+ if (step.onItemError === 'skip') {
473
+ this.log.warn(`Map item ${idx} failed and was skipped: ${error.message}`);
474
+ return { idx, output: null, error };
475
+ }
476
+ throw error;
477
+ }
478
+ }
479
+ }
480
+ return { idx, output: null, error: null };
481
+ }),
482
+ );
483
+
484
+ for (const r of batchResults) {
485
+ results[r.idx] = r.output;
486
+ }
487
+ }
488
+
489
+ // Filter out nulls from skipped items
490
+ return step.onItemError === 'skip'
491
+ ? results.filter((r) => r !== null)
492
+ : results;
493
+ }
494
+
495
+ /**
496
+ * Resolve a source field path from the step input.
497
+ */
498
+ private resolveSource(sourceField: string, input: unknown): unknown {
499
+ if (sourceField === '*') return input;
500
+ if (Array.isArray(input)) return input;
501
+ const parts = sourceField.split('.');
502
+ let current: unknown = input;
503
+ for (const part of parts) {
504
+ if (current === null || current === undefined) return undefined;
505
+ current = (current as Record<string, unknown>)[part];
506
+ }
507
+ return current;
508
+ }
509
+
510
+ /** Get the state store (for external inspection). */
511
+ get stateStore(): StateStore {
512
+ return this.state;
513
+ }
514
+
515
+ get llm(): LLMRegistry {
516
+ return this.llmRegistry;
517
+ }
518
+
519
+ get mcp(): Record<string, MCPClientLike> {
520
+ return this.mcpRegistry;
521
+ }
522
+ }