@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.
- package/dist/agent.d.ts +92 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +137 -0
- package/dist/agent.js.map +1 -0
- package/dist/cli-utils.d.ts +41 -0
- package/dist/cli-utils.d.ts.map +1 -0
- package/dist/cli-utils.js +64 -0
- package/dist/cli-utils.js.map +1 -0
- package/dist/errors.d.ts +52 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +105 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/llm-provider.d.ts +29 -0
- package/dist/llm-provider.d.ts.map +1 -0
- package/dist/llm-provider.js +44 -0
- package/dist/llm-provider.js.map +1 -0
- package/dist/logger.d.ts +32 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +75 -0
- package/dist/logger.js.map +1 -0
- package/dist/mock-llm.d.ts +70 -0
- package/dist/mock-llm.d.ts.map +1 -0
- package/dist/mock-llm.js +385 -0
- package/dist/mock-llm.js.map +1 -0
- package/dist/orchestrator-helpers.d.ts +20 -0
- package/dist/orchestrator-helpers.d.ts.map +1 -0
- package/dist/orchestrator-helpers.js +38 -0
- package/dist/orchestrator-helpers.js.map +1 -0
- package/dist/orchestrator.d.ts +124 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +349 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/pipeline-registry.d.ts +120 -0
- package/dist/pipeline-registry.d.ts.map +1 -0
- package/dist/pipeline-registry.js +171 -0
- package/dist/pipeline-registry.js.map +1 -0
- package/dist/pipeline.d.ts +122 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +152 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/skill.d.ts +112 -0
- package/dist/skill.d.ts.map +1 -0
- package/dist/skill.js +12 -0
- package/dist/skill.js.map +1 -0
- package/dist/skills/io-skill.d.ts +49 -0
- package/dist/skills/io-skill.d.ts.map +1 -0
- package/dist/skills/io-skill.js +103 -0
- package/dist/skills/io-skill.js.map +1 -0
- package/dist/skills/llm-skill.d.ts +64 -0
- package/dist/skills/llm-skill.d.ts.map +1 -0
- package/dist/skills/llm-skill.js +112 -0
- package/dist/skills/llm-skill.js.map +1 -0
- package/dist/skills/transform-skill.d.ts +27 -0
- package/dist/skills/transform-skill.d.ts.map +1 -0
- package/dist/skills/transform-skill.js +32 -0
- package/dist/skills/transform-skill.js.map +1 -0
- package/dist/state/file-store.d.ts +25 -0
- package/dist/state/file-store.d.ts.map +1 -0
- package/dist/state/file-store.js +92 -0
- package/dist/state/file-store.js.map +1 -0
- package/dist/state/memory-store.d.ts +24 -0
- package/dist/state/memory-store.d.ts.map +1 -0
- package/dist/state/memory-store.js +65 -0
- package/dist/state/memory-store.js.map +1 -0
- package/dist/state/types.d.ts +40 -0
- package/dist/state/types.d.ts.map +1 -0
- package/dist/state/types.js +8 -0
- package/dist/state/types.js.map +1 -0
- package/dist/strategies/custom.d.ts +12 -0
- package/dist/strategies/custom.d.ts.map +1 -0
- package/dist/strategies/custom.js +14 -0
- package/dist/strategies/custom.js.map +1 -0
- package/dist/strategies/plan-and-execute.d.ts +27 -0
- package/dist/strategies/plan-and-execute.d.ts.map +1 -0
- package/dist/strategies/plan-and-execute.js +195 -0
- package/dist/strategies/plan-and-execute.js.map +1 -0
- package/dist/strategies/react.d.ts +27 -0
- package/dist/strategies/react.d.ts.map +1 -0
- package/dist/strategies/react.js +172 -0
- package/dist/strategies/react.js.map +1 -0
- package/dist/strategies/router.d.ts +11 -0
- package/dist/strategies/router.d.ts.map +1 -0
- package/dist/strategies/router.js +70 -0
- package/dist/strategies/router.js.map +1 -0
- package/dist/strategies/sequential.d.ts +12 -0
- package/dist/strategies/sequential.d.ts.map +1 -0
- package/dist/strategies/sequential.js +39 -0
- package/dist/strategies/sequential.js.map +1 -0
- package/dist/strategies/types.d.ts +62 -0
- package/dist/strategies/types.d.ts.map +1 -0
- package/dist/strategies/types.js +5 -0
- package/dist/strategies/types.js.map +1 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +28 -0
- package/src/agent.ts +243 -0
- package/src/cli-utils.ts +73 -0
- package/src/errors.ts +146 -0
- package/src/index.ts +124 -0
- package/src/llm-provider.ts +88 -0
- package/src/logger.ts +97 -0
- package/src/mock-llm.ts +433 -0
- package/src/orchestrator-helpers.ts +40 -0
- package/src/orchestrator.ts +522 -0
- package/src/pipeline-registry.ts +253 -0
- package/src/pipeline.ts +265 -0
- package/src/skill.ts +127 -0
- package/src/skills/io-skill.ts +133 -0
- package/src/skills/llm-skill.ts +207 -0
- package/src/skills/transform-skill.ts +61 -0
- package/src/state/file-store.ts +119 -0
- package/src/state/memory-store.ts +82 -0
- package/src/state/types.ts +53 -0
- package/src/strategies/custom.ts +24 -0
- package/src/strategies/plan-and-execute.ts +268 -0
- package/src/strategies/react.ts +239 -0
- package/src/strategies/router.ts +101 -0
- package/src/strategies/sequential.ts +55 -0
- package/src/strategies/types.ts +97 -0
- package/src/types.ts +102 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-and-Execute strategy — LLM creates a plan upfront, then executes it.
|
|
3
|
+
*
|
|
4
|
+
* 1. LLM receives goal + skill catalog → produces ordered execution plan
|
|
5
|
+
* 2. Each planned step is executed
|
|
6
|
+
* 3. Optional: re-plan after each step based on actual outputs
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Skill } from '../skill.js';
|
|
10
|
+
import type { Strategy, AgentContext, AgentResult, AgentTraceEntry } from './types.js';
|
|
11
|
+
|
|
12
|
+
interface PlanStep {
|
|
13
|
+
step_name: string;
|
|
14
|
+
skill: string;
|
|
15
|
+
input_description: string;
|
|
16
|
+
expected_output: string;
|
|
17
|
+
depends_on: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Plan {
|
|
21
|
+
reasoning: string;
|
|
22
|
+
steps: PlanStep[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PlanAndExecuteOptions {
|
|
26
|
+
/** Re-plan after each step based on actual output (default: false). */
|
|
27
|
+
replanAfterEachStep?: boolean;
|
|
28
|
+
/** Max number of re-plans allowed (default: 3). */
|
|
29
|
+
maxReplans?: number;
|
|
30
|
+
/** Custom planning prompt override. Receives skill catalog + goal. */
|
|
31
|
+
planPromptTemplate?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class PlanAndExecuteStrategy implements Strategy {
|
|
35
|
+
constructor(private options: PlanAndExecuteOptions = {}) {}
|
|
36
|
+
|
|
37
|
+
async execute(
|
|
38
|
+
skills: Skill[],
|
|
39
|
+
input: unknown,
|
|
40
|
+
ctx: AgentContext,
|
|
41
|
+
): Promise<AgentResult> {
|
|
42
|
+
const trace: AgentTraceEntry[] = [];
|
|
43
|
+
const startMs = Date.now();
|
|
44
|
+
const results = new Map<string, unknown>();
|
|
45
|
+
let replanCount = 0;
|
|
46
|
+
const maxReplans = this.options.maxReplans ?? 3;
|
|
47
|
+
|
|
48
|
+
// ── Phase 1: Plan ──────────────────────────────────────────────────────
|
|
49
|
+
let plan = await this.createPlan(skills, input, results, ctx);
|
|
50
|
+
trace.push({
|
|
51
|
+
type: 'plan',
|
|
52
|
+
content: plan,
|
|
53
|
+
timestamp: new Date().toISOString(),
|
|
54
|
+
});
|
|
55
|
+
ctx.logger.debug(`[plan-and-execute] Plan created: ${plan.steps.length} steps`);
|
|
56
|
+
|
|
57
|
+
// ── Phase 2: Execute ───────────────────────────────────────────────────
|
|
58
|
+
const sortedSteps = this.topologicalSort(plan.steps);
|
|
59
|
+
|
|
60
|
+
for (const planStep of sortedSteps) {
|
|
61
|
+
if (ctx.abortSignal.aborted) throw new Error(`Agent "${ctx.agentName}" aborted`);
|
|
62
|
+
|
|
63
|
+
const skill = skills.find((s) => s.meta.name === planStep.skill);
|
|
64
|
+
if (!skill) {
|
|
65
|
+
ctx.logger.warn(`[plan-and-execute] Skill "${planStep.skill}" not found — skipping`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Derive input from previous results
|
|
70
|
+
const stepInput = this.resolveStepInput(planStep, input, results);
|
|
71
|
+
|
|
72
|
+
ctx.logger.debug(
|
|
73
|
+
`[plan-and-execute] Executing step "${planStep.step_name}" with skill "${planStep.skill}"`,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const t0 = Date.now();
|
|
77
|
+
const output = await skill.execute(
|
|
78
|
+
stepInput as Parameters<typeof skill.execute>[0],
|
|
79
|
+
ctx.toSkillContext(),
|
|
80
|
+
);
|
|
81
|
+
const durationMs = Date.now() - t0;
|
|
82
|
+
|
|
83
|
+
results.set(planStep.step_name, output);
|
|
84
|
+
|
|
85
|
+
trace.push({
|
|
86
|
+
type: 'action',
|
|
87
|
+
skill: planStep.skill,
|
|
88
|
+
input: stepInput,
|
|
89
|
+
output,
|
|
90
|
+
timestamp: new Date().toISOString(),
|
|
91
|
+
content: { step: planStep.step_name, durationMs },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Optional re-plan
|
|
95
|
+
if (this.options.replanAfterEachStep && replanCount < maxReplans) {
|
|
96
|
+
const newPlan = await this.replan(skills, input, results, plan, ctx);
|
|
97
|
+
if (JSON.stringify(newPlan.steps) !== JSON.stringify(plan.steps)) {
|
|
98
|
+
plan = newPlan;
|
|
99
|
+
replanCount++;
|
|
100
|
+
trace.push({
|
|
101
|
+
type: 'replan',
|
|
102
|
+
content: { plan, replanCount },
|
|
103
|
+
timestamp: new Date().toISOString(),
|
|
104
|
+
});
|
|
105
|
+
ctx.logger.debug(`[plan-and-execute] Replanned (${replanCount}/${maxReplans})`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const finalOutput = results.size === 1
|
|
111
|
+
? results.values().next().value
|
|
112
|
+
: Object.fromEntries(results);
|
|
113
|
+
|
|
114
|
+
trace.push({ type: 'finish', output: finalOutput, timestamp: new Date().toISOString() });
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
output: finalOutput,
|
|
118
|
+
trace,
|
|
119
|
+
tokensUsed: ctx.totalTokens,
|
|
120
|
+
durationMs: Date.now() - startMs,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async createPlan(
|
|
125
|
+
skills: Skill[],
|
|
126
|
+
input: unknown,
|
|
127
|
+
existingResults: Map<string, unknown>,
|
|
128
|
+
ctx: AgentContext,
|
|
129
|
+
): Promise<Plan> {
|
|
130
|
+
const skillCatalog = skills.map((s) => ({
|
|
131
|
+
name: s.meta.name,
|
|
132
|
+
description: s.meta.description,
|
|
133
|
+
tags: s.meta.tags ?? [],
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
const completedStr =
|
|
137
|
+
existingResults.size > 0
|
|
138
|
+
? `\nALREADY COMPLETED: ${[...existingResults.keys()].join(', ')}`
|
|
139
|
+
: '';
|
|
140
|
+
|
|
141
|
+
const prompt =
|
|
142
|
+
this.options.planPromptTemplate ??
|
|
143
|
+
`You are a planning agent. Create an execution plan to achieve the goal.
|
|
144
|
+
|
|
145
|
+
ROLE: ${ctx.agentRole}
|
|
146
|
+
|
|
147
|
+
AVAILABLE SKILLS:
|
|
148
|
+
${JSON.stringify(skillCatalog, null, 2)}
|
|
149
|
+
|
|
150
|
+
GOAL INPUT:
|
|
151
|
+
${JSON.stringify(input, null, 2).substring(0, 3000)}
|
|
152
|
+
${completedStr}
|
|
153
|
+
|
|
154
|
+
Rules:
|
|
155
|
+
- Only include skills that are necessary
|
|
156
|
+
- You may use the same skill multiple times with different step_names
|
|
157
|
+
- depends_on must reference step_names defined earlier in the plan
|
|
158
|
+
- Keep input_description concise — it will be used to build the actual input
|
|
159
|
+
|
|
160
|
+
Respond with ONLY valid JSON:
|
|
161
|
+
{
|
|
162
|
+
"reasoning": "why this plan",
|
|
163
|
+
"steps": [
|
|
164
|
+
{
|
|
165
|
+
"step_name": "unique-step-id",
|
|
166
|
+
"skill": "skill-name",
|
|
167
|
+
"input_description": "what input to pass and where to get it from",
|
|
168
|
+
"expected_output": "what this produces",
|
|
169
|
+
"depends_on": ["step-name"]
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
}`;
|
|
173
|
+
|
|
174
|
+
const response = await ctx.llm.chat(
|
|
175
|
+
[{ role: 'user', content: prompt }],
|
|
176
|
+
{ temperature: 0.1, responseFormat: 'json' },
|
|
177
|
+
);
|
|
178
|
+
ctx.addTokens(response.usage.totalTokens);
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(response.content) as Plan;
|
|
182
|
+
} catch {
|
|
183
|
+
const match = response.content.match(/\{[\s\S]*\}/);
|
|
184
|
+
if (!match) throw new Error(`Plan-and-execute failed to produce valid JSON plan`);
|
|
185
|
+
return JSON.parse(match[0]) as Plan;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private async replan(
|
|
190
|
+
skills: Skill[],
|
|
191
|
+
input: unknown,
|
|
192
|
+
results: Map<string, unknown>,
|
|
193
|
+
currentPlan: Plan,
|
|
194
|
+
ctx: AgentContext,
|
|
195
|
+
): Promise<Plan> {
|
|
196
|
+
const completedStr = [...results.entries()]
|
|
197
|
+
.map(([k, v]) => `${k}: ${JSON.stringify(v).substring(0, 200)}`)
|
|
198
|
+
.join('\n');
|
|
199
|
+
|
|
200
|
+
const prompt = `You are re-planning based on actual results.
|
|
201
|
+
|
|
202
|
+
ROLE: ${ctx.agentRole}
|
|
203
|
+
|
|
204
|
+
ORIGINAL GOAL: ${JSON.stringify(input).substring(0, 1000)}
|
|
205
|
+
|
|
206
|
+
ORIGINAL PLAN:
|
|
207
|
+
${JSON.stringify(currentPlan.steps.map((s) => s.step_name), null, 2)}
|
|
208
|
+
|
|
209
|
+
COMPLETED STEPS AND OUTPUTS:
|
|
210
|
+
${completedStr}
|
|
211
|
+
|
|
212
|
+
AVAILABLE SKILLS:
|
|
213
|
+
${skills.map((s) => `${s.meta.name}: ${s.meta.description}`).join('\n')}
|
|
214
|
+
|
|
215
|
+
Do you need to adjust the remaining plan? Produce a complete updated plan (include completed steps too).
|
|
216
|
+
Respond with ONLY valid JSON using the same format as the original plan.`;
|
|
217
|
+
|
|
218
|
+
const response = await ctx.llm.chat(
|
|
219
|
+
[{ role: 'user', content: prompt }],
|
|
220
|
+
{ temperature: 0.1, responseFormat: 'json' },
|
|
221
|
+
);
|
|
222
|
+
ctx.addTokens(response.usage.totalTokens);
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
return JSON.parse(response.content) as Plan;
|
|
226
|
+
} catch {
|
|
227
|
+
return currentPlan; // keep original on parse failure
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private topologicalSort(steps: PlanStep[]): PlanStep[] {
|
|
232
|
+
const nameToStep = new Map(steps.map((s) => [s.step_name, s]));
|
|
233
|
+
const sorted: PlanStep[] = [];
|
|
234
|
+
const visited = new Set<string>();
|
|
235
|
+
|
|
236
|
+
const visit = (name: string): void => {
|
|
237
|
+
if (visited.has(name)) return;
|
|
238
|
+
const step = nameToStep.get(name);
|
|
239
|
+
if (!step) return;
|
|
240
|
+
for (const dep of step.depends_on) {
|
|
241
|
+
visit(dep);
|
|
242
|
+
}
|
|
243
|
+
visited.add(name);
|
|
244
|
+
sorted.push(step);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
for (const step of steps) visit(step.step_name);
|
|
248
|
+
return sorted;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private resolveStepInput(
|
|
252
|
+
step: PlanStep,
|
|
253
|
+
originalInput: unknown,
|
|
254
|
+
results: Map<string, unknown>,
|
|
255
|
+
): unknown {
|
|
256
|
+
// If there are dependencies, merge their outputs
|
|
257
|
+
if (step.depends_on.length === 0) return originalInput;
|
|
258
|
+
if (step.depends_on.length === 1) {
|
|
259
|
+
return results.get(step.depends_on[0]!) ?? originalInput;
|
|
260
|
+
}
|
|
261
|
+
// Multiple dependencies: merge all into one object
|
|
262
|
+
const merged: Record<string, unknown> = { _original: originalInput };
|
|
263
|
+
for (const dep of step.depends_on) {
|
|
264
|
+
merged[dep] = results.get(dep);
|
|
265
|
+
}
|
|
266
|
+
return merged;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReAct strategy — iterative Reasoning + Acting loop.
|
|
3
|
+
*
|
|
4
|
+
* Each iteration:
|
|
5
|
+
* 1. THINK — LLM reasons about what to do next
|
|
6
|
+
* 2. ACT — execute the chosen skill
|
|
7
|
+
* 3. OBSERVE — feed output back to LLM
|
|
8
|
+
* 4. Repeat until LLM decides to FINISH
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Skill } from '../skill.js';
|
|
12
|
+
import type {
|
|
13
|
+
Strategy,
|
|
14
|
+
AgentContext,
|
|
15
|
+
AgentResult,
|
|
16
|
+
AgentTraceEntry,
|
|
17
|
+
} from './types.js';
|
|
18
|
+
import { AgentMaxIterationsError } from '../errors.js';
|
|
19
|
+
|
|
20
|
+
interface ThoughtAction {
|
|
21
|
+
thought: string;
|
|
22
|
+
action:
|
|
23
|
+
| { type: 'use_skill'; skill: string; input: unknown }
|
|
24
|
+
| { type: 'finish'; output: unknown };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ReActOptions {
|
|
28
|
+
/** Maximum reasoning iterations before hard stop (default: 10). */
|
|
29
|
+
maxIterations?: number;
|
|
30
|
+
/** Reflect and self-correct every N steps (0 = disabled, default: 0). */
|
|
31
|
+
reflectionInterval?: number;
|
|
32
|
+
/** Custom system prompt additions. */
|
|
33
|
+
systemPromptSuffix?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class ReActStrategy implements Strategy {
|
|
37
|
+
constructor(private options: ReActOptions = {}) {}
|
|
38
|
+
|
|
39
|
+
async execute(
|
|
40
|
+
skills: Skill[],
|
|
41
|
+
input: unknown,
|
|
42
|
+
ctx: AgentContext,
|
|
43
|
+
): Promise<AgentResult> {
|
|
44
|
+
const trace: AgentTraceEntry[] = [];
|
|
45
|
+
const startMs = Date.now();
|
|
46
|
+
const maxIter = this.options.maxIterations ?? 10;
|
|
47
|
+
const reflectionInterval = this.options.reflectionInterval ?? 0;
|
|
48
|
+
|
|
49
|
+
const history: Array<{ thought: string; action: unknown; observation: unknown }> = [];
|
|
50
|
+
const skillMap = new Map(skills.map((s) => [s.meta.name, s]));
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < maxIter; i++) {
|
|
53
|
+
if (ctx.abortSignal.aborted) throw new Error(`Agent "${ctx.agentName}" aborted`);
|
|
54
|
+
|
|
55
|
+
// ── Periodic reflection ────────────────────────────────────────────
|
|
56
|
+
if (reflectionInterval > 0 && i > 0 && i % reflectionInterval === 0) {
|
|
57
|
+
const reflection = await this.reflect(history, ctx);
|
|
58
|
+
trace.push({
|
|
59
|
+
type: 'reflection',
|
|
60
|
+
content: reflection,
|
|
61
|
+
iteration: i,
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
});
|
|
64
|
+
ctx.logger.debug(`[react] Reflection at step ${i}: ${reflection.substring(0, 100)}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Think ─────────────────────────────────────────────────────────
|
|
68
|
+
const thought = await this.think(skills, input, history, ctx, i);
|
|
69
|
+
|
|
70
|
+
trace.push({
|
|
71
|
+
type: 'thought',
|
|
72
|
+
iteration: i,
|
|
73
|
+
content: thought.thought,
|
|
74
|
+
timestamp: new Date().toISOString(),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
ctx.logger.debug(`[react:${i}] Thought: ${thought.thought.substring(0, 100)}`);
|
|
78
|
+
|
|
79
|
+
// ── Check for finish ──────────────────────────────────────────────
|
|
80
|
+
if (thought.action.type === 'finish') {
|
|
81
|
+
trace.push({
|
|
82
|
+
type: 'finish',
|
|
83
|
+
output: thought.action.output,
|
|
84
|
+
iteration: i,
|
|
85
|
+
timestamp: new Date().toISOString(),
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
output: thought.action.output,
|
|
89
|
+
trace,
|
|
90
|
+
tokensUsed: ctx.totalTokens,
|
|
91
|
+
durationMs: Date.now() - startMs,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Act ───────────────────────────────────────────────────────────
|
|
96
|
+
const skillName = thought.action.skill;
|
|
97
|
+
const skill = skillMap.get(skillName);
|
|
98
|
+
|
|
99
|
+
let observation: unknown;
|
|
100
|
+
|
|
101
|
+
if (!skill) {
|
|
102
|
+
observation = {
|
|
103
|
+
error: `Skill "${skillName}" not found. Available: ${[...skillMap.keys()].join(', ')}`,
|
|
104
|
+
};
|
|
105
|
+
ctx.logger.warn(`[react:${i}] Unknown skill "${skillName}"`);
|
|
106
|
+
} else {
|
|
107
|
+
try {
|
|
108
|
+
ctx.logger.debug(`[react:${i}] Calling skill "${skillName}"`);
|
|
109
|
+
observation = await skill.execute(
|
|
110
|
+
thought.action.input as Parameters<typeof skill.execute>[0],
|
|
111
|
+
ctx.toSkillContext(),
|
|
112
|
+
);
|
|
113
|
+
trace.push({
|
|
114
|
+
type: 'action',
|
|
115
|
+
skill: skillName,
|
|
116
|
+
input: thought.action.input,
|
|
117
|
+
output: observation,
|
|
118
|
+
iteration: i,
|
|
119
|
+
timestamp: new Date().toISOString(),
|
|
120
|
+
});
|
|
121
|
+
} catch (err) {
|
|
122
|
+
observation = {
|
|
123
|
+
error: err instanceof Error ? err.message : String(err),
|
|
124
|
+
};
|
|
125
|
+
ctx.logger.warn(`[react:${i}] Skill "${skillName}" error: ${observation}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
trace.push({
|
|
130
|
+
type: 'observation',
|
|
131
|
+
content: observation,
|
|
132
|
+
iteration: i,
|
|
133
|
+
timestamp: new Date().toISOString(),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
history.push({
|
|
137
|
+
thought: thought.thought,
|
|
138
|
+
action: thought.action,
|
|
139
|
+
observation,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
throw new AgentMaxIterationsError(ctx.agentName, maxIter);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async think(
|
|
147
|
+
skills: Skill[],
|
|
148
|
+
originalInput: unknown,
|
|
149
|
+
history: Array<{ thought: string; action: unknown; observation: unknown }>,
|
|
150
|
+
ctx: AgentContext,
|
|
151
|
+
iteration: number,
|
|
152
|
+
): Promise<ThoughtAction> {
|
|
153
|
+
const skillList = skills
|
|
154
|
+
.map((s) => `- ${s.meta.name}: ${s.meta.description}`)
|
|
155
|
+
.join('\n');
|
|
156
|
+
|
|
157
|
+
const historyText =
|
|
158
|
+
history.length === 0
|
|
159
|
+
? 'No previous steps.'
|
|
160
|
+
: history
|
|
161
|
+
.map(
|
|
162
|
+
(h, i) =>
|
|
163
|
+
`[Step ${i + 1}]\nThought: ${h.thought}\nAction: ${JSON.stringify(h.action)}\nObservation: ${JSON.stringify(h.observation).substring(0, 800)}`,
|
|
164
|
+
)
|
|
165
|
+
.join('\n\n');
|
|
166
|
+
|
|
167
|
+
const systemPrompt = `${ctx.agentRole}
|
|
168
|
+
${this.options.systemPromptSuffix ?? ''}
|
|
169
|
+
|
|
170
|
+
You operate in a Thought → Action → Observation loop.
|
|
171
|
+
After each action you will see the result (observation) and decide what to do next.
|
|
172
|
+
When you have enough information, use { "type": "finish", "output": {...} } to stop.`;
|
|
173
|
+
|
|
174
|
+
const userPrompt = `AVAILABLE SKILLS:
|
|
175
|
+
${skillList}
|
|
176
|
+
|
|
177
|
+
ORIGINAL INPUT:
|
|
178
|
+
${JSON.stringify(originalInput, null, 2).substring(0, 2000)}
|
|
179
|
+
|
|
180
|
+
HISTORY (${history.length} steps so far):
|
|
181
|
+
${historyText}
|
|
182
|
+
|
|
183
|
+
Step ${iteration + 1}: What do you do next?
|
|
184
|
+
|
|
185
|
+
Respond with ONLY valid JSON:
|
|
186
|
+
{
|
|
187
|
+
"thought": "your step-by-step reasoning",
|
|
188
|
+
"action": { "type": "use_skill", "skill": "skill-name", "input": {...} }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
OR to finish:
|
|
192
|
+
{
|
|
193
|
+
"thought": "why I'm done",
|
|
194
|
+
"action": { "type": "finish", "output": {...} }
|
|
195
|
+
}`;
|
|
196
|
+
|
|
197
|
+
const response = await ctx.llm.chat(
|
|
198
|
+
[
|
|
199
|
+
{ role: 'system', content: systemPrompt },
|
|
200
|
+
{ role: 'user', content: userPrompt },
|
|
201
|
+
],
|
|
202
|
+
{ temperature: 0.2 },
|
|
203
|
+
);
|
|
204
|
+
ctx.addTokens(response.usage.totalTokens);
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
return JSON.parse(response.content) as ThoughtAction;
|
|
208
|
+
} catch {
|
|
209
|
+
const match = response.content.match(/\{[\s\S]*\}/);
|
|
210
|
+
if (!match) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`ReAct: LLM did not produce valid JSON at step ${iteration}. Got: ${response.content.substring(0, 300)}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return JSON.parse(match[0]) as ThoughtAction;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private async reflect(
|
|
220
|
+
history: Array<{ thought: string; action: unknown; observation: unknown }>,
|
|
221
|
+
ctx: AgentContext,
|
|
222
|
+
): Promise<string> {
|
|
223
|
+
const summary = history
|
|
224
|
+
.map((h, i) => `Step ${i + 1}: ${h.thought.substring(0, 80)} → ${JSON.stringify(h.action).substring(0, 80)}`)
|
|
225
|
+
.join('\n');
|
|
226
|
+
|
|
227
|
+
const response = await ctx.llm.chat(
|
|
228
|
+
[
|
|
229
|
+
{
|
|
230
|
+
role: 'user',
|
|
231
|
+
content: `Review your progress:\n${summary}\n\nAre you on track? Stuck in a loop? Should you change approach? Be brief.`,
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
{ temperature: 0.1 },
|
|
235
|
+
);
|
|
236
|
+
ctx.addTokens(response.usage.totalTokens);
|
|
237
|
+
return response.content;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router strategy — LLM picks ONE skill to handle the input.
|
|
3
|
+
*
|
|
4
|
+
* Useful for triage/classification: incoming request → route to specialist.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Skill } from '../skill.js';
|
|
8
|
+
import type { Strategy, AgentContext, AgentResult, AgentTraceEntry } from './types.js';
|
|
9
|
+
|
|
10
|
+
interface RouterDecision {
|
|
11
|
+
skill_index: number;
|
|
12
|
+
reasoning: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class RouterStrategy implements Strategy {
|
|
16
|
+
async execute(
|
|
17
|
+
skills: Skill[],
|
|
18
|
+
input: unknown,
|
|
19
|
+
ctx: AgentContext,
|
|
20
|
+
): Promise<AgentResult> {
|
|
21
|
+
const trace: AgentTraceEntry[] = [];
|
|
22
|
+
const startMs = Date.now();
|
|
23
|
+
|
|
24
|
+
// Build skill catalog
|
|
25
|
+
const skillList = skills
|
|
26
|
+
.map((s, i) => `${i}: ${s.meta.name} — ${s.meta.description}`)
|
|
27
|
+
.join('\n');
|
|
28
|
+
|
|
29
|
+
const routingPrompt = `You are a routing agent. Pick the BEST single skill to handle the input.
|
|
30
|
+
|
|
31
|
+
ROLE: ${ctx.agentRole}
|
|
32
|
+
|
|
33
|
+
AVAILABLE SKILLS:
|
|
34
|
+
${skillList}
|
|
35
|
+
|
|
36
|
+
INPUT:
|
|
37
|
+
${JSON.stringify(input, null, 2).substring(0, 3000)}
|
|
38
|
+
|
|
39
|
+
Respond with ONLY valid JSON — no markdown, no explanation:
|
|
40
|
+
{ "skill_index": <number 0-${skills.length - 1}>, "reasoning": "<brief explanation>" }`;
|
|
41
|
+
|
|
42
|
+
ctx.logger.debug(`[router] Routing to best skill among ${skills.length} options`);
|
|
43
|
+
|
|
44
|
+
const response = await ctx.llm.chat(
|
|
45
|
+
[{ role: 'user', content: routingPrompt }],
|
|
46
|
+
{ temperature: 0.1, responseFormat: 'json' },
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
ctx.addTokens(response.usage.totalTokens);
|
|
50
|
+
|
|
51
|
+
let decision: RouterDecision;
|
|
52
|
+
try {
|
|
53
|
+
decision = JSON.parse(response.content) as RouterDecision;
|
|
54
|
+
} catch {
|
|
55
|
+
// Fallback: find JSON in response
|
|
56
|
+
const match = response.content.match(/\{[\s\S]*\}/);
|
|
57
|
+
if (!match) {
|
|
58
|
+
throw new Error(`Router failed to produce valid JSON. Got: ${response.content.substring(0, 200)}`);
|
|
59
|
+
}
|
|
60
|
+
decision = JSON.parse(match[0]) as RouterDecision;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const chosenSkill = skills[decision.skill_index];
|
|
64
|
+
if (!chosenSkill) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Router chose invalid skill index ${decision.skill_index}. Available: 0-${skills.length - 1}`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
trace.push({
|
|
71
|
+
type: 'route',
|
|
72
|
+
content: {
|
|
73
|
+
chosen: chosenSkill.meta.name,
|
|
74
|
+
index: decision.skill_index,
|
|
75
|
+
reasoning: decision.reasoning,
|
|
76
|
+
},
|
|
77
|
+
timestamp: new Date().toISOString(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
ctx.logger.debug(`[router] Chose "${chosenSkill.meta.name}": ${decision.reasoning}`);
|
|
81
|
+
|
|
82
|
+
const output = await chosenSkill.execute(
|
|
83
|
+
input as Parameters<typeof chosenSkill.execute>[0],
|
|
84
|
+
ctx.toSkillContext(),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
trace.push({
|
|
88
|
+
type: 'finish',
|
|
89
|
+
skill: chosenSkill.meta.name,
|
|
90
|
+
output,
|
|
91
|
+
timestamp: new Date().toISOString(),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
output,
|
|
96
|
+
trace,
|
|
97
|
+
tokensUsed: ctx.totalTokens,
|
|
98
|
+
durationMs: Date.now() - startMs,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sequential strategy — runs all skills in order, passing output → input.
|
|
3
|
+
*
|
|
4
|
+
* The simplest strategy. No LLM calls at the agent level.
|
|
5
|
+
* Equivalent to a pipeline but wrapped in an agent for observability.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Skill } from '../skill.js';
|
|
9
|
+
import type { Strategy, AgentContext, AgentResult, AgentTraceEntry } from './types.js';
|
|
10
|
+
|
|
11
|
+
export class SequentialStrategy implements Strategy {
|
|
12
|
+
async execute(
|
|
13
|
+
skills: Skill[],
|
|
14
|
+
input: unknown,
|
|
15
|
+
ctx: AgentContext,
|
|
16
|
+
): Promise<AgentResult> {
|
|
17
|
+
const trace: AgentTraceEntry[] = [];
|
|
18
|
+
const startMs = Date.now();
|
|
19
|
+
let current = input;
|
|
20
|
+
|
|
21
|
+
for (const skill of skills) {
|
|
22
|
+
if (ctx.abortSignal.aborted) {
|
|
23
|
+
throw new Error(`Agent "${ctx.agentName}" aborted`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const skillCtx = ctx.toSkillContext();
|
|
27
|
+
ctx.logger.debug(`[sequential] Running skill "${skill.meta.name}"`);
|
|
28
|
+
const t0 = Date.now();
|
|
29
|
+
|
|
30
|
+
const output = await skill.execute(current as Parameters<typeof skill.execute>[0], skillCtx);
|
|
31
|
+
|
|
32
|
+
trace.push({
|
|
33
|
+
type: 'action',
|
|
34
|
+
skill: skill.meta.name,
|
|
35
|
+
input: current,
|
|
36
|
+
output,
|
|
37
|
+
timestamp: new Date().toISOString(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
ctx.logger.debug(
|
|
41
|
+
`[sequential] Skill "${skill.meta.name}" done in ${Date.now() - t0}ms`,
|
|
42
|
+
);
|
|
43
|
+
current = output;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
trace.push({ type: 'finish', output: current, timestamp: new Date().toISOString() });
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
output: current,
|
|
50
|
+
trace,
|
|
51
|
+
tokensUsed: ctx.totalTokens,
|
|
52
|
+
durationMs: Date.now() - startMs,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|