@multiplier-labs/stepflow 0.3.4 → 0.3.6
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.
- package/README.md +9 -0
- package/dist/{chunk-U3PCVK6O.js → chunk-D5HW7VEG.js} +4 -2
- package/dist/chunk-D5HW7VEG.js.map +1 -0
- package/dist/{chunk-5XJBAPXF.js → chunk-JUPZEBR3.js} +3 -1
- package/dist/{chunk-5XJBAPXF.js.map → chunk-JUPZEBR3.js.map} +1 -1
- package/dist/events/index.d.ts +2 -0
- package/dist/events/index.js +1 -1
- package/dist/index.js +2 -2
- package/dist/storage/index.js +1 -1
- package/package.json +23 -12
- package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +0 -146
- package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +0 -74
- package/.codekin/reports/comments/2026-04-04_comment-assessment.md +0 -179
- package/.codekin/reports/comments/2026-04-11_comment-assessment.md +0 -161
- package/.codekin/reports/comments/2026-04-14_comment-assessment.md +0 -34
- package/.codekin/reports/complexity/2026-04-03_complexity-report.md +0 -152
- package/.codekin/reports/complexity/2026-04-11_complexity-report.md +0 -152
- package/.codekin/reports/complexity/2026-04-14_complexity-report.md +0 -34
- package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +0 -94
- package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +0 -93
- package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +0 -10
- package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +0 -138
- package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +0 -121
- package/.codekin/reports/security/2026-04-03_security-audit.md +0 -208
- package/.codekin/reports/security/2026-04-11_security-audit.md +0 -297
- package/.github/CODEOWNERS +0 -15
- package/.github/dependabot.yml +0 -23
- package/.github/workflows/ci.yml +0 -38
- package/.github/workflows/publish.yml +0 -42
- package/.github/workflows/security.yml +0 -29
- package/CHANGELOG.md +0 -34
- package/SECURITY.md +0 -40
- package/dist/chunk-U3PCVK6O.js.map +0 -1
- package/docs/github-packages-setup.md +0 -97
- package/docs/stepflow-api-reference.md +0 -1663
- package/src/core/engine/concurrency.ts +0 -229
- package/src/core/engine/lifecycle.ts +0 -176
- package/src/core/engine.test.ts +0 -1435
- package/src/core/engine.ts +0 -598
- package/src/core/index.ts +0 -7
- package/src/core/orchestrator/persistence.ts +0 -165
- package/src/core/orchestrator/plan-resolution.ts +0 -140
- package/src/core/orchestrator/state-transitions.ts +0 -109
- package/src/core/orchestrator/step-runner.ts +0 -427
- package/src/core/orchestrator.ts +0 -252
- package/src/core/types.ts +0 -220
- package/src/events/index.ts +0 -8
- package/src/events/memory.test.ts +0 -107
- package/src/events/memory.ts +0 -92
- package/src/events/socketio.test.ts +0 -378
- package/src/events/socketio.ts +0 -262
- package/src/events/types.ts +0 -118
- package/src/events/webhook.test.ts +0 -949
- package/src/events/webhook.ts +0 -807
- package/src/index.ts +0 -270
- package/src/planning/evaluate-condition.test.ts +0 -153
- package/src/planning/evaluate-condition.ts +0 -105
- package/src/planning/index.ts +0 -55
- package/src/planning/planner.ts +0 -494
- package/src/planning/planning.test.ts +0 -727
- package/src/planning/registry.test.ts +0 -398
- package/src/planning/registry.ts +0 -283
- package/src/planning/types.ts +0 -566
- package/src/scheduler/cron.test.ts +0 -479
- package/src/scheduler/cron.ts +0 -438
- package/src/scheduler/index.ts +0 -8
- package/src/scheduler/postgres-persistence.ts +0 -530
- package/src/scheduler/sqlite-persistence.test.ts +0 -235
- package/src/scheduler/sqlite-persistence.ts +0 -286
- package/src/scheduler/types.ts +0 -73
- package/src/storage/index.ts +0 -27
- package/src/storage/memory.test.ts +0 -349
- package/src/storage/memory.ts +0 -227
- package/src/storage/postgres-core.ts +0 -550
- package/src/storage/postgres-migration.test.ts +0 -69
- package/src/storage/postgres-transaction-pagination.test.ts +0 -107
- package/src/storage/postgres.ts +0 -835
- package/src/storage/sqlite-core.ts +0 -151
- package/src/storage/sqlite.test.ts +0 -642
- package/src/storage/sqlite.ts +0 -717
- package/src/storage/types.ts +0 -343
- package/src/utils/errors.test.ts +0 -150
- package/src/utils/errors.ts +0 -161
- package/src/utils/id.ts +0 -21
- package/src/utils/index.ts +0 -10
- package/src/utils/logger.test.ts +0 -121
- package/src/utils/logger.ts +0 -96
- package/src/utils/pinned-fetch.test.ts +0 -84
- package/src/utils/pinned-fetch.ts +0 -118
- package/src/utils/postgres-deps.ts +0 -79
- package/src/utils/retry.test.ts +0 -177
- package/src/utils/retry.ts +0 -121
- package/src/utils/safe-json.test.ts +0 -166
- package/src/utils/safe-json.ts +0 -107
- package/src/utils/webhook-signature.test.ts +0 -111
- package/src/utils/webhook-signature.ts +0 -74
- package/tsconfig.json +0 -21
- package/tsup.config.ts +0 -14
- package/vitest.config.ts +0 -9
package/src/planning/planner.ts
DELETED
|
@@ -1,494 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rule-based planner implementation.
|
|
3
|
-
* Selects recipes and generates plans based on conditions and input analysis.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { generateId } from '../utils/id';
|
|
7
|
-
import { evaluateCondition, getNestedValue } from './evaluate-condition';
|
|
8
|
-
import type {
|
|
9
|
-
Plan,
|
|
10
|
-
PlannedStep,
|
|
11
|
-
Planner,
|
|
12
|
-
PlanModification,
|
|
13
|
-
PlanningContext,
|
|
14
|
-
PlanValidationResult,
|
|
15
|
-
Recipe,
|
|
16
|
-
RecipeCondition,
|
|
17
|
-
RecipeRegistry,
|
|
18
|
-
RecipeSelectionResult,
|
|
19
|
-
ResourceEstimate,
|
|
20
|
-
StepHandlerRegistry,
|
|
21
|
-
WorkflowKind,
|
|
22
|
-
} from './types';
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Score a recipe's conditions against input.
|
|
26
|
-
* Returns a score from 0-100 based on how well conditions match.
|
|
27
|
-
*/
|
|
28
|
-
function scoreConditions(
|
|
29
|
-
conditions: RecipeCondition[] | undefined,
|
|
30
|
-
input: Record<string, unknown>
|
|
31
|
-
): number {
|
|
32
|
-
// No conditions = default match with low score
|
|
33
|
-
if (!conditions || conditions.length === 0) {
|
|
34
|
-
return 10;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
let matched = 0;
|
|
38
|
-
for (const condition of conditions) {
|
|
39
|
-
const fieldValue = getNestedValue(input, condition.field);
|
|
40
|
-
if (evaluateCondition(condition.operator, fieldValue, condition.value)) {
|
|
41
|
-
matched++;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// All conditions must match for any positive score
|
|
46
|
-
if (matched < conditions.length) {
|
|
47
|
-
return 0;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Base score 50 ensures condition-matched recipes beat unconditional defaults (score 10).
|
|
51
|
-
// Each additional condition adds 10 points, capped at 100 so forced recipes (100) still win.
|
|
52
|
-
return Math.min(100, 50 + conditions.length * 10);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ============================================================================
|
|
56
|
-
// Rule-Based Planner Implementation
|
|
57
|
-
// ============================================================================
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Configuration for the RuleBasedPlanner.
|
|
61
|
-
*/
|
|
62
|
-
export interface RuleBasedPlannerConfig {
|
|
63
|
-
/** Recipe registry to use */
|
|
64
|
-
recipeRegistry: RecipeRegistry;
|
|
65
|
-
|
|
66
|
-
/** Step handler registry for validation */
|
|
67
|
-
handlerRegistry?: StepHandlerRegistry;
|
|
68
|
-
|
|
69
|
-
/** Whether to validate handler references during planning */
|
|
70
|
-
validateHandlers?: boolean;
|
|
71
|
-
|
|
72
|
-
/** Resource estimation defaults (used by estimateResources) */
|
|
73
|
-
resourceEstimates?: {
|
|
74
|
-
/** Estimated API calls per step (default: 1) */
|
|
75
|
-
apiCallsPerStep?: number;
|
|
76
|
-
/** Estimated tokens per step (default: 500) */
|
|
77
|
-
tokensPerStep?: number;
|
|
78
|
-
/** Estimated duration per step in ms (default: 2000) */
|
|
79
|
-
durationPerStep?: number;
|
|
80
|
-
/** Estimated API calls per child workflow (default: 5) */
|
|
81
|
-
apiCallsPerChild?: number;
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Rule-based planner that selects recipes based on condition matching.
|
|
87
|
-
*/
|
|
88
|
-
export class RuleBasedPlanner implements Planner {
|
|
89
|
-
private recipeRegistry: RecipeRegistry;
|
|
90
|
-
private handlerRegistry?: StepHandlerRegistry;
|
|
91
|
-
private validateHandlers: boolean;
|
|
92
|
-
private resourceEstimates: Required<NonNullable<RuleBasedPlannerConfig['resourceEstimates']>>;
|
|
93
|
-
|
|
94
|
-
constructor(config: RuleBasedPlannerConfig) {
|
|
95
|
-
this.recipeRegistry = config.recipeRegistry;
|
|
96
|
-
this.handlerRegistry = config.handlerRegistry;
|
|
97
|
-
this.validateHandlers = config.validateHandlers ?? false;
|
|
98
|
-
this.resourceEstimates = {
|
|
99
|
-
apiCallsPerStep: config.resourceEstimates?.apiCallsPerStep ?? 1,
|
|
100
|
-
tokensPerStep: config.resourceEstimates?.tokensPerStep ?? 500,
|
|
101
|
-
durationPerStep: config.resourceEstimates?.durationPerStep ?? 2000,
|
|
102
|
-
apiCallsPerChild: config.resourceEstimates?.apiCallsPerChild ?? 5,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Select the best recipe for a workflow kind and input.
|
|
108
|
-
*/
|
|
109
|
-
async selectRecipe(
|
|
110
|
-
workflowKind: WorkflowKind,
|
|
111
|
-
input: Record<string, unknown>,
|
|
112
|
-
context?: PlanningContext
|
|
113
|
-
): Promise<RecipeSelectionResult> {
|
|
114
|
-
// Check for forced recipe in hints
|
|
115
|
-
if (context?.hints?.forceRecipeId) {
|
|
116
|
-
const forced = this.recipeRegistry.get(context.hints.forceRecipeId);
|
|
117
|
-
if (forced) {
|
|
118
|
-
return {
|
|
119
|
-
recipe: forced,
|
|
120
|
-
score: 100,
|
|
121
|
-
reason: `Forced recipe: ${context.hints.forceRecipeId}`,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Check for preferred variant in hints
|
|
127
|
-
if (context?.hints?.preferredVariant) {
|
|
128
|
-
const preferred = this.recipeRegistry.getVariant(
|
|
129
|
-
workflowKind,
|
|
130
|
-
context.hints.preferredVariant
|
|
131
|
-
);
|
|
132
|
-
if (preferred) {
|
|
133
|
-
return {
|
|
134
|
-
recipe: preferred,
|
|
135
|
-
score: 90,
|
|
136
|
-
reason: `Preferred variant: ${context.hints.preferredVariant}`,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Get all recipes for this workflow kind
|
|
142
|
-
const recipes = this.recipeRegistry.getByKind(workflowKind);
|
|
143
|
-
|
|
144
|
-
if (recipes.length === 0) {
|
|
145
|
-
throw new Error(`No recipes found for workflow kind: ${workflowKind}`);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Score each recipe
|
|
149
|
-
const scored = recipes.map(recipe => ({
|
|
150
|
-
recipe,
|
|
151
|
-
conditionScore: scoreConditions(recipe.conditions, input),
|
|
152
|
-
priorityScore: recipe.priority ?? 0,
|
|
153
|
-
}));
|
|
154
|
-
|
|
155
|
-
// Sort by condition score (desc), then priority (desc)
|
|
156
|
-
scored.sort((a, b) => {
|
|
157
|
-
if (b.conditionScore !== a.conditionScore) {
|
|
158
|
-
return b.conditionScore - a.conditionScore;
|
|
159
|
-
}
|
|
160
|
-
return b.priorityScore - a.priorityScore;
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// Find best matching recipe
|
|
164
|
-
const best = scored.find(s => s.conditionScore > 0);
|
|
165
|
-
|
|
166
|
-
if (best) {
|
|
167
|
-
const reason = this.buildSelectionReason(best.recipe, input);
|
|
168
|
-
return {
|
|
169
|
-
recipe: best.recipe,
|
|
170
|
-
score: best.conditionScore,
|
|
171
|
-
reason,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Fall back to default recipe
|
|
176
|
-
const defaultRecipe = this.recipeRegistry.getDefault(workflowKind);
|
|
177
|
-
if (defaultRecipe) {
|
|
178
|
-
return {
|
|
179
|
-
recipe: defaultRecipe,
|
|
180
|
-
score: 10,
|
|
181
|
-
reason: `Fallback to default recipe: ${defaultRecipe.id}`,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Last resort: use first recipe
|
|
186
|
-
return {
|
|
187
|
-
recipe: recipes[0],
|
|
188
|
-
score: 5,
|
|
189
|
-
reason: `No matching recipe, using first available: ${recipes[0].id}`,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Generate a plan from a recipe and input.
|
|
195
|
-
*/
|
|
196
|
-
async generatePlan(
|
|
197
|
-
recipe: Recipe,
|
|
198
|
-
input: Record<string, unknown>,
|
|
199
|
-
context?: PlanningContext
|
|
200
|
-
): Promise<Plan> {
|
|
201
|
-
const modifications: PlanModification[] = [];
|
|
202
|
-
|
|
203
|
-
// Start with recipe steps
|
|
204
|
-
let steps = recipe.steps.map(step => this.recipeStepToPlannedStep(step));
|
|
205
|
-
|
|
206
|
-
// Apply hint-based modifications
|
|
207
|
-
if (context?.hints) {
|
|
208
|
-
const { skipSteps, includeSteps, additionalConfig } = context.hints;
|
|
209
|
-
|
|
210
|
-
// Mark steps to skip
|
|
211
|
-
if (skipSteps && skipSteps.length > 0) {
|
|
212
|
-
for (const stepKey of skipSteps) {
|
|
213
|
-
const stepIndex = steps.findIndex(s => s.key === stepKey);
|
|
214
|
-
if (stepIndex >= 0) {
|
|
215
|
-
steps = steps.filter(s => s.key !== stepKey);
|
|
216
|
-
modifications.push({
|
|
217
|
-
type: 'remove_step',
|
|
218
|
-
stepKey,
|
|
219
|
-
value: null,
|
|
220
|
-
reason: `Skipped via planning hints`,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Apply additional config to all steps
|
|
227
|
-
if (additionalConfig) {
|
|
228
|
-
for (const step of steps) {
|
|
229
|
-
step.config = { ...step.config, ...additionalConfig };
|
|
230
|
-
}
|
|
231
|
-
modifications.push({
|
|
232
|
-
type: 'modify_step',
|
|
233
|
-
value: additionalConfig,
|
|
234
|
-
reason: 'Applied additional config from planning hints',
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Apply constraint-based modifications
|
|
240
|
-
if (context?.constraints) {
|
|
241
|
-
const constraintMods = this.applyConstraints(steps, context.constraints);
|
|
242
|
-
modifications.push(...constraintMods.modifications);
|
|
243
|
-
steps = constraintMods.steps;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Build the plan
|
|
247
|
-
const plan: Plan = {
|
|
248
|
-
id: generateId(),
|
|
249
|
-
recipeId: recipe.id,
|
|
250
|
-
variant: recipe.variant,
|
|
251
|
-
modifications,
|
|
252
|
-
steps,
|
|
253
|
-
defaults: recipe.defaults ?? {},
|
|
254
|
-
reasoning: this.buildPlanReasoning(recipe, modifications, context),
|
|
255
|
-
createdAt: new Date(),
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
// Estimate resources
|
|
259
|
-
plan.resourceEstimate = this.estimateResources(plan);
|
|
260
|
-
|
|
261
|
-
return plan;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Combined operation: select recipe and generate plan.
|
|
266
|
-
*/
|
|
267
|
-
async plan(
|
|
268
|
-
workflowKind: WorkflowKind,
|
|
269
|
-
input: Record<string, unknown>,
|
|
270
|
-
context?: PlanningContext
|
|
271
|
-
): Promise<Plan> {
|
|
272
|
-
const selection = await this.selectRecipe(workflowKind, input, context);
|
|
273
|
-
const plan = await this.generatePlan(selection.recipe, input, context);
|
|
274
|
-
|
|
275
|
-
// Add selection reasoning to plan
|
|
276
|
-
plan.reasoning = `${selection.reason}. ${plan.reasoning ?? ''}`.trim();
|
|
277
|
-
|
|
278
|
-
return plan;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Validate a plan before execution.
|
|
283
|
-
*/
|
|
284
|
-
validatePlan(plan: Plan): PlanValidationResult {
|
|
285
|
-
const errors: string[] = [];
|
|
286
|
-
const warnings: string[] = [];
|
|
287
|
-
|
|
288
|
-
// Check plan has steps
|
|
289
|
-
if (!plan.steps || plan.steps.length === 0) {
|
|
290
|
-
errors.push('Plan has no steps');
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Check for duplicate step keys
|
|
294
|
-
const stepKeys = new Set<string>();
|
|
295
|
-
for (const step of plan.steps) {
|
|
296
|
-
if (stepKeys.has(step.key)) {
|
|
297
|
-
errors.push(`Duplicate step key: ${step.key}`);
|
|
298
|
-
}
|
|
299
|
-
stepKeys.add(step.key);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Validate each step
|
|
303
|
-
for (const step of plan.steps) {
|
|
304
|
-
if (!step.key) {
|
|
305
|
-
errors.push('Step missing key');
|
|
306
|
-
}
|
|
307
|
-
if (!step.name) {
|
|
308
|
-
warnings.push(`Step '${step.key}' missing name`);
|
|
309
|
-
}
|
|
310
|
-
if (!step.handlerRef) {
|
|
311
|
-
errors.push(`Step '${step.key}' missing handlerRef`);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Validate handler exists (if registry available)
|
|
315
|
-
if (this.validateHandlers && this.handlerRegistry) {
|
|
316
|
-
if (!this.handlerRegistry.has(step.handlerRef)) {
|
|
317
|
-
errors.push(`Step '${step.key}' references unknown handler: ${step.handlerRef}`);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Check child workflows
|
|
323
|
-
if (plan.childWorkflows) {
|
|
324
|
-
for (const child of plan.childWorkflows) {
|
|
325
|
-
if (!child.kind) {
|
|
326
|
-
errors.push('Child workflow missing kind');
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
valid: errors.length === 0,
|
|
333
|
-
errors,
|
|
334
|
-
warnings,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Estimate resources required for a plan.
|
|
340
|
-
*/
|
|
341
|
-
estimateResources(plan: Plan): ResourceEstimate {
|
|
342
|
-
const { apiCallsPerStep, tokensPerStep, durationPerStep, apiCallsPerChild } = this.resourceEstimates;
|
|
343
|
-
|
|
344
|
-
const stepCount = plan.steps.length;
|
|
345
|
-
const childCount = plan.childWorkflows?.length ?? 0;
|
|
346
|
-
|
|
347
|
-
return {
|
|
348
|
-
apiCalls: stepCount * apiCallsPerStep + childCount * apiCallsPerChild,
|
|
349
|
-
tokens: stepCount * tokensPerStep,
|
|
350
|
-
duration: stepCount * durationPerStep,
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// ============================================================================
|
|
355
|
-
// Private Helpers
|
|
356
|
-
// ============================================================================
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Convert a recipe step to a planned step.
|
|
360
|
-
*/
|
|
361
|
-
private recipeStepToPlannedStep(step: Recipe['steps'][0]): PlannedStep {
|
|
362
|
-
return {
|
|
363
|
-
key: step.key,
|
|
364
|
-
name: step.name,
|
|
365
|
-
handlerRef: step.handlerRef,
|
|
366
|
-
config: step.config ?? {},
|
|
367
|
-
onError: step.onError,
|
|
368
|
-
maxRetries: step.maxRetries,
|
|
369
|
-
retryDelay: step.retryDelay,
|
|
370
|
-
retryBackoff: step.retryBackoff,
|
|
371
|
-
timeout: step.timeout,
|
|
372
|
-
skipCondition: step.skipCondition,
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Apply constraints to steps and generate modifications.
|
|
378
|
-
*/
|
|
379
|
-
private applyConstraints(
|
|
380
|
-
steps: PlannedStep[],
|
|
381
|
-
constraints: NonNullable<PlanningContext['constraints']>
|
|
382
|
-
): { steps: PlannedStep[]; modifications: PlanModification[] } {
|
|
383
|
-
const modifications: PlanModification[] = [];
|
|
384
|
-
const perStepTimeout = constraints.maxDuration
|
|
385
|
-
? Math.floor(constraints.maxDuration / steps.length)
|
|
386
|
-
: undefined;
|
|
387
|
-
|
|
388
|
-
// Apply all constraint mutations in a single pass
|
|
389
|
-
const modifiedSteps = steps.map(step => {
|
|
390
|
-
const modified = { ...step };
|
|
391
|
-
|
|
392
|
-
if (constraints.priority === 'speed') {
|
|
393
|
-
modified.timeout = modified.timeout ? Math.min(modified.timeout, 30000) : 30000;
|
|
394
|
-
modified.maxRetries = Math.min(modified.maxRetries ?? 3, 1);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
if (constraints.priority === 'cost') {
|
|
398
|
-
modified.maxRetries = 0;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (perStepTimeout !== undefined) {
|
|
402
|
-
modified.timeout = modified.timeout
|
|
403
|
-
? Math.min(modified.timeout, perStepTimeout)
|
|
404
|
-
: perStepTimeout;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return modified;
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
// Record what was applied
|
|
411
|
-
if (constraints.priority === 'speed') {
|
|
412
|
-
modifications.push({
|
|
413
|
-
type: 'set_default',
|
|
414
|
-
value: { priority: 'speed' },
|
|
415
|
-
reason: 'Optimized for speed: reduced timeouts and retries',
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
if (constraints.priority === 'cost') {
|
|
419
|
-
modifications.push({
|
|
420
|
-
type: 'set_default',
|
|
421
|
-
value: { priority: 'cost' },
|
|
422
|
-
reason: 'Optimized for cost: disabled retries',
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
if (constraints.maxDuration) {
|
|
426
|
-
modifications.push({
|
|
427
|
-
type: 'set_default',
|
|
428
|
-
value: { maxDuration: constraints.maxDuration },
|
|
429
|
-
reason: `Applied duration constraint: ${constraints.maxDuration}ms total`,
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
return { steps: modifiedSteps, modifications };
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Build a human-readable selection reason.
|
|
438
|
-
*/
|
|
439
|
-
private buildSelectionReason(
|
|
440
|
-
recipe: Recipe,
|
|
441
|
-
input: Record<string, unknown>
|
|
442
|
-
): string {
|
|
443
|
-
const parts: string[] = [`Selected recipe: ${recipe.id}`];
|
|
444
|
-
|
|
445
|
-
if (recipe.conditions && recipe.conditions.length > 0) {
|
|
446
|
-
const conditionDescriptions = recipe.conditions.map(c => {
|
|
447
|
-
const value = getNestedValue(input, c.field);
|
|
448
|
-
return `${c.field} ${c.operator} ${JSON.stringify(c.value)} (actual: ${JSON.stringify(value)})`;
|
|
449
|
-
});
|
|
450
|
-
parts.push(`Matched conditions: ${conditionDescriptions.join(', ')}`);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
if (recipe.priority !== undefined && recipe.priority > 0) {
|
|
454
|
-
parts.push(`Priority: ${recipe.priority}`);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
return parts.join('. ');
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Build reasoning text for a plan.
|
|
462
|
-
*/
|
|
463
|
-
private buildPlanReasoning(
|
|
464
|
-
recipe: Recipe,
|
|
465
|
-
modifications: PlanModification[],
|
|
466
|
-
context?: PlanningContext
|
|
467
|
-
): string {
|
|
468
|
-
const parts: string[] = [];
|
|
469
|
-
|
|
470
|
-
parts.push(`Using recipe '${recipe.name}' (${recipe.variant} variant)`);
|
|
471
|
-
|
|
472
|
-
if (modifications.length > 0) {
|
|
473
|
-
parts.push(`Applied ${modifications.length} modification(s)`);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (context?.constraints) {
|
|
477
|
-
const constraintList: string[] = [];
|
|
478
|
-
if (context.constraints.priority) {
|
|
479
|
-
constraintList.push(`priority=${context.constraints.priority}`);
|
|
480
|
-
}
|
|
481
|
-
if (context.constraints.maxDuration) {
|
|
482
|
-
constraintList.push(`maxDuration=${context.constraints.maxDuration}ms`);
|
|
483
|
-
}
|
|
484
|
-
if (context.constraints.maxApiCalls) {
|
|
485
|
-
constraintList.push(`maxApiCalls=${context.constraints.maxApiCalls}`);
|
|
486
|
-
}
|
|
487
|
-
if (constraintList.length > 0) {
|
|
488
|
-
parts.push(`Constraints: ${constraintList.join(', ')}`);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
return parts.join('. ') + '.';
|
|
493
|
-
}
|
|
494
|
-
}
|