@flowcodex/core 0.3.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/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/index-LbxYtxxS.d.ts +560 -0
- package/dist/index.d.ts +995 -0
- package/dist/index.js +3840 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/index.d.ts +1 -0
- package/dist/kernel/index.js +551 -0
- package/dist/kernel/index.js.map +1 -0
- package/package.json +39 -0
- package/src/agent/agent-loop.ts +254 -0
- package/src/agent/context.ts +99 -0
- package/src/agent/conversation-state.ts +44 -0
- package/src/agent/provider-runner.ts +241 -0
- package/src/agent/system-prompt-builder.ts +193 -0
- package/src/execution/compactor.ts +256 -0
- package/src/execution/index.ts +7 -0
- package/src/execution/output-serializer.ts +90 -0
- package/src/execution/schema-validator.ts +124 -0
- package/src/execution/tool-executor.ts +276 -0
- package/src/execution/tool-registry.ts +104 -0
- package/src/index.ts +215 -0
- package/src/infrastructure/catalog-parser.ts +218 -0
- package/src/infrastructure/index.ts +16 -0
- package/src/infrastructure/path-resolver.ts +123 -0
- package/src/infrastructure/provider-factory.ts +116 -0
- package/src/infrastructure/provider-presets.ts +19 -0
- package/src/infrastructure/retry-policy.ts +50 -0
- package/src/infrastructure/secret-scrubber.ts +67 -0
- package/src/infrastructure/token-counter.ts +156 -0
- package/src/infrastructure/tracer.ts +23 -0
- package/src/kernel/container.ts +166 -0
- package/src/kernel/events.ts +323 -0
- package/src/kernel/index.ts +18 -0
- package/src/kernel/pipeline.ts +152 -0
- package/src/kernel/run-controller.ts +85 -0
- package/src/kernel/tokens.ts +21 -0
- package/src/security/index.ts +13 -0
- package/src/security/permission-policy.ts +273 -0
- package/src/session/audit-log.ts +201 -0
- package/src/session/auth-service.ts +178 -0
- package/src/session/index.ts +26 -0
- package/src/session/secret-vault.ts +183 -0
- package/src/session/session-store.ts +339 -0
- package/src/session/types.ts +100 -0
- package/src/types/blocks.ts +56 -0
- package/src/types/context.ts +54 -0
- package/src/types/errors.ts +359 -0
- package/src/types/index.ts +34 -0
- package/src/types/provider.ts +58 -0
- package/src/types/tool.ts +39 -0
- package/src/utils/error.ts +3 -0
- package/src/utils/fs.ts +185 -0
- package/src/utils/image-resize.ts +76 -0
- package/src/utils/ssrf-guard.ts +133 -0
- package/src/utils/ulid.ts +72 -0
- package/src/utils/version-check.ts +59 -0
- package/tests/agent-loop.test.ts +490 -0
- package/tests/audit-log.test.ts +199 -0
- package/tests/auth-service.test.ts +170 -0
- package/tests/blocks.test.ts +79 -0
- package/tests/catalog-parser.test.ts +174 -0
- package/tests/compactor.test.ts +180 -0
- package/tests/container.test.ts +224 -0
- package/tests/conversation-state.test.ts +75 -0
- package/tests/errors.test.ts +429 -0
- package/tests/events-v021.test.ts +60 -0
- package/tests/events-v022.test.ts +75 -0
- package/tests/events.test.ts +340 -0
- package/tests/fixtures/large-image.png +0 -0
- package/tests/fixtures/small-image.png +0 -0
- package/tests/fs-utils.test.ts +164 -0
- package/tests/image-resize.test.ts +51 -0
- package/tests/output-serializer.test.ts +79 -0
- package/tests/path-resolver.test.ts +91 -0
- package/tests/permission-policy.test.ts +174 -0
- package/tests/pipeline.test.ts +193 -0
- package/tests/provider-factory.test.ts +245 -0
- package/tests/provider-runner.test.ts +535 -0
- package/tests/retry-policy.test.ts +104 -0
- package/tests/run-controller.test.ts +115 -0
- package/tests/sanity.test.ts +26 -0
- package/tests/schema-validator.test.ts +109 -0
- package/tests/secret-scrubber.test.ts +133 -0
- package/tests/secret-vault.test.ts +130 -0
- package/tests/session-store.test.ts +429 -0
- package/tests/ssrf-guard.test.ts +112 -0
- package/tests/system-prompt-builder.test.ts +116 -0
- package/tests/token-counter.test.ts +163 -0
- package/tests/tokens.test.ts +42 -0
- package/tests/tool-executor.test.ts +452 -0
- package/tests/tool-registry.test.ts +143 -0
- package/tests/tracer.test.ts +32 -0
- package/tests/ulid.test.ts +53 -0
- package/tests/version-check.test.ts +57 -0
- package/tsconfig.json +11 -0
- package/tsup.config.ts +16 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import type { ModelInfo } from '../src/infrastructure/catalog-parser.js';
|
|
3
|
+
import { DefaultCatalogParser } from '../src/infrastructure/catalog-parser.js';
|
|
4
|
+
import {
|
|
5
|
+
classifyFamily,
|
|
6
|
+
createProvider,
|
|
7
|
+
setProviderConstructors,
|
|
8
|
+
} from '../src/infrastructure/index.js';
|
|
9
|
+
import type {
|
|
10
|
+
FlowCodexLikeConfig,
|
|
11
|
+
ProviderConstructors,
|
|
12
|
+
} from '../src/infrastructure/provider-factory.js';
|
|
13
|
+
import type { Provider } from '../src/types/provider.js';
|
|
14
|
+
|
|
15
|
+
const EMPTY_STREAM = <T>(): AsyncIterable<T> => ({
|
|
16
|
+
[Symbol.asyncIterator]() {
|
|
17
|
+
return { next: async () => ({ done: true }) as IteratorResult<T> };
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
class FakeAnthropic implements Provider {
|
|
22
|
+
name = 'anthropic';
|
|
23
|
+
private baseUrl?: string;
|
|
24
|
+
constructor(opts: { apiKey: string; baseUrl?: string }) {
|
|
25
|
+
this.baseUrl = opts.baseUrl;
|
|
26
|
+
}
|
|
27
|
+
stream(): AsyncIterable<never> {
|
|
28
|
+
return EMPTY_STREAM();
|
|
29
|
+
}
|
|
30
|
+
async complete() {
|
|
31
|
+
return { content: [], usage: { input: 0, output: 0 }, stopReason: 'end_turn' };
|
|
32
|
+
}
|
|
33
|
+
buildUrl() {
|
|
34
|
+
return this.baseUrl ?? 'https://api.anthropic.com/v1/messages';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
class FakeOpenAI implements Provider {
|
|
38
|
+
name = 'openai';
|
|
39
|
+
private baseUrl?: string;
|
|
40
|
+
constructor(opts: { apiKey: string; baseUrl?: string; organization?: string; project?: string }) {
|
|
41
|
+
this.baseUrl = opts.baseUrl;
|
|
42
|
+
}
|
|
43
|
+
stream(): AsyncIterable<never> {
|
|
44
|
+
return EMPTY_STREAM();
|
|
45
|
+
}
|
|
46
|
+
async complete() {
|
|
47
|
+
return { content: [], usage: { input: 0, output: 0 }, stopReason: 'end_turn' };
|
|
48
|
+
}
|
|
49
|
+
buildUrl() {
|
|
50
|
+
return `${this.baseUrl ?? 'https://api.openai.com'}/v1/chat/completions`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
class FakeCompatible implements Provider {
|
|
54
|
+
name = 'openai-compatible';
|
|
55
|
+
private baseUrl: string;
|
|
56
|
+
private extraHeaders?: Record<string, string>;
|
|
57
|
+
private extraBody?: Record<string, unknown>;
|
|
58
|
+
constructor(opts: {
|
|
59
|
+
apiKey: string;
|
|
60
|
+
baseUrl: string;
|
|
61
|
+
extraHeaders?: Record<string, string>;
|
|
62
|
+
extraBody?: Record<string, unknown>;
|
|
63
|
+
}) {
|
|
64
|
+
this.baseUrl = opts.baseUrl;
|
|
65
|
+
this.extraHeaders = opts.extraHeaders;
|
|
66
|
+
this.extraBody = opts.extraBody;
|
|
67
|
+
}
|
|
68
|
+
stream(): AsyncIterable<never> {
|
|
69
|
+
return EMPTY_STREAM();
|
|
70
|
+
}
|
|
71
|
+
async complete() {
|
|
72
|
+
return { content: [], usage: { input: 0, output: 0 }, stopReason: 'end_turn' };
|
|
73
|
+
}
|
|
74
|
+
buildUrl() {
|
|
75
|
+
return `${this.baseUrl}/chat/completions`;
|
|
76
|
+
}
|
|
77
|
+
buildHeaders() {
|
|
78
|
+
return { authorization: 'Bearer k', ...(this.extraHeaders ?? {}) };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function makeCatalog(models: ModelInfo[]): DefaultCatalogParser {
|
|
83
|
+
const parser = new DefaultCatalogParser({ disableNetwork: true });
|
|
84
|
+
(
|
|
85
|
+
parser as unknown as { cached: { models: ModelInfo[]; fetchedAt: number; source: string } }
|
|
86
|
+
).cached = {
|
|
87
|
+
models,
|
|
88
|
+
fetchedAt: Date.now(),
|
|
89
|
+
source: 'snapshot',
|
|
90
|
+
};
|
|
91
|
+
return parser;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const ANTHROPIC_MODELS: ModelInfo[] = [
|
|
95
|
+
{
|
|
96
|
+
id: 'claude-sonnet-4-6',
|
|
97
|
+
name: 'Sonnet',
|
|
98
|
+
provider: 'anthropic',
|
|
99
|
+
npm: '@anthropic-ai/sdk',
|
|
100
|
+
reasoning: false,
|
|
101
|
+
limit: { context: 200_000, output: 8192 },
|
|
102
|
+
cost: { input: 3, output: 15 },
|
|
103
|
+
tool_call: true,
|
|
104
|
+
modalities: { input: ['text'], output: ['text'] },
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
const OPENAI_MODELS: ModelInfo[] = [
|
|
108
|
+
{
|
|
109
|
+
id: 'gpt-4o',
|
|
110
|
+
name: 'GPT-4o',
|
|
111
|
+
provider: 'openai',
|
|
112
|
+
npm: 'openai',
|
|
113
|
+
reasoning: false,
|
|
114
|
+
limit: { context: 128_000, output: 4096 },
|
|
115
|
+
cost: { input: 2.5, output: 10 },
|
|
116
|
+
tool_call: true,
|
|
117
|
+
modalities: { input: ['text'], output: ['text'] },
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
const DEEPSEEK_MODELS: ModelInfo[] = [
|
|
121
|
+
{
|
|
122
|
+
id: 'deepseek-chat',
|
|
123
|
+
name: 'DeepSeek Chat',
|
|
124
|
+
provider: 'deepseek',
|
|
125
|
+
npm: 'openai-compatible',
|
|
126
|
+
reasoning: false,
|
|
127
|
+
limit: { context: 64_000, output: 4096 },
|
|
128
|
+
cost: { input: 0.14, output: 0.28 },
|
|
129
|
+
tool_call: true,
|
|
130
|
+
modalities: { input: ['text'], output: ['text'] },
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
const BASE_CONFIG: FlowCodexLikeConfig = {
|
|
135
|
+
providers: {},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
beforeEach(() => {
|
|
139
|
+
setProviderConstructors({
|
|
140
|
+
anthropic: FakeAnthropic as unknown as ProviderConstructors['anthropic'],
|
|
141
|
+
openai: FakeOpenAI as unknown as ProviderConstructors['openai'],
|
|
142
|
+
openaiCompatible: FakeCompatible as unknown as ProviderConstructors['openaiCompatible'],
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('createProvider', () => {
|
|
147
|
+
it('creates AnthropicProvider for anthropic family', () => {
|
|
148
|
+
const catalog = makeCatalog(ANTHROPIC_MODELS);
|
|
149
|
+
const p = createProvider({
|
|
150
|
+
providerId: 'anthropic',
|
|
151
|
+
apiKey: 'sk',
|
|
152
|
+
config: BASE_CONFIG,
|
|
153
|
+
catalog,
|
|
154
|
+
});
|
|
155
|
+
expect(p.name).toBe('anthropic');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('creates OpenAIProvider for openai family from catalog npm', () => {
|
|
159
|
+
const catalog = makeCatalog(OPENAI_MODELS);
|
|
160
|
+
const p = createProvider({ providerId: 'openai', apiKey: 'sk', config: BASE_CONFIG, catalog });
|
|
161
|
+
expect(p.name).toBe('openai');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('creates OpenAICompatibleProvider for openai-compatible family from catalog', () => {
|
|
165
|
+
const catalog = makeCatalog(DEEPSEEK_MODELS);
|
|
166
|
+
const config = { providers: { deepseek: { baseUrl: 'https://api.deepseek.com/v1' } } };
|
|
167
|
+
const p = createProvider({ providerId: 'deepseek', apiKey: 'sk', config, catalog });
|
|
168
|
+
expect(p.name).toBe('openai-compatible');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('config family override wins over catalog npm', () => {
|
|
172
|
+
const catalog = makeCatalog(DEEPSEEK_MODELS);
|
|
173
|
+
const config = {
|
|
174
|
+
providers: {
|
|
175
|
+
deepseek: { family: 'openai-compatible', baseUrl: 'https://custom.example.com/v1' },
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
const p = createProvider({ providerId: 'deepseek', apiKey: 'sk', config, catalog });
|
|
179
|
+
expect(p.name).toBe('openai-compatible');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('throws PROVIDER_UNSUPPORTED for unknown provider with no catalog entry', () => {
|
|
183
|
+
const catalog = makeCatalog(ANTHROPIC_MODELS);
|
|
184
|
+
expect(() =>
|
|
185
|
+
createProvider({ providerId: 'unknown', apiKey: 'sk', config: BASE_CONFIG, catalog }),
|
|
186
|
+
).toThrow(/not supported/);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('throws PROVIDER_NOT_WIRED for google family', () => {
|
|
190
|
+
const GEMINI_MODELS: ModelInfo[] = [
|
|
191
|
+
{
|
|
192
|
+
id: 'gemini-2-pro',
|
|
193
|
+
name: 'Gemini 2 Pro',
|
|
194
|
+
provider: 'google',
|
|
195
|
+
npm: '@google/genai',
|
|
196
|
+
reasoning: false,
|
|
197
|
+
limit: { context: 1_000_000, output: 8192 },
|
|
198
|
+
tool_call: true,
|
|
199
|
+
modalities: { input: ['text'], output: ['text'] },
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
const catalog = makeCatalog(GEMINI_MODELS);
|
|
203
|
+
expect(() =>
|
|
204
|
+
createProvider({ providerId: 'google', apiKey: 'sk', config: BASE_CONFIG, catalog }),
|
|
205
|
+
).toThrow(/not wired/);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('passes baseUrl from config to the provider', () => {
|
|
209
|
+
const catalog = makeCatalog(DEEPSEEK_MODELS);
|
|
210
|
+
const config = { providers: { deepseek: { baseUrl: 'https://my-proxy.example.com/v1' } } };
|
|
211
|
+
const p = createProvider({ providerId: 'deepseek', apiKey: 'sk', config, catalog });
|
|
212
|
+
const url = (p as unknown as { buildUrl: () => string }).buildUrl.call(p);
|
|
213
|
+
expect(url).toContain('my-proxy.example.com');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('passes extraHeaders and extraBody from config to compatible provider', () => {
|
|
217
|
+
const catalog = makeCatalog(DEEPSEEK_MODELS);
|
|
218
|
+
const config = {
|
|
219
|
+
providers: {
|
|
220
|
+
deepseek: {
|
|
221
|
+
baseUrl: 'https://api.deepseek.com/v1',
|
|
222
|
+
extraHeaders: { 'X-Test': '1' },
|
|
223
|
+
extraBody: { reasoning: true },
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
const p = createProvider({ providerId: 'deepseek', apiKey: 'sk', config, catalog });
|
|
228
|
+
const headers = (
|
|
229
|
+
p as unknown as { buildHeaders: () => Record<string, string> }
|
|
230
|
+
).buildHeaders.call(p);
|
|
231
|
+
expect(headers['X-Test']).toBe('1');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('classifyFamily (re-exported from core)', () => {
|
|
236
|
+
it('classifies @anthropic-ai/sdk as anthropic', () => {
|
|
237
|
+
expect(classifyFamily('@anthropic-ai/sdk')).toBe('anthropic');
|
|
238
|
+
});
|
|
239
|
+
it('classifies openai npm as openai', () => {
|
|
240
|
+
expect(classifyFamily('openai')).toBe('openai');
|
|
241
|
+
});
|
|
242
|
+
it('classifies openai-compatible npm', () => {
|
|
243
|
+
expect(classifyFamily('openai-compatible')).toBe('openai-compatible');
|
|
244
|
+
});
|
|
245
|
+
});
|