@jun133/kitty 0.0.7 → 0.0.9

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.
@@ -0,0 +1,2701 @@
1
+ // src/provider/capabilities.ts
2
+ var DEFAULT_PROVIDER = "openai-compatible";
3
+ var DEFAULT_REQUEST_TIMEOUT_MS = 10 * 60 * 1e3;
4
+ var DEFAULT_DOCTOR_PROBE_TIMEOUT_MS = 1e4;
5
+ var RELAY_REQUEST_TIMEOUT_MS = 15 * 60 * 1e3;
6
+ var RELAY_DOCTOR_PROBE_TIMEOUT_MS = 45e3;
7
+ function resolveProviderCapabilities(input) {
8
+ const provider = normalizeProviderName(input.provider);
9
+ const model = normalizeModelName(input.model);
10
+ if (provider === "deepseek" || model.startsWith("deepseek-")) {
11
+ return {
12
+ provider: "deepseek",
13
+ model,
14
+ wireApi: "chat.completions",
15
+ supportsReasoningContent: true,
16
+ defaultReasoningEnabled: true,
17
+ defaultReasoningEffort: "high",
18
+ requestTimeoutMs: DEFAULT_REQUEST_TIMEOUT_MS,
19
+ doctorProbeTimeoutMs: DEFAULT_DOCTOR_PROBE_TIMEOUT_MS
20
+ };
21
+ }
22
+ if (provider === "openai" || model === "gpt-5.4") {
23
+ return {
24
+ provider: "openai",
25
+ model,
26
+ wireApi: "responses",
27
+ supportsReasoningContent: false,
28
+ defaultReasoningEnabled: true,
29
+ defaultReasoningEffort: "xhigh",
30
+ requestTimeoutMs: RELAY_REQUEST_TIMEOUT_MS,
31
+ doctorProbeTimeoutMs: RELAY_DOCTOR_PROBE_TIMEOUT_MS
32
+ };
33
+ }
34
+ return {
35
+ provider,
36
+ model,
37
+ wireApi: "chat.completions",
38
+ supportsReasoningContent: false,
39
+ defaultReasoningEnabled: false,
40
+ requestTimeoutMs: DEFAULT_REQUEST_TIMEOUT_MS,
41
+ doctorProbeTimeoutMs: DEFAULT_DOCTOR_PROBE_TIMEOUT_MS
42
+ };
43
+ }
44
+ function normalizeProviderName(value) {
45
+ const normalized = String(value ?? "").trim().toLowerCase();
46
+ return normalized || DEFAULT_PROVIDER;
47
+ }
48
+ function normalizeModelName(value) {
49
+ return String(value ?? "").trim();
50
+ }
51
+
52
+ // src/utils/abort.ts
53
+ function createAbortError(message = "Operation aborted") {
54
+ const error = new Error(message);
55
+ error.name = "AbortError";
56
+ error.code = "ABORT_ERR";
57
+ return error;
58
+ }
59
+ function isAbortError(error) {
60
+ if (!error) {
61
+ return false;
62
+ }
63
+ if (error instanceof Error) {
64
+ if (error.name === "AbortError") {
65
+ return true;
66
+ }
67
+ const code = String(error.code ?? "");
68
+ if (code === "ABORT_ERR" || code === "ERR_ABORTED" || code === "ABORTED") {
69
+ return true;
70
+ }
71
+ const message = error.message.toLowerCase();
72
+ if (message.includes("abort") || message.includes("aborted") || message.includes("cancelled") || message.includes("canceled")) {
73
+ return true;
74
+ }
75
+ }
76
+ if (typeof error === "object" && error && "cause" in error) {
77
+ return isAbortError(error.cause);
78
+ }
79
+ return false;
80
+ }
81
+ function throwIfAborted(signal, message) {
82
+ if (signal?.aborted) {
83
+ throw createAbortError(message ?? "Operation aborted");
84
+ }
85
+ }
86
+ function sleepWithSignal(ms, signal) {
87
+ if (!signal) {
88
+ return new Promise((resolve) => {
89
+ setTimeout(resolve, ms);
90
+ });
91
+ }
92
+ if (signal.aborted) {
93
+ return Promise.reject(createAbortError("Sleep aborted"));
94
+ }
95
+ return new Promise((resolve, reject) => {
96
+ const timer = setTimeout(() => {
97
+ signal.removeEventListener("abort", onAbort);
98
+ resolve();
99
+ }, ms);
100
+ const onAbort = () => {
101
+ clearTimeout(timer);
102
+ signal.removeEventListener("abort", onAbort);
103
+ reject(createAbortError("Sleep aborted"));
104
+ };
105
+ signal.addEventListener("abort", onAbort);
106
+ });
107
+ }
108
+
109
+ // src/provider/apiRetry.ts
110
+ var API_MAX_RETRIES = 3;
111
+ var API_RETRY_BASE_DELAY_MS = 1200;
112
+ async function withApiRetries(operation, abortSignal) {
113
+ let lastError;
114
+ for (let attempt = 1; attempt <= API_MAX_RETRIES; attempt += 1) {
115
+ try {
116
+ return await operation();
117
+ } catch (error) {
118
+ if (isAbortError(error)) {
119
+ throw error;
120
+ }
121
+ lastError = error;
122
+ if (!isRetryableApiError(error) || attempt === API_MAX_RETRIES) {
123
+ break;
124
+ }
125
+ await sleepWithSignal(API_RETRY_BASE_DELAY_MS * attempt, abortSignal);
126
+ }
127
+ }
128
+ throw lastError;
129
+ }
130
+ function isRetryableApiError(error) {
131
+ const status = error.status;
132
+ if (typeof status === "number") {
133
+ return status === 408 || status === 409 || status === 429 || status >= 500;
134
+ }
135
+ const message = String(error.message ?? error).toLowerCase();
136
+ return message.includes("timeout") || message.includes("network") || message.includes("connection error") || message.includes("connection reset") || message.includes("econnreset") || message.includes("econnrefused") || message.includes("connect timeout") || message.includes("temporarily") || message.includes("rate limit") || message.includes("overloaded");
137
+ }
138
+
139
+ // src/provider/usageNormalizer.ts
140
+ function normalizeProviderUsage(usage) {
141
+ if (!usage || typeof usage !== "object") {
142
+ return void 0;
143
+ }
144
+ const record = usage;
145
+ const promptDetails = readObject(record.prompt_tokens_details);
146
+ const completionDetails = readObject(record.completion_tokens_details);
147
+ const outputDetails = readObject(record.output_tokens_details);
148
+ const cacheCreation = readObject(record.cache_creation);
149
+ const inputTokens = readUsageNumber(record.prompt_tokens ?? record.input_tokens);
150
+ const outputTokens = readUsageNumber(record.completion_tokens ?? record.output_tokens);
151
+ const totalTokens = readUsageNumber(record.total_tokens);
152
+ const reasoningTokens = readUsageNumber(
153
+ completionDetails?.reasoning_tokens ?? outputDetails?.reasoning_tokens
154
+ );
155
+ const openAiCachedTokens = readUsageNumber(promptDetails?.cached_tokens);
156
+ const deepSeekHitTokens = readUsageNumber(record.prompt_cache_hit_tokens);
157
+ const deepSeekMissTokens = readUsageNumber(record.prompt_cache_miss_tokens);
158
+ const anthropicCacheReadTokens = readUsageNumber(record.cache_read_input_tokens);
159
+ const anthropicCacheCreationTokens = readUsageNumber(record.cache_creation_input_tokens) ?? sumUsageNumbers([
160
+ cacheCreation?.ephemeral_1h_input_tokens,
161
+ cacheCreation?.ephemeral_5m_input_tokens
162
+ ]);
163
+ const geminiCachedTokens = readUsageNumber(record.cachedContentTokenCount ?? record.cached_content_token_count);
164
+ const cacheReadTokens = firstNumber(
165
+ anthropicCacheReadTokens,
166
+ openAiCachedTokens,
167
+ geminiCachedTokens
168
+ );
169
+ const cacheHitTokens = firstNumber(deepSeekHitTokens, cacheReadTokens);
170
+ const cacheMissTokens = deepSeekMissTokens;
171
+ const cacheCreationTokens = anthropicCacheCreationTokens;
172
+ const snapshot = {
173
+ inputTokens,
174
+ outputTokens,
175
+ totalTokens,
176
+ reasoningTokens,
177
+ cacheReadTokens,
178
+ cacheCreationTokens,
179
+ cacheHitTokens,
180
+ cacheMissTokens
181
+ };
182
+ const cacheHitRate = computeCacheHitRate(snapshot);
183
+ if (cacheHitRate !== void 0) {
184
+ snapshot.cacheHitRate = cacheHitRate;
185
+ }
186
+ return Object.values(snapshot).some((value) => typeof value === "number") ? snapshot : void 0;
187
+ }
188
+ function hasProviderUsageSnapshot(usage) {
189
+ return Boolean(usage && Object.values(usage).some((value) => typeof value === "number"));
190
+ }
191
+ function computeCacheHitRate(snapshot) {
192
+ if (typeof snapshot.cacheHitTokens === "number" && typeof snapshot.cacheMissTokens === "number") {
193
+ return ratio(snapshot.cacheHitTokens, snapshot.cacheHitTokens + snapshot.cacheMissTokens);
194
+ }
195
+ if (typeof snapshot.cacheReadTokens === "number") {
196
+ const denominator = (snapshot.inputTokens ?? 0) + snapshot.cacheReadTokens + (snapshot.cacheCreationTokens ?? 0);
197
+ return ratio(snapshot.cacheReadTokens, denominator);
198
+ }
199
+ return void 0;
200
+ }
201
+ function ratio(numerator, denominator) {
202
+ if (denominator <= 0) {
203
+ return void 0;
204
+ }
205
+ return Math.round(numerator / denominator * 1e4) / 1e4;
206
+ }
207
+ function readObject(value) {
208
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
209
+ }
210
+ function firstNumber(...values) {
211
+ return values.find((value) => typeof value === "number");
212
+ }
213
+ function sumUsageNumbers(values) {
214
+ const numbers = values.map(readUsageNumber).filter((value) => typeof value === "number");
215
+ if (numbers.length === 0) {
216
+ return void 0;
217
+ }
218
+ return numbers.reduce((total, value) => total + value, 0);
219
+ }
220
+ function readUsageNumber(value) {
221
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.round(value) : void 0;
222
+ }
223
+
224
+ // src/observability/writer.ts
225
+ import fs2 from "fs/promises";
226
+ import path2 from "path";
227
+
228
+ // src/project/statePaths.ts
229
+ import fs from "fs/promises";
230
+ import path from "path";
231
+ var PROJECT_STATE_DIR_NAME = ".kitty";
232
+ var PROJECT_STATE_ENV_FILE_NAME = ".env";
233
+ var PROJECT_STATE_ENV_EXAMPLE_FILE_NAME = ".env.example";
234
+ var PROJECT_STATE_IGNORE_FILE_NAME = ".kittyignore";
235
+ var PRESERVED_PROJECT_STATE_ENTRY_NAMES = [
236
+ PROJECT_STATE_ENV_FILE_NAME,
237
+ PROJECT_STATE_ENV_EXAMPLE_FILE_NAME
238
+ ];
239
+ function getProjectStatePaths(rootDir) {
240
+ const normalizedRoot = path.resolve(rootDir);
241
+ const kittyDir = path.join(normalizedRoot, PROJECT_STATE_DIR_NAME);
242
+ const extensionsDir = path.join(kittyDir, "extensions");
243
+ const memoryDir = path.join(kittyDir, "memory");
244
+ const observabilityDir = path.join(kittyDir, "observability");
245
+ return {
246
+ rootDir: normalizedRoot,
247
+ kittyDir,
248
+ cacheDir: path.join(kittyDir, "cache"),
249
+ sessionsDir: path.join(kittyDir, "sessions"),
250
+ changesDir: path.join(kittyDir, "changes"),
251
+ eventsDir: path.join(kittyDir, "events"),
252
+ extensionsDir,
253
+ memoryDir,
254
+ evidenceMemoryDir: path.join(memoryDir, "evidence"),
255
+ projectMemoryDir: path.join(memoryDir, "project"),
256
+ sessionMemoryDir: path.join(memoryDir, "sessions"),
257
+ userMemoryDir: path.join(memoryDir, "user"),
258
+ controlPlaneLedgerFile: path.join(kittyDir, "control-plane.sqlite"),
259
+ observabilityDir,
260
+ observabilityEventsDir: path.join(observabilityDir, "events"),
261
+ observabilityCrashesDir: path.join(observabilityDir, "crashes")
262
+ };
263
+ }
264
+ async function ensureProjectStateDirectories(rootDir) {
265
+ const paths = getProjectStatePaths(rootDir);
266
+ await fs.mkdir(paths.extensionsDir, { recursive: true });
267
+ await fs.mkdir(paths.cacheDir, { recursive: true });
268
+ await fs.mkdir(paths.sessionsDir, { recursive: true });
269
+ await fs.mkdir(paths.changesDir, { recursive: true });
270
+ await fs.mkdir(paths.eventsDir, { recursive: true });
271
+ await fs.mkdir(paths.evidenceMemoryDir, { recursive: true });
272
+ await fs.mkdir(paths.projectMemoryDir, { recursive: true });
273
+ await fs.mkdir(paths.sessionMemoryDir, { recursive: true });
274
+ await fs.mkdir(paths.userMemoryDir, { recursive: true });
275
+ await fs.mkdir(paths.observabilityEventsDir, { recursive: true });
276
+ await fs.mkdir(paths.observabilityCrashesDir, { recursive: true });
277
+ return paths;
278
+ }
279
+
280
+ // src/observability/schema.ts
281
+ var OBSERVABILITY_VERSION = 1;
282
+ function buildObservabilityEventRecord(input) {
283
+ return {
284
+ version: OBSERVABILITY_VERSION,
285
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
286
+ event: normalizeText(input.event, "unknown"),
287
+ status: normalizeText(input.status, "unknown"),
288
+ host: normalizeOptionalText(input.host),
289
+ sessionId: normalizeOptionalText(input.sessionId),
290
+ executionId: normalizeOptionalText(input.executionId),
291
+ identityKind: normalizeOptionalText(input.identityKind),
292
+ identityName: normalizeOptionalText(input.identityName),
293
+ durationMs: normalizeOptionalNumber(input.durationMs),
294
+ toolName: normalizeOptionalText(input.toolName),
295
+ model: normalizeOptionalText(input.model),
296
+ error: normalizeObservabilityError(input.error),
297
+ details: normalizeDetails(input.details)
298
+ };
299
+ }
300
+ function normalizeObservabilityError(error) {
301
+ if (error == null) {
302
+ return void 0;
303
+ }
304
+ if (typeof error === "object" && error !== null && "message" in error) {
305
+ const record = error;
306
+ const message2 = normalizeText(record.message, "");
307
+ if (!message2) {
308
+ return void 0;
309
+ }
310
+ return {
311
+ message: message2,
312
+ code: normalizeOptionalText(record.code),
313
+ details: normalizeValue(record.details)
314
+ };
315
+ }
316
+ const message = readErrorMessage(error);
317
+ return message ? { message } : void 0;
318
+ }
319
+ function normalizeDetails(details) {
320
+ if (!details || typeof details !== "object") {
321
+ return void 0;
322
+ }
323
+ const normalized = normalizeValue(details);
324
+ return normalized && typeof normalized === "object" && !Array.isArray(normalized) ? normalized : void 0;
325
+ }
326
+ function normalizeValue(value, depth = 0) {
327
+ if (value == null) {
328
+ return void 0;
329
+ }
330
+ if (depth >= 4) {
331
+ return "[truncated]";
332
+ }
333
+ if (typeof value === "string") {
334
+ return value.length <= 2e3 ? value : `${value.slice(0, 1997)}...`;
335
+ }
336
+ if (typeof value === "number") {
337
+ return Number.isFinite(value) ? value : void 0;
338
+ }
339
+ if (typeof value === "boolean") {
340
+ return value;
341
+ }
342
+ if (value instanceof Error) {
343
+ return {
344
+ name: normalizeText(value.name, "Error"),
345
+ message: normalizeText(value.message, "Unknown error"),
346
+ stack: normalizeValue(value.stack, depth + 1)
347
+ };
348
+ }
349
+ if (Array.isArray(value)) {
350
+ return value.slice(0, 20).map((item) => normalizeValue(item, depth + 1)).filter((item) => item !== void 0);
351
+ }
352
+ if (typeof value === "object") {
353
+ const entries = Object.entries(value).slice(0, 30);
354
+ const normalizedEntries = entries.map(([key, item]) => [key, normalizeValue(item, depth + 1)]).filter(([, item]) => item !== void 0);
355
+ return Object.fromEntries(normalizedEntries);
356
+ }
357
+ return normalizeText(String(value), "");
358
+ }
359
+ function normalizeText(value, fallback) {
360
+ const normalized = String(value ?? "").trim();
361
+ return normalized || fallback;
362
+ }
363
+ function normalizeOptionalText(value) {
364
+ const normalized = String(value ?? "").trim();
365
+ return normalized || void 0;
366
+ }
367
+ function normalizeOptionalNumber(value) {
368
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.round(value) : void 0;
369
+ }
370
+ function readErrorMessage(error) {
371
+ if (error instanceof Error) {
372
+ return normalizeText(error.message, error.name || "Unknown error");
373
+ }
374
+ if (typeof error === "object" && error !== null && "message" in error) {
375
+ return normalizeText(error.message, "Unknown error");
376
+ }
377
+ return normalizeText(String(error ?? "Unknown error"), "Unknown error");
378
+ }
379
+
380
+ // src/observability/writer.ts
381
+ async function appendObservabilityEvent(rootDir, input) {
382
+ const paths = await ensureProjectStateDirectories(rootDir);
383
+ const record = buildObservabilityEventRecord(input);
384
+ const filePath = path2.join(paths.observabilityEventsDir, `${record.timestamp.slice(0, 10)}.jsonl`);
385
+ await fs2.appendFile(filePath, `${JSON.stringify(record)}
386
+ `, "utf8");
387
+ return record;
388
+ }
389
+ async function recordObservabilityEvent(rootDir, input) {
390
+ try {
391
+ await appendObservabilityEvent(rootDir, input);
392
+ } catch {
393
+ }
394
+ }
395
+
396
+ // src/provider/cachePolicy.ts
397
+ function resolveProviderCachePolicy(input) {
398
+ const capabilities = resolveProviderCapabilities(input);
399
+ if (capabilities.provider === "openai") {
400
+ return {
401
+ provider: "openai",
402
+ automaticPrefixCache: true,
403
+ promptCacheKey: buildPromptCacheKey(input)
404
+ };
405
+ }
406
+ if (capabilities.provider === "deepseek") {
407
+ return {
408
+ provider: "deepseek",
409
+ automaticPrefixCache: true
410
+ };
411
+ }
412
+ return {
413
+ provider: "generic",
414
+ automaticPrefixCache: false
415
+ };
416
+ }
417
+ function buildPromptCacheKey(input) {
418
+ const seed = input.sessionId || input.projectRoot;
419
+ if (!seed) {
420
+ return void 0;
421
+ }
422
+ return `kitty:${stableHash(seed)}`;
423
+ }
424
+ function stableHash(value) {
425
+ let hash = 2166136261;
426
+ for (let index = 0; index < value.length; index += 1) {
427
+ hash ^= value.charCodeAt(index);
428
+ hash = Math.imul(hash, 16777619);
429
+ }
430
+ return (hash >>> 0).toString(16).padStart(8, "0");
431
+ }
432
+
433
+ // src/provider/chatRequestBody.ts
434
+ function buildProviderRequestBody(input) {
435
+ const capabilities = resolveProviderCapabilities(input);
436
+ const thinking = capabilities.provider === "deepseek" ? resolveDeepSeekThinking(input.messages, input.thinking ?? "enabled") : input.thinking;
437
+ const body = {
438
+ model: input.model,
439
+ messages: toChatCompletionMessages(input.messages),
440
+ tools: input.tools,
441
+ stream: input.stream
442
+ };
443
+ if (capabilities.provider !== "deepseek" && input.tools?.length) {
444
+ body.tool_choice = "auto";
445
+ }
446
+ if (input.stream) {
447
+ body.stream_options = {
448
+ include_usage: true
449
+ };
450
+ }
451
+ const cachePolicy = resolveProviderCachePolicy(input);
452
+ if (cachePolicy.promptCacheKey) {
453
+ body.prompt_cache_key = cachePolicy.promptCacheKey;
454
+ }
455
+ if (typeof input.maxOutputTokens === "number" && Number.isFinite(input.maxOutputTokens)) {
456
+ body.max_tokens = Math.max(1, Math.trunc(input.maxOutputTokens));
457
+ }
458
+ if (capabilities.provider === "deepseek") {
459
+ body.thinking = { type: thinking };
460
+ if (thinking === "enabled") {
461
+ body.reasoning_effort = normalizeDeepSeekReasoningEffort(input.reasoningEffort ?? capabilities.defaultReasoningEffort);
462
+ }
463
+ } else if (input.forceReasoning || capabilities.defaultReasoningEnabled) {
464
+ body.thinking = { type: "enabled" };
465
+ }
466
+ return body;
467
+ }
468
+ function resolveDeepSeekThinking(messages, requested) {
469
+ if (requested === "disabled") {
470
+ return "disabled";
471
+ }
472
+ return hasUnreplayableAssistantReasoning(messages) ? "disabled" : "enabled";
473
+ }
474
+ function hasUnreplayableAssistantReasoning(messages) {
475
+ return messages.some(
476
+ (message) => message.role === "assistant" && Array.isArray(message.toolCalls) && message.toolCalls.length > 0 && message.reasoningContent === void 0
477
+ );
478
+ }
479
+ function normalizeDeepSeekReasoningEffort(effort) {
480
+ if (effort === void 0 || effort === "minimal" || effort === "low" || effort === "medium" || effort === "high") {
481
+ return "high";
482
+ }
483
+ if (effort === "xhigh" || effort === "max") {
484
+ return "max";
485
+ }
486
+ return "high";
487
+ }
488
+
489
+ // src/provider/chatCompletionsAdapter.ts
490
+ var chatCompletionsAdapter = {
491
+ wireApi: "chat.completions",
492
+ async fetchStreaming(client, request) {
493
+ const startedAt = Date.now();
494
+ let usage;
495
+ throwIfAborted(request.abortSignal, "Streaming request aborted");
496
+ try {
497
+ const stream = await client.chat.completions.create(
498
+ {
499
+ ...buildProviderRequestBody({
500
+ provider: request.provider,
501
+ model: request.model,
502
+ messages: request.messages,
503
+ tools: request.tools,
504
+ stream: true,
505
+ forceReasoning: request.forceReasoning,
506
+ thinking: request.thinking,
507
+ reasoningEffort: request.reasoningEffort,
508
+ maxOutputTokens: request.maxOutputTokens,
509
+ sessionId: request.sessionId,
510
+ projectRoot: request.projectRoot
511
+ }),
512
+ signal: request.abortSignal
513
+ }
514
+ );
515
+ if (request.abortSignal?.aborted) {
516
+ abortStream(stream);
517
+ throw createAbortError("Streaming aborted");
518
+ }
519
+ let content = "";
520
+ let reasoningContent = "";
521
+ const toolCallParts = /* @__PURE__ */ new Map();
522
+ for await (const chunk of stream) {
523
+ if (request.abortSignal?.aborted) {
524
+ abortStream(stream);
525
+ throw createAbortError("Streaming aborted");
526
+ }
527
+ usage = normalizeProviderUsage(chunk.usage) ?? usage;
528
+ const delta = chunk.choices?.[0]?.delta;
529
+ if (!delta) {
530
+ continue;
531
+ }
532
+ if (typeof delta.content === "string" && delta.content.length > 0) {
533
+ content += delta.content;
534
+ request.callbacks?.onAssistantDelta?.(delta.content);
535
+ }
536
+ if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) {
537
+ reasoningContent += delta.reasoning_content;
538
+ request.callbacks?.onReasoningDelta?.(delta.reasoning_content);
539
+ }
540
+ if (Array.isArray(delta.tool_calls)) {
541
+ for (const toolCall of delta.tool_calls) {
542
+ const index = typeof toolCall.index === "number" ? toolCall.index : 0;
543
+ const existing = toolCallParts.get(index) ?? {
544
+ id: toolCall.id ?? `tool-${index}`,
545
+ name: "",
546
+ arguments: ""
547
+ };
548
+ if (toolCall.id) {
549
+ existing.id = toolCall.id;
550
+ }
551
+ if (toolCall.function?.name) {
552
+ existing.name += toolCall.function.name;
553
+ }
554
+ if (toolCall.function?.arguments) {
555
+ existing.arguments += toolCall.function.arguments;
556
+ }
557
+ toolCallParts.set(index, existing);
558
+ }
559
+ }
560
+ }
561
+ return {
562
+ content: content.length > 0 ? content : null,
563
+ reasoningContent: reasoningContent.length > 0 ? reasoningContent : void 0,
564
+ streamedAssistantContent: content.length > 0,
565
+ streamedReasoningContent: reasoningContent.length > 0,
566
+ toolCalls: [...toolCallParts.entries()].sort((left, right) => left[0] - right[0]).map(([, toolCall]) => ({
567
+ id: toolCall.id,
568
+ type: "function",
569
+ function: {
570
+ name: toolCall.name,
571
+ arguments: toolCall.arguments
572
+ }
573
+ }))
574
+ };
575
+ } finally {
576
+ request.onRequestMetric?.({
577
+ durationMs: Date.now() - startedAt,
578
+ usage
579
+ });
580
+ }
581
+ },
582
+ async fetchNonStreaming(client, request) {
583
+ const startedAt = Date.now();
584
+ let usage;
585
+ throwIfAborted(request.abortSignal, "Request aborted");
586
+ try {
587
+ const completion = await client.chat.completions.create(
588
+ {
589
+ ...buildProviderRequestBody({
590
+ provider: request.provider,
591
+ model: request.model,
592
+ messages: request.messages,
593
+ tools: request.tools,
594
+ stream: false,
595
+ forceReasoning: request.forceReasoning,
596
+ thinking: request.thinking,
597
+ reasoningEffort: request.reasoningEffort,
598
+ maxOutputTokens: request.maxOutputTokens,
599
+ sessionId: request.sessionId,
600
+ projectRoot: request.projectRoot
601
+ }),
602
+ signal: request.abortSignal
603
+ }
604
+ );
605
+ usage = normalizeProviderUsage(completion.usage);
606
+ const message = completion.choices[0]?.message;
607
+ if (!message) {
608
+ throw new Error("API returned no message.");
609
+ }
610
+ return {
611
+ content: typeof message.content === "string" ? message.content : collapseContentParts(message.content),
612
+ reasoningContent: readReasoningContent(message),
613
+ streamedAssistantContent: false,
614
+ streamedReasoningContent: false,
615
+ toolCalls: (message.tool_calls ?? []).filter((call) => call.type === "function").map((call) => ({
616
+ id: call.id,
617
+ type: "function",
618
+ function: {
619
+ name: call.function.name,
620
+ arguments: call.function.arguments
621
+ }
622
+ }))
623
+ };
624
+ } finally {
625
+ request.onRequestMetric?.({
626
+ durationMs: Date.now() - startedAt,
627
+ usage
628
+ });
629
+ }
630
+ }
631
+ };
632
+ function abortStream(stream) {
633
+ try {
634
+ stream?.controller?.abort();
635
+ } catch {
636
+ }
637
+ }
638
+ function toChatCompletionMessages(messages) {
639
+ return messages.map((message) => {
640
+ if (message.role === "tool") {
641
+ return {
642
+ role: "tool",
643
+ content: message.content ?? "",
644
+ tool_call_id: message.toolCallId ?? ""
645
+ };
646
+ }
647
+ if (message.role === "assistant" && message.toolCalls?.length) {
648
+ const assistantMessage = {
649
+ role: "assistant",
650
+ content: message.content ?? "",
651
+ tool_calls: message.toolCalls
652
+ };
653
+ if (message.reasoningContent !== void 0) {
654
+ assistantMessage.reasoning_content = message.reasoningContent;
655
+ }
656
+ return assistantMessage;
657
+ }
658
+ const baseMessage = {
659
+ role: message.role,
660
+ content: message.content ?? "",
661
+ name: message.name
662
+ };
663
+ if (message.role === "assistant" && message.reasoningContent !== void 0) {
664
+ baseMessage.reasoning_content = message.reasoningContent;
665
+ }
666
+ return baseMessage;
667
+ });
668
+ }
669
+
670
+ // src/provider/responsesAdapter.ts
671
+ var responsesAdapter = {
672
+ wireApi: "responses",
673
+ async fetchStreaming(client, request) {
674
+ const startedAt = Date.now();
675
+ let usage;
676
+ throwIfAborted(request.abortSignal, "Streaming request aborted");
677
+ try {
678
+ const stream = await client.responses.create(
679
+ {
680
+ ...buildResponsesRequestBody(request),
681
+ stream: true
682
+ },
683
+ {
684
+ signal: request.abortSignal
685
+ }
686
+ );
687
+ if (request.abortSignal?.aborted) {
688
+ abortStream2(stream);
689
+ throw createAbortError("Streaming aborted");
690
+ }
691
+ let content = "";
692
+ let reasoningContent = "";
693
+ const toolCalls = /* @__PURE__ */ new Map();
694
+ for await (const event of stream) {
695
+ if (request.abortSignal?.aborted) {
696
+ abortStream2(stream);
697
+ throw createAbortError("Streaming aborted");
698
+ }
699
+ usage = normalizeProviderUsage(event.response?.usage) ?? usage;
700
+ if (event.type === "response.output_text.delta" && typeof event.delta === "string") {
701
+ content += event.delta;
702
+ request.callbacks?.onAssistantDelta?.(event.delta);
703
+ continue;
704
+ }
705
+ if ((event.type === "response.reasoning_text.delta" || event.type === "response.reasoning_summary_text.delta") && typeof event.delta === "string") {
706
+ reasoningContent += event.delta;
707
+ request.callbacks?.onReasoningDelta?.(event.delta);
708
+ continue;
709
+ }
710
+ if (event.type === "response.function_call_arguments.delta" && typeof event.delta === "string") {
711
+ const index = typeof event.output_index === "number" ? event.output_index : 0;
712
+ const existing = toolCalls.get(index) ?? {
713
+ id: event.item_id ?? `tool-${index}`,
714
+ name: "",
715
+ arguments: ""
716
+ };
717
+ existing.arguments += event.delta;
718
+ toolCalls.set(index, existing);
719
+ continue;
720
+ }
721
+ if (event.type === "response.function_call_arguments.done") {
722
+ const index = typeof event.output_index === "number" ? event.output_index : 0;
723
+ const existing = toolCalls.get(index) ?? {
724
+ id: event.item_id ?? `tool-${index}`,
725
+ name: "",
726
+ arguments: ""
727
+ };
728
+ if (typeof event.name === "string") {
729
+ existing.name = event.name;
730
+ }
731
+ if (typeof event.arguments === "string" && event.arguments.length > 0) {
732
+ existing.arguments = event.arguments;
733
+ }
734
+ toolCalls.set(index, existing);
735
+ continue;
736
+ }
737
+ if (event.type === "response.output_item.done" && event.item?.type === "function_call") {
738
+ const index = typeof event.output_index === "number" ? event.output_index : 0;
739
+ toolCalls.set(index, {
740
+ id: event.item.call_id ?? event.item.id ?? `tool-${index}`,
741
+ name: event.item.name ?? "",
742
+ arguments: event.item.arguments ?? ""
743
+ });
744
+ }
745
+ }
746
+ return {
747
+ content: content.length > 0 ? content : null,
748
+ reasoningContent: reasoningContent.length > 0 ? reasoningContent : void 0,
749
+ streamedAssistantContent: content.length > 0,
750
+ streamedReasoningContent: reasoningContent.length > 0,
751
+ toolCalls: [...toolCalls.entries()].sort((left, right) => left[0] - right[0]).map(([, toolCall]) => ({
752
+ id: toolCall.id,
753
+ type: "function",
754
+ function: {
755
+ name: toolCall.name,
756
+ arguments: toolCall.arguments
757
+ }
758
+ }))
759
+ };
760
+ } finally {
761
+ request.onRequestMetric?.({
762
+ durationMs: Date.now() - startedAt,
763
+ usage
764
+ });
765
+ }
766
+ },
767
+ async fetchNonStreaming(client, request) {
768
+ const startedAt = Date.now();
769
+ let usage;
770
+ throwIfAborted(request.abortSignal, "Request aborted");
771
+ try {
772
+ const response = await client.responses.create(
773
+ {
774
+ ...buildResponsesRequestBody(request),
775
+ stream: false
776
+ },
777
+ {
778
+ signal: request.abortSignal
779
+ }
780
+ );
781
+ usage = normalizeProviderUsage(response.usage);
782
+ return {
783
+ content: normalizeOutputText(response),
784
+ reasoningContent: readResponseReasoning(response),
785
+ streamedAssistantContent: false,
786
+ streamedReasoningContent: false,
787
+ toolCalls: readResponseToolCalls(response)
788
+ };
789
+ } finally {
790
+ request.onRequestMetric?.({
791
+ durationMs: Date.now() - startedAt,
792
+ usage
793
+ });
794
+ }
795
+ }
796
+ };
797
+ function buildResponsesRequestBody(request) {
798
+ const capabilities = resolveProviderCapabilities({
799
+ provider: request.provider,
800
+ model: request.model
801
+ });
802
+ const body = {
803
+ model: request.model,
804
+ input: toResponsesInput(request.messages),
805
+ tools: request.tools?.map((tool) => ({
806
+ type: "function",
807
+ name: tool.function.name,
808
+ description: tool.function.description,
809
+ parameters: tool.function.parameters ?? null,
810
+ strict: false
811
+ })),
812
+ tool_choice: request.tools?.length ? "auto" : void 0
813
+ };
814
+ if (typeof request.maxOutputTokens === "number" && Number.isFinite(request.maxOutputTokens)) {
815
+ body.max_output_tokens = Math.max(1, Math.trunc(request.maxOutputTokens));
816
+ }
817
+ const cachePolicy = resolveProviderCachePolicy({
818
+ provider: request.provider,
819
+ model: request.model,
820
+ sessionId: request.sessionId,
821
+ projectRoot: request.projectRoot
822
+ });
823
+ if (cachePolicy.promptCacheKey) {
824
+ body.prompt_cache_key = cachePolicy.promptCacheKey;
825
+ }
826
+ const reasoningEffort = normalizeResponsesReasoningEffort(
827
+ request.reasoningEffort ?? capabilities.defaultReasoningEffort
828
+ );
829
+ if (request.forceReasoning || capabilities.defaultReasoningEnabled || reasoningEffort) {
830
+ body.reasoning = {
831
+ effort: reasoningEffort ?? "high",
832
+ summary: "detailed"
833
+ };
834
+ }
835
+ return body;
836
+ }
837
+ function normalizeResponsesReasoningEffort(effort) {
838
+ return effort === "max" ? void 0 : effort;
839
+ }
840
+ function toResponsesInput(messages) {
841
+ const items = [];
842
+ for (const message of messages) {
843
+ if (message.role === "tool") {
844
+ items.push({
845
+ type: "function_call_output",
846
+ call_id: message.toolCallId ?? "",
847
+ output: message.content ?? ""
848
+ });
849
+ continue;
850
+ }
851
+ if (message.role === "assistant" && message.toolCalls?.length) {
852
+ if (typeof message.content === "string" && message.content.trim().length > 0) {
853
+ items.push({
854
+ type: "message",
855
+ role: "assistant",
856
+ content: message.content
857
+ });
858
+ }
859
+ for (const toolCall of message.toolCalls) {
860
+ items.push({
861
+ type: "function_call",
862
+ call_id: toolCall.id,
863
+ name: toolCall.function.name,
864
+ arguments: toolCall.function.arguments
865
+ });
866
+ }
867
+ continue;
868
+ }
869
+ items.push({
870
+ type: "message",
871
+ role: message.role,
872
+ content: message.content ?? ""
873
+ });
874
+ }
875
+ return items;
876
+ }
877
+ function normalizeOutputText(response) {
878
+ const outputText = response.output_text;
879
+ if (typeof outputText === "string" && outputText.trim().length > 0) {
880
+ return outputText;
881
+ }
882
+ const output = response.output;
883
+ if (!Array.isArray(output)) {
884
+ return null;
885
+ }
886
+ const fragments = output.flatMap((item) => {
887
+ if (!item || typeof item !== "object" || item.type !== "message") {
888
+ return [];
889
+ }
890
+ const content = item.content;
891
+ if (!Array.isArray(content)) {
892
+ return [];
893
+ }
894
+ return content.flatMap((part) => {
895
+ if (!part || typeof part !== "object" || part.type !== "output_text") {
896
+ return [];
897
+ }
898
+ return typeof part.text === "string" ? [part.text] : [];
899
+ });
900
+ });
901
+ return fragments.length > 0 ? fragments.join("") : null;
902
+ }
903
+ function readResponseToolCalls(response) {
904
+ const output = response.output;
905
+ if (!Array.isArray(output)) {
906
+ return [];
907
+ }
908
+ return output.filter((item) => Boolean(item) && typeof item === "object" && item.type === "function_call").map((item) => ({
909
+ id: item.call_id ?? item.id ?? crypto.randomUUID(),
910
+ type: "function",
911
+ function: {
912
+ name: item.name ?? "",
913
+ arguments: item.arguments ?? ""
914
+ }
915
+ }));
916
+ }
917
+ function readResponseReasoning(response) {
918
+ const output = response.output;
919
+ if (!Array.isArray(output)) {
920
+ return void 0;
921
+ }
922
+ const fragments = output.flatMap((item) => {
923
+ if (!item || typeof item !== "object" || item.type !== "reasoning") {
924
+ return [];
925
+ }
926
+ const reasoningItem = item;
927
+ const summary = Array.isArray(reasoningItem.summary) ? reasoningItem.summary.map((entry) => typeof entry?.text === "string" ? entry.text : "").filter(Boolean) : [];
928
+ const content = Array.isArray(reasoningItem.content) ? reasoningItem.content.map((entry) => typeof entry?.text === "string" ? entry.text : "").filter(Boolean) : [];
929
+ return [...content, ...summary];
930
+ });
931
+ return fragments.length > 0 ? fragments.join("") : void 0;
932
+ }
933
+ function abortStream2(stream) {
934
+ try {
935
+ stream?.controller?.abort();
936
+ } catch {
937
+ }
938
+ }
939
+
940
+ // src/provider/client.ts
941
+ import OpenAI from "openai";
942
+
943
+ // src/provider/connection.ts
944
+ function buildProviderBaseUrlCandidates(baseUrl) {
945
+ const normalized = trimTrailingSlash(baseUrl);
946
+ if (!normalized) {
947
+ return [normalized];
948
+ }
949
+ const candidates = [normalized];
950
+ try {
951
+ const parsed = new URL(normalized);
952
+ if (parsed.pathname === "" || parsed.pathname === "/") {
953
+ candidates.push(trimTrailingSlash(new URL("v1", ensureTrailingSlash(parsed.toString())).toString()));
954
+ }
955
+ } catch {
956
+ return candidates;
957
+ }
958
+ return [...new Set(candidates)];
959
+ }
960
+ function ensureTrailingSlash(baseUrl) {
961
+ return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
962
+ }
963
+ function trimTrailingSlash(baseUrl) {
964
+ const trimmed = String(baseUrl ?? "").trim();
965
+ if (!trimmed) {
966
+ return trimmed;
967
+ }
968
+ return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
969
+ }
970
+
971
+ // src/provider/client.ts
972
+ function createProviderClientPool(config) {
973
+ const capabilities = resolveProviderCapabilities({
974
+ provider: config.provider,
975
+ model: config.model
976
+ });
977
+ const baseUrls = buildProviderBaseUrlCandidates(config.baseUrl);
978
+ const clients = /* @__PURE__ */ new Map();
979
+ let preferredBaseUrl;
980
+ return {
981
+ candidates() {
982
+ const ordered = preferredBaseUrl ? [preferredBaseUrl, ...baseUrls.filter((baseUrl) => baseUrl !== preferredBaseUrl)] : baseUrls;
983
+ return ordered.map((baseUrl) => ({
984
+ baseUrl,
985
+ client: getOrCreateClient(baseUrl)
986
+ }));
987
+ },
988
+ markHealthy(baseUrl) {
989
+ preferredBaseUrl = baseUrl;
990
+ }
991
+ };
992
+ function getOrCreateClient(baseUrl) {
993
+ const existing = clients.get(baseUrl);
994
+ if (existing) {
995
+ return existing;
996
+ }
997
+ const client = new OpenAI({
998
+ apiKey: config.apiKey,
999
+ baseURL: baseUrl,
1000
+ timeout: capabilities.requestTimeoutMs,
1001
+ maxRetries: 0
1002
+ });
1003
+ clients.set(baseUrl, client);
1004
+ return client;
1005
+ }
1006
+ }
1007
+ function isProviderClientPool(value) {
1008
+ return Boolean(
1009
+ value && typeof value === "object" && typeof value.candidates === "function" && typeof value.markHealthy === "function"
1010
+ );
1011
+ }
1012
+
1013
+ // src/provider/request.ts
1014
+ async function fetchAssistantResponse(client, messages, request, tools, callbacks, abortSignal, onRequestMetric, observability) {
1015
+ const capabilities = resolveProviderCapabilities(request);
1016
+ const adapter = selectProviderWireAdapter(capabilities.wireApi);
1017
+ return tryFetch(
1018
+ adapter,
1019
+ client,
1020
+ messages,
1021
+ request,
1022
+ tools,
1023
+ callbacks,
1024
+ false,
1025
+ abortSignal,
1026
+ onRequestMetric,
1027
+ observability
1028
+ );
1029
+ }
1030
+ async function tryFetch(adapter, client, messages, request, tools, callbacks, forceReasoning, abortSignal, onRequestMetric, observability) {
1031
+ const startedAt = Date.now();
1032
+ let latestMetric;
1033
+ let resolvedBaseUrl;
1034
+ const forwardMetric = (metric) => {
1035
+ latestMetric = metric;
1036
+ onRequestMetric?.(metric);
1037
+ };
1038
+ if (observability) {
1039
+ await recordObservabilityEvent(observability.rootDir, {
1040
+ event: "model.request",
1041
+ status: "started",
1042
+ sessionId: observability.sessionId,
1043
+ identityKind: observability.identityKind,
1044
+ identityName: observability.identityName,
1045
+ model: request.model,
1046
+ details: {
1047
+ provider: request.provider,
1048
+ configuredModel: observability.configuredModel,
1049
+ requestModel: request.model,
1050
+ wireApi: adapter.wireApi,
1051
+ baseUrl: resolvedBaseUrl
1052
+ }
1053
+ });
1054
+ }
1055
+ try {
1056
+ const response = await withApiRetries(
1057
+ () => invokeWithProviderClients(client, async (providerClient, baseUrl) => {
1058
+ resolvedBaseUrl = baseUrl;
1059
+ return adapter.fetchStreaming(providerClient, {
1060
+ provider: request.provider,
1061
+ model: request.model,
1062
+ messages,
1063
+ tools,
1064
+ callbacks,
1065
+ forceReasoning,
1066
+ thinking: request.thinking,
1067
+ reasoningEffort: request.reasoningEffort,
1068
+ maxOutputTokens: request.maxOutputTokens,
1069
+ sessionId: request.sessionId,
1070
+ projectRoot: request.projectRoot,
1071
+ abortSignal,
1072
+ onRequestMetric: forwardMetric
1073
+ });
1074
+ }),
1075
+ abortSignal
1076
+ );
1077
+ if (observability) {
1078
+ await recordObservabilityEvent(observability.rootDir, {
1079
+ event: "model.request",
1080
+ status: "completed",
1081
+ sessionId: observability.sessionId,
1082
+ identityKind: observability.identityKind,
1083
+ identityName: observability.identityName,
1084
+ model: request.model,
1085
+ durationMs: Date.now() - startedAt,
1086
+ details: {
1087
+ provider: request.provider,
1088
+ configuredModel: observability.configuredModel,
1089
+ requestModel: request.model,
1090
+ wireApi: adapter.wireApi,
1091
+ baseUrl: resolvedBaseUrl,
1092
+ usage: latestMetric?.usage,
1093
+ usageAvailable: hasProviderUsageSnapshot(latestMetric?.usage)
1094
+ }
1095
+ });
1096
+ }
1097
+ return response;
1098
+ } catch (error) {
1099
+ if (isAbortError(error)) {
1100
+ throw error;
1101
+ }
1102
+ try {
1103
+ const response = await withApiRetries(
1104
+ () => invokeWithProviderClients(client, async (providerClient, baseUrl) => {
1105
+ resolvedBaseUrl = baseUrl;
1106
+ return adapter.fetchNonStreaming(providerClient, {
1107
+ provider: request.provider,
1108
+ model: request.model,
1109
+ messages,
1110
+ tools,
1111
+ callbacks,
1112
+ forceReasoning,
1113
+ thinking: request.thinking,
1114
+ reasoningEffort: request.reasoningEffort,
1115
+ maxOutputTokens: request.maxOutputTokens,
1116
+ sessionId: request.sessionId,
1117
+ projectRoot: request.projectRoot,
1118
+ abortSignal,
1119
+ onRequestMetric: forwardMetric
1120
+ });
1121
+ }),
1122
+ abortSignal
1123
+ );
1124
+ if (observability) {
1125
+ await recordObservabilityEvent(observability.rootDir, {
1126
+ event: "model.request",
1127
+ status: "completed",
1128
+ sessionId: observability.sessionId,
1129
+ identityKind: observability.identityKind,
1130
+ identityName: observability.identityName,
1131
+ model: request.model,
1132
+ durationMs: Date.now() - startedAt,
1133
+ details: {
1134
+ provider: request.provider,
1135
+ configuredModel: observability.configuredModel,
1136
+ requestModel: request.model,
1137
+ wireApi: adapter.wireApi,
1138
+ baseUrl: resolvedBaseUrl,
1139
+ usage: latestMetric?.usage,
1140
+ usageAvailable: hasProviderUsageSnapshot(latestMetric?.usage)
1141
+ }
1142
+ });
1143
+ }
1144
+ return response;
1145
+ } catch (fallbackError) {
1146
+ if (!isAbortError(fallbackError) && observability) {
1147
+ await recordObservabilityEvent(observability.rootDir, {
1148
+ event: "model.request",
1149
+ status: "failed",
1150
+ sessionId: observability.sessionId,
1151
+ identityKind: observability.identityKind,
1152
+ identityName: observability.identityName,
1153
+ model: request.model,
1154
+ durationMs: Date.now() - startedAt,
1155
+ error: fallbackError,
1156
+ details: {
1157
+ provider: request.provider,
1158
+ configuredModel: observability.configuredModel,
1159
+ requestModel: request.model,
1160
+ wireApi: adapter.wireApi,
1161
+ baseUrl: resolvedBaseUrl,
1162
+ usage: latestMetric?.usage,
1163
+ usageAvailable: hasProviderUsageSnapshot(latestMetric?.usage)
1164
+ }
1165
+ });
1166
+ }
1167
+ throw fallbackError;
1168
+ }
1169
+ }
1170
+ }
1171
+ function selectProviderWireAdapter(wireApi) {
1172
+ if (wireApi === "responses") {
1173
+ return responsesAdapter;
1174
+ }
1175
+ return chatCompletionsAdapter;
1176
+ }
1177
+ async function invokeWithProviderClients(client, operation) {
1178
+ if (!isProviderClientPool(client)) {
1179
+ return operation(client, void 0);
1180
+ }
1181
+ let lastError;
1182
+ const candidates = client.candidates();
1183
+ for (let index = 0; index < candidates.length; index += 1) {
1184
+ const candidate = candidates[index];
1185
+ try {
1186
+ const result = await operation(candidate.client, candidate.baseUrl);
1187
+ client.markHealthy(candidate.baseUrl);
1188
+ return result;
1189
+ } catch (error) {
1190
+ lastError = error;
1191
+ if (isAbortError(error)) {
1192
+ throw error;
1193
+ }
1194
+ const hasMoreCandidates = index < candidates.length - 1;
1195
+ if (!hasMoreCandidates || !canRetryWithAlternateBaseUrl(error)) {
1196
+ throw error;
1197
+ }
1198
+ }
1199
+ }
1200
+ throw lastError;
1201
+ }
1202
+ function canRetryWithAlternateBaseUrl(error) {
1203
+ const status = error.status;
1204
+ const message = String(error.message ?? error).toLowerCase();
1205
+ return status === 404 || status === 405 || message.includes("404") || message.includes("not found");
1206
+ }
1207
+
1208
+ // src/session/messages.ts
1209
+ function buildChatMessages(systemPrompt, messages, contextWindowMessages, model) {
1210
+ const recentMessages = messages.slice(-contextWindowMessages);
1211
+ return [
1212
+ {
1213
+ role: "system",
1214
+ content: systemPrompt
1215
+ },
1216
+ ...recentMessages.map(
1217
+ (message, index) => toChatMessage(message, {
1218
+ includeReasoning: shouldIncludeStoredAssistantReasoning(recentMessages, index, model)
1219
+ })
1220
+ )
1221
+ ];
1222
+ }
1223
+ function createMessage(role, content, options = {}) {
1224
+ return {
1225
+ role,
1226
+ content,
1227
+ source: options.source,
1228
+ name: options.name,
1229
+ tool_calls: options.toolCalls,
1230
+ reasoningContent: options.reasoningContent,
1231
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1232
+ };
1233
+ }
1234
+ function createToolMessage(toolCallId, content, name) {
1235
+ return {
1236
+ role: "tool",
1237
+ content,
1238
+ tool_call_id: toolCallId,
1239
+ name,
1240
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1241
+ };
1242
+ }
1243
+ function readReasoningContent(message) {
1244
+ const candidate = message.reasoning_content;
1245
+ return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
1246
+ }
1247
+ function collapseContentParts(content) {
1248
+ if (!Array.isArray(content)) {
1249
+ return null;
1250
+ }
1251
+ const fragments = content.map((part) => {
1252
+ if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
1253
+ return part.text;
1254
+ }
1255
+ return "";
1256
+ }).filter(Boolean);
1257
+ return fragments.length > 0 ? fragments.join("") : null;
1258
+ }
1259
+ function toChatMessage(message, options) {
1260
+ if (message.role === "tool") {
1261
+ return {
1262
+ role: "tool",
1263
+ content: message.content ?? "",
1264
+ tool_call_id: message.tool_call_id ?? ""
1265
+ };
1266
+ }
1267
+ if (message.role === "assistant" && message.tool_calls?.length) {
1268
+ const assistantMessage = {
1269
+ role: "assistant",
1270
+ content: message.content,
1271
+ tool_calls: message.tool_calls
1272
+ };
1273
+ if (options.includeReasoning && message.reasoningContent !== void 0) {
1274
+ assistantMessage.reasoning_content = message.reasoningContent;
1275
+ }
1276
+ return assistantMessage;
1277
+ }
1278
+ const baseMessage = {
1279
+ role: message.role,
1280
+ content: message.content ?? "",
1281
+ name: message.name
1282
+ };
1283
+ if (message.role === "assistant" && options.includeReasoning && message.reasoningContent !== void 0) {
1284
+ baseMessage.reasoning_content = message.reasoningContent;
1285
+ }
1286
+ return baseMessage;
1287
+ }
1288
+ function findLatestUserIndex(messages) {
1289
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
1290
+ if (messages[index]?.role === "user") {
1291
+ return index;
1292
+ }
1293
+ }
1294
+ return -1;
1295
+ }
1296
+ function expandStartToToolBoundary(messages, startIndex) {
1297
+ let index = Math.max(0, Math.min(startIndex, messages.length - 1));
1298
+ while (index > 0 && messages[index]?.role === "tool") {
1299
+ index -= 1;
1300
+ }
1301
+ return index;
1302
+ }
1303
+ function modelUsesReasoningContent(model) {
1304
+ return resolveProviderCapabilities({ model }).supportsReasoningContent;
1305
+ }
1306
+ function isAssistantMessageInLatestTurn(messages, index) {
1307
+ return messages[index]?.role === "assistant" && index > findLatestUserIndex(messages);
1308
+ }
1309
+ function shouldIncludeStoredAssistantReasoning(messages, index, model) {
1310
+ return modelUsesReasoningContent(model) && messages[index]?.role === "assistant";
1311
+ }
1312
+
1313
+ // src/session/turnFrame.ts
1314
+ function isInternalMessage(message) {
1315
+ if (typeof message === "object" && message !== null && "source" in message) {
1316
+ return message.source === "internal";
1317
+ }
1318
+ return false;
1319
+ }
1320
+ function createInternalReminder(text) {
1321
+ return text.trim();
1322
+ }
1323
+ function readUserInput(message) {
1324
+ if (typeof message === "object" && message !== null) {
1325
+ if (isInternalMessage(message)) {
1326
+ return void 0;
1327
+ }
1328
+ const normalized2 = oneLine(message.content ?? "");
1329
+ return normalized2 || void 0;
1330
+ }
1331
+ if (isInternalMessage(message)) {
1332
+ return void 0;
1333
+ }
1334
+ const normalized = oneLine(message ?? "");
1335
+ return normalized || void 0;
1336
+ }
1337
+ function findLatestUserInputIndex(messages) {
1338
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
1339
+ const message = messages[index];
1340
+ if (message?.role === "user" && readUserInput(message)) {
1341
+ return index;
1342
+ }
1343
+ }
1344
+ return -1;
1345
+ }
1346
+ function sliceCurrentUserInputFrame(messages) {
1347
+ const frameStart = findLatestUserInputIndex(messages);
1348
+ if (frameStart < 0) {
1349
+ return [];
1350
+ }
1351
+ const frame = messages.slice(frameStart);
1352
+ return frame.filter((message) => !(message.role === "user" && isInternalMessage(message)));
1353
+ }
1354
+ function oneLine(value) {
1355
+ return value.replace(/\s+/g, " ").trim();
1356
+ }
1357
+
1358
+ // src/session/taskStateHistory.ts
1359
+ import path3 from "path";
1360
+ function collectActiveFiles(messages) {
1361
+ const files = [];
1362
+ for (const message of messages) {
1363
+ if (!message) {
1364
+ continue;
1365
+ }
1366
+ if (message.role === "assistant" && message.tool_calls?.length) {
1367
+ for (const toolCall of message.tool_calls) {
1368
+ const parsed = safeParseObject(toolCall.function.arguments);
1369
+ collectPathsFromValue(parsed, files);
1370
+ }
1371
+ continue;
1372
+ }
1373
+ if (message.role === "tool") {
1374
+ const parsed = safeParseObject(message.content ?? "");
1375
+ collectPathsFromValue(parsed, files);
1376
+ }
1377
+ }
1378
+ return files.map((value) => normalizeFilePath(value)).filter(Boolean);
1379
+ }
1380
+ function collectPlannedActions(messages) {
1381
+ const actions = [];
1382
+ for (const message of messages) {
1383
+ if (message?.role !== "assistant" || !message.tool_calls?.length) {
1384
+ continue;
1385
+ }
1386
+ const names = message.tool_calls.map((toolCall) => toolCall.function.name).join(", ");
1387
+ if (names) {
1388
+ actions.push(`plan ${names}`);
1389
+ }
1390
+ }
1391
+ return actions;
1392
+ }
1393
+ function collectCompletedActions(messages) {
1394
+ const actions = [];
1395
+ for (const message of messages) {
1396
+ if (message?.role !== "tool" || !message.name) {
1397
+ continue;
1398
+ }
1399
+ const content = message.content ?? "";
1400
+ const parsed = safeParseObject(content);
1401
+ if (parsed && typeof parsed.error === "string" && parsed.error.length > 0) {
1402
+ continue;
1403
+ }
1404
+ actions.push(formatCompletedAction(message.name, parsed));
1405
+ }
1406
+ return actions.filter(Boolean);
1407
+ }
1408
+ function collectBlockers(messages) {
1409
+ const blockers = [];
1410
+ for (const message of messages) {
1411
+ if (message?.role !== "tool") {
1412
+ continue;
1413
+ }
1414
+ const parsed = safeParseObject(message.content ?? "");
1415
+ if (!parsed || typeof parsed.error !== "string" || parsed.error.length === 0) {
1416
+ continue;
1417
+ }
1418
+ blockers.push(`${message.name ?? "tool"}: ${truncate(oneLine2(parsed.error), 180)}`);
1419
+ }
1420
+ return blockers;
1421
+ }
1422
+ function oneLine2(value) {
1423
+ return value.replace(/\s+/g, " ").trim();
1424
+ }
1425
+ function truncate(value, maxChars) {
1426
+ return value.length <= maxChars ? value : `${value.slice(0, maxChars)}...`;
1427
+ }
1428
+ function formatCompletedAction(toolName, payload) {
1429
+ const pathValue = normalizeFilePath(readPath(payload?.path));
1430
+ if (toolName === "bash") {
1431
+ const command = typeof payload?.command === "string" ? payload.command : "command";
1432
+ const exitCode = typeof payload?.exitCode === "number" ? payload.exitCode : "unknown";
1433
+ return `bash ${truncate(oneLine2(command), 120)} (exit ${exitCode})`;
1434
+ }
1435
+ return pathValue ? `${toolName} ${truncate(pathValue, 160)}` : toolName;
1436
+ }
1437
+ function collectPathsFromValue(value, bucket) {
1438
+ if (!value || typeof value !== "object") {
1439
+ return;
1440
+ }
1441
+ if (Array.isArray(value)) {
1442
+ for (const item of value) {
1443
+ collectPathsFromValue(item, bucket);
1444
+ }
1445
+ return;
1446
+ }
1447
+ for (const [key, item] of Object.entries(value)) {
1448
+ if (typeof item === "string" && isPathLikeKey(key)) {
1449
+ bucket.push(item);
1450
+ continue;
1451
+ }
1452
+ if (Array.isArray(item)) {
1453
+ for (const nested of item) {
1454
+ collectPathsFromValue(nested, bucket);
1455
+ }
1456
+ continue;
1457
+ }
1458
+ if (item && typeof item === "object") {
1459
+ collectPathsFromValue(item, bucket);
1460
+ }
1461
+ }
1462
+ }
1463
+ function isPathLikeKey(key) {
1464
+ return key === "path" || key === "cwd" || key.endsWith("Path");
1465
+ }
1466
+ function readPath(value) {
1467
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1468
+ }
1469
+ function safeParseObject(raw) {
1470
+ try {
1471
+ const parsed = JSON.parse(raw);
1472
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
1473
+ } catch {
1474
+ return null;
1475
+ }
1476
+ }
1477
+ function normalizeFilePath(value) {
1478
+ if (!value) {
1479
+ return void 0;
1480
+ }
1481
+ const trimmed = value.trim();
1482
+ if (!trimmed || trimmed.startsWith("<") || trimmed.includes("\n")) {
1483
+ return void 0;
1484
+ }
1485
+ if (trimmed.length > 260) {
1486
+ return truncate(trimmed, 260);
1487
+ }
1488
+ return trimmed.includes(path3.sep) || trimmed.includes("/") || trimmed.includes(".") ? trimmed : void 0;
1489
+ }
1490
+
1491
+ // src/session/taskState.ts
1492
+ var MAX_ACTIVE_FILES = 12;
1493
+ var MAX_PLANNED_ACTIONS = 8;
1494
+ var MAX_COMPLETED_ACTIONS = 12;
1495
+ var MAX_BLOCKERS = 8;
1496
+ function createEmptyTaskState(timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
1497
+ return {
1498
+ activeFiles: [],
1499
+ plannedActions: [],
1500
+ completedActions: [],
1501
+ blockers: [],
1502
+ lastUpdatedAt: timestamp
1503
+ };
1504
+ }
1505
+ function deriveTaskState(messages, previous) {
1506
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1507
+ const currentTurn = findCurrentTurn(messages);
1508
+ const frameMessages = currentTurn ? messages.slice(currentTurn.startIndex) : messages;
1509
+ return {
1510
+ focus: previous?.focus,
1511
+ activeFiles: takeLastUnique(collectActiveFiles(frameMessages), MAX_ACTIVE_FILES),
1512
+ plannedActions: takeLastUnique(collectPlannedActions(frameMessages), MAX_PLANNED_ACTIONS),
1513
+ completedActions: takeLastUnique(collectCompletedActions(frameMessages), MAX_COMPLETED_ACTIONS),
1514
+ blockers: takeLastUnique(collectBlockers(frameMessages), MAX_BLOCKERS),
1515
+ lastUpdatedAt: now
1516
+ };
1517
+ }
1518
+ function normalizeTaskState(taskState) {
1519
+ if (!taskState) {
1520
+ return void 0;
1521
+ }
1522
+ return {
1523
+ focus: typeof taskState.focus === "string" ? taskState.focus : void 0,
1524
+ activeFiles: takeLastUnique(taskState.activeFiles ?? [], MAX_ACTIVE_FILES),
1525
+ plannedActions: takeLastUnique(taskState.plannedActions ?? [], MAX_PLANNED_ACTIONS),
1526
+ completedActions: takeLastUnique(taskState.completedActions ?? [], MAX_COMPLETED_ACTIONS),
1527
+ blockers: takeLastUnique(taskState.blockers ?? [], MAX_BLOCKERS),
1528
+ lastUpdatedAt: typeof taskState.lastUpdatedAt === "string" && taskState.lastUpdatedAt.length > 0 ? taskState.lastUpdatedAt : (/* @__PURE__ */ new Date()).toISOString()
1529
+ };
1530
+ }
1531
+ function formatTaskStateBlock(taskState) {
1532
+ if (!taskState) {
1533
+ return "- none";
1534
+ }
1535
+ const parts = [
1536
+ taskState.focus ? `- Focus: ${taskState.focus}` : "- Focus: none",
1537
+ `- Planned actions: ${formatList(taskState.plannedActions)}`,
1538
+ `- Blockers: ${formatList(taskState.blockers)}`,
1539
+ `- Updated at: ${taskState.lastUpdatedAt}`
1540
+ ];
1541
+ return parts.join("\n");
1542
+ }
1543
+ function normalizeSessionRecord(session) {
1544
+ return {
1545
+ ...session,
1546
+ messageCount: Array.isArray(session.messages) ? session.messages.length : 0,
1547
+ messages: Array.isArray(session.messages) ? session.messages : [],
1548
+ taskState: normalizeTaskState(deriveTaskState(Array.isArray(session.messages) ? session.messages : [], session.taskState))
1549
+ };
1550
+ }
1551
+ function applyCurrentTurnFrame(session, input, timestamp = (/* @__PURE__ */ new Date()).toISOString(), source = "external") {
1552
+ const userInput = readUserInput({ content: input, source });
1553
+ if (!userInput) {
1554
+ return {
1555
+ ...session,
1556
+ taskState: normalizeTaskState(session.taskState ?? createEmptyTaskState(timestamp))
1557
+ };
1558
+ }
1559
+ return {
1560
+ ...session,
1561
+ taskState: {
1562
+ focus: session.taskState?.focus,
1563
+ activeFiles: [],
1564
+ plannedActions: [],
1565
+ completedActions: [],
1566
+ blockers: [],
1567
+ lastUpdatedAt: timestamp
1568
+ }
1569
+ };
1570
+ }
1571
+ function findCurrentTurn(messages) {
1572
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
1573
+ const message = messages[index];
1574
+ if (message?.role !== "user") {
1575
+ continue;
1576
+ }
1577
+ const normalized = readUserInput(message);
1578
+ if (normalized) {
1579
+ return {
1580
+ startIndex: index
1581
+ };
1582
+ }
1583
+ }
1584
+ return void 0;
1585
+ }
1586
+ function takeLastUnique(values, limit) {
1587
+ const seen = /* @__PURE__ */ new Set();
1588
+ const result = [];
1589
+ for (let index = values.length - 1; index >= 0; index -= 1) {
1590
+ const value = values[index]?.trim();
1591
+ if (!value || seen.has(value)) {
1592
+ continue;
1593
+ }
1594
+ seen.add(value);
1595
+ result.unshift(value);
1596
+ if (result.length >= limit) {
1597
+ break;
1598
+ }
1599
+ }
1600
+ return result;
1601
+ }
1602
+ function formatList(values) {
1603
+ return values.length > 0 ? values.join(" | ") : "none";
1604
+ }
1605
+
1606
+ // src/utils/normalize.ts
1607
+ function normalizeTimestamp(value, fallback) {
1608
+ return typeof value === "string" && value.trim().length > 0 ? value : fallback;
1609
+ }
1610
+
1611
+ // src/session/sessionDiff.ts
1612
+ var MAX_SESSION_DIFF_CHANGES = 20;
1613
+ var MAX_SESSION_DIFF_PATHS = 24;
1614
+ var MAX_DIFF_PREVIEW_CHARS = 2e3;
1615
+ function createEmptySessionDiff(timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
1616
+ return {
1617
+ version: 1,
1618
+ changedPaths: [],
1619
+ changes: [],
1620
+ updatedAt: timestamp
1621
+ };
1622
+ }
1623
+ function normalizeSessionDiff(sessionDiff, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
1624
+ const normalizedChanges = (sessionDiff?.changes ?? []).map((change) => normalizeSessionDiffChange(change, timestamp)).filter((change) => Boolean(change));
1625
+ return {
1626
+ version: 1,
1627
+ changedPaths: takeLastUniquePaths(sessionDiff?.changedPaths ?? []),
1628
+ changes: normalizedChanges.slice(-MAX_SESSION_DIFF_CHANGES),
1629
+ updatedAt: normalizeTimestamp(sessionDiff?.updatedAt, timestamp)
1630
+ };
1631
+ }
1632
+ function normalizeSessionDiffState(session) {
1633
+ return {
1634
+ ...session,
1635
+ sessionDiff: normalizeSessionDiff(session.sessionDiff)
1636
+ };
1637
+ }
1638
+ function noteSessionDiff(session, change, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
1639
+ if (!change) {
1640
+ return normalizeSessionDiffState(session);
1641
+ }
1642
+ const current = normalizeSessionDiff(session.sessionDiff, timestamp);
1643
+ const normalizedChange = normalizeSessionDiffChange(change, timestamp);
1644
+ if (!normalizedChange) {
1645
+ return {
1646
+ ...session,
1647
+ sessionDiff: current
1648
+ };
1649
+ }
1650
+ return {
1651
+ ...session,
1652
+ sessionDiff: {
1653
+ version: 1,
1654
+ changedPaths: takeLastUniquePaths([...current.changedPaths, ...normalizedChange.changedPaths]),
1655
+ changes: [...current.changes, normalizedChange].slice(-MAX_SESSION_DIFF_CHANGES),
1656
+ updatedAt: timestamp
1657
+ }
1658
+ };
1659
+ }
1660
+ function normalizeSessionDiffChange(change, timestamp) {
1661
+ const toolName = normalizeText2(change?.toolName);
1662
+ const changedPaths = takeLastUniquePaths(change?.changedPaths ?? []);
1663
+ if (!toolName || changedPaths.length === 0) {
1664
+ return null;
1665
+ }
1666
+ return {
1667
+ toolName,
1668
+ changeId: normalizeText2(change?.changeId) || void 0,
1669
+ changedPaths,
1670
+ diff: truncate2(change?.diff, MAX_DIFF_PREVIEW_CHARS),
1671
+ diagnosticsStatus: normalizeDiagnosticsStatus(change?.diagnosticsStatus),
1672
+ errorCount: normalizeCount(change?.errorCount),
1673
+ warningCount: normalizeCount(change?.warningCount),
1674
+ recordedAt: normalizeTimestamp(change?.recordedAt, timestamp)
1675
+ };
1676
+ }
1677
+ function takeLastUniquePaths(paths) {
1678
+ const seen = /* @__PURE__ */ new Set();
1679
+ const result = [];
1680
+ for (let index = paths.length - 1; index >= 0; index -= 1) {
1681
+ const normalized = normalizeText2(paths[index]);
1682
+ if (!normalized || seen.has(normalized)) {
1683
+ continue;
1684
+ }
1685
+ seen.add(normalized);
1686
+ result.unshift(normalized);
1687
+ if (result.length >= MAX_SESSION_DIFF_PATHS) {
1688
+ break;
1689
+ }
1690
+ }
1691
+ return result;
1692
+ }
1693
+ function normalizeDiagnosticsStatus(value) {
1694
+ return value === "issues" || value === "unavailable" ? value : "clean";
1695
+ }
1696
+ function normalizeCount(value) {
1697
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.trunc(value) : 0;
1698
+ }
1699
+ function normalizeText2(value) {
1700
+ return String(value ?? "").trim();
1701
+ }
1702
+ function truncate2(value, maxChars) {
1703
+ if (!value) {
1704
+ return void 0;
1705
+ }
1706
+ return value.length <= maxChars ? value : `${value.slice(0, maxChars)}...`;
1707
+ }
1708
+
1709
+ // src/agent/runtimeTransition/shared.ts
1710
+ var MAX_REASON_ITEMS = 6;
1711
+ var MAX_REASON_TEXT_CHARS = 220;
1712
+ function takeLastUnique2(values) {
1713
+ const seen = /* @__PURE__ */ new Set();
1714
+ const result = [];
1715
+ for (let index = values.length - 1; index >= 0; index -= 1) {
1716
+ const normalized = normalizeText3(values[index]);
1717
+ if (!normalized || seen.has(normalized)) {
1718
+ continue;
1719
+ }
1720
+ seen.add(normalized);
1721
+ result.unshift(normalized);
1722
+ if (result.length >= MAX_REASON_ITEMS) {
1723
+ break;
1724
+ }
1725
+ }
1726
+ return result;
1727
+ }
1728
+ function truncate3(value) {
1729
+ return value.length <= MAX_REASON_TEXT_CHARS ? value : `${value.slice(0, MAX_REASON_TEXT_CHARS)}...`;
1730
+ }
1731
+ function normalizeText3(value) {
1732
+ return String(value ?? "").replace(/\s+/g, " ").trim();
1733
+ }
1734
+ function clampWholeNumber(value, min, max, fallback) {
1735
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1736
+ return fallback;
1737
+ }
1738
+ return Math.max(min, Math.min(max, Math.trunc(value)));
1739
+ }
1740
+
1741
+ // src/agent/runtimeTransition/builders.ts
1742
+ function createToolBatchTransition(input, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
1743
+ return {
1744
+ action: "continue",
1745
+ reason: {
1746
+ code: "continue.after_tool_batch",
1747
+ toolNames: takeLastUnique2(input.toolNames),
1748
+ changedPaths: takeLastUnique2(input.changedPaths ?? [])
1749
+ },
1750
+ timestamp
1751
+ };
1752
+ }
1753
+ function createEmptyAssistantResponseTransition(timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
1754
+ return {
1755
+ action: "continue",
1756
+ reason: {
1757
+ code: "continue.empty_assistant_response"
1758
+ },
1759
+ timestamp
1760
+ };
1761
+ }
1762
+ function createProviderRecoveryTransition(input, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
1763
+ return {
1764
+ action: "recover",
1765
+ reason: {
1766
+ code: "recover.provider_request_retry",
1767
+ consecutiveFailures: Math.max(1, Math.trunc(input.consecutiveFailures)),
1768
+ error: truncate3(normalizeText3(input.error?.message ?? input.error) || "request failed"),
1769
+ configuredModel: normalizeText3(input.configuredModel) || "unknown_model",
1770
+ requestModel: normalizeText3(input.requestModel) || "unknown_model",
1771
+ contextWindowMessages: Math.max(1, Math.trunc(input.requestConfig.contextWindowMessages)),
1772
+ maxContextChars: Math.max(1, Math.trunc(input.requestConfig.maxContextChars)),
1773
+ contextSummaryChars: Math.max(1, Math.trunc(input.requestConfig.contextSummaryChars)),
1774
+ delayMs: Math.max(0, Math.trunc(input.delayMs))
1775
+ },
1776
+ timestamp
1777
+ };
1778
+ }
1779
+ function createFinalizeTransition(input, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
1780
+ return {
1781
+ action: "finalize",
1782
+ reason: {
1783
+ code: "finalize.completed",
1784
+ changedPaths: takeLastUnique2([...input.changedPaths])
1785
+ },
1786
+ timestamp
1787
+ };
1788
+ }
1789
+ function createExecutionWaitYieldTransition(input, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
1790
+ return {
1791
+ action: "yield",
1792
+ reason: {
1793
+ code: "yield.execution_wait",
1794
+ executionIds: takeLastUnique2([...input.executionIds]),
1795
+ toolNames: takeLastUnique2([...input.toolNames])
1796
+ },
1797
+ timestamp
1798
+ };
1799
+ }
1800
+ function buildRunTurnResult(input) {
1801
+ return {
1802
+ session: input.session,
1803
+ changedPaths: [...input.changedPaths],
1804
+ transition: input.transition
1805
+ };
1806
+ }
1807
+
1808
+ // src/agent/runtimeTransition/normalize.ts
1809
+ function normalizeRuntimeTransition(transition, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
1810
+ if (!transition || typeof transition !== "object") {
1811
+ return void 0;
1812
+ }
1813
+ const action = normalizeAction(transition.action);
1814
+ const reason = transition.reason;
1815
+ const normalizedTimestamp = normalizeTimestamp(transition.timestamp, timestamp);
1816
+ if (!reason || typeof reason !== "object") {
1817
+ return void 0;
1818
+ }
1819
+ switch (action) {
1820
+ case "continue":
1821
+ return normalizeContinueTransition(reason, normalizedTimestamp);
1822
+ case "recover":
1823
+ return normalizeRecoverTransition(reason, normalizedTimestamp);
1824
+ case "finalize":
1825
+ return normalizeFinalizeTransition(reason, normalizedTimestamp);
1826
+ case "yield":
1827
+ return normalizeYieldTransition(reason, normalizedTimestamp);
1828
+ default:
1829
+ return void 0;
1830
+ }
1831
+ }
1832
+ function normalizeContinueTransition(reason, timestamp) {
1833
+ switch (reason.code) {
1834
+ case "continue.after_tool_batch": {
1835
+ const toolNames = takeLastUnique2(reason.toolNames);
1836
+ if (toolNames.length === 0) {
1837
+ return void 0;
1838
+ }
1839
+ return {
1840
+ action: "continue",
1841
+ reason: {
1842
+ code: reason.code,
1843
+ toolNames,
1844
+ changedPaths: takeLastUnique2(reason.changedPaths ?? [])
1845
+ },
1846
+ timestamp
1847
+ };
1848
+ }
1849
+ case "continue.empty_assistant_response":
1850
+ return {
1851
+ action: "continue",
1852
+ reason: {
1853
+ code: reason.code
1854
+ },
1855
+ timestamp
1856
+ };
1857
+ default:
1858
+ return void 0;
1859
+ }
1860
+ }
1861
+ function normalizeRecoverTransition(reason, timestamp) {
1862
+ if (reason.code !== "recover.provider_request_retry") {
1863
+ return void 0;
1864
+ }
1865
+ return {
1866
+ action: "recover",
1867
+ reason: {
1868
+ code: reason.code,
1869
+ consecutiveFailures: clampWholeNumber(reason.consecutiveFailures, 1, 50, 1) ?? 1,
1870
+ error: truncate3(normalizeText3(reason.error) || "request failed"),
1871
+ configuredModel: normalizeText3(reason.configuredModel) || "unknown_model",
1872
+ requestModel: normalizeText3(reason.requestModel) || "unknown_model",
1873
+ contextWindowMessages: clampWholeNumber(reason.contextWindowMessages, 1, 999, 1) ?? 1,
1874
+ maxContextChars: clampWholeNumber(reason.maxContextChars, 1, 1e6, 1) ?? 1,
1875
+ contextSummaryChars: clampWholeNumber(reason.contextSummaryChars, 1, 1e6, 1) ?? 1,
1876
+ delayMs: clampWholeNumber(reason.delayMs, 0, 36e5, 0) ?? 0
1877
+ },
1878
+ timestamp
1879
+ };
1880
+ }
1881
+ function normalizeFinalizeTransition(reason, timestamp) {
1882
+ if (reason.code !== "finalize.completed") {
1883
+ return void 0;
1884
+ }
1885
+ return {
1886
+ action: "finalize",
1887
+ reason: {
1888
+ code: reason.code,
1889
+ changedPaths: takeLastUnique2(reason.changedPaths ?? [])
1890
+ },
1891
+ timestamp
1892
+ };
1893
+ }
1894
+ function normalizeYieldTransition(reason, timestamp) {
1895
+ if (reason.code !== "yield.execution_wait") {
1896
+ return void 0;
1897
+ }
1898
+ const executionIds = takeLastUnique2(reason.executionIds ?? []);
1899
+ if (executionIds.length === 0) {
1900
+ return void 0;
1901
+ }
1902
+ return {
1903
+ action: "yield",
1904
+ reason: {
1905
+ code: reason.code,
1906
+ executionIds,
1907
+ toolNames: takeLastUnique2(reason.toolNames ?? [])
1908
+ },
1909
+ timestamp
1910
+ };
1911
+ }
1912
+ function normalizeAction(value) {
1913
+ return value === "continue" || value === "recover" || value === "finalize" || value === "yield" ? value : void 0;
1914
+ }
1915
+
1916
+ // src/agent/runtimeTransition/flow.ts
1917
+ function normalizeCheckpointFlow(flow, status, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
1918
+ const lastTransition = normalizeRuntimeTransition(flow?.lastTransition, timestamp);
1919
+ const phase = normalizePhase(lastTransition ? getRuntimeTransitionPhase(lastTransition) : flow?.phase, status);
1920
+ const runState = normalizeRunState({
1921
+ current: flow?.runState,
1922
+ status,
1923
+ timestamp
1924
+ });
1925
+ return {
1926
+ phase,
1927
+ reason: lastTransition ? formatRuntimeTransitionReason(lastTransition) : normalizeText3(flow?.reason) || void 0,
1928
+ recoveryFailures: lastTransition?.action === "recover" ? lastTransition.reason.consecutiveFailures : phase === "recovery" ? clampWholeNumber(flow?.recoveryFailures, 1, 50, void 0) : void 0,
1929
+ runState,
1930
+ lastTransition,
1931
+ updatedAt: normalizeTimestamp(flow?.updatedAt, timestamp)
1932
+ };
1933
+ }
1934
+ function buildCheckpointFlow(input) {
1935
+ const timestamp = input.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
1936
+ const transition = normalizeRuntimeTransition(input.transition, timestamp);
1937
+ const phase = normalizePhase(
1938
+ transition ? getRuntimeTransitionPhase(transition) : input.defaultPhase ?? input.current?.phase,
1939
+ input.status
1940
+ );
1941
+ const runState = normalizeRunState({
1942
+ current: input.current?.runState,
1943
+ status: input.status,
1944
+ override: input.runState,
1945
+ timestamp
1946
+ });
1947
+ return {
1948
+ phase,
1949
+ reason: transition ? formatRuntimeTransitionReason(transition) : void 0,
1950
+ recoveryFailures: transition?.action === "recover" ? transition.reason.consecutiveFailures : void 0,
1951
+ runState,
1952
+ lastTransition: transition,
1953
+ updatedAt: timestamp
1954
+ };
1955
+ }
1956
+ function getTurnInputTransition(input, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
1957
+ return void 0;
1958
+ }
1959
+ function formatRuntimeTransitionReason(transition) {
1960
+ return transition.reason.code;
1961
+ }
1962
+ function getRuntimeTransitionPhase(transition) {
1963
+ if (transition.action === "recover") {
1964
+ return "recovery";
1965
+ }
1966
+ return "active";
1967
+ }
1968
+ function normalizePhase(value, status) {
1969
+ if (status === "completed") {
1970
+ return "active";
1971
+ }
1972
+ return value === "recovery" ? value : "active";
1973
+ }
1974
+ function normalizeRunState(input) {
1975
+ const normalizedStatus = input.status === "completed" ? "idle" : input.override?.status === "busy" || input.override?.status === "idle" ? input.override.status : input.current?.status === "busy" ? "busy" : "idle";
1976
+ const source = normalizeRunStateSource(
1977
+ input.status === "completed" ? "checkpoint" : input.override?.source ?? input.current?.source,
1978
+ normalizedStatus
1979
+ );
1980
+ return {
1981
+ status: normalizedStatus,
1982
+ source,
1983
+ pendingToolCallCount: 0,
1984
+ updatedAt: input.timestamp
1985
+ };
1986
+ }
1987
+ function normalizeRunStateSource(source, status) {
1988
+ if (status === "idle") {
1989
+ return source === "turn" ? "checkpoint" : source ?? "checkpoint";
1990
+ }
1991
+ if (source === "turn" || source === "tool_batch") {
1992
+ return source;
1993
+ }
1994
+ return "checkpoint";
1995
+ }
1996
+
1997
+ // src/session/checkpoint/shared.ts
1998
+ import crypto2 from "crypto";
1999
+ import path4 from "path";
2000
+ var MAX_COMPLETED_STEPS = 8;
2001
+ var MAX_BATCH_TOOLS = 6;
2002
+ var MAX_BATCH_PATHS = 6;
2003
+ var MAX_SUMMARY_CHARS = 220;
2004
+ function fingerprintFocus(focus) {
2005
+ return crypto2.createHash("sha1").update(focus.trim().toLowerCase()).digest("hex");
2006
+ }
2007
+ function normalizeText4(value) {
2008
+ return String(value ?? "").replace(/\s+/g, " ").trim();
2009
+ }
2010
+ function truncate4(value, maxChars) {
2011
+ if (!value) {
2012
+ return void 0;
2013
+ }
2014
+ return value.length <= maxChars ? value : `${value.slice(0, maxChars)}...`;
2015
+ }
2016
+ function takeLastUnique3(values, limit) {
2017
+ const seen = /* @__PURE__ */ new Set();
2018
+ const result = [];
2019
+ for (let index = values.length - 1; index >= 0; index -= 1) {
2020
+ const normalized = normalizeText4(values[index]);
2021
+ if (!normalized || seen.has(normalized)) {
2022
+ continue;
2023
+ }
2024
+ seen.add(normalized);
2025
+ result.unshift(normalized);
2026
+ if (result.length >= limit) {
2027
+ break;
2028
+ }
2029
+ }
2030
+ return result;
2031
+ }
2032
+ function safeParseObject2(raw) {
2033
+ if (typeof raw !== "string" || raw.trim().length === 0) {
2034
+ return null;
2035
+ }
2036
+ try {
2037
+ const parsed = JSON.parse(raw);
2038
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2039
+ return null;
2040
+ }
2041
+ return parsed;
2042
+ } catch {
2043
+ return null;
2044
+ }
2045
+ }
2046
+ function readString(value) {
2047
+ const normalized = normalizeText4(value);
2048
+ return normalized || void 0;
2049
+ }
2050
+ function normalizeToolBatch(toolBatch) {
2051
+ if (!toolBatch) {
2052
+ return void 0;
2053
+ }
2054
+ const tools = takeLastUnique3(toolBatch.tools ?? [], MAX_BATCH_TOOLS);
2055
+ if (tools.length === 0) {
2056
+ return void 0;
2057
+ }
2058
+ return {
2059
+ tools,
2060
+ summary: truncate4(normalizeText4(toolBatch.summary) || `Ran ${tools.join(", ")}`, MAX_SUMMARY_CHARS),
2061
+ changedPaths: takeLastUnique3(toolBatch.changedPaths ?? [], MAX_BATCH_PATHS),
2062
+ recordedAt: normalizeTimestamp(toolBatch.recordedAt, (/* @__PURE__ */ new Date()).toISOString())
2063
+ };
2064
+ }
2065
+
2066
+ // src/session/checkpoint/derivation.ts
2067
+ function deriveCompletedSteps(session) {
2068
+ const completedActions = session.taskState?.completedActions ?? [];
2069
+ return takeLastUnique3(completedActions, MAX_COMPLETED_STEPS);
2070
+ }
2071
+ function deriveRecentToolBatchFromMessages(messages, timestamp) {
2072
+ let lastToolIndex = messages.length - 1;
2073
+ while (lastToolIndex >= 0 && messages[lastToolIndex]?.role !== "tool") {
2074
+ lastToolIndex -= 1;
2075
+ }
2076
+ if (lastToolIndex < 0) {
2077
+ return void 0;
2078
+ }
2079
+ let startIndex = lastToolIndex;
2080
+ while (startIndex >= 0 && messages[startIndex]?.role === "tool") {
2081
+ startIndex -= 1;
2082
+ }
2083
+ const toolMessages = messages.slice(startIndex + 1, lastToolIndex + 1).filter((message) => message.role === "tool");
2084
+ const toolNames = toolMessages.map((message) => normalizeText4(message.name)).filter(Boolean);
2085
+ return buildToolBatch(toolNames, toolMessages, void 0, timestamp);
2086
+ }
2087
+ function buildToolBatch(toolNames, toolMessages, changedPaths, timestamp) {
2088
+ const tools = takeLastUnique3(toolNames, MAX_BATCH_TOOLS);
2089
+ if (tools.length === 0) {
2090
+ return void 0;
2091
+ }
2092
+ const batchChangedPaths = takeLastUnique3(
2093
+ [
2094
+ ...changedPaths ?? [],
2095
+ ...toolMessages.map((message) => readPathFromMessage(message)).filter(Boolean)
2096
+ ],
2097
+ MAX_BATCH_PATHS
2098
+ );
2099
+ const recordedAt = normalizeTimestamp(
2100
+ toolMessages[toolMessages.length - 1]?.createdAt,
2101
+ timestamp
2102
+ );
2103
+ return {
2104
+ tools,
2105
+ summary: buildToolBatchSummary(tools, batchChangedPaths),
2106
+ changedPaths: batchChangedPaths,
2107
+ recordedAt
2108
+ };
2109
+ }
2110
+ function readPathFromMessage(message) {
2111
+ const payload = safeParseObject2(message.content);
2112
+ return readString(payload?.path) ?? readString(payload?.requestedPath);
2113
+ }
2114
+ function buildToolBatchSummary(toolNames, changedPaths) {
2115
+ const fragments = [`Ran ${toolNames.join(", ")}`];
2116
+ if (changedPaths.length > 0) {
2117
+ fragments.push(`changed ${changedPaths.join(" | ")}`);
2118
+ }
2119
+ return truncate4(fragments.join("; "), MAX_SUMMARY_CHARS);
2120
+ }
2121
+
2122
+ // src/session/checkpoint/base.ts
2123
+ function createEmptyCheckpoint(timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
2124
+ return {
2125
+ version: 1,
2126
+ status: "active",
2127
+ completedSteps: [],
2128
+ flow: {
2129
+ phase: "active",
2130
+ runState: {
2131
+ status: "idle",
2132
+ source: "checkpoint",
2133
+ pendingToolCallCount: 0,
2134
+ updatedAt: timestamp
2135
+ },
2136
+ updatedAt: timestamp
2137
+ },
2138
+ updatedAt: timestamp
2139
+ };
2140
+ }
2141
+ function createCheckpointForFocus(focus, timestamp) {
2142
+ return {
2143
+ ...createEmptyCheckpoint(timestamp),
2144
+ focus,
2145
+ focusFingerprint: focus ? fingerprintFocus(focus) : void 0
2146
+ };
2147
+ }
2148
+ function deriveCheckpointFromSession(session, timestamp) {
2149
+ const recentToolBatch = deriveRecentToolBatchFromMessages(session.messages, timestamp);
2150
+ return {
2151
+ ...createCheckpointForFocus(normalizeText4(session.taskState?.focus) || void 0, timestamp),
2152
+ completedSteps: deriveCompletedSteps(session),
2153
+ recentToolBatch
2154
+ };
2155
+ }
2156
+
2157
+ // src/session/checkpoint/state.ts
2158
+ function normalizeCheckpoint(checkpoint, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
2159
+ if (!checkpoint) {
2160
+ return void 0;
2161
+ }
2162
+ const focus = normalizeText4(checkpoint.focus) || void 0;
2163
+ const status = checkpoint.status === "completed" ? "completed" : "active";
2164
+ return {
2165
+ version: 1,
2166
+ focus,
2167
+ focusFingerprint: normalizeText4(checkpoint.focusFingerprint) || (focus ? fingerprintFocus(focus) : void 0),
2168
+ status,
2169
+ completedSteps: takeLastUnique3(checkpoint.completedSteps ?? [], 8),
2170
+ recentToolBatch: normalizeToolBatch(checkpoint.recentToolBatch),
2171
+ flow: normalizeCheckpointFlow(checkpoint.flow, status, timestamp),
2172
+ updatedAt: normalizeTimestamp(checkpoint.updatedAt, timestamp)
2173
+ };
2174
+ }
2175
+ function normalizeSessionCheckpoint(session) {
2176
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2177
+ const normalized = normalizeCheckpoint(session.checkpoint, timestamp);
2178
+ const checkpoint = normalized ?? deriveCheckpointFromSession(session, timestamp);
2179
+ if (checkpoint.completedSteps.length === 0) {
2180
+ checkpoint.completedSteps = deriveCompletedSteps(session);
2181
+ }
2182
+ checkpoint.flow = normalizeCheckpointFlow(checkpoint.flow, checkpoint.status, timestamp);
2183
+ checkpoint.updatedAt = normalizeTimestamp(checkpoint.updatedAt, timestamp);
2184
+ return {
2185
+ ...session,
2186
+ checkpoint
2187
+ };
2188
+ }
2189
+ function noteCheckpointTurnInput(session, input, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
2190
+ const checkpoint = normalizeSessionCheckpoint(session).checkpoint ?? createEmptyCheckpoint(timestamp);
2191
+ const transition = getTurnInputTransition(input, timestamp);
2192
+ return {
2193
+ ...session,
2194
+ checkpoint: {
2195
+ ...checkpoint,
2196
+ flow: buildCheckpointFlow({
2197
+ current: checkpoint.flow,
2198
+ status: checkpoint.status,
2199
+ transition,
2200
+ runState: checkpoint.status === "completed" ? {
2201
+ status: "idle",
2202
+ source: "checkpoint"
2203
+ } : {
2204
+ status: "busy",
2205
+ source: "turn"
2206
+ },
2207
+ defaultPhase: "active",
2208
+ timestamp
2209
+ }),
2210
+ updatedAt: timestamp
2211
+ }
2212
+ };
2213
+ }
2214
+ function resolveCurrentFocusCheckpoint(session, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
2215
+ const focus = normalizeText4(session.taskState?.focus) || void 0;
2216
+ const fingerprint = focus ? fingerprintFocus(focus) : void 0;
2217
+ const checkpoint = normalizeCheckpoint(session.checkpoint, timestamp) ?? createEmptyCheckpoint(timestamp);
2218
+ if (!focus) {
2219
+ return checkpoint;
2220
+ }
2221
+ if (checkpoint.focusFingerprint === fingerprint) {
2222
+ return checkpoint;
2223
+ }
2224
+ return createCheckpointForFocus(focus, timestamp);
2225
+ }
2226
+ function noteCheckpointToolBatch(session, input, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
2227
+ const checkpoint = resolveCurrentFocusCheckpoint(session, timestamp);
2228
+ const recentToolBatch = buildToolBatch(input.toolNames, input.toolMessages, input.changedPaths, timestamp);
2229
+ const phase = checkpoint.flow.phase === "recovery" ? "active" : checkpoint.flow.phase;
2230
+ const transition = createToolBatchTransition({
2231
+ toolNames: input.toolNames,
2232
+ changedPaths: input.changedPaths
2233
+ }, timestamp);
2234
+ return {
2235
+ ...session,
2236
+ checkpoint: {
2237
+ ...checkpoint,
2238
+ completedSteps: deriveCompletedSteps(session),
2239
+ recentToolBatch,
2240
+ flow: buildCheckpointFlow({
2241
+ current: checkpoint.flow,
2242
+ status: checkpoint.status,
2243
+ transition,
2244
+ defaultPhase: phase,
2245
+ timestamp
2246
+ }),
2247
+ updatedAt: timestamp
2248
+ }
2249
+ };
2250
+ }
2251
+
2252
+ // src/session/checkpoint/transitions.ts
2253
+ function noteCheckpointTransition(session, transition, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
2254
+ const checkpoint = resolveCurrentFocusCheckpoint(session, timestamp);
2255
+ return {
2256
+ ...session,
2257
+ checkpoint: {
2258
+ ...checkpoint,
2259
+ flow: buildCheckpointFlow({
2260
+ current: checkpoint.flow,
2261
+ status: checkpoint.status,
2262
+ transition,
2263
+ defaultPhase: checkpoint.flow.phase,
2264
+ timestamp
2265
+ }),
2266
+ updatedAt: timestamp
2267
+ }
2268
+ };
2269
+ }
2270
+ function noteCheckpointRecovery(session, transition, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
2271
+ const checkpoint = resolveCurrentFocusCheckpoint(session, timestamp);
2272
+ if (checkpoint.status === "completed") {
2273
+ return {
2274
+ ...session,
2275
+ checkpoint
2276
+ };
2277
+ }
2278
+ return {
2279
+ ...session,
2280
+ checkpoint: {
2281
+ ...checkpoint,
2282
+ flow: buildCheckpointFlow({
2283
+ current: checkpoint.flow,
2284
+ status: checkpoint.status,
2285
+ transition,
2286
+ defaultPhase: "recovery",
2287
+ timestamp
2288
+ }),
2289
+ updatedAt: timestamp
2290
+ }
2291
+ };
2292
+ }
2293
+ function noteCheckpointCompleted(session, transition, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
2294
+ const checkpoint = resolveCurrentFocusCheckpoint(session, timestamp);
2295
+ return {
2296
+ ...session,
2297
+ checkpoint: {
2298
+ ...checkpoint,
2299
+ status: "completed",
2300
+ completedSteps: checkpoint.completedSteps.length > 0 ? checkpoint.completedSteps : deriveCompletedSteps(session),
2301
+ flow: buildCheckpointFlow({
2302
+ current: checkpoint.flow,
2303
+ status: "completed",
2304
+ transition,
2305
+ defaultPhase: "active",
2306
+ timestamp
2307
+ }),
2308
+ updatedAt: timestamp
2309
+ }
2310
+ };
2311
+ }
2312
+
2313
+ // src/session/todos.ts
2314
+ var MAX_TODO_ITEMS = 20;
2315
+ var MAX_TODO_TEXT_CHARS = 240;
2316
+ function deriveTodoItems(messages, previous = []) {
2317
+ const turnBoundaryIndex = findLatestUserInputBoundaryIndex(messages);
2318
+ const stopIndex = turnBoundaryIndex >= 0 ? turnBoundaryIndex : -1;
2319
+ for (let index = messages.length - 1; index > stopIndex; index -= 1) {
2320
+ const message = messages[index];
2321
+ if (message?.role !== "tool" || message.name !== "todo_write" || typeof message.content !== "string") {
2322
+ continue;
2323
+ }
2324
+ const parsed = safeParseJson(message.content);
2325
+ if (!parsed || typeof parsed !== "object" || !("items" in parsed)) {
2326
+ continue;
2327
+ }
2328
+ try {
2329
+ return normalizeTodoItems(parsed.items);
2330
+ } catch {
2331
+ continue;
2332
+ }
2333
+ }
2334
+ return normalizeTodoItems(previous);
2335
+ }
2336
+ function normalizeTodoItems(value) {
2337
+ if (!Array.isArray(value)) {
2338
+ return [];
2339
+ }
2340
+ if (value.length > MAX_TODO_ITEMS) {
2341
+ throw new Error(`Too many todo items: max ${MAX_TODO_ITEMS}.`);
2342
+ }
2343
+ const normalized = [];
2344
+ const seenIds = /* @__PURE__ */ new Set();
2345
+ let inProgressCount = 0;
2346
+ for (const entry of value) {
2347
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
2348
+ throw new Error("Each todo item must be an object.");
2349
+ }
2350
+ const record = entry;
2351
+ const id = String(record.id ?? "").trim();
2352
+ const text = compactTodoText(record.text);
2353
+ const status = normalizeTodoStatus(record.status);
2354
+ if (!id) {
2355
+ throw new Error("Todo item id is required.");
2356
+ }
2357
+ if (!text) {
2358
+ throw new Error(`Todo item ${id} text is required.`);
2359
+ }
2360
+ if (seenIds.has(id)) {
2361
+ throw new Error(`Duplicate todo item id: ${id}.`);
2362
+ }
2363
+ seenIds.add(id);
2364
+ if (status === "in_progress") {
2365
+ inProgressCount += 1;
2366
+ if (inProgressCount > 1) {
2367
+ throw new Error("Only one todo item can be in_progress.");
2368
+ }
2369
+ }
2370
+ normalized.push({
2371
+ id,
2372
+ text,
2373
+ status
2374
+ });
2375
+ }
2376
+ return normalized;
2377
+ }
2378
+ function formatTodoBlock(items) {
2379
+ const todos = normalizeTodoItems(items);
2380
+ if (todos.length === 0) {
2381
+ return "- none";
2382
+ }
2383
+ const lines = todos.map((item) => `${statusMarker(item.status)} #${item.id}: ${item.text}`);
2384
+ const completed = todos.filter((item) => item.status === "completed").length;
2385
+ lines.push(`- Progress: ${completed}/${todos.length} completed`);
2386
+ return lines.join("\n");
2387
+ }
2388
+ function normalizeSessionTodos(session) {
2389
+ return {
2390
+ ...session,
2391
+ todoItems: deriveTodoItems(session.messages ?? [], session.todoItems ?? [])
2392
+ };
2393
+ }
2394
+ function findLatestUserInputBoundaryIndex(messages) {
2395
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
2396
+ const message = messages[index];
2397
+ if (message?.role === "user" && readUserInput(message)) {
2398
+ return index;
2399
+ }
2400
+ }
2401
+ return -1;
2402
+ }
2403
+ function normalizeTodoStatus(value) {
2404
+ const normalized = String(value ?? "").trim().toLowerCase();
2405
+ if (normalized === "pending" || normalized === "in_progress" || normalized === "completed") {
2406
+ return normalized;
2407
+ }
2408
+ throw new Error(`Invalid todo status: ${String(value ?? "")}.`);
2409
+ }
2410
+ function compactTodoText(value) {
2411
+ const normalized = String(value ?? "").replace(/\s+/g, " ").trim();
2412
+ if (normalized.length <= MAX_TODO_TEXT_CHARS) {
2413
+ return normalized;
2414
+ }
2415
+ return `${normalized.slice(0, MAX_TODO_TEXT_CHARS)}...`;
2416
+ }
2417
+ function statusMarker(status) {
2418
+ switch (status) {
2419
+ case "completed":
2420
+ return "[x]";
2421
+ case "in_progress":
2422
+ return "[>]";
2423
+ default:
2424
+ return "[ ]";
2425
+ }
2426
+ }
2427
+ function safeParseJson(raw) {
2428
+ try {
2429
+ return JSON.parse(raw);
2430
+ } catch {
2431
+ return null;
2432
+ }
2433
+ }
2434
+
2435
+ // src/session/memory.ts
2436
+ var MAX_SESSION_MEMORY_CHARS = 12e3;
2437
+ var SESSION_MEMORY_SECTIONS = [
2438
+ {
2439
+ title: "Current Focus",
2440
+ description: "Model-written current work focus that still affects the next turn."
2441
+ },
2442
+ {
2443
+ title: "User Constraints",
2444
+ description: "Stable user constraints or preferences that affect future action."
2445
+ },
2446
+ {
2447
+ title: "Decisions",
2448
+ description: "Decisions already made that should guide later work."
2449
+ },
2450
+ {
2451
+ title: "Open Threads",
2452
+ description: "Unresolved work, next steps, blockers, or questions."
2453
+ },
2454
+ {
2455
+ title: "Verification Facts",
2456
+ description: "Concrete tool, test, file-change, or runtime facts."
2457
+ },
2458
+ {
2459
+ title: "Reusable Lessons",
2460
+ description: "Stable lessons worth moving into spec notes or skill references."
2461
+ }
2462
+ ];
2463
+ function formatSessionMemorySectionTemplate() {
2464
+ return SESSION_MEMORY_SECTIONS.map((section) => `## ${section.title}
2465
+ ${section.description}
2466
+ Write None when no supplied fact belongs here.`).join("\n\n");
2467
+ }
2468
+ function formatSessionMemorySectionList() {
2469
+ return SESSION_MEMORY_SECTIONS.map((section) => `- ${section.title}: ${section.description}`).join("\n");
2470
+ }
2471
+ function createSessionMemoryState(summary, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
2472
+ return {
2473
+ version: 1,
2474
+ summary: normalizeSummary(summary),
2475
+ updatedAt: timestamp
2476
+ };
2477
+ }
2478
+ function updateSessionMemory(session, summary, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
2479
+ const memory = createSessionMemoryState(summary, timestamp);
2480
+ const focus = readSessionMemoryCurrentFocus(memory.summary);
2481
+ return {
2482
+ ...session,
2483
+ sessionMemory: memory,
2484
+ taskState: {
2485
+ ...session.taskState ?? {
2486
+ activeFiles: [],
2487
+ plannedActions: [],
2488
+ completedActions: [],
2489
+ blockers: [],
2490
+ lastUpdatedAt: timestamp
2491
+ },
2492
+ focus,
2493
+ lastUpdatedAt: timestamp
2494
+ }
2495
+ };
2496
+ }
2497
+ function normalizeSessionMemory(value, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
2498
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2499
+ return void 0;
2500
+ }
2501
+ const record = value;
2502
+ if (record.version !== 1 || typeof record.summary !== "string") {
2503
+ return void 0;
2504
+ }
2505
+ return {
2506
+ version: 1,
2507
+ summary: normalizeSummary(record.summary),
2508
+ updatedAt: typeof record.updatedAt === "string" && record.updatedAt.trim() ? record.updatedAt : timestamp
2509
+ };
2510
+ }
2511
+ function normalizeSummary(value) {
2512
+ return value.replace(/\r\n/g, "\n").trim().slice(0, MAX_SESSION_MEMORY_CHARS);
2513
+ }
2514
+ function readSessionMemoryCurrentFocus(summary) {
2515
+ const lines = summary.split("\n");
2516
+ let inFocusSection = false;
2517
+ const values = [];
2518
+ for (const line of lines) {
2519
+ if (line.startsWith("## ")) {
2520
+ if (inFocusSection) {
2521
+ break;
2522
+ }
2523
+ inFocusSection = line.slice(3).trim() === "Current Focus";
2524
+ continue;
2525
+ }
2526
+ if (inFocusSection) {
2527
+ values.push(line);
2528
+ }
2529
+ }
2530
+ const focus = values.join("\n").trim();
2531
+ return focus && focus.toLowerCase() !== "none" ? focus : void 0;
2532
+ }
2533
+
2534
+ // src/session/workset.ts
2535
+ import path5 from "path";
2536
+ var MAX_WORKSET_FILES = 20;
2537
+ function createEmptySessionWorkset(timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
2538
+ return {
2539
+ version: 1,
2540
+ files: [],
2541
+ updatedAt: timestamp
2542
+ };
2543
+ }
2544
+ function normalizeSessionWorkset(value) {
2545
+ if (value === void 0) {
2546
+ return void 0;
2547
+ }
2548
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2549
+ return createEmptySessionWorkset();
2550
+ }
2551
+ const record = value;
2552
+ if (record.version !== 1 || !Array.isArray(record.files)) {
2553
+ return createEmptySessionWorkset();
2554
+ }
2555
+ const files = record.files.map(normalizeWorksetEntry).filter((entry) => Boolean(entry)).slice(-MAX_WORKSET_FILES);
2556
+ return {
2557
+ version: 1,
2558
+ files,
2559
+ updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : latestWorksetTimestamp(files)
2560
+ };
2561
+ }
2562
+ function recordSessionWorksetFile(session, input) {
2563
+ const timestamp = input.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
2564
+ const workset = normalizeSessionWorkset(session.workset) ?? createEmptySessionWorkset(timestamp);
2565
+ const displayPath = normalizeDisplayPath(input.cwd, input.path);
2566
+ const existing = workset.files.find((entry) => entry.path === displayPath);
2567
+ const nextEntry = existing ? {
2568
+ ...existing,
2569
+ lastSeenAt: timestamp,
2570
+ readCount: existing.readCount + (input.changed ? 0 : 1),
2571
+ changedCount: existing.changedCount + (input.changed ? 1 : 0),
2572
+ lastTool: input.toolName,
2573
+ lastChangeId: input.changeId ?? existing.lastChangeId,
2574
+ reason: input.reason ?? existing.reason
2575
+ } : {
2576
+ path: displayPath,
2577
+ firstSeenAt: timestamp,
2578
+ lastSeenAt: timestamp,
2579
+ readCount: input.changed ? 0 : 1,
2580
+ changedCount: input.changed ? 1 : 0,
2581
+ lastTool: input.toolName,
2582
+ lastChangeId: input.changeId,
2583
+ reason: input.reason
2584
+ };
2585
+ const files = [
2586
+ ...workset.files.filter((entry) => entry.path !== displayPath),
2587
+ nextEntry
2588
+ ].slice(-MAX_WORKSET_FILES);
2589
+ return {
2590
+ ...session,
2591
+ workset: {
2592
+ version: 1,
2593
+ files,
2594
+ updatedAt: timestamp
2595
+ }
2596
+ };
2597
+ }
2598
+ function normalizeWorksetEntry(value) {
2599
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2600
+ return void 0;
2601
+ }
2602
+ const record = value;
2603
+ const filePath = typeof record.path === "string" ? record.path.trim() : "";
2604
+ if (!filePath) {
2605
+ return void 0;
2606
+ }
2607
+ return {
2608
+ path: filePath,
2609
+ firstSeenAt: readString2(record.firstSeenAt),
2610
+ lastSeenAt: readString2(record.lastSeenAt),
2611
+ readCount: readCount(record.readCount),
2612
+ changedCount: readCount(record.changedCount),
2613
+ lastTool: typeof record.lastTool === "string" && record.lastTool.trim() ? record.lastTool.trim() : "unknown",
2614
+ lastChangeId: typeof record.lastChangeId === "string" && record.lastChangeId.trim() ? record.lastChangeId.trim() : void 0,
2615
+ reason: typeof record.reason === "string" && record.reason.trim() ? record.reason.trim() : void 0
2616
+ };
2617
+ }
2618
+ function normalizeDisplayPath(cwd, targetPath) {
2619
+ const absolutePath = path5.resolve(cwd, targetPath);
2620
+ const relative = path5.relative(cwd, absolutePath);
2621
+ return relative && !relative.startsWith("..") && !path5.isAbsolute(relative) ? relative : absolutePath;
2622
+ }
2623
+ function readString2(value) {
2624
+ return typeof value === "string" && value.trim() ? value : (/* @__PURE__ */ new Date()).toISOString();
2625
+ }
2626
+ function readCount(value) {
2627
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.trunc(value) : 0;
2628
+ }
2629
+ function latestWorksetTimestamp(files) {
2630
+ return files.map((entry) => entry.lastSeenAt).sort().at(-1) ?? (/* @__PURE__ */ new Date()).toISOString();
2631
+ }
2632
+
2633
+ export {
2634
+ PROJECT_STATE_DIR_NAME,
2635
+ PROJECT_STATE_ENV_FILE_NAME,
2636
+ PROJECT_STATE_ENV_EXAMPLE_FILE_NAME,
2637
+ PROJECT_STATE_IGNORE_FILE_NAME,
2638
+ PRESERVED_PROJECT_STATE_ENTRY_NAMES,
2639
+ getProjectStatePaths,
2640
+ ensureProjectStateDirectories,
2641
+ createEmptyAssistantResponseTransition,
2642
+ createProviderRecoveryTransition,
2643
+ createFinalizeTransition,
2644
+ createExecutionWaitYieldTransition,
2645
+ buildRunTurnResult,
2646
+ fingerprintFocus,
2647
+ normalizeText4 as normalizeText,
2648
+ takeLastUnique3 as takeLastUnique,
2649
+ createEmptyCheckpoint,
2650
+ normalizeCheckpoint,
2651
+ normalizeSessionCheckpoint,
2652
+ noteCheckpointTurnInput,
2653
+ noteCheckpointToolBatch,
2654
+ noteCheckpointTransition,
2655
+ noteCheckpointRecovery,
2656
+ noteCheckpointCompleted,
2657
+ isAbortError,
2658
+ throwIfAborted,
2659
+ sleepWithSignal,
2660
+ isRetryableApiError,
2661
+ recordObservabilityEvent,
2662
+ createProviderClientPool,
2663
+ fetchAssistantResponse,
2664
+ buildChatMessages,
2665
+ createMessage,
2666
+ createToolMessage,
2667
+ readReasoningContent,
2668
+ collapseContentParts,
2669
+ toChatMessage,
2670
+ findLatestUserIndex,
2671
+ expandStartToToolBoundary,
2672
+ modelUsesReasoningContent,
2673
+ isAssistantMessageInLatestTurn,
2674
+ shouldIncludeStoredAssistantReasoning,
2675
+ isInternalMessage,
2676
+ createInternalReminder,
2677
+ readUserInput,
2678
+ findLatestUserInputIndex,
2679
+ sliceCurrentUserInputFrame,
2680
+ oneLine,
2681
+ createEmptyTaskState,
2682
+ deriveTaskState,
2683
+ normalizeTaskState,
2684
+ formatTaskStateBlock,
2685
+ normalizeSessionRecord,
2686
+ applyCurrentTurnFrame,
2687
+ deriveTodoItems,
2688
+ normalizeTodoItems,
2689
+ formatTodoBlock,
2690
+ normalizeSessionTodos,
2691
+ formatSessionMemorySectionTemplate,
2692
+ formatSessionMemorySectionList,
2693
+ updateSessionMemory,
2694
+ normalizeSessionMemory,
2695
+ createEmptySessionDiff,
2696
+ normalizeSessionDiff,
2697
+ normalizeSessionDiffState,
2698
+ noteSessionDiff,
2699
+ normalizeSessionWorkset,
2700
+ recordSessionWorksetFile
2701
+ };