@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,339 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { existsSync, promises as fsp } from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import type { SessionEvent, SessionSummary, SessionStore } from './types.js';
|
|
6
|
+
import type { ContentBlock, Message } from '../types/blocks.js';
|
|
7
|
+
import type { SecretScrubber } from '../infrastructure/secret-scrubber.js';
|
|
8
|
+
import { DefaultSecretScrubber } from '../infrastructure/secret-scrubber.js';
|
|
9
|
+
|
|
10
|
+
export interface SessionStoreOptions {
|
|
11
|
+
dir: string;
|
|
12
|
+
model?: string | undefined;
|
|
13
|
+
sessionId?: string | undefined;
|
|
14
|
+
secretScrubber?: SecretScrubber | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sanitizeModel(model: string): string {
|
|
18
|
+
return model
|
|
19
|
+
.replace(/[^a-zA-Z0-9_-]/g, '-')
|
|
20
|
+
.replace(/-+/g, '-')
|
|
21
|
+
.replace(/^-|-$/g, '')
|
|
22
|
+
.slice(0, 40);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function generateSessionId(now: Date = new Date(), model?: string): string {
|
|
26
|
+
const iso = now.toISOString();
|
|
27
|
+
const date = iso.slice(0, 10);
|
|
28
|
+
const time = iso.slice(11, 19).replace(/:/g, '-');
|
|
29
|
+
const suffix = randomBytes(2).toString('hex');
|
|
30
|
+
const modelPart = model ? `_${sanitizeModel(model)}` : '';
|
|
31
|
+
return `${date}/${time}Z${modelPart}_${suffix}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function projectHash(cwd: string): string {
|
|
35
|
+
return createHash('sha256').update(cwd).digest('hex').slice(0, 12);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function flowcodexHome(): string {
|
|
39
|
+
return process.env.FLOWCODEX_HOME ?? path.join(os.homedir(), '.flowcodex');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function sessionsDir(cwd: string): string {
|
|
43
|
+
return path.join(flowcodexHome(), 'projects', projectHash(cwd), 'sessions');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function recordsDir(sessionId: string): string {
|
|
47
|
+
return path.join(flowcodexHome(), 'records', sessionId);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function keyFilePath(): string {
|
|
51
|
+
return path.join(flowcodexHome(), '.key');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function configFilePath(): string {
|
|
55
|
+
const home = flowcodexHome();
|
|
56
|
+
const jsoncPath = path.join(home, 'config.jsonc');
|
|
57
|
+
const jsonPath = path.join(home, 'config.json');
|
|
58
|
+
try {
|
|
59
|
+
if (existsSync(jsoncPath)) return jsoncPath;
|
|
60
|
+
} catch {
|
|
61
|
+
// fall through
|
|
62
|
+
}
|
|
63
|
+
return jsonPath;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function authFilePath(): string {
|
|
67
|
+
return path.join(flowcodexHome(), 'auth.json');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function reconstructMessages(events: SessionEvent[]): Message[] {
|
|
71
|
+
const messages: Message[] = [];
|
|
72
|
+
let pendingToolUses: ContentBlock[] = [];
|
|
73
|
+
|
|
74
|
+
const flushToolUses = (): void => {
|
|
75
|
+
if (pendingToolUses.length > 0) {
|
|
76
|
+
messages.push({ role: 'assistant', content: pendingToolUses });
|
|
77
|
+
pendingToolUses = [];
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
for (const event of events) {
|
|
82
|
+
if (event.type === 'user_input') {
|
|
83
|
+
flushToolUses();
|
|
84
|
+
messages.push({ role: 'user', content: event.content });
|
|
85
|
+
} else if (event.type === 'tool_use') {
|
|
86
|
+
pendingToolUses.push({
|
|
87
|
+
type: 'tool_use',
|
|
88
|
+
id: event.id,
|
|
89
|
+
name: event.name,
|
|
90
|
+
input: event.input,
|
|
91
|
+
});
|
|
92
|
+
} else if (event.type === 'tool_result') {
|
|
93
|
+
flushToolUses();
|
|
94
|
+
const block: ContentBlock = {
|
|
95
|
+
type: 'tool_result',
|
|
96
|
+
tool_use_id: event.id,
|
|
97
|
+
content: event.content as string,
|
|
98
|
+
is_error: event.isError,
|
|
99
|
+
};
|
|
100
|
+
const last = messages[messages.length - 1];
|
|
101
|
+
if (
|
|
102
|
+
last &&
|
|
103
|
+
last.role === 'user' &&
|
|
104
|
+
Array.isArray(last.content) &&
|
|
105
|
+
last.content.length > 0 &&
|
|
106
|
+
last.content[0]?.type === 'tool_result'
|
|
107
|
+
) {
|
|
108
|
+
(last.content as ContentBlock[]).push(block);
|
|
109
|
+
} else {
|
|
110
|
+
messages.push({ role: 'user', content: [block] });
|
|
111
|
+
}
|
|
112
|
+
} else if (event.type === 'llm_response') {
|
|
113
|
+
flushToolUses();
|
|
114
|
+
messages.push({ role: 'assistant', content: event.content });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
flushToolUses();
|
|
118
|
+
return messages;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class DefaultSessionStore implements SessionStore {
|
|
122
|
+
readonly sessionId: string;
|
|
123
|
+
readonly filePath: string;
|
|
124
|
+
private readonly dir: string;
|
|
125
|
+
private readonly scrubber: SecretScrubber;
|
|
126
|
+
private closed = false;
|
|
127
|
+
private summaryStats: {
|
|
128
|
+
totalTokens?: number | undefined;
|
|
129
|
+
totalCost?: number | undefined;
|
|
130
|
+
iterations?: number | undefined;
|
|
131
|
+
provider?: string | undefined;
|
|
132
|
+
} = {};
|
|
133
|
+
|
|
134
|
+
constructor(opts: SessionStoreOptions) {
|
|
135
|
+
this.dir = opts.dir;
|
|
136
|
+
this.scrubber = opts.secretScrubber ?? new DefaultSecretScrubber();
|
|
137
|
+
this.sessionId = opts.sessionId ?? generateSessionId(new Date(), opts.model);
|
|
138
|
+
this.filePath = path.join(this.dir, `${this.sessionId}.jsonl`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get sessionsDirPath(): string {
|
|
142
|
+
return this.dir;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
get summaryPath(): string {
|
|
146
|
+
return this.filePath.replace(/\.jsonl$/, '.summary.json');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
setSummaryStats(stats: {
|
|
150
|
+
totalTokens?: number | undefined;
|
|
151
|
+
totalCost?: number | undefined;
|
|
152
|
+
iterations?: number | undefined;
|
|
153
|
+
provider?: string | undefined;
|
|
154
|
+
}): void {
|
|
155
|
+
this.summaryStats = stats;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
static async create(cwd: string, model?: string): Promise<DefaultSessionStore> {
|
|
159
|
+
const dir = sessionsDir(cwd);
|
|
160
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
161
|
+
const store = new DefaultSessionStore({ dir, model });
|
|
162
|
+
await fsp.mkdir(path.dirname(store.filePath), { recursive: true });
|
|
163
|
+
return store;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
static async openExisting(cwd: string, sessionId: string): Promise<DefaultSessionStore> {
|
|
167
|
+
const dir = sessionsDir(cwd);
|
|
168
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
169
|
+
const store = new DefaultSessionStore({ dir, sessionId });
|
|
170
|
+
await fsp.mkdir(path.dirname(store.filePath), { recursive: true });
|
|
171
|
+
return store;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async append(event: SessionEvent): Promise<void> {
|
|
175
|
+
if (this.closed) return;
|
|
176
|
+
await fsp.mkdir(path.dirname(this.filePath), { recursive: true }).catch(() => {});
|
|
177
|
+
const scrubbed = this.scrubEvent(event);
|
|
178
|
+
const line = JSON.stringify(scrubbed) + '\n';
|
|
179
|
+
await fsp.appendFile(this.filePath, line, 'utf8');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async readAll(): Promise<SessionEvent[]> {
|
|
183
|
+
let raw: string;
|
|
184
|
+
try {
|
|
185
|
+
raw = await fsp.readFile(this.filePath, 'utf8');
|
|
186
|
+
} catch (err) {
|
|
187
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return [];
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
const out: SessionEvent[] = [];
|
|
191
|
+
for (const line of raw.split('\n')) {
|
|
192
|
+
if (!line.trim()) continue;
|
|
193
|
+
try {
|
|
194
|
+
out.push(JSON.parse(line) as SessionEvent);
|
|
195
|
+
} catch {
|
|
196
|
+
// skip corrupt lines
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async writeSummary(summary: SessionSummary): Promise<void> {
|
|
203
|
+
await fsp.mkdir(path.dirname(this.summaryPath), { recursive: true }).catch(() => {});
|
|
204
|
+
await fsp.writeFile(this.summaryPath, JSON.stringify(summary, null, 2), 'utf8');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async close(reason: 'normal' | 'aborted' | 'error' | 'crashed' = 'normal'): Promise<void> {
|
|
208
|
+
if (this.closed) return;
|
|
209
|
+
await this.append({ type: 'session_end', ts: new Date().toISOString(), reason });
|
|
210
|
+
this.closed = true;
|
|
211
|
+
const events = await this.readAll();
|
|
212
|
+
const summary = this.computeSummary(events, reason);
|
|
213
|
+
await this.writeSummary(summary);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
static async listSessions(projectRoot: string): Promise<SessionSummary[]> {
|
|
217
|
+
const dir = sessionsDir(projectRoot);
|
|
218
|
+
let entries: string[];
|
|
219
|
+
try {
|
|
220
|
+
entries = await fsp.readdir(dir, { recursive: true });
|
|
221
|
+
} catch (err) {
|
|
222
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return [];
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
const summaries: SessionSummary[] = [];
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
if (entry.endsWith('.summary.json')) {
|
|
228
|
+
try {
|
|
229
|
+
const raw = await fsp.readFile(path.join(dir, entry), 'utf8');
|
|
230
|
+
summaries.push(JSON.parse(raw) as SessionSummary);
|
|
231
|
+
} catch {
|
|
232
|
+
// skip corrupt
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return summaries;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
static async deleteSession(projectRoot: string, sessionIdPrefix: string): Promise<void> {
|
|
240
|
+
const dir = sessionsDir(projectRoot);
|
|
241
|
+
let entries: string[];
|
|
242
|
+
try {
|
|
243
|
+
entries = await fsp.readdir(dir, { recursive: true });
|
|
244
|
+
} catch (err) {
|
|
245
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return;
|
|
246
|
+
throw err;
|
|
247
|
+
}
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
const base = path.basename(entry);
|
|
250
|
+
const idPart = base.replace(/\.(jsonl|summary\.json)$/, '');
|
|
251
|
+
if (
|
|
252
|
+
idPart === sessionIdPrefix ||
|
|
253
|
+
idPart.startsWith(sessionIdPrefix) ||
|
|
254
|
+
idPart.endsWith(sessionIdPrefix)
|
|
255
|
+
) {
|
|
256
|
+
await fsp.unlink(path.join(dir, entry)).catch(() => {});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private computeSummary(events: SessionEvent[], outcome: string): SessionSummary {
|
|
262
|
+
let title = '(empty session)';
|
|
263
|
+
let startedAt = '';
|
|
264
|
+
let endedAt = '';
|
|
265
|
+
let messageCount = 0;
|
|
266
|
+
let toolCallCount = 0;
|
|
267
|
+
let toolErrorCount = 0;
|
|
268
|
+
let model = this.summaryStats.provider ?? '';
|
|
269
|
+
|
|
270
|
+
for (const event of events) {
|
|
271
|
+
switch (event.type) {
|
|
272
|
+
case 'session_start':
|
|
273
|
+
startedAt = event.ts;
|
|
274
|
+
model = event.model;
|
|
275
|
+
break;
|
|
276
|
+
case 'session_resumed':
|
|
277
|
+
startedAt = event.ts;
|
|
278
|
+
model = event.model;
|
|
279
|
+
break;
|
|
280
|
+
case 'user_input':
|
|
281
|
+
if (title === '(empty session)') {
|
|
282
|
+
title =
|
|
283
|
+
typeof event.content === 'string' ? event.content.slice(0, 50) : '(structured input)';
|
|
284
|
+
}
|
|
285
|
+
messageCount++;
|
|
286
|
+
break;
|
|
287
|
+
case 'llm_response':
|
|
288
|
+
messageCount++;
|
|
289
|
+
break;
|
|
290
|
+
case 'tool_use':
|
|
291
|
+
toolCallCount++;
|
|
292
|
+
break;
|
|
293
|
+
case 'tool_result':
|
|
294
|
+
if (event.isError) toolErrorCount++;
|
|
295
|
+
break;
|
|
296
|
+
case 'session_end':
|
|
297
|
+
endedAt = event.ts;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!endedAt) endedAt = new Date().toISOString();
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
id: this.sessionId,
|
|
306
|
+
title,
|
|
307
|
+
model,
|
|
308
|
+
provider: this.summaryStats.provider ?? model,
|
|
309
|
+
startedAt,
|
|
310
|
+
endedAt,
|
|
311
|
+
messageCount,
|
|
312
|
+
iterationCount: this.summaryStats.iterations ?? 0,
|
|
313
|
+
toolCallCount,
|
|
314
|
+
toolErrorCount,
|
|
315
|
+
totalTokens: this.summaryStats.totalTokens ?? 0,
|
|
316
|
+
totalCost: this.summaryStats.totalCost ?? 0,
|
|
317
|
+
outcome: outcome === 'normal' ? 'completed' : outcome === 'aborted' ? 'aborted' : 'error',
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private scrubEvent(event: SessionEvent): SessionEvent {
|
|
322
|
+
if (event.type === 'user_input') {
|
|
323
|
+
if (typeof event.content === 'string') {
|
|
324
|
+
return { ...event, content: this.scrubber.scrub(event.content) };
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
...event,
|
|
328
|
+
content: this.scrubber.scrubObject(event.content),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
if (event.type === 'llm_response') {
|
|
332
|
+
return { ...event, content: this.scrubber.scrubObject(event.content) };
|
|
333
|
+
}
|
|
334
|
+
if (event.type === 'tool_result') {
|
|
335
|
+
return { ...event, content: this.scrubber.scrubObject(event.content) };
|
|
336
|
+
}
|
|
337
|
+
return event;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { ContentBlock } from '../types/blocks.js';
|
|
2
|
+
import type { Usage } from '../infrastructure/token-counter.js';
|
|
3
|
+
|
|
4
|
+
export type SessionEvent =
|
|
5
|
+
| { type: 'session_start'; ts: string; id: string; model: string; cwd: string }
|
|
6
|
+
| { type: 'session_resumed'; ts: string; id: string; model: string }
|
|
7
|
+
| { type: 'user_input'; ts: string; content: string | ContentBlock[] }
|
|
8
|
+
| {
|
|
9
|
+
type: 'llm_request';
|
|
10
|
+
ts: string;
|
|
11
|
+
model: string;
|
|
12
|
+
messageCount: number;
|
|
13
|
+
estimatedInputTokens?: number | undefined;
|
|
14
|
+
toolCount?: number | undefined;
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
type: 'llm_response';
|
|
18
|
+
ts: string;
|
|
19
|
+
content: ContentBlock[];
|
|
20
|
+
stopReason: string;
|
|
21
|
+
usage: Usage;
|
|
22
|
+
}
|
|
23
|
+
| { type: 'tool_use'; ts: string; name: string; id: string; input: unknown }
|
|
24
|
+
| { type: 'tool_result'; ts: string; id: string; content: unknown; isError: boolean }
|
|
25
|
+
| {
|
|
26
|
+
type: 'compaction';
|
|
27
|
+
ts: string;
|
|
28
|
+
before: number;
|
|
29
|
+
after: number;
|
|
30
|
+
level: string;
|
|
31
|
+
aggressive: boolean;
|
|
32
|
+
}
|
|
33
|
+
| { type: 'ctx_pct'; ts: string; pct: number }
|
|
34
|
+
| { type: 'in_flight_start'; ts: string; kind: 'tool_batch' | 'llm_call' }
|
|
35
|
+
| { type: 'in_flight_end'; ts: string; kind: 'tool_batch' | 'llm_call' }
|
|
36
|
+
| { type: 'checkpoint'; ts: string; label?: string | undefined }
|
|
37
|
+
| { type: 'error'; ts: string; error: unknown }
|
|
38
|
+
| { type: 'session_end'; ts: string; reason: 'normal' | 'aborted' | 'error' | 'crashed' };
|
|
39
|
+
|
|
40
|
+
export interface AuditEntry {
|
|
41
|
+
index: number;
|
|
42
|
+
id: string;
|
|
43
|
+
ts: string;
|
|
44
|
+
prevHash: string;
|
|
45
|
+
hash: string;
|
|
46
|
+
toolName: string;
|
|
47
|
+
toolUseId: string;
|
|
48
|
+
input: unknown;
|
|
49
|
+
output: unknown;
|
|
50
|
+
isError: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type VerifyResult =
|
|
54
|
+
| { ok: true; entries: number }
|
|
55
|
+
| { ok: false; brokenAt: number; reason: string };
|
|
56
|
+
|
|
57
|
+
export interface SessionSummary {
|
|
58
|
+
id: string;
|
|
59
|
+
title: string;
|
|
60
|
+
model: string;
|
|
61
|
+
provider: string;
|
|
62
|
+
startedAt: string;
|
|
63
|
+
endedAt: string;
|
|
64
|
+
messageCount: number;
|
|
65
|
+
iterationCount: number;
|
|
66
|
+
toolCallCount: number;
|
|
67
|
+
toolErrorCount: number;
|
|
68
|
+
totalTokens: number;
|
|
69
|
+
totalCost: number;
|
|
70
|
+
outcome: 'completed' | 'error' | 'aborted';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface SessionStore {
|
|
74
|
+
append(event: SessionEvent): Promise<void>;
|
|
75
|
+
readAll(): Promise<SessionEvent[]>;
|
|
76
|
+
close(reason?: 'normal' | 'aborted' | 'error' | 'crashed'): Promise<void>;
|
|
77
|
+
writeSummary(summary: SessionSummary): Promise<void>;
|
|
78
|
+
readonly sessionId: string;
|
|
79
|
+
readonly filePath: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SecretVault {
|
|
83
|
+
isEncrypted(value: string): boolean;
|
|
84
|
+
encrypt(plaintext: string): string;
|
|
85
|
+
decrypt(value: string): string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface AuditLog {
|
|
89
|
+
record(input: {
|
|
90
|
+
sessionId: string;
|
|
91
|
+
toolName: string;
|
|
92
|
+
toolUseId: string;
|
|
93
|
+
input: unknown;
|
|
94
|
+
output: unknown;
|
|
95
|
+
isError: boolean;
|
|
96
|
+
}): Promise<AuditEntry>;
|
|
97
|
+
verify(sessionId: string): Promise<VerifyResult>;
|
|
98
|
+
load(sessionId: string): Promise<AuditEntry[]>;
|
|
99
|
+
flush(sessionId: string): Promise<void>;
|
|
100
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type ImageSource =
|
|
2
|
+
| { type: 'base64'; media_type: string; data: string }
|
|
3
|
+
| { type: 'url'; url: string };
|
|
4
|
+
|
|
5
|
+
export interface CacheControl {
|
|
6
|
+
type: 'ephemeral';
|
|
7
|
+
ttl?: '5m' | '1h' | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TextBlock {
|
|
11
|
+
type: 'text';
|
|
12
|
+
text: string;
|
|
13
|
+
cache_control?: CacheControl | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ContentBlock =
|
|
17
|
+
| TextBlock
|
|
18
|
+
| { type: 'thinking'; text: string; signature?: string | undefined }
|
|
19
|
+
| { type: 'tool_use'; id: string; name: string; input: unknown }
|
|
20
|
+
| { type: 'tool_result'; tool_use_id: string; content: string | ContentBlock[]; is_error?: boolean | undefined }
|
|
21
|
+
| { type: 'image'; source: ImageSource }
|
|
22
|
+
| { type: 'pdf'; source: { type: 'base64'; media_type: 'application/pdf'; data: string } };
|
|
23
|
+
|
|
24
|
+
export type Role = 'system' | 'user' | 'assistant';
|
|
25
|
+
|
|
26
|
+
export interface Message {
|
|
27
|
+
role: Role;
|
|
28
|
+
content: ContentBlock[] | string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ModelId = string;
|
|
32
|
+
export type ModelRef = { provider: string; model: ModelId };
|
|
33
|
+
|
|
34
|
+
export function isToolUseBlock(block: ContentBlock): block is { type: 'tool_use'; id: string; name: string; input: unknown } {
|
|
35
|
+
return block.type === 'tool_use';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isToolResultBlock(block: ContentBlock): block is { type: 'tool_result'; tool_use_id: string; content: string | ContentBlock[]; is_error?: boolean | undefined } {
|
|
39
|
+
return block.type === 'tool_result';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isTextBlock(block: ContentBlock): block is TextBlock {
|
|
43
|
+
return block.type === 'text';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isThinkingBlock(block: ContentBlock): block is { type: 'thinking'; text: string; signature?: string | undefined } {
|
|
47
|
+
return block.type === 'thinking';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isImageBlock(block: ContentBlock): block is { type: 'image'; source: ImageSource } {
|
|
51
|
+
return block.type === 'image';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isPdfBlock(block: ContentBlock): block is { type: 'pdf'; source: { type: 'base64'; media_type: 'application/pdf'; data: string } } {
|
|
55
|
+
return block.type === 'pdf';
|
|
56
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ContentBlock, Message, ModelRef } from './blocks.js';
|
|
2
|
+
import type { StructuredOutputConfig } from './provider.js';
|
|
3
|
+
import type { ConversationState } from '../agent/conversation-state.js';
|
|
4
|
+
|
|
5
|
+
export interface ToolUse {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
input: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ToolResult {
|
|
12
|
+
toolUseId: string;
|
|
13
|
+
block: { type: 'tool_result'; tool_use_id: string; content: string | ContentBlock[]; is_error?: boolean | undefined };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Budget {
|
|
17
|
+
maxIterations: number;
|
|
18
|
+
maxTokens: number;
|
|
19
|
+
maxCost: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type AgentStatus = 'completed' | 'aborted' | 'limit_reached' | 'budget_exceeded' | 'failed' | 'structured';
|
|
23
|
+
|
|
24
|
+
export interface RunResult {
|
|
25
|
+
status: AgentStatus;
|
|
26
|
+
iterations: number;
|
|
27
|
+
finalText: string;
|
|
28
|
+
error?: unknown | undefined;
|
|
29
|
+
abortReason?: string | undefined;
|
|
30
|
+
structuredResult?: unknown | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Context {
|
|
34
|
+
messages: Message[];
|
|
35
|
+
state: ConversationState;
|
|
36
|
+
model: ModelRef;
|
|
37
|
+
projectRoot: string;
|
|
38
|
+
workingDir: string;
|
|
39
|
+
tools: unknown[];
|
|
40
|
+
readFiles: Map<string, number>;
|
|
41
|
+
btwNotes: string[];
|
|
42
|
+
lastRequestTokens: number;
|
|
43
|
+
budget: Budget;
|
|
44
|
+
signal: AbortSignal;
|
|
45
|
+
systemPrompt: ContentBlock[];
|
|
46
|
+
readOnly: boolean;
|
|
47
|
+
structuredOutput?: StructuredOutputConfig | undefined;
|
|
48
|
+
maxTokens?: number | undefined;
|
|
49
|
+
batchToolUse?: boolean | undefined;
|
|
50
|
+
registerAbortHook(fn: () => void | Promise<void>): () => void;
|
|
51
|
+
setWorkingDir(path: string): void;
|
|
52
|
+
recordRead(absPath: string, mtimeMs: number): void;
|
|
53
|
+
hasRead(absPath: string): boolean;
|
|
54
|
+
}
|