@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,359 @@
|
|
|
1
|
+
import { toErrorMessage } from '../utils/error.js';
|
|
2
|
+
|
|
3
|
+
export const ERROR_CODES = {
|
|
4
|
+
PROVIDER_RATE_LIMITED: 'PROVIDER_RATE_LIMITED',
|
|
5
|
+
PROVIDER_AUTH_FAILED: 'PROVIDER_AUTH_FAILED',
|
|
6
|
+
PROVIDER_OVERLOADED: 'PROVIDER_OVERLOADED',
|
|
7
|
+
PROVIDER_INVALID_REQUEST: 'PROVIDER_INVALID_REQUEST',
|
|
8
|
+
PROVIDER_SERVER_ERROR: 'PROVIDER_SERVER_ERROR',
|
|
9
|
+
PROVIDER_NETWORK_ERROR: 'PROVIDER_NETWORK_ERROR',
|
|
10
|
+
PROVIDER_CONTEXT_OVERFLOW: 'PROVIDER_CONTEXT_OVERFLOW',
|
|
11
|
+
PROVIDER_STREAM_HANG: 'PROVIDER_STREAM_HANG',
|
|
12
|
+
PROVIDER_UNSUPPORTED: 'PROVIDER_UNSUPPORTED',
|
|
13
|
+
PROVIDER_NOT_WIRED: 'PROVIDER_NOT_WIRED',
|
|
14
|
+
TOOL_NOT_FOUND: 'TOOL_NOT_FOUND',
|
|
15
|
+
TOOL_PERMISSION_DENIED: 'TOOL_PERMISSION_DENIED',
|
|
16
|
+
TOOL_EXECUTION_FAILED: 'TOOL_EXECUTION_FAILED',
|
|
17
|
+
TOOL_TIMEOUT: 'TOOL_TIMEOUT',
|
|
18
|
+
TOOL_INPUT_INVALID: 'TOOL_INPUT_INVALID',
|
|
19
|
+
CONFIG_INVALID: 'CONFIG_INVALID',
|
|
20
|
+
CONFIG_NOT_FOUND: 'CONFIG_NOT_FOUND',
|
|
21
|
+
CONFIG_PARSE_FAILED: 'CONFIG_PARSE_FAILED',
|
|
22
|
+
CONFIG_MIGRATION_NEEDED: 'CONFIG_MIGRATION_NEEDED',
|
|
23
|
+
PLUGIN_LOAD_FAILED: 'PLUGIN_LOAD_FAILED',
|
|
24
|
+
PLUGIN_API_MISMATCH: 'PLUGIN_API_MISMATCH',
|
|
25
|
+
PLUGIN_MISSING_DEPENDENCY: 'PLUGIN_MISSING_DEPENDENCY',
|
|
26
|
+
AGENT_ITERATION_LIMIT: 'AGENT_ITERATION_LIMIT',
|
|
27
|
+
AGENT_CONTEXT_OVERFLOW: 'AGENT_CONTEXT_OVERFLOW',
|
|
28
|
+
AGENT_ABORTED: 'AGENT_ABORTED',
|
|
29
|
+
AGENT_RUN_FAILED: 'AGENT_RUN_FAILED',
|
|
30
|
+
AGENT_BUDGET_EXCEEDED: 'AGENT_BUDGET_EXCEEDED',
|
|
31
|
+
COMPACTION_FAILED: 'COMPACTION_FAILED',
|
|
32
|
+
PROMPT_BUDGET_EXCEEDED: 'PROMPT_BUDGET_EXCEEDED',
|
|
33
|
+
SESSION_NOT_FOUND: 'SESSION_NOT_FOUND',
|
|
34
|
+
SESSION_CORRUPTED: 'SESSION_CORRUPTED',
|
|
35
|
+
SESSION_WRITE_FAILED: 'SESSION_WRITE_FAILED',
|
|
36
|
+
CONTAINER_TOKEN_ALREADY_BOUND: 'CONTAINER_TOKEN_ALREADY_BOUND',
|
|
37
|
+
CONTAINER_TOKEN_NOT_BOUND: 'CONTAINER_TOKEN_NOT_BOUND',
|
|
38
|
+
CONTAINER_CIRCULAR_DEPENDENCY: 'CONTAINER_CIRCULAR_DEPENDENCY',
|
|
39
|
+
REGISTRY_DUPLICATE: 'REGISTRY_DUPLICATE',
|
|
40
|
+
REGISTRY_NOT_FOUND: 'REGISTRY_NOT_FOUND',
|
|
41
|
+
REGISTRY_INVALID: 'REGISTRY_INVALID',
|
|
42
|
+
FS_READ_FAILED: 'FS_READ_FAILED',
|
|
43
|
+
FS_WRITE_FAILED: 'FS_WRITE_FAILED',
|
|
44
|
+
FS_MKDIR_FAILED: 'FS_MKDIR_FAILED',
|
|
45
|
+
FS_DELETE_FAILED: 'FS_DELETE_FAILED',
|
|
46
|
+
FS_ATOMIC_WRITE_FAILED: 'FS_ATOMIC_WRITE_FAILED',
|
|
47
|
+
FS_PATH_ESCAPE: 'FS_PATH_ESCAPE',
|
|
48
|
+
SSRF_BLOCKED: 'SSRF_BLOCKED',
|
|
49
|
+
WEBFETCH_FAILED: 'WEBFETCH_FAILED',
|
|
50
|
+
WEBSEARCH_FAILED: 'WEBSEARCH_FAILED',
|
|
51
|
+
DIFF_FILE_NOT_FOUND: 'DIFF_FILE_NOT_FOUND',
|
|
52
|
+
PATCH_HUNK_FAILED: 'PATCH_HUNK_FAILED',
|
|
53
|
+
DEV_TOOL_NOT_FOUND: 'DEV_TOOL_NOT_FOUND',
|
|
54
|
+
REPLAY_MISS: 'REPLAY_MISS',
|
|
55
|
+
SESSION_EXPORT_FAILED: 'SESSION_EXPORT_FAILED',
|
|
56
|
+
PROVIDER_UNSUPPORTED_MODALITY: 'PROVIDER_UNSUPPORTED_MODALITY',
|
|
57
|
+
TOOL_BATCH_FAILED: 'TOOL_BATCH_FAILED',
|
|
58
|
+
TOOL_BATCH_TOO_LARGE: 'TOOL_BATCH_TOO_LARGE',
|
|
59
|
+
TOOL_NESTED_BATCH: 'TOOL_NESTED_BATCH',
|
|
60
|
+
TOOL_INVALID_ATTACHMENT: 'TOOL_INVALID_ATTACHMENT',
|
|
61
|
+
TOOL_UNSUPPORTED_PROVIDER: 'TOOL_UNSUPPORTED_PROVIDER',
|
|
62
|
+
AGENT_STRUCTURED_OUTPUT_NOT_PRODUCED: 'AGENT_STRUCTURED_OUTPUT_NOT_PRODUCED',
|
|
63
|
+
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
64
|
+
UNKNOWN: 'UNKNOWN',
|
|
65
|
+
} as const;
|
|
66
|
+
|
|
67
|
+
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
|
68
|
+
|
|
69
|
+
export type ErrorSubsystem =
|
|
70
|
+
| 'provider'
|
|
71
|
+
| 'tool'
|
|
72
|
+
| 'config'
|
|
73
|
+
| 'plugin'
|
|
74
|
+
| 'agent'
|
|
75
|
+
| 'session'
|
|
76
|
+
| 'container'
|
|
77
|
+
| 'fs'
|
|
78
|
+
| 'general';
|
|
79
|
+
|
|
80
|
+
export type ErrorSeverity = 'fatal' | 'error' | 'warning';
|
|
81
|
+
|
|
82
|
+
export class FlowCodexError extends Error {
|
|
83
|
+
readonly code: ErrorCode;
|
|
84
|
+
readonly subsystem: ErrorSubsystem;
|
|
85
|
+
readonly severity: ErrorSeverity;
|
|
86
|
+
readonly recoverable: boolean;
|
|
87
|
+
readonly context?: Record<string, unknown> | undefined;
|
|
88
|
+
|
|
89
|
+
constructor(opts: {
|
|
90
|
+
message: string;
|
|
91
|
+
code: ErrorCode;
|
|
92
|
+
subsystem: ErrorSubsystem;
|
|
93
|
+
severity?: ErrorSeverity | undefined;
|
|
94
|
+
recoverable?: boolean | undefined;
|
|
95
|
+
context?: Record<string, unknown> | undefined;
|
|
96
|
+
cause?: unknown | undefined;
|
|
97
|
+
}) {
|
|
98
|
+
super(opts.message, { cause: opts.cause });
|
|
99
|
+
this.name = 'FlowCodexError';
|
|
100
|
+
this.code = opts.code;
|
|
101
|
+
this.subsystem = opts.subsystem;
|
|
102
|
+
this.severity = opts.severity ?? 'error';
|
|
103
|
+
this.recoverable = opts.recoverable ?? false;
|
|
104
|
+
this.context = opts.context;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
describe(): string {
|
|
108
|
+
const ctx = this.context ? ` ${formatContext(this.context)}` : '';
|
|
109
|
+
return `${this.code}: ${this.message}${ctx}`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function formatContext(ctx: Record<string, unknown>): string {
|
|
114
|
+
const parts = Object.entries(ctx)
|
|
115
|
+
.filter(([, v]) => v !== undefined)
|
|
116
|
+
.slice(0, 3)
|
|
117
|
+
.map(([k, v]) => `${k}=${String(v)}`);
|
|
118
|
+
return parts.length > 0 ? `[${parts.join(' ')}]` : '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class ToolError extends FlowCodexError {
|
|
122
|
+
readonly toolName: string;
|
|
123
|
+
|
|
124
|
+
constructor(opts: {
|
|
125
|
+
message: string;
|
|
126
|
+
code: Extract<
|
|
127
|
+
ErrorCode,
|
|
128
|
+
| 'TOOL_NOT_FOUND'
|
|
129
|
+
| 'TOOL_PERMISSION_DENIED'
|
|
130
|
+
| 'TOOL_EXECUTION_FAILED'
|
|
131
|
+
| 'TOOL_TIMEOUT'
|
|
132
|
+
| 'TOOL_INPUT_INVALID'
|
|
133
|
+
>;
|
|
134
|
+
toolName: string;
|
|
135
|
+
recoverable?: boolean | undefined;
|
|
136
|
+
context?: Record<string, unknown> | undefined;
|
|
137
|
+
cause?: unknown | undefined;
|
|
138
|
+
}) {
|
|
139
|
+
super({
|
|
140
|
+
message: opts.message,
|
|
141
|
+
code: opts.code,
|
|
142
|
+
subsystem: 'tool',
|
|
143
|
+
recoverable: opts.recoverable,
|
|
144
|
+
context: { tool: opts.toolName, ...opts.context },
|
|
145
|
+
cause: opts.cause,
|
|
146
|
+
});
|
|
147
|
+
this.name = 'ToolError';
|
|
148
|
+
this.toolName = opts.toolName;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export class ConfigError extends FlowCodexError {
|
|
153
|
+
constructor(opts: {
|
|
154
|
+
message: string;
|
|
155
|
+
code: Extract<
|
|
156
|
+
ErrorCode,
|
|
157
|
+
'CONFIG_INVALID' | 'CONFIG_NOT_FOUND' | 'CONFIG_PARSE_FAILED' | 'CONFIG_MIGRATION_NEEDED'
|
|
158
|
+
>;
|
|
159
|
+
context?: Record<string, unknown> | undefined;
|
|
160
|
+
cause?: unknown | undefined;
|
|
161
|
+
}) {
|
|
162
|
+
super({
|
|
163
|
+
message: opts.message,
|
|
164
|
+
code: opts.code,
|
|
165
|
+
subsystem: 'config',
|
|
166
|
+
severity: 'fatal',
|
|
167
|
+
recoverable: false,
|
|
168
|
+
context: opts.context,
|
|
169
|
+
cause: opts.cause,
|
|
170
|
+
});
|
|
171
|
+
this.name = 'ConfigError';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export class PluginError extends FlowCodexError {
|
|
176
|
+
readonly pluginName: string;
|
|
177
|
+
|
|
178
|
+
constructor(opts: {
|
|
179
|
+
message: string;
|
|
180
|
+
code: Extract<
|
|
181
|
+
ErrorCode,
|
|
182
|
+
'PLUGIN_LOAD_FAILED' | 'PLUGIN_API_MISMATCH' | 'PLUGIN_MISSING_DEPENDENCY'
|
|
183
|
+
>;
|
|
184
|
+
pluginName: string;
|
|
185
|
+
context?: Record<string, unknown> | undefined;
|
|
186
|
+
cause?: unknown | undefined;
|
|
187
|
+
}) {
|
|
188
|
+
super({
|
|
189
|
+
message: opts.message,
|
|
190
|
+
code: opts.code,
|
|
191
|
+
subsystem: 'plugin',
|
|
192
|
+
severity: 'error',
|
|
193
|
+
recoverable: opts.code === ERROR_CODES.PLUGIN_MISSING_DEPENDENCY,
|
|
194
|
+
context: { plugin: opts.pluginName, ...opts.context },
|
|
195
|
+
cause: opts.cause,
|
|
196
|
+
});
|
|
197
|
+
this.name = 'PluginError';
|
|
198
|
+
this.pluginName = opts.pluginName;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export class AgentError extends FlowCodexError {
|
|
203
|
+
constructor(opts: {
|
|
204
|
+
message: string;
|
|
205
|
+
code: Extract<
|
|
206
|
+
ErrorCode,
|
|
207
|
+
| 'AGENT_ITERATION_LIMIT'
|
|
208
|
+
| 'AGENT_CONTEXT_OVERFLOW'
|
|
209
|
+
| 'AGENT_ABORTED'
|
|
210
|
+
| 'AGENT_RUN_FAILED'
|
|
211
|
+
| 'AGENT_BUDGET_EXCEEDED'
|
|
212
|
+
| 'COMPACTION_FAILED'
|
|
213
|
+
| 'PROMPT_BUDGET_EXCEEDED'
|
|
214
|
+
>;
|
|
215
|
+
recoverable?: boolean | undefined;
|
|
216
|
+
context?: Record<string, unknown> | undefined;
|
|
217
|
+
cause?: unknown | undefined;
|
|
218
|
+
}) {
|
|
219
|
+
super({
|
|
220
|
+
message: opts.message,
|
|
221
|
+
code: opts.code,
|
|
222
|
+
subsystem: 'agent',
|
|
223
|
+
severity: opts.code === ERROR_CODES.AGENT_ABORTED ? 'warning' : 'error',
|
|
224
|
+
recoverable: opts.recoverable ?? opts.code === ERROR_CODES.AGENT_ITERATION_LIMIT,
|
|
225
|
+
context: opts.context,
|
|
226
|
+
cause: opts.cause,
|
|
227
|
+
});
|
|
228
|
+
this.name = 'AgentError';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export class PermissionError extends FlowCodexError {
|
|
233
|
+
readonly tool: string;
|
|
234
|
+
readonly pattern?: string | undefined;
|
|
235
|
+
readonly source: string;
|
|
236
|
+
|
|
237
|
+
constructor(opts: {
|
|
238
|
+
message: string;
|
|
239
|
+
tool: string;
|
|
240
|
+
pattern?: string | undefined;
|
|
241
|
+
source: string;
|
|
242
|
+
context?: Record<string, unknown> | undefined;
|
|
243
|
+
cause?: unknown | undefined;
|
|
244
|
+
}) {
|
|
245
|
+
super({
|
|
246
|
+
message: opts.message,
|
|
247
|
+
code: ERROR_CODES.TOOL_PERMISSION_DENIED,
|
|
248
|
+
subsystem: 'tool',
|
|
249
|
+
recoverable: false,
|
|
250
|
+
context: { tool: opts.tool, pattern: opts.pattern, source: opts.source, ...opts.context },
|
|
251
|
+
cause: opts.cause,
|
|
252
|
+
});
|
|
253
|
+
this.name = 'PermissionError';
|
|
254
|
+
this.tool = opts.tool;
|
|
255
|
+
this.pattern = opts.pattern;
|
|
256
|
+
this.source = opts.source;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function toFlowCodexError(
|
|
261
|
+
err: unknown,
|
|
262
|
+
code: Extract<ErrorCode, 'AGENT_RUN_FAILED' | 'AGENT_ABORTED' | 'UNKNOWN'> = ERROR_CODES.AGENT_RUN_FAILED,
|
|
263
|
+
): FlowCodexError {
|
|
264
|
+
if (err instanceof FlowCodexError) return err;
|
|
265
|
+
const message = toErrorMessage(err);
|
|
266
|
+
return new AgentError({
|
|
267
|
+
message,
|
|
268
|
+
code: code === 'UNKNOWN' ? ERROR_CODES.AGENT_RUN_FAILED : code,
|
|
269
|
+
cause: err,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export class SessionError extends FlowCodexError {
|
|
274
|
+
readonly sessionId?: string | undefined;
|
|
275
|
+
|
|
276
|
+
constructor(opts: {
|
|
277
|
+
message: string;
|
|
278
|
+
code: Extract<ErrorCode, 'SESSION_NOT_FOUND' | 'SESSION_CORRUPTED' | 'SESSION_WRITE_FAILED'>;
|
|
279
|
+
sessionId?: string | undefined;
|
|
280
|
+
context?: Record<string, unknown> | undefined;
|
|
281
|
+
cause?: unknown | undefined;
|
|
282
|
+
}) {
|
|
283
|
+
super({
|
|
284
|
+
message: opts.message,
|
|
285
|
+
code: opts.code,
|
|
286
|
+
subsystem: 'session',
|
|
287
|
+
severity: opts.code === ERROR_CODES.SESSION_WRITE_FAILED ? 'error' : 'warning',
|
|
288
|
+
recoverable: opts.code !== ERROR_CODES.SESSION_CORRUPTED,
|
|
289
|
+
context: { sessionId: opts.sessionId, ...opts.context },
|
|
290
|
+
cause: opts.cause,
|
|
291
|
+
});
|
|
292
|
+
this.name = 'SessionError';
|
|
293
|
+
this.sessionId = opts.sessionId;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export class FsError extends FlowCodexError {
|
|
298
|
+
readonly path?: string | undefined;
|
|
299
|
+
|
|
300
|
+
constructor(opts: {
|
|
301
|
+
message: string;
|
|
302
|
+
code: Extract<
|
|
303
|
+
ErrorCode,
|
|
304
|
+
| 'FS_READ_FAILED'
|
|
305
|
+
| 'FS_WRITE_FAILED'
|
|
306
|
+
| 'FS_MKDIR_FAILED'
|
|
307
|
+
| 'FS_DELETE_FAILED'
|
|
308
|
+
| 'FS_ATOMIC_WRITE_FAILED'
|
|
309
|
+
| 'FS_PATH_ESCAPE'
|
|
310
|
+
>;
|
|
311
|
+
path?: string | undefined;
|
|
312
|
+
context?: Record<string, unknown> | undefined;
|
|
313
|
+
cause?: unknown | undefined;
|
|
314
|
+
}) {
|
|
315
|
+
super({
|
|
316
|
+
message: opts.message,
|
|
317
|
+
code: opts.code,
|
|
318
|
+
subsystem: 'fs',
|
|
319
|
+
severity: 'error',
|
|
320
|
+
recoverable: opts.code !== ERROR_CODES.FS_READ_FAILED,
|
|
321
|
+
context: { path: opts.path, ...opts.context },
|
|
322
|
+
cause: opts.cause,
|
|
323
|
+
});
|
|
324
|
+
this.name = 'FsError';
|
|
325
|
+
this.path = opts.path;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function isFlowCodexError(err: unknown): err is FlowCodexError {
|
|
330
|
+
return err instanceof FlowCodexError;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function isToolError(err: unknown): err is ToolError {
|
|
334
|
+
return err instanceof ToolError;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function isConfigError(err: unknown): err is ConfigError {
|
|
338
|
+
return err instanceof ConfigError;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function isPluginError(err: unknown): err is PluginError {
|
|
342
|
+
return err instanceof PluginError;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function isSessionError(err: unknown): err is SessionError {
|
|
346
|
+
return err instanceof SessionError;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function isAgentError(err: unknown): err is AgentError {
|
|
350
|
+
return err instanceof AgentError;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function isFsError(err: unknown): err is FsError {
|
|
354
|
+
return err instanceof FsError;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function isPermissionError(err: unknown): err is PermissionError {
|
|
358
|
+
return err instanceof PermissionError;
|
|
359
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
ContentBlock,
|
|
3
|
+
Role,
|
|
4
|
+
Message,
|
|
5
|
+
ModelId,
|
|
6
|
+
ModelRef,
|
|
7
|
+
} from './blocks.js';
|
|
8
|
+
export { isToolUseBlock, isToolResultBlock, isTextBlock, isThinkingBlock } from './blocks.js';
|
|
9
|
+
|
|
10
|
+
export type {
|
|
11
|
+
ToolUse,
|
|
12
|
+
ToolResult,
|
|
13
|
+
Budget,
|
|
14
|
+
AgentStatus,
|
|
15
|
+
RunResult,
|
|
16
|
+
Context,
|
|
17
|
+
} from './context.js';
|
|
18
|
+
|
|
19
|
+
export type {
|
|
20
|
+
Usage,
|
|
21
|
+
LLMRequest,
|
|
22
|
+
LLMResponse,
|
|
23
|
+
LLMEvent,
|
|
24
|
+
ProviderError,
|
|
25
|
+
Provider,
|
|
26
|
+
} from './provider.js';
|
|
27
|
+
|
|
28
|
+
export type {
|
|
29
|
+
Tool,
|
|
30
|
+
ToolStreamEvent,
|
|
31
|
+
Permission,
|
|
32
|
+
RiskTier,
|
|
33
|
+
ToolContext,
|
|
34
|
+
} from './tool.js';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ContentBlock, Message } from './blocks.js';
|
|
2
|
+
|
|
3
|
+
export interface Usage {
|
|
4
|
+
input: number;
|
|
5
|
+
output: number;
|
|
6
|
+
cache_read?: number | undefined;
|
|
7
|
+
cache_creation?: number | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ToolChoice {
|
|
11
|
+
type: 'auto' | 'any' | 'tool';
|
|
12
|
+
name?: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface StructuredOutputConfig {
|
|
16
|
+
name: string;
|
|
17
|
+
description?: string | undefined;
|
|
18
|
+
schema: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LLMRequest {
|
|
22
|
+
model: string;
|
|
23
|
+
system: ContentBlock[];
|
|
24
|
+
messages: Message[];
|
|
25
|
+
tools?: unknown[] | undefined;
|
|
26
|
+
max_tokens?: number | undefined;
|
|
27
|
+
thinking?: unknown | undefined;
|
|
28
|
+
output_config?: StructuredOutputConfig | undefined;
|
|
29
|
+
tool_choice?: ToolChoice | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface LLMResponse {
|
|
33
|
+
content: ContentBlock[];
|
|
34
|
+
usage: Usage;
|
|
35
|
+
stopReason: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type LLMEvent =
|
|
39
|
+
| { type: 'text_delta'; text: string }
|
|
40
|
+
| { type: 'thinking_delta'; text: string }
|
|
41
|
+
| { type: 'tool_use_start'; id: string; name: string }
|
|
42
|
+
| { type: 'tool_use_input_delta'; id: string; partialJson: string }
|
|
43
|
+
| { type: 'tool_use_stop'; id: string }
|
|
44
|
+
| { type: 'finish'; usage: Usage; stopReason: string }
|
|
45
|
+
| { type: 'error'; error: { status: number; message: string; retryable: boolean; retryAfterMs?: number | undefined } };
|
|
46
|
+
|
|
47
|
+
export interface ProviderError {
|
|
48
|
+
status: number;
|
|
49
|
+
message: string;
|
|
50
|
+
retryable: boolean;
|
|
51
|
+
describe(): string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface Provider {
|
|
55
|
+
name: string;
|
|
56
|
+
stream(request: LLMRequest): AsyncIterable<LLMEvent>;
|
|
57
|
+
complete(request: LLMRequest): Promise<LLMResponse>;
|
|
58
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface ToolStreamEvent<O = unknown> {
|
|
2
|
+
type: 'log' | 'warning' | 'metric' | 'file_changed' | 'partial_output' | 'final';
|
|
3
|
+
text?: string | undefined;
|
|
4
|
+
data?: Record<string, unknown> | undefined;
|
|
5
|
+
output?: O | undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type Permission = 'auto' | 'confirm' | 'deny';
|
|
9
|
+
export type RiskTier = 'safe' | 'standard' | 'destructive';
|
|
10
|
+
|
|
11
|
+
export interface ToolContext {
|
|
12
|
+
projectRoot: string;
|
|
13
|
+
workingDir: string;
|
|
14
|
+
signal: AbortSignal;
|
|
15
|
+
registerAbortHook(fn: () => void | Promise<void>): () => void;
|
|
16
|
+
recordRead(absPath: string, mtimeMs: number): void;
|
|
17
|
+
hasRead(absPath: string): boolean;
|
|
18
|
+
lastReadMtime(absPath: string): number | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Tool<I = unknown, O = unknown> {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
/** Short, model-facing usage hint. Falls back to `description` in the system
|
|
25
|
+
* prompt. Trimmed to 80 chars (60 in token-saving mode) by SystemPromptBuilder. */
|
|
26
|
+
usageHint?: string | undefined;
|
|
27
|
+
inputSchema: Record<string, unknown>;
|
|
28
|
+
permission: Permission;
|
|
29
|
+
mutating: boolean;
|
|
30
|
+
tier?: 'essential' | 'extended' | undefined;
|
|
31
|
+
riskTier?: RiskTier | undefined;
|
|
32
|
+
subjectKey?: string | undefined;
|
|
33
|
+
maxOutputBytes?: number | undefined;
|
|
34
|
+
timeoutMs?: number | undefined;
|
|
35
|
+
capabilities?: readonly string[] | undefined;
|
|
36
|
+
execute(input: I, ctx: ToolContext, opts: { signal: AbortSignal }): Promise<O>;
|
|
37
|
+
executeStream?(input: I, ctx: ToolContext, opts: { signal: AbortSignal }): AsyncIterable<ToolStreamEvent<O>>;
|
|
38
|
+
cleanup?(input: I, ctx: ToolContext): Promise<void>;
|
|
39
|
+
}
|
package/src/utils/fs.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { promises as fsp } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const TMP_PREFIX = '.flowcodex-tmp-';
|
|
5
|
+
|
|
6
|
+
export async function atomicWrite(filePath: string, content: string): Promise<void> {
|
|
7
|
+
const dir = path.dirname(filePath);
|
|
8
|
+
const base = path.basename(filePath);
|
|
9
|
+
const tmp = path.join(dir, `${TMP_PREFIX}${base}-${process.pid}-${Date.now()}`);
|
|
10
|
+
await fsp.writeFile(tmp, content, 'utf8');
|
|
11
|
+
try {
|
|
12
|
+
await fsp.rename(tmp, filePath);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
await fsp.unlink(tmp).catch(() => {});
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isBinaryBuffer(buf: Uint8Array): boolean {
|
|
20
|
+
const len = Math.min(buf.length, 8192);
|
|
21
|
+
for (let i = 0; i < len; i++) {
|
|
22
|
+
if (buf[i] === 0) return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type NewlineStyle = 'lf' | 'crlf';
|
|
28
|
+
|
|
29
|
+
export function detectNewlineStyle(text: string): NewlineStyle {
|
|
30
|
+
const crlf = text.indexOf('\r\n');
|
|
31
|
+
const lf = text.indexOf('\n');
|
|
32
|
+
if (crlf !== -1 && (lf === -1 || crlf < lf)) return 'crlf';
|
|
33
|
+
return 'lf';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function normalizeToLf(text: string): string {
|
|
37
|
+
return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function toStyle(text: string, style: NewlineStyle): string {
|
|
41
|
+
if (style === 'crlf') return text.replace(/\n/g, '\r\n');
|
|
42
|
+
return text;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function compileGlob(pattern: string): RegExp {
|
|
46
|
+
let re = '';
|
|
47
|
+
let i = 0;
|
|
48
|
+
while (i < pattern.length) {
|
|
49
|
+
const c = pattern[i] ?? '';
|
|
50
|
+
switch (c) {
|
|
51
|
+
case '*':
|
|
52
|
+
if (pattern[i + 1] === '*') {
|
|
53
|
+
i += 2;
|
|
54
|
+
if (pattern[i] === '/') i++;
|
|
55
|
+
re += '.*';
|
|
56
|
+
} else {
|
|
57
|
+
i++;
|
|
58
|
+
re += '[^/]*';
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
case '?':
|
|
62
|
+
i++;
|
|
63
|
+
re += '[^/]';
|
|
64
|
+
continue;
|
|
65
|
+
case '.':
|
|
66
|
+
case '+':
|
|
67
|
+
case '^':
|
|
68
|
+
case '$':
|
|
69
|
+
case '(':
|
|
70
|
+
case ')':
|
|
71
|
+
case '[':
|
|
72
|
+
case ']':
|
|
73
|
+
case '{':
|
|
74
|
+
case '}':
|
|
75
|
+
case '|':
|
|
76
|
+
case '\\':
|
|
77
|
+
re += '\\' + c;
|
|
78
|
+
break;
|
|
79
|
+
default:
|
|
80
|
+
re += c;
|
|
81
|
+
}
|
|
82
|
+
i++;
|
|
83
|
+
}
|
|
84
|
+
return new RegExp('^' + re + '$');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface DiffOptions {
|
|
88
|
+
fromFile?: string | undefined;
|
|
89
|
+
toFile?: string | undefined;
|
|
90
|
+
contextLines?: number | undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function unifiedDiff(oldText: string, newText: string, opts: DiffOptions = {}): string {
|
|
94
|
+
const oldLines = oldText.split('\n');
|
|
95
|
+
const newLines = newText.split('\n');
|
|
96
|
+
const lcs = computeLcsTable(oldLines, newLines);
|
|
97
|
+
const hunks = extractHunks(oldLines, newLines, lcs, opts.contextLines ?? 3);
|
|
98
|
+
if (hunks.length === 0) return '';
|
|
99
|
+
|
|
100
|
+
const from = opts.fromFile ?? 'original';
|
|
101
|
+
const to = opts.toFile ?? 'modified';
|
|
102
|
+
const header = `--- ${from}\n+++ ${to}\n`;
|
|
103
|
+
return header + hunks.join('\n') + '\n';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function computeLcsTable(a: string[], b: string[]): number[][] {
|
|
107
|
+
const m = a.length;
|
|
108
|
+
const n = b.length;
|
|
109
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
110
|
+
for (let i = m - 1; i >= 0; i--) {
|
|
111
|
+
for (let j = n - 1; j >= 0; j--) {
|
|
112
|
+
dp[i]![j]! = a[i] === b[j]
|
|
113
|
+
? dp[i + 1]![j + 1]! + 1
|
|
114
|
+
: Math.max(dp[i + 1]![j]!, dp[i]![j + 1]!);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return dp;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function extractHunks(
|
|
121
|
+
a: string[],
|
|
122
|
+
b: string[],
|
|
123
|
+
lcs: number[][],
|
|
124
|
+
contextLines: number,
|
|
125
|
+
): string[] {
|
|
126
|
+
const lines: Array<{ type: 'ctx' | 'add' | 'del'; aLine: number; bLine: number; text: string }> = [];
|
|
127
|
+
let i = 0;
|
|
128
|
+
let j = 0;
|
|
129
|
+
while (i < a.length || j < b.length) {
|
|
130
|
+
if (i < a.length && j < b.length && a[i] === b[j]) {
|
|
131
|
+
lines.push({ type: 'ctx', aLine: i, bLine: j, text: a[i]! });
|
|
132
|
+
i++;
|
|
133
|
+
j++;
|
|
134
|
+
} else if (i < a.length && (j >= b.length || lcs[i + 1]![j]! >= lcs[i]![j + 1]!)) {
|
|
135
|
+
lines.push({ type: 'del', aLine: i, bLine: j, text: a[i]! });
|
|
136
|
+
i++;
|
|
137
|
+
} else {
|
|
138
|
+
lines.push({ type: 'add', aLine: i, bLine: j, text: b[j]! });
|
|
139
|
+
j++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const hunks: string[] = [];
|
|
144
|
+
let idx = 0;
|
|
145
|
+
while (idx < lines.length) {
|
|
146
|
+
while (idx < lines.length && lines[idx]!.type === 'ctx') idx++;
|
|
147
|
+
if (idx >= lines.length) break;
|
|
148
|
+
|
|
149
|
+
let start = idx;
|
|
150
|
+
while (start > 0 && lines[start - 1]!.type === 'ctx' && idx - start < contextLines) {
|
|
151
|
+
start--;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let end = idx;
|
|
155
|
+
while (end < lines.length && lines[end]!.type !== 'ctx') end++;
|
|
156
|
+
while (end < lines.length && lines[end]!.type === 'ctx' && end - idx < contextLines) {
|
|
157
|
+
end++;
|
|
158
|
+
idx = end;
|
|
159
|
+
while (end < lines.length && lines[end]!.type !== 'ctx') end++;
|
|
160
|
+
}
|
|
161
|
+
idx = end;
|
|
162
|
+
|
|
163
|
+
const hunkLines = lines.slice(start, Math.min(end + contextLines, lines.length));
|
|
164
|
+
const aStart = hunkLines[0]!.aLine + 1;
|
|
165
|
+
const aCount = hunkLines.filter(l => l.type !== 'add').length;
|
|
166
|
+
const bStart = hunkLines[0]!.bLine + 1;
|
|
167
|
+
const bCount = hunkLines.filter(l => l.type !== 'del').length;
|
|
168
|
+
|
|
169
|
+
const body = hunkLines.map(l => {
|
|
170
|
+
const prefix = l.type === 'add' ? '+' : l.type === 'del' ? '-' : ' ';
|
|
171
|
+
return `${prefix}${l.text}`;
|
|
172
|
+
}).join('\n');
|
|
173
|
+
|
|
174
|
+
hunks.push(`@@ -${aStart},${aCount} +${bStart},${bCount} @@\n${body}`);
|
|
175
|
+
}
|
|
176
|
+
return hunks;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function stripAnsi(text: string): string {
|
|
180
|
+
return text.replace(
|
|
181
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ANSI escape sequence matching
|
|
182
|
+
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
|
|
183
|
+
'',
|
|
184
|
+
);
|
|
185
|
+
}
|