@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,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IOSkill — a skill that performs file, HTTP, or other IO operations.
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper around TransformSkill but with 'io' tag and
|
|
5
|
+
* helper factories for common patterns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z, type ZodSchema } from 'zod';
|
|
9
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
10
|
+
import { dirname } from 'path';
|
|
11
|
+
import type { Skill, SkillContext } from '../skill.js';
|
|
12
|
+
import { createTransformSkill } from './transform-skill.js';
|
|
13
|
+
|
|
14
|
+
// ── HTTP GET ─────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export function createHttpGetSkill(options: {
|
|
17
|
+
name: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
/** URL factory from input. */
|
|
20
|
+
url: (input: Record<string, unknown>) => string;
|
|
21
|
+
/** Headers factory from input. */
|
|
22
|
+
headers?: (input: Record<string, unknown>) => Record<string, string>;
|
|
23
|
+
/** Parse the response. Default: parse as JSON. */
|
|
24
|
+
parseResponse?: (body: string, status: number) => unknown;
|
|
25
|
+
timeout?: number;
|
|
26
|
+
retries?: number;
|
|
27
|
+
}): Skill {
|
|
28
|
+
return createTransformSkill({
|
|
29
|
+
name: options.name,
|
|
30
|
+
description: options.description ?? `HTTP GET skill: ${options.name}`,
|
|
31
|
+
tags: ['io', 'http'],
|
|
32
|
+
inputSchema: z.record(z.unknown()),
|
|
33
|
+
outputSchema: z.unknown() as ZodSchema<unknown>,
|
|
34
|
+
timeout: options.timeout,
|
|
35
|
+
async transform(input) {
|
|
36
|
+
const url = options.url(input);
|
|
37
|
+
const headers = options.headers?.(input) ?? {};
|
|
38
|
+
const res = await fetch(url, { headers });
|
|
39
|
+
const body = await res.text();
|
|
40
|
+
if (options.parseResponse) {
|
|
41
|
+
return options.parseResponse(body, res.status);
|
|
42
|
+
}
|
|
43
|
+
try { return JSON.parse(body); } catch { return body; }
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── HTTP POST ────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export function createHttpPostSkill(options: {
|
|
51
|
+
name: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
url: (input: Record<string, unknown>) => string;
|
|
54
|
+
body: (input: Record<string, unknown>) => unknown;
|
|
55
|
+
headers?: (input: Record<string, unknown>) => Record<string, string>;
|
|
56
|
+
parseResponse?: (body: string, status: number) => unknown;
|
|
57
|
+
timeout?: number;
|
|
58
|
+
}): Skill {
|
|
59
|
+
return createTransformSkill({
|
|
60
|
+
name: options.name,
|
|
61
|
+
description: options.description ?? `HTTP POST skill: ${options.name}`,
|
|
62
|
+
tags: ['io', 'http'],
|
|
63
|
+
inputSchema: z.record(z.unknown()),
|
|
64
|
+
outputSchema: z.unknown() as ZodSchema<unknown>,
|
|
65
|
+
timeout: options.timeout,
|
|
66
|
+
async transform(input) {
|
|
67
|
+
const url = options.url(input);
|
|
68
|
+
const headers = { 'Content-Type': 'application/json', ...(options.headers?.(input) ?? {}) };
|
|
69
|
+
const body = JSON.stringify(options.body(input));
|
|
70
|
+
const res = await fetch(url, { method: 'POST', headers, body });
|
|
71
|
+
const text = await res.text();
|
|
72
|
+
if (options.parseResponse) {
|
|
73
|
+
return options.parseResponse(text, res.status);
|
|
74
|
+
}
|
|
75
|
+
try { return JSON.parse(text); } catch { return text; }
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── File Read ─────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export function createReadFileSkill(options: {
|
|
83
|
+
name: string;
|
|
84
|
+
description?: string;
|
|
85
|
+
path: (input: Record<string, unknown>) => string;
|
|
86
|
+
encoding?: BufferEncoding;
|
|
87
|
+
}): Skill {
|
|
88
|
+
return createTransformSkill({
|
|
89
|
+
name: options.name,
|
|
90
|
+
description: options.description ?? `Read file skill: ${options.name}`,
|
|
91
|
+
tags: ['io', 'file'],
|
|
92
|
+
inputSchema: z.record(z.unknown()),
|
|
93
|
+
outputSchema: z.object({ content: z.string(), path: z.string() }),
|
|
94
|
+
async transform(input) {
|
|
95
|
+
const path = options.path(input);
|
|
96
|
+
const content = await readFile(path, options.encoding ?? 'utf-8');
|
|
97
|
+
return { content, path };
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── File Write ────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
export function createWriteFileSkill(options: {
|
|
105
|
+
name: string;
|
|
106
|
+
description?: string;
|
|
107
|
+
path: (input: Record<string, unknown>) => string;
|
|
108
|
+
content: (input: Record<string, unknown>) => string;
|
|
109
|
+
encoding?: BufferEncoding;
|
|
110
|
+
}): Skill {
|
|
111
|
+
return createTransformSkill({
|
|
112
|
+
name: options.name,
|
|
113
|
+
description: options.description ?? `Write file skill: ${options.name}`,
|
|
114
|
+
tags: ['io', 'file'],
|
|
115
|
+
inputSchema: z.record(z.unknown()),
|
|
116
|
+
outputSchema: z.object({ path: z.string(), bytesWritten: z.number() }),
|
|
117
|
+
async transform(input) {
|
|
118
|
+
const path = options.path(input);
|
|
119
|
+
const content = options.content(input);
|
|
120
|
+
await mkdir(dirname(path), { recursive: true });
|
|
121
|
+
await writeFile(path, content, options.encoding ?? 'utf-8');
|
|
122
|
+
return { path, bytesWritten: Buffer.byteLength(content) };
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Namespace export */
|
|
128
|
+
export const IOSkill = {
|
|
129
|
+
httpGet: createHttpGetSkill,
|
|
130
|
+
httpPost: createHttpPostSkill,
|
|
131
|
+
readFile: createReadFileSkill,
|
|
132
|
+
writeFile: createWriteFileSkill,
|
|
133
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLMSkill — builds a Skill that calls an LLM with a prompt template.
|
|
3
|
+
*
|
|
4
|
+
* The primary skill type for LLM-backed operations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z, type ZodSchema } from 'zod';
|
|
8
|
+
import type { Skill, SkillMeta, SkillContext } from '../skill.js';
|
|
9
|
+
import type { Message } from '../types.js';
|
|
10
|
+
import { SkillError, SkillValidationError } from '../errors.js';
|
|
11
|
+
import { withRetry, withTimeout } from '../llm-provider.js';
|
|
12
|
+
|
|
13
|
+
// ── LLMSkill Config ───────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface LLMSkillConfig<TInput, TOutput> {
|
|
16
|
+
/** Unique skill name. */
|
|
17
|
+
name: string;
|
|
18
|
+
/** Human-readable description used in agent routing/planning prompts. */
|
|
19
|
+
description: string;
|
|
20
|
+
/** Version string (default '1.0.0'). */
|
|
21
|
+
version?: string;
|
|
22
|
+
/** Tags for categorization. */
|
|
23
|
+
tags?: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Which LLM from the Orchestrator registry to use.
|
|
26
|
+
* Defaults to 'default'. Can be overridden per-execution.
|
|
27
|
+
*/
|
|
28
|
+
llm?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Prompt factory. Receives validated input, returns prompt string or messages array.
|
|
31
|
+
*/
|
|
32
|
+
prompt: (input: TInput) => string | Message[];
|
|
33
|
+
/**
|
|
34
|
+
* Optional system message prepended to every call.
|
|
35
|
+
*/
|
|
36
|
+
systemMessage?: string | ((input: TInput) => string);
|
|
37
|
+
/**
|
|
38
|
+
* Zod schema for input validation.
|
|
39
|
+
*/
|
|
40
|
+
inputSchema: ZodSchema<TInput>;
|
|
41
|
+
/**
|
|
42
|
+
* Zod schema for output validation.
|
|
43
|
+
*/
|
|
44
|
+
outputSchema: ZodSchema<TOutput>;
|
|
45
|
+
/**
|
|
46
|
+
* Function to parse the raw LLM text response into TOutput.
|
|
47
|
+
* If not provided, the raw text is returned as-is (must match outputSchema).
|
|
48
|
+
*/
|
|
49
|
+
parseOutput?: (raw: string, input: TInput) => TOutput;
|
|
50
|
+
/**
|
|
51
|
+
* LLM call options (temperature, maxTokens, etc.).
|
|
52
|
+
*/
|
|
53
|
+
llmOptions?: {
|
|
54
|
+
temperature?: number;
|
|
55
|
+
maxTokens?: number;
|
|
56
|
+
responseFormat?: 'text' | 'json';
|
|
57
|
+
};
|
|
58
|
+
/** Number of retries on LLM failure (default 1). */
|
|
59
|
+
retries?: number;
|
|
60
|
+
/** Timeout in ms (default none). */
|
|
61
|
+
timeout?: number;
|
|
62
|
+
/** Cache config. */
|
|
63
|
+
cache?: SkillMeta['cache'];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── LLMSkill Builder ──────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export function createLLMSkill<TInput, TOutput>(
|
|
69
|
+
config: LLMSkillConfig<TInput, TOutput>,
|
|
70
|
+
): Skill<TInput, TOutput> {
|
|
71
|
+
const meta: SkillMeta = {
|
|
72
|
+
name: config.name,
|
|
73
|
+
description: config.description,
|
|
74
|
+
version: config.version ?? '1.0.0',
|
|
75
|
+
tags: config.tags ?? ['llm'],
|
|
76
|
+
retries: config.retries ?? 1,
|
|
77
|
+
timeout: config.timeout,
|
|
78
|
+
cache: config.cache,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
meta,
|
|
83
|
+
inputSchema: config.inputSchema,
|
|
84
|
+
outputSchema: config.outputSchema,
|
|
85
|
+
|
|
86
|
+
async execute(input: TInput, ctx: SkillContext): Promise<TOutput> {
|
|
87
|
+
// ── Input validation ──
|
|
88
|
+
const parsed = config.inputSchema.safeParse(input);
|
|
89
|
+
if (!parsed.success) {
|
|
90
|
+
throw new SkillValidationError(
|
|
91
|
+
config.name,
|
|
92
|
+
'input',
|
|
93
|
+
`Input validation failed: ${parsed.error.message}`,
|
|
94
|
+
parsed.error.issues,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const validInput = parsed.data;
|
|
98
|
+
|
|
99
|
+
// ── Cache lookup ──
|
|
100
|
+
if (config.cache) {
|
|
101
|
+
const cacheKey = `llmskill:${config.name}:${config.cache.key(validInput)}`;
|
|
102
|
+
const cached = await ctx.state.get<TOutput>(cacheKey);
|
|
103
|
+
if (cached !== null) {
|
|
104
|
+
ctx.logger.debug(`Cache hit for skill "${config.name}"`);
|
|
105
|
+
return cached;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Resolve LLM ──
|
|
110
|
+
const llm = ctx.getLLM(config.llm ?? 'default');
|
|
111
|
+
|
|
112
|
+
// ── Build messages ──
|
|
113
|
+
const promptResult = config.prompt(validInput);
|
|
114
|
+
let messages: Message[];
|
|
115
|
+
|
|
116
|
+
if (typeof promptResult === 'string') {
|
|
117
|
+
messages = [{ role: 'user', content: promptResult }];
|
|
118
|
+
} else {
|
|
119
|
+
messages = promptResult;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Prepend system message if provided
|
|
123
|
+
if (config.systemMessage) {
|
|
124
|
+
const sysContent =
|
|
125
|
+
typeof config.systemMessage === 'string'
|
|
126
|
+
? config.systemMessage
|
|
127
|
+
: config.systemMessage(validInput);
|
|
128
|
+
messages = [{ role: 'system', content: sysContent }, ...messages];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Execute with retry/timeout ──
|
|
132
|
+
const retries = config.retries ?? 1;
|
|
133
|
+
const timeout = config.timeout;
|
|
134
|
+
|
|
135
|
+
const doCall = () =>
|
|
136
|
+
llm.chat(messages, {
|
|
137
|
+
temperature: config.llmOptions?.temperature,
|
|
138
|
+
maxTokens: config.llmOptions?.maxTokens,
|
|
139
|
+
responseFormat: config.llmOptions?.responseFormat,
|
|
140
|
+
retries: 0, // retries handled here
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const callWithRetry = () =>
|
|
144
|
+
withRetry(
|
|
145
|
+
doCall,
|
|
146
|
+
retries,
|
|
147
|
+
(attempt, err) =>
|
|
148
|
+
ctx.logger.warn(
|
|
149
|
+
`Skill "${config.name}" retry ${attempt}/${retries}: ${err.message}`,
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const response = await (timeout
|
|
154
|
+
? withTimeout(callWithRetry, timeout, config.name)
|
|
155
|
+
: callWithRetry());
|
|
156
|
+
|
|
157
|
+
ctx.emit('llm:response', {
|
|
158
|
+
skill: config.name,
|
|
159
|
+
tokens: response.usage.totalTokens,
|
|
160
|
+
model: response.model,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ── Parse output ──
|
|
164
|
+
let output: TOutput;
|
|
165
|
+
try {
|
|
166
|
+
if (config.parseOutput) {
|
|
167
|
+
output = config.parseOutput(response.content, validInput);
|
|
168
|
+
} else {
|
|
169
|
+
// Try JSON parse if schema expects object, else return raw string
|
|
170
|
+
try {
|
|
171
|
+
output = JSON.parse(response.content) as TOutput;
|
|
172
|
+
} catch {
|
|
173
|
+
output = response.content as unknown as TOutput;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
throw new SkillError(
|
|
178
|
+
config.name,
|
|
179
|
+
`Output parsing failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
180
|
+
{ raw: response.content.substring(0, 500) },
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Output validation ──
|
|
185
|
+
const outParsed = config.outputSchema.safeParse(output);
|
|
186
|
+
if (!outParsed.success) {
|
|
187
|
+
ctx.logger.warn(
|
|
188
|
+
`Skill "${config.name}" output failed schema validation — returning raw`,
|
|
189
|
+
outParsed.error.issues,
|
|
190
|
+
);
|
|
191
|
+
// Don't hard-fail on output validation — return what we have
|
|
192
|
+
// This matches real-world LLM usage where output schema is a guide not a gate
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Cache store ──
|
|
196
|
+
if (config.cache) {
|
|
197
|
+
const cacheKey = `llmskill:${config.name}:${config.cache.key(validInput)}`;
|
|
198
|
+
await ctx.state.set(cacheKey, output, config.cache.ttl);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return output;
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Namespace export for ergonomic usage: LLMSkill.create(...) */
|
|
207
|
+
export const LLMSkill = { create: createLLMSkill };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TransformSkill — a pure data transformation skill (no LLM).
|
|
3
|
+
*
|
|
4
|
+
* Pure data transformation skill.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ZodSchema } from 'zod';
|
|
8
|
+
import type { Skill, SkillMeta, SkillContext } from '../skill.js';
|
|
9
|
+
import { SkillValidationError } from '../errors.js';
|
|
10
|
+
|
|
11
|
+
export interface TransformSkillConfig<TInput, TOutput> {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
version?: string;
|
|
15
|
+
tags?: string[];
|
|
16
|
+
inputSchema: ZodSchema<TInput>;
|
|
17
|
+
outputSchema: ZodSchema<TOutput>;
|
|
18
|
+
/**
|
|
19
|
+
* The transformation function. Pure sync or async.
|
|
20
|
+
* The second argument (ctx) is optional — most transforms won't need it.
|
|
21
|
+
*/
|
|
22
|
+
transform: (input: TInput, ctx?: SkillContext) => TOutput | Promise<TOutput>;
|
|
23
|
+
timeout?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createTransformSkill<TInput, TOutput>(
|
|
27
|
+
config: TransformSkillConfig<TInput, TOutput>,
|
|
28
|
+
): Skill<TInput, TOutput> {
|
|
29
|
+
const meta: SkillMeta = {
|
|
30
|
+
name: config.name,
|
|
31
|
+
description: config.description,
|
|
32
|
+
version: config.version ?? '1.0.0',
|
|
33
|
+
tags: config.tags ?? ['transform'],
|
|
34
|
+
timeout: config.timeout,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
meta,
|
|
39
|
+
inputSchema: config.inputSchema,
|
|
40
|
+
outputSchema: config.outputSchema,
|
|
41
|
+
|
|
42
|
+
async execute(input: TInput, ctx: SkillContext): Promise<TOutput> {
|
|
43
|
+
// Input validation
|
|
44
|
+
const parsed = config.inputSchema.safeParse(input);
|
|
45
|
+
if (!parsed.success) {
|
|
46
|
+
throw new SkillValidationError(
|
|
47
|
+
config.name,
|
|
48
|
+
'input',
|
|
49
|
+
`Input validation failed: ${parsed.error.message}`,
|
|
50
|
+
parsed.error.issues,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const output = await config.transform(parsed.data, ctx);
|
|
55
|
+
return output;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Namespace export: TransformSkill.create(...) */
|
|
61
|
+
export const TransformSkill = { create: createTransformSkill };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-based StateStore — persists data as JSON files.
|
|
3
|
+
* Good for development and small-scale use. No external dependencies.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from 'fs';
|
|
7
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
8
|
+
import { join, dirname } from 'path';
|
|
9
|
+
import type { StateStore, RunFilter } from './types.js';
|
|
10
|
+
import type { PipelineRun } from '../types.js';
|
|
11
|
+
|
|
12
|
+
interface CacheEntry {
|
|
13
|
+
value: unknown;
|
|
14
|
+
expiresAt: number | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface FileStoreData {
|
|
18
|
+
cache: Record<string, CacheEntry>;
|
|
19
|
+
runs: Record<string, PipelineRun>;
|
|
20
|
+
checkpoints: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class FileStore implements StateStore {
|
|
24
|
+
private readonly dataFile: string;
|
|
25
|
+
private data: FileStoreData = { cache: {}, runs: {}, checkpoints: {} };
|
|
26
|
+
|
|
27
|
+
constructor(dataDir = '.flomatai') {
|
|
28
|
+
this.dataFile = join(dataDir, 'store.json');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async init(): Promise<void> {
|
|
32
|
+
await mkdir(dirname(this.dataFile), { recursive: true });
|
|
33
|
+
if (existsSync(this.dataFile)) {
|
|
34
|
+
try {
|
|
35
|
+
const raw = await readFile(this.dataFile, 'utf-8');
|
|
36
|
+
this.data = JSON.parse(raw) as FileStoreData;
|
|
37
|
+
} catch {
|
|
38
|
+
this.data = { cache: {}, runs: {}, checkpoints: {} };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async close(): Promise<void> {
|
|
44
|
+
await this.flush();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private async flush(): Promise<void> {
|
|
48
|
+
await mkdir(dirname(this.dataFile), { recursive: true });
|
|
49
|
+
await writeFile(this.dataFile, JSON.stringify(this.data, null, 2), 'utf-8');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async get<T>(key: string): Promise<T | null> {
|
|
53
|
+
const entry = this.data.cache[key];
|
|
54
|
+
if (!entry) return null;
|
|
55
|
+
if (entry.expiresAt !== null && Date.now() > entry.expiresAt) {
|
|
56
|
+
delete this.data.cache[key];
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return entry.value as T;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async set<T>(key: string, value: T, ttlSeconds?: number): Promise<void> {
|
|
63
|
+
this.data.cache[key] = {
|
|
64
|
+
value,
|
|
65
|
+
expiresAt: ttlSeconds ? Date.now() + ttlSeconds * 1000 : null,
|
|
66
|
+
};
|
|
67
|
+
await this.flush();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async delete(key: string): Promise<void> {
|
|
71
|
+
delete this.data.cache[key];
|
|
72
|
+
await this.flush();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async has(key: string): Promise<boolean> {
|
|
76
|
+
const v = await this.get(key);
|
|
77
|
+
return v !== null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async saveRun(run: PipelineRun): Promise<void> {
|
|
81
|
+
this.data.runs[run.id] = { ...run };
|
|
82
|
+
await this.flush();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async getRun(runId: string): Promise<PipelineRun | null> {
|
|
86
|
+
return this.data.runs[runId] ?? null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async listRuns(filter?: RunFilter): Promise<PipelineRun[]> {
|
|
90
|
+
let runs = Object.values(this.data.runs).sort(
|
|
91
|
+
(a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime(),
|
|
92
|
+
);
|
|
93
|
+
if (filter?.pipelineName) {
|
|
94
|
+
runs = runs.filter((r) => r.pipelineName === filter.pipelineName);
|
|
95
|
+
}
|
|
96
|
+
if (filter?.status) {
|
|
97
|
+
runs = runs.filter((r) => r.status === filter.status);
|
|
98
|
+
}
|
|
99
|
+
const offset = filter?.offset ?? 0;
|
|
100
|
+
const limit = filter?.limit ?? 50;
|
|
101
|
+
return runs.slice(offset, offset + limit);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async saveCheckpoint(runId: string, stepName: string, data: unknown): Promise<void> {
|
|
105
|
+
this.data.checkpoints[`${runId}::${stepName}`] = data;
|
|
106
|
+
await this.flush();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async getCheckpoint(runId: string, stepName: string): Promise<unknown | null> {
|
|
110
|
+
return this.data.checkpoints[`${runId}::${stepName}`] ?? null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async clearCheckpoints(runId: string): Promise<void> {
|
|
114
|
+
for (const key of Object.keys(this.data.checkpoints)) {
|
|
115
|
+
if (key.startsWith(`${runId}::`)) delete this.data.checkpoints[key];
|
|
116
|
+
}
|
|
117
|
+
await this.flush();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory StateStore — useful for testing and single-run pipelines.
|
|
3
|
+
* No persistence; data is lost on process exit.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { StateStore, RunFilter } from './types.js';
|
|
7
|
+
import type { PipelineRun } from '../types.js';
|
|
8
|
+
|
|
9
|
+
interface CacheEntry {
|
|
10
|
+
value: unknown;
|
|
11
|
+
expiresAt: number | null; // null = no expiry
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class MemoryStore implements StateStore {
|
|
15
|
+
private cache = new Map<string, CacheEntry>();
|
|
16
|
+
private runs = new Map<string, PipelineRun>();
|
|
17
|
+
private checkpoints = new Map<string, unknown>(); // key: `${runId}::${stepName}`
|
|
18
|
+
|
|
19
|
+
async init(): Promise<void> { /* no-op */ }
|
|
20
|
+
async close(): Promise<void> { /* no-op */ }
|
|
21
|
+
|
|
22
|
+
async get<T>(key: string): Promise<T | null> {
|
|
23
|
+
const entry = this.cache.get(key);
|
|
24
|
+
if (!entry) return null;
|
|
25
|
+
if (entry.expiresAt !== null && Date.now() > entry.expiresAt) {
|
|
26
|
+
this.cache.delete(key);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return entry.value as T;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async set<T>(key: string, value: T, ttlSeconds?: number): Promise<void> {
|
|
33
|
+
this.cache.set(key, {
|
|
34
|
+
value,
|
|
35
|
+
expiresAt: ttlSeconds ? Date.now() + ttlSeconds * 1000 : null,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async delete(key: string): Promise<void> {
|
|
40
|
+
this.cache.delete(key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async has(key: string): Promise<boolean> {
|
|
44
|
+
const v = await this.get(key);
|
|
45
|
+
return v !== null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async saveRun(run: PipelineRun): Promise<void> {
|
|
49
|
+
this.runs.set(run.id, { ...run });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async getRun(runId: string): Promise<PipelineRun | null> {
|
|
53
|
+
return this.runs.get(runId) ?? null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async listRuns(filter?: RunFilter): Promise<PipelineRun[]> {
|
|
57
|
+
let runs = [...this.runs.values()].reverse(); // newest first
|
|
58
|
+
if (filter?.pipelineName) {
|
|
59
|
+
runs = runs.filter((r) => r.pipelineName === filter.pipelineName);
|
|
60
|
+
}
|
|
61
|
+
if (filter?.status) {
|
|
62
|
+
runs = runs.filter((r) => r.status === filter.status);
|
|
63
|
+
}
|
|
64
|
+
const offset = filter?.offset ?? 0;
|
|
65
|
+
const limit = filter?.limit ?? 50;
|
|
66
|
+
return runs.slice(offset, offset + limit);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async saveCheckpoint(runId: string, stepName: string, data: unknown): Promise<void> {
|
|
70
|
+
this.checkpoints.set(`${runId}::${stepName}`, data);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async getCheckpoint(runId: string, stepName: string): Promise<unknown | null> {
|
|
74
|
+
return this.checkpoints.get(`${runId}::${stepName}`) ?? null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async clearCheckpoints(runId: string): Promise<void> {
|
|
78
|
+
for (const key of this.checkpoints.keys()) {
|
|
79
|
+
if (key.startsWith(`${runId}::`)) this.checkpoints.delete(key);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StateStore — pluggable persistence interface.
|
|
3
|
+
*
|
|
4
|
+
* Adapters: @flomatai/state-file (default), @flomatai/state-sqlite,
|
|
5
|
+
* @flomatai/state-redis, @flomatai/state-postgres
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PipelineRun } from '../types.js';
|
|
9
|
+
|
|
10
|
+
// ── Core Store Interface ──────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export interface StateStore {
|
|
13
|
+
/** Initialize the store (open connections, create tables, etc.). */
|
|
14
|
+
init(): Promise<void>;
|
|
15
|
+
/** Tear down the store (close connections). */
|
|
16
|
+
close(): Promise<void>;
|
|
17
|
+
|
|
18
|
+
// ── Key/Value ────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/** Get a value by key. Returns null if not found or expired. */
|
|
21
|
+
get<T>(key: string): Promise<T | null>;
|
|
22
|
+
/** Set a value with optional TTL (seconds). */
|
|
23
|
+
set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
|
|
24
|
+
/** Delete a key. */
|
|
25
|
+
delete(key: string): Promise<void>;
|
|
26
|
+
/** Check if a key exists. */
|
|
27
|
+
has(key: string): Promise<boolean>;
|
|
28
|
+
|
|
29
|
+
// ── Run History ──────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/** Persist a pipeline run (upsert by run.id). */
|
|
32
|
+
saveRun(run: PipelineRun): Promise<void>;
|
|
33
|
+
/** Retrieve a run by ID. */
|
|
34
|
+
getRun(runId: string): Promise<PipelineRun | null>;
|
|
35
|
+
/** List runs, newest first. */
|
|
36
|
+
listRuns(filter?: RunFilter): Promise<PipelineRun[]>;
|
|
37
|
+
|
|
38
|
+
// ── Checkpoints (for resume) ─────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/** Save a step checkpoint so a failed run can be resumed. */
|
|
41
|
+
saveCheckpoint(runId: string, stepName: string, data: unknown): Promise<void>;
|
|
42
|
+
/** Get a step checkpoint. */
|
|
43
|
+
getCheckpoint(runId: string, stepName: string): Promise<unknown | null>;
|
|
44
|
+
/** Clear all checkpoints for a run (after successful completion). */
|
|
45
|
+
clearCheckpoints(runId: string): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface RunFilter {
|
|
49
|
+
pipelineName?: string;
|
|
50
|
+
status?: string;
|
|
51
|
+
limit?: number;
|
|
52
|
+
offset?: number;
|
|
53
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom strategy — user provides their own strategy function.
|
|
3
|
+
* Maximum flexibility.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Skill } from '../skill.js';
|
|
7
|
+
import type {
|
|
8
|
+
Strategy,
|
|
9
|
+
AgentContext,
|
|
10
|
+
AgentResult,
|
|
11
|
+
CustomStrategyFn,
|
|
12
|
+
} from './types.js';
|
|
13
|
+
|
|
14
|
+
export class CustomStrategy implements Strategy {
|
|
15
|
+
constructor(private fn: CustomStrategyFn) {}
|
|
16
|
+
|
|
17
|
+
async execute(
|
|
18
|
+
skills: Skill[],
|
|
19
|
+
input: unknown,
|
|
20
|
+
ctx: AgentContext,
|
|
21
|
+
): Promise<AgentResult> {
|
|
22
|
+
return this.fn(skills, input, ctx);
|
|
23
|
+
}
|
|
24
|
+
}
|