@standardagents/builder 0.9.16 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,7 +4,9 @@ import { fileURLToPath } from 'url';
4
4
  import { DurableObject } from 'cloudflare:workers';
5
5
 
6
6
  var __defProp = Object.defineProperty;
7
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
8
  var __getOwnPropNames = Object.getOwnPropertyNames;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
10
  var __esm = (fn, res) => function __init() {
9
11
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
12
  };
@@ -12,6 +14,15 @@ var __export = (target, all) => {
12
14
  for (var name in all)
13
15
  __defProp(target, name, { get: all[name], enumerable: true });
14
16
  };
17
+ var __copyProps = (to, from, except, desc) => {
18
+ if (from && typeof from === "object" || typeof from === "function") {
19
+ for (let key of __getOwnPropNames(from))
20
+ if (!__hasOwnProp.call(to, key) && key !== except)
21
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
22
+ }
23
+ return to;
24
+ };
25
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
15
26
 
16
27
  // src/agents/types.ts
17
28
  var MAX_TURNS, MAX_RETRIES_PER_MODEL, TIMESTAMP_MULTIPLIER, STREAM_COOLDOWN, BACKOFF_BASE;
@@ -250,13 +261,16 @@ var init_BaseProvider = __esm({
250
261
  /**
251
262
  * Helper: Update log with actual request body before making API call
252
263
  * This ensures we have the complete request even if the call fails
264
+ *
265
+ * NOTE: Only updates if request_body is NULL - LLMRequest may have already
266
+ * set a transformed request_body (with image paths instead of base64 data URLs)
253
267
  */
254
268
  async logActualRequest(request, logId, state) {
255
269
  try {
256
270
  const requestBody = JSON.stringify(request);
257
271
  await state.stream.waitFor(async () => {
258
272
  await state.storage.sql.exec(
259
- `UPDATE logs SET request_body = ?1 WHERE id = ?2`,
273
+ `UPDATE logs SET request_body = ?1 WHERE id = ?2 AND request_body IS NULL`,
260
274
  requestBody,
261
275
  logId
262
276
  );
@@ -324,6 +338,81 @@ ${errorStack}` : ""}`,
324
338
  console.error(`[${this.name}] Failed to log error:`, logError);
325
339
  }
326
340
  }
341
+ /**
342
+ * Helper: Check if a MIME type is an image
343
+ */
344
+ isImageMimeType(mimeType) {
345
+ return mimeType.startsWith("image/");
346
+ }
347
+ /**
348
+ * Helper: Create image content part for OpenAI/OpenRouter format
349
+ * Converts base64 data to a data URL
350
+ */
351
+ createImageContentPart(base64Data, mimeType, detail = "auto") {
352
+ const dataUrl = `data:${mimeType};base64,${base64Data}`;
353
+ return {
354
+ type: "image_url",
355
+ image_url: {
356
+ url: dataUrl,
357
+ detail
358
+ }
359
+ };
360
+ }
361
+ /**
362
+ * Helper: Create text content part
363
+ */
364
+ createTextContentPart(text) {
365
+ return {
366
+ type: "text",
367
+ text
368
+ };
369
+ }
370
+ /**
371
+ * Helper: Convert message content and attachments to multimodal format
372
+ * Returns multimodal content array if there are images, otherwise returns string
373
+ *
374
+ * @param textContent The text content of the message
375
+ * @param attachments Array of attachment references with resolved base64 data
376
+ * @returns MessageContent - either string or multimodal array
377
+ */
378
+ buildMultimodalContent(textContent, attachments) {
379
+ if (!attachments || attachments.length === 0) {
380
+ return textContent || "";
381
+ }
382
+ const imageAttachments = attachments.filter(
383
+ (a) => this.isImageMimeType(a.ref.mimeType)
384
+ );
385
+ if (imageAttachments.length === 0) {
386
+ return textContent || "";
387
+ }
388
+ const parts = [];
389
+ if (textContent) {
390
+ parts.push(this.createTextContentPart(textContent));
391
+ }
392
+ for (const attachment of imageAttachments) {
393
+ parts.push(
394
+ this.createImageContentPart(attachment.base64, attachment.ref.mimeType)
395
+ );
396
+ }
397
+ return parts;
398
+ }
399
+ /**
400
+ * Helper: Convert MessageContent to string for logging/display
401
+ * Extracts text from multimodal content, describes images
402
+ */
403
+ contentToString(content) {
404
+ if (!content) return "";
405
+ if (typeof content === "string") return content;
406
+ const parts = [];
407
+ for (const part of content) {
408
+ if (part.type === "text") {
409
+ parts.push(part.text);
410
+ } else if (part.type === "image_url") {
411
+ parts.push("[Image]");
412
+ }
413
+ }
414
+ return parts.join(" ");
415
+ }
327
416
  };
328
417
  }
329
418
  });
@@ -762,6 +851,11 @@ var init_TestScript = __esm({
762
851
  });
763
852
 
764
853
  // src/agents/providers/TestProvider.ts
854
+ function contentToString(content) {
855
+ if (!content) return "";
856
+ if (typeof content === "string") return content;
857
+ return content.filter((part) => part.type === "text").map((part) => part.text).join(" ");
858
+ }
765
859
  var TestProvider;
766
860
  var init_TestProvider = __esm({
767
861
  "src/agents/providers/TestProvider.ts"() {
@@ -804,8 +898,9 @@ var init_TestProvider = __esm({
804
898
  const responses = this.script.getResponses();
805
899
  if (this.responseIndex >= responses.length) {
806
900
  const lastMessage = context.messages.slice(-1)[0];
901
+ const lastContent = contentToString(lastMessage?.content);
807
902
  throw new Error(
808
- `TestProvider: Script exhausted after ${this.responseIndex} responses. Expected ${responses.length} total requests. Received request with ${context.messages.length} messages. Last message role: "${lastMessage?.role}", content: "${lastMessage?.content?.substring(0, 100)}..."`
903
+ `TestProvider: Script exhausted after ${this.responseIndex} responses. Expected ${responses.length} total requests. Received request with ${context.messages.length} messages. Last message role: "${lastMessage?.role}", content: "${lastContent.substring(0, 100)}..."`
809
904
  );
810
905
  }
811
906
  const scripted = responses[this.responseIndex];
@@ -882,8 +977,9 @@ var init_TestProvider = __esm({
882
977
  if (expectations.containsMessage) {
883
978
  const pattern = expectations.containsMessage;
884
979
  const found = context.messages.some((m) => {
885
- if (!m.content) return false;
886
- return typeof pattern === "string" ? m.content.includes(pattern) : pattern.test(m.content);
980
+ const content = contentToString(m.content);
981
+ if (!content) return false;
982
+ return typeof pattern === "string" ? content.includes(pattern) : pattern.test(content);
887
983
  });
888
984
  if (!found) {
889
985
  throw new Error(
@@ -895,8 +991,9 @@ var init_TestProvider = __esm({
895
991
  const { toolName, resultContains } = expectations.containsToolResult;
896
992
  const found = context.messages.some((m) => {
897
993
  if (m.role !== "tool") return false;
898
- if (!m.content) return false;
899
- return !resultContains || m.content.includes(resultContains);
994
+ const content = contentToString(m.content);
995
+ if (!content) return false;
996
+ return !resultContains || content.includes(resultContains);
900
997
  });
901
998
  if (!found) {
902
999
  throw new Error(
@@ -907,10 +1004,11 @@ var init_TestProvider = __esm({
907
1004
  if (expectations.systemPromptContains) {
908
1005
  const pattern = expectations.systemPromptContains;
909
1006
  const systemMessage = context.messages.find((m) => m.role === "system");
910
- if (!systemMessage?.content) {
1007
+ const systemContent = contentToString(systemMessage?.content);
1008
+ if (!systemContent) {
911
1009
  throw new Error(`TestProvider: No system message found`);
912
1010
  }
913
- const matches = typeof pattern === "string" ? systemMessage.content.includes(pattern) : pattern.test(systemMessage.content);
1011
+ const matches = typeof pattern === "string" ? systemContent.includes(pattern) : pattern.test(systemContent);
914
1012
  if (!matches) {
915
1013
  throw new Error(
916
1014
  `TestProvider: System prompt does not contain "${pattern}"`
@@ -923,7 +1021,7 @@ var init_TestProvider = __esm({
923
1021
  */
924
1022
  estimateTokens(messages) {
925
1023
  return messages.reduce((sum, m) => {
926
- const content = m.content || "";
1024
+ const content = contentToString(m.content);
927
1025
  return sum + Math.ceil(content.length / 4);
928
1026
  }, 0);
929
1027
  }
@@ -1131,6 +1229,37 @@ var init_providers = __esm({
1131
1229
  });
1132
1230
 
1133
1231
  // src/agents/LLMRequest.ts
1232
+ function transformMessagesForLog(messages, imagePathMap) {
1233
+ console.log("[DEBUG] transformMessagesForLog called, imagePathMap size:", imagePathMap?.size ?? 0);
1234
+ if (!imagePathMap || imagePathMap.size === 0) {
1235
+ console.log("[DEBUG] No imagePathMap, returning original");
1236
+ return messages;
1237
+ }
1238
+ const result = messages.map((msg) => {
1239
+ if (!Array.isArray(msg.content)) {
1240
+ return msg;
1241
+ }
1242
+ return {
1243
+ ...msg,
1244
+ content: msg.content.map((part) => {
1245
+ if (typeof part === "object" && part !== null && "type" in part && part.type === "image_url" && "image_url" in part && part.image_url?.url) {
1246
+ const path4 = imagePathMap.get(part.image_url.url);
1247
+ console.log("[DEBUG] Found image_url, lookup result:", path4 ? path4 : "NOT FOUND");
1248
+ if (path4) {
1249
+ return {
1250
+ ...part,
1251
+ image_url: { ...part.image_url, url: path4 }
1252
+ };
1253
+ }
1254
+ }
1255
+ return part;
1256
+ })
1257
+ };
1258
+ });
1259
+ const hasDataUrl = JSON.stringify(result).includes("data:image");
1260
+ console.log("[DEBUG] After transform, still has data URL:", hasDataUrl);
1261
+ return result;
1262
+ }
1134
1263
  var LLMRequest;
1135
1264
  var init_LLMRequest = __esm({
1136
1265
  "src/agents/LLMRequest.ts"() {
@@ -1278,6 +1407,23 @@ var init_LLMRequest = __esm({
1278
1407
  } catch (err) {
1279
1408
  console.error("Failed to fetch model name:", err);
1280
1409
  }
1410
+ const messagesForLog = transformMessagesForLog(context.messages, context.imagePathMap);
1411
+ const requestBodyObj = {
1412
+ model: modelName || modelId,
1413
+ messages: messagesForLog,
1414
+ tools: context.tools,
1415
+ stream: context.stream ?? true
1416
+ };
1417
+ const requestBodyStr = JSON.stringify(requestBodyObj);
1418
+ const hasDataUrlInFinal = requestBodyStr.includes("data:image");
1419
+ console.log("[DEBUG] request_body has data URL:", hasDataUrlInFinal);
1420
+ if (hasDataUrlInFinal) {
1421
+ console.log("[DEBUG] PROBLEM! Data URL still in request_body after transform");
1422
+ console.log(
1423
+ "[DEBUG] messagesForLog first user msg content type:",
1424
+ messagesForLog.find((m) => m.role === "user")?.content
1425
+ );
1426
+ }
1281
1427
  const logData = {
1282
1428
  id,
1283
1429
  message_id: state.rootMessageId || crypto.randomUUID(),
@@ -1285,13 +1431,7 @@ var init_LLMRequest = __esm({
1285
1431
  model: modelId,
1286
1432
  model_name: modelName ?? void 0,
1287
1433
  endpoint: "chat.completions",
1288
- request_body: JSON.stringify({
1289
- model: modelName || modelId,
1290
- // Use actual model name sent to API, fallback to ID
1291
- messages: context.messages,
1292
- tools: context.tools,
1293
- stream: context.stream ?? true
1294
- }),
1434
+ request_body: requestBodyStr,
1295
1435
  tools_available: context.tools ? context.tools.length : 0,
1296
1436
  message_history_length: context.messages.length,
1297
1437
  prompt_name: context.promptName ?? void 0,
@@ -2543,6 +2683,112 @@ ${errorStack}` : ""}`;
2543
2683
  }
2544
2684
  });
2545
2685
 
2686
+ // src/agents/context.ts
2687
+ function hasImageAttachments(message) {
2688
+ if (!message.attachments) return false;
2689
+ try {
2690
+ const attachments = typeof message.attachments === "string" ? JSON.parse(message.attachments) : message.attachments;
2691
+ return attachments.some((att) => att.mimeType.startsWith("image/"));
2692
+ } catch {
2693
+ return false;
2694
+ }
2695
+ }
2696
+ function getUnsummarizedImageAttachments(message) {
2697
+ if (!message.attachments) return [];
2698
+ try {
2699
+ const attachments = typeof message.attachments === "string" ? JSON.parse(message.attachments) : message.attachments;
2700
+ return attachments.filter(
2701
+ (att) => att.mimeType.startsWith("image/") && !att.description
2702
+ );
2703
+ } catch {
2704
+ return [];
2705
+ }
2706
+ }
2707
+ function getMessagesToSummarize(messages, config = DEFAULT_CONFIG) {
2708
+ const indices = [];
2709
+ const threshold = messages.length - config.recentMessageThreshold;
2710
+ for (let i = 0; i < threshold; i++) {
2711
+ const msg = messages[i];
2712
+ if (msg.role === "user" && hasImageAttachments(msg)) {
2713
+ indices.push(i);
2714
+ }
2715
+ }
2716
+ return indices;
2717
+ }
2718
+ function buildImageDescription(attachment) {
2719
+ if (attachment.description) {
2720
+ return `[Image: ${attachment.name}] ${attachment.description}`;
2721
+ }
2722
+ const dimensions = attachment.width && attachment.height ? ` (${attachment.width}x${attachment.height})` : "";
2723
+ return `[Image: ${attachment.name}${dimensions}]`;
2724
+ }
2725
+ function replaceImagesWithDescriptions(content, attachments) {
2726
+ if (typeof content === "string") {
2727
+ const imageDescriptions2 = attachments.filter((att) => att.mimeType.startsWith("image/")).map(buildImageDescription).join("\n");
2728
+ if (!imageDescriptions2) return content;
2729
+ if (!content) return imageDescriptions2;
2730
+ return `${imageDescriptions2}
2731
+
2732
+ ${content}`;
2733
+ }
2734
+ const textParts = content.filter((part) => part.type === "text");
2735
+ const imageAttachments = attachments.filter(
2736
+ (att) => att.mimeType.startsWith("image/")
2737
+ );
2738
+ if (imageAttachments.length === 0) {
2739
+ return content;
2740
+ }
2741
+ const imageDescriptions = imageAttachments.map(buildImageDescription).join("\n");
2742
+ if (textParts.length > 0) {
2743
+ const existingText = textParts.map((p) => p.text).join(" ");
2744
+ return `${imageDescriptions}
2745
+
2746
+ ${existingText}`;
2747
+ }
2748
+ return imageDescriptions;
2749
+ }
2750
+ function optimizeImageContext(messages, config = DEFAULT_CONFIG) {
2751
+ const messagesToSummarize = getMessagesToSummarize(messages, config);
2752
+ if (messagesToSummarize.length === 0) {
2753
+ return messages;
2754
+ }
2755
+ return messages.map((msg, index) => {
2756
+ if (!messagesToSummarize.includes(index)) {
2757
+ return msg;
2758
+ }
2759
+ let attachments;
2760
+ try {
2761
+ attachments = typeof msg.attachments === "string" ? JSON.parse(msg.attachments) : msg.attachments || [];
2762
+ } catch {
2763
+ return msg;
2764
+ }
2765
+ const optimizedContent = replaceImagesWithDescriptions(
2766
+ msg.content || "",
2767
+ attachments
2768
+ );
2769
+ return {
2770
+ ...msg,
2771
+ content: typeof optimizedContent === "string" ? optimizedContent : JSON.stringify(optimizedContent),
2772
+ // Clear attachments since we've inlined descriptions
2773
+ attachments: JSON.stringify(
2774
+ attachments.filter((att) => !att.mimeType.startsWith("image/"))
2775
+ )
2776
+ };
2777
+ });
2778
+ }
2779
+ async function generateImageDescription(_imageBase64, _mimeType, _state) {
2780
+ return null;
2781
+ }
2782
+ var DEFAULT_CONFIG;
2783
+ var init_context = __esm({
2784
+ "src/agents/context.ts"() {
2785
+ DEFAULT_CONFIG = {
2786
+ recentMessageThreshold: 10,
2787
+ descriptionPrompt: "Describe this image concisely in 1-2 sentences, focusing on the key visual elements and any text visible."
2788
+ };
2789
+ }
2790
+ });
2791
+
2546
2792
  // src/agents/FlowEngine.ts
2547
2793
  var FlowEngine_exports = {};
2548
2794
  __export(FlowEngine_exports, {
@@ -2555,6 +2801,7 @@ var init_FlowEngine = __esm({
2555
2801
  init_StreamManager();
2556
2802
  init_LLMRequest();
2557
2803
  init_ToolExecutor();
2804
+ init_context();
2558
2805
  FlowEngine = class {
2559
2806
  /**
2560
2807
  * Main execution entry point
@@ -3014,6 +3261,7 @@ var init_FlowEngine = __esm({
3014
3261
  */
3015
3262
  static async assembleContext(state) {
3016
3263
  const messages = [];
3264
+ const imagePathMap = /* @__PURE__ */ new Map();
3017
3265
  const model = state.prompt.model;
3018
3266
  const promptName = state.prompt.name;
3019
3267
  const parallelToolCalls = state.prompt.parallel_tool_calls;
@@ -3042,6 +3290,9 @@ var init_FlowEngine = __esm({
3042
3290
  toolCallsWithResponses.add(msg.tool_call_id);
3043
3291
  }
3044
3292
  }
3293
+ const recentMessageThreshold = state.prompt.recentImageThreshold ?? 10;
3294
+ const oldMessageThreshold = completedMessages.length - recentMessageThreshold;
3295
+ let messageIndex = 0;
3045
3296
  for (const msg of completedMessages) {
3046
3297
  if (!includePastTools && msg.role === "tool") {
3047
3298
  continue;
@@ -3071,7 +3322,36 @@ var init_FlowEngine = __esm({
3071
3322
  role = state.currentSide === "a" ? "user" : "assistant";
3072
3323
  }
3073
3324
  }
3074
- const messageContent = msg.content || void 0;
3325
+ let messageContent;
3326
+ const hasAttachments = msg.attachments && msg.attachments !== "[]";
3327
+ const isOldMessage = messageIndex < oldMessageThreshold;
3328
+ if (hasAttachments && msg.role === "user") {
3329
+ const attachments = JSON.parse(msg.attachments);
3330
+ const imageAttachments = attachments.filter(
3331
+ (a) => a.mimeType.startsWith("image/")
3332
+ );
3333
+ if (isOldMessage && imageAttachments.length > 0) {
3334
+ const imageDescriptions = imageAttachments.map(buildImageDescription).join("\n");
3335
+ const nonImageFiles = attachments.filter((a) => !a.mimeType.startsWith("image/")).map((a) => a.name);
3336
+ const nonImageList = nonImageFiles.length > 0 ? `
3337
+ [Other files: ${nonImageFiles.join(", ")}]` : "";
3338
+ messageContent = msg.content ? `${imageDescriptions}${nonImageList}
3339
+
3340
+ ${msg.content}` : `${imageDescriptions}${nonImageList}`;
3341
+ } else {
3342
+ if (imageAttachments.length > 0) {
3343
+ messageContent = await this.resolveAttachmentsToContent(msg, state, imagePathMap);
3344
+ } else {
3345
+ const fileList = attachments.map((a) => a.name).join(", ");
3346
+ messageContent = msg.content ? `${msg.content}
3347
+
3348
+ [Attached files: ${fileList}]` : `[Attached files: ${fileList}]`;
3349
+ }
3350
+ }
3351
+ } else {
3352
+ messageContent = msg.content || "";
3353
+ }
3354
+ messageIndex++;
3075
3355
  const messageToAdd = {
3076
3356
  role,
3077
3357
  content: messageContent,
@@ -3170,7 +3450,8 @@ var init_FlowEngine = __esm({
3170
3450
  // Retries now apply to all prompts (top-level and sub-prompts), only for provider errors
3171
3451
  parallel_tool_calls: parallelToolCalls,
3172
3452
  tool_choice: finalToolChoice,
3173
- reasoning
3453
+ reasoning,
3454
+ imagePathMap: imagePathMap.size > 0 ? imagePathMap : void 0
3174
3455
  };
3175
3456
  }
3176
3457
  /**
@@ -4039,6 +4320,7 @@ var init_FlowEngine = __esm({
4039
4320
  "role",
4040
4321
  "content",
4041
4322
  "name",
4323
+ "attachments",
4042
4324
  ...includeToolCalls ? ["tool_calls", "tool_call_id", "tool_status"] : [],
4043
4325
  "log_id",
4044
4326
  "created_at",
@@ -4070,6 +4352,7 @@ var init_FlowEngine = __esm({
4070
4352
  role: row.role,
4071
4353
  content: row.content,
4072
4354
  name: row.name,
4355
+ attachments: row.attachments,
4073
4356
  tool_calls: row.tool_calls,
4074
4357
  tool_call_id: row.tool_call_id,
4075
4358
  log_id: row.log_id,
@@ -4148,6 +4431,510 @@ ${errorStack}` : ""}`,
4148
4431
  console.error("Failed to log error to database:", logError);
4149
4432
  }
4150
4433
  }
4434
+ /**
4435
+ * Check if a MIME type is an image
4436
+ */
4437
+ static isImageMimeType(mimeType) {
4438
+ return mimeType.startsWith("image/");
4439
+ }
4440
+ /**
4441
+ * Resolve attachments from a message and build multimodal content
4442
+ * Returns the message content (string or multimodal array)
4443
+ *
4444
+ * @param msg - Message with potential attachments
4445
+ * @param state - Flow state with storage access
4446
+ * @returns MessageContent - either string or multimodal array
4447
+ */
4448
+ static async resolveAttachmentsToContent(msg, state, imagePathMap) {
4449
+ const textContent = msg.content || "";
4450
+ if (!msg.attachments) {
4451
+ return textContent;
4452
+ }
4453
+ let attachments;
4454
+ try {
4455
+ attachments = JSON.parse(msg.attachments);
4456
+ } catch (e) {
4457
+ console.error(`Failed to parse attachments for message ${msg.id}:`, e);
4458
+ return textContent;
4459
+ }
4460
+ if (!attachments || attachments.length === 0) {
4461
+ return textContent;
4462
+ }
4463
+ const imageAttachments = attachments.filter(
4464
+ (a) => this.isImageMimeType(a.mimeType)
4465
+ );
4466
+ if (imageAttachments.length === 0) {
4467
+ const nonImageFiles2 = attachments.filter(
4468
+ (a) => !this.isImageMimeType(a.mimeType)
4469
+ );
4470
+ if (nonImageFiles2.length > 0) {
4471
+ const fileList = nonImageFiles2.map((a) => a.name).join(", ");
4472
+ return `${textContent}
4473
+
4474
+ [Attached files: ${fileList}]`;
4475
+ }
4476
+ return textContent;
4477
+ }
4478
+ const multimodalParts = [];
4479
+ if (textContent) {
4480
+ multimodalParts.push({
4481
+ type: "text",
4482
+ text: textContent
4483
+ });
4484
+ }
4485
+ for (const attachment of imageAttachments) {
4486
+ try {
4487
+ const result = await state.thread.instance.readFile(
4488
+ attachment.path
4489
+ );
4490
+ if (result.success && result.data) {
4491
+ const dataUrl = `data:${attachment.mimeType};base64,${result.data}`;
4492
+ if (imagePathMap) {
4493
+ imagePathMap.set(dataUrl, attachment.path);
4494
+ }
4495
+ multimodalParts.push({
4496
+ type: "image_url",
4497
+ image_url: {
4498
+ url: dataUrl,
4499
+ detail: "auto"
4500
+ }
4501
+ });
4502
+ } else {
4503
+ console.warn(
4504
+ `Failed to load image attachment ${attachment.path}:`,
4505
+ result.error
4506
+ );
4507
+ multimodalParts.push({
4508
+ type: "text",
4509
+ text: `[Image not available: ${attachment.name}]`
4510
+ });
4511
+ }
4512
+ } catch (error) {
4513
+ console.error(`Error loading image attachment ${attachment.path}:`, error);
4514
+ multimodalParts.push({
4515
+ type: "text",
4516
+ text: `[Error loading image: ${attachment.name}]`
4517
+ });
4518
+ }
4519
+ }
4520
+ const nonImageFiles = attachments.filter(
4521
+ (a) => !this.isImageMimeType(a.mimeType)
4522
+ );
4523
+ if (nonImageFiles.length > 0) {
4524
+ const fileList = nonImageFiles.map((a) => a.name).join(", ");
4525
+ multimodalParts.push({
4526
+ type: "text",
4527
+ text: `
4528
+
4529
+ [Attached files: ${fileList}]`
4530
+ });
4531
+ }
4532
+ return multimodalParts;
4533
+ }
4534
+ };
4535
+ }
4536
+ });
4537
+
4538
+ // src/durable-objects/files.ts
4539
+ var files_exports = {};
4540
+ __export(files_exports, {
4541
+ FileStorage: () => FileStorage,
4542
+ basename: () => basename,
4543
+ detectStorageBackend: () => detectStorageBackend,
4544
+ dirname: () => dirname,
4545
+ isTextMimeType: () => isTextMimeType,
4546
+ normalizePath: () => normalizePath,
4547
+ rowToFileRecord: () => rowToFileRecord
4548
+ });
4549
+ function isTextMimeType(mimeType) {
4550
+ return TEXT_MIME_TYPES.some((prefix) => mimeType.startsWith(prefix));
4551
+ }
4552
+ function detectStorageBackend(location) {
4553
+ if (location.startsWith("s3://")) return "s3";
4554
+ if (location.startsWith("r2://")) return "r2";
4555
+ if (location.startsWith("http://") || location.startsWith("https://"))
4556
+ return "url";
4557
+ return "local";
4558
+ }
4559
+ function basename(path4) {
4560
+ const parts = path4.split("/").filter(Boolean);
4561
+ return parts[parts.length - 1] || "";
4562
+ }
4563
+ function normalizePath(path4) {
4564
+ let normalized = path4.replace(/\/+/g, "/");
4565
+ if (!normalized.startsWith("/")) normalized = "/" + normalized;
4566
+ if (normalized.length > 1 && normalized.endsWith("/")) {
4567
+ normalized = normalized.slice(0, -1);
4568
+ }
4569
+ return normalized;
4570
+ }
4571
+ function dirname(path4) {
4572
+ const normalized = normalizePath(path4);
4573
+ const lastSlash = normalized.lastIndexOf("/");
4574
+ if (lastSlash <= 0) return "/";
4575
+ return normalized.slice(0, lastSlash);
4576
+ }
4577
+ function rowToFileRecord(row) {
4578
+ return {
4579
+ path: row.path,
4580
+ name: row.name,
4581
+ mimeType: row.mime_type,
4582
+ storage: row.storage,
4583
+ location: row.location,
4584
+ size: row.size,
4585
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
4586
+ isDirectory: row.is_directory === 1,
4587
+ createdAt: row.created_at
4588
+ };
4589
+ }
4590
+ function inferMimeType(filename) {
4591
+ const ext = filename.split(".").pop()?.toLowerCase();
4592
+ const mimeTypes = {
4593
+ // Text
4594
+ txt: "text/plain",
4595
+ md: "text/markdown",
4596
+ html: "text/html",
4597
+ css: "text/css",
4598
+ csv: "text/csv",
4599
+ // Code
4600
+ js: "application/javascript",
4601
+ ts: "application/typescript",
4602
+ json: "application/json",
4603
+ xml: "application/xml",
4604
+ yaml: "application/yaml",
4605
+ yml: "application/yaml",
4606
+ // Images
4607
+ jpg: "image/jpeg",
4608
+ jpeg: "image/jpeg",
4609
+ png: "image/png",
4610
+ gif: "image/gif",
4611
+ webp: "image/webp",
4612
+ svg: "image/svg+xml",
4613
+ ico: "image/x-icon",
4614
+ // Documents
4615
+ pdf: "application/pdf",
4616
+ // Archives
4617
+ zip: "application/zip",
4618
+ tar: "application/x-tar",
4619
+ gz: "application/gzip"
4620
+ };
4621
+ return mimeTypes[ext || ""] || "application/octet-stream";
4622
+ }
4623
+ var TEXT_MIME_TYPES, FileStorage;
4624
+ var init_files = __esm({
4625
+ "src/durable-objects/files.ts"() {
4626
+ TEXT_MIME_TYPES = [
4627
+ "text/",
4628
+ "application/json",
4629
+ "application/javascript",
4630
+ "application/xml",
4631
+ "application/x-yaml",
4632
+ "application/yaml"
4633
+ ];
4634
+ FileStorage = class {
4635
+ constructor(sql) {
4636
+ this.sql = sql;
4637
+ }
4638
+ /**
4639
+ * Write a file to storage
4640
+ */
4641
+ async writeFile(path4, data, mimeType, options) {
4642
+ const normalizedPath = normalizePath(path4);
4643
+ const name = basename(normalizedPath);
4644
+ const isText = isTextMimeType(mimeType);
4645
+ const now = Date.now();
4646
+ let content = null;
4647
+ let blobData = null;
4648
+ let size;
4649
+ if (isText) {
4650
+ content = typeof data === "string" ? data : new TextDecoder().decode(data);
4651
+ size = new TextEncoder().encode(content).length;
4652
+ } else {
4653
+ blobData = typeof data === "string" ? new TextEncoder().encode(data) : data;
4654
+ size = blobData.byteLength;
4655
+ }
4656
+ const metadataJson = options?.metadata ? JSON.stringify(options.metadata) : null;
4657
+ await this.sql.exec(
4658
+ `INSERT OR REPLACE INTO files (path, name, mime_type, storage, location, data, content, size, metadata, thumbnail, is_directory, created_at)
4659
+ VALUES (?, ?, ?, 'local', NULL, ?, ?, ?, ?, ?, 0, ?)`,
4660
+ normalizedPath,
4661
+ name,
4662
+ mimeType,
4663
+ blobData,
4664
+ content,
4665
+ size,
4666
+ metadataJson,
4667
+ options?.thumbnail || null,
4668
+ now
4669
+ );
4670
+ await this.updateStats();
4671
+ return {
4672
+ path: normalizedPath,
4673
+ name,
4674
+ mimeType,
4675
+ storage: "local",
4676
+ location: null,
4677
+ size,
4678
+ metadata: options?.metadata || null,
4679
+ isDirectory: false,
4680
+ createdAt: now
4681
+ };
4682
+ }
4683
+ /**
4684
+ * Link to an external file (URL, S3, R2)
4685
+ */
4686
+ async linkFile(path4, location, options) {
4687
+ const normalizedPath = normalizePath(path4);
4688
+ const name = basename(normalizedPath);
4689
+ const storage = detectStorageBackend(location);
4690
+ const now = Date.now();
4691
+ const mimeType = options?.mimeType || inferMimeType(name);
4692
+ const metadataJson = options?.metadata ? JSON.stringify(options.metadata) : null;
4693
+ await this.sql.exec(
4694
+ `INSERT OR REPLACE INTO files (path, name, mime_type, storage, location, data, content, size, metadata, thumbnail, is_directory, created_at)
4695
+ VALUES (?, ?, ?, ?, ?, NULL, NULL, ?, ?, NULL, 0, ?)`,
4696
+ normalizedPath,
4697
+ name,
4698
+ mimeType,
4699
+ storage,
4700
+ location,
4701
+ options?.size || 0,
4702
+ metadataJson,
4703
+ now
4704
+ );
4705
+ await this.updateStats();
4706
+ return {
4707
+ path: normalizedPath,
4708
+ name,
4709
+ mimeType,
4710
+ storage,
4711
+ location,
4712
+ size: options?.size || 0,
4713
+ metadata: options?.metadata || null,
4714
+ isDirectory: false,
4715
+ createdAt: now
4716
+ };
4717
+ }
4718
+ /**
4719
+ * Read file data from storage
4720
+ * Returns null if file doesn't exist or is external
4721
+ */
4722
+ async readFile(path4) {
4723
+ const normalizedPath = normalizePath(path4);
4724
+ const cursor = await this.sql.exec(
4725
+ `SELECT data, content, storage FROM files WHERE path = ? AND is_directory = 0`,
4726
+ normalizedPath
4727
+ );
4728
+ const rows = cursor.toArray();
4729
+ if (rows.length === 0) return null;
4730
+ const row = rows[0];
4731
+ if (row.storage !== "local") return null;
4732
+ if (row.content !== null) {
4733
+ return new TextEncoder().encode(row.content);
4734
+ }
4735
+ return row.data;
4736
+ }
4737
+ /**
4738
+ * Read text file content
4739
+ */
4740
+ async readTextFile(path4) {
4741
+ const normalizedPath = normalizePath(path4);
4742
+ const cursor = await this.sql.exec(
4743
+ `SELECT content, data, storage FROM files WHERE path = ? AND is_directory = 0`,
4744
+ normalizedPath
4745
+ );
4746
+ const rows = cursor.toArray();
4747
+ if (rows.length === 0) return null;
4748
+ const row = rows[0];
4749
+ if (row.storage !== "local") return null;
4750
+ if (row.content !== null) return row.content;
4751
+ if (row.data !== null) {
4752
+ return new TextDecoder().decode(row.data);
4753
+ }
4754
+ return null;
4755
+ }
4756
+ /**
4757
+ * Get file metadata (stat)
4758
+ */
4759
+ async stat(path4) {
4760
+ const normalizedPath = normalizePath(path4);
4761
+ const cursor = await this.sql.exec(
4762
+ `SELECT path, name, mime_type, storage, location, size, metadata, is_directory, created_at
4763
+ FROM files WHERE path = ?`,
4764
+ normalizedPath
4765
+ );
4766
+ const rows = cursor.toArray();
4767
+ if (rows.length === 0) return null;
4768
+ return rowToFileRecord(rows[0]);
4769
+ }
4770
+ /**
4771
+ * Check if file or directory exists
4772
+ */
4773
+ async exists(path4) {
4774
+ const normalizedPath = normalizePath(path4);
4775
+ const cursor = await this.sql.exec(
4776
+ `SELECT COUNT(*) as count FROM files WHERE path = ?`,
4777
+ normalizedPath
4778
+ );
4779
+ return cursor.one().count > 0;
4780
+ }
4781
+ /**
4782
+ * Delete a file
4783
+ */
4784
+ async unlink(path4) {
4785
+ const normalizedPath = normalizePath(path4);
4786
+ await this.sql.exec(
4787
+ `DELETE FROM files WHERE path = ? AND is_directory = 0`,
4788
+ normalizedPath
4789
+ );
4790
+ await this.updateStats();
4791
+ }
4792
+ /**
4793
+ * Create a directory marker
4794
+ */
4795
+ async mkdir(path4) {
4796
+ const normalizedPath = normalizePath(path4);
4797
+ const name = basename(normalizedPath);
4798
+ const now = Date.now();
4799
+ await this.sql.exec(
4800
+ `INSERT OR IGNORE INTO files (path, name, mime_type, storage, location, data, content, size, metadata, thumbnail, is_directory, created_at)
4801
+ VALUES (?, ?, 'inode/directory', 'local', NULL, NULL, NULL, 0, NULL, NULL, 1, ?)`,
4802
+ normalizedPath,
4803
+ name,
4804
+ now
4805
+ );
4806
+ return {
4807
+ path: normalizedPath,
4808
+ name,
4809
+ mimeType: "inode/directory",
4810
+ storage: "local",
4811
+ location: null,
4812
+ size: 0,
4813
+ metadata: null,
4814
+ isDirectory: true,
4815
+ createdAt: now
4816
+ };
4817
+ }
4818
+ /**
4819
+ * List directory contents
4820
+ */
4821
+ async readdir(path4) {
4822
+ const normalizedPath = normalizePath(path4);
4823
+ const prefix = normalizedPath === "/" ? "/" : normalizedPath + "/";
4824
+ const cursor = await this.sql.exec(
4825
+ `SELECT path, name, mime_type, storage, location, size, metadata, is_directory, created_at
4826
+ FROM files
4827
+ WHERE path LIKE ? || '%'
4828
+ AND path != ?
4829
+ AND SUBSTR(path, LENGTH(?) + 1) NOT LIKE '%/%'
4830
+ ORDER BY is_directory DESC, name ASC`,
4831
+ prefix,
4832
+ normalizedPath,
4833
+ prefix
4834
+ );
4835
+ return cursor.toArray().map(rowToFileRecord);
4836
+ }
4837
+ /**
4838
+ * Remove empty directory
4839
+ */
4840
+ async rmdir(path4) {
4841
+ const normalizedPath = normalizePath(path4);
4842
+ const prefix = normalizedPath + "/";
4843
+ const cursor = await this.sql.exec(
4844
+ `SELECT COUNT(*) as count FROM files WHERE path LIKE ? || '%'`,
4845
+ prefix
4846
+ );
4847
+ if (cursor.one().count > 0) {
4848
+ throw new Error("Directory not empty");
4849
+ }
4850
+ await this.sql.exec(
4851
+ `DELETE FROM files WHERE path = ? AND is_directory = 1`,
4852
+ normalizedPath
4853
+ );
4854
+ }
4855
+ /**
4856
+ * Get storage statistics
4857
+ */
4858
+ async getFileStats() {
4859
+ const cursor = await this.sql.exec(`SELECT total_size, file_count FROM file_stats WHERE id = 1`);
4860
+ const rows = cursor.toArray();
4861
+ if (rows.length === 0) {
4862
+ return { totalSize: 0, fileCount: 0 };
4863
+ }
4864
+ return {
4865
+ totalSize: rows[0].total_size,
4866
+ fileCount: rows[0].file_count
4867
+ };
4868
+ }
4869
+ /**
4870
+ * Update file statistics (call after writes/deletes)
4871
+ */
4872
+ async updateStats() {
4873
+ await this.sql.exec(`
4874
+ UPDATE file_stats SET
4875
+ total_size = (SELECT COALESCE(SUM(size), 0) FROM files WHERE is_directory = 0),
4876
+ file_count = (SELECT COUNT(*) FROM files WHERE is_directory = 0)
4877
+ WHERE id = 1
4878
+ `);
4879
+ }
4880
+ /**
4881
+ * Get thumbnail for an image
4882
+ */
4883
+ async getThumbnail(path4) {
4884
+ const normalizedPath = normalizePath(path4);
4885
+ const cursor = await this.sql.exec(
4886
+ `SELECT thumbnail FROM files WHERE path = ? AND is_directory = 0`,
4887
+ normalizedPath
4888
+ );
4889
+ const rows = cursor.toArray();
4890
+ if (rows.length === 0) return null;
4891
+ return rows[0].thumbnail;
4892
+ }
4893
+ /**
4894
+ * Search file contents using FTS5
4895
+ */
4896
+ async grep(pattern, options) {
4897
+ const limit = options?.limit || 100;
4898
+ let query = `
4899
+ SELECT files.path, files.name, snippet(files_fts, 2, '<mark>', '</mark>', '...', 32) as snippet
4900
+ FROM files_fts
4901
+ JOIN files ON files.rowid = files_fts.rowid
4902
+ WHERE files_fts MATCH ?
4903
+ `;
4904
+ const params = [pattern];
4905
+ if (options?.path) {
4906
+ query += ` AND files.path LIKE ? || '%'`;
4907
+ params.push(normalizePath(options.path));
4908
+ }
4909
+ query += ` LIMIT ?`;
4910
+ params.push(limit);
4911
+ const cursor = await this.sql.exec(query, ...params);
4912
+ return cursor.toArray();
4913
+ }
4914
+ /**
4915
+ * Find files by path pattern (glob-like)
4916
+ */
4917
+ async find(pattern, options) {
4918
+ const limit = options?.limit || 100;
4919
+ const type = options?.type || "all";
4920
+ let sqlPattern = pattern.replace(/\*\*/g, "%").replace(/\*/g, "%").replace(/\?/g, "_");
4921
+ if (!sqlPattern.startsWith("/")) {
4922
+ sqlPattern = "%" + sqlPattern;
4923
+ }
4924
+ let typeClause = "";
4925
+ if (type === "file") typeClause = " AND is_directory = 0";
4926
+ if (type === "directory") typeClause = " AND is_directory = 1";
4927
+ const cursor = await this.sql.exec(
4928
+ `SELECT path, name, mime_type, storage, location, size, metadata, is_directory, created_at
4929
+ FROM files
4930
+ WHERE path LIKE ?${typeClause}
4931
+ ORDER BY path
4932
+ LIMIT ?`,
4933
+ sqlPattern,
4934
+ limit
4935
+ );
4936
+ return cursor.toArray().map(rowToFileRecord);
4937
+ }
4151
4938
  };
4152
4939
  }
4153
4940
  });
@@ -6204,7 +6991,14 @@ function agentbuilder(options = {}) {
6204
6991
  const depsToExclude = [
6205
6992
  "@standardagents/builder",
6206
6993
  "@standardagents/builder/runtime",
6207
- "@standardagents/builder/built-in-routes"
6994
+ "@standardagents/builder/built-in-routes",
6995
+ "@standardagents/builder/image-processing",
6996
+ // WASM image processing deps - must be excluded to avoid pre-bundle cache issues
6997
+ "@cf-wasm/photon",
6998
+ "@cf-wasm/photon/workerd",
6999
+ "@jsquash/avif",
7000
+ "@jsquash/jpeg",
7001
+ "@jsquash/png"
6208
7002
  ];
6209
7003
  return {
6210
7004
  optimizeDeps: {
@@ -6228,7 +7022,14 @@ function agentbuilder(options = {}) {
6228
7022
  const depsToExclude = [
6229
7023
  "@standardagents/builder",
6230
7024
  "@standardagents/builder/runtime",
6231
- "@standardagents/builder/built-in-routes"
7025
+ "@standardagents/builder/built-in-routes",
7026
+ "@standardagents/builder/image-processing",
7027
+ // WASM image processing deps
7028
+ "@cf-wasm/photon",
7029
+ "@cf-wasm/photon/workerd",
7030
+ "@jsquash/avif",
7031
+ "@jsquash/jpeg",
7032
+ "@jsquash/png"
6232
7033
  ];
6233
7034
  config.optimizeDeps = config.optimizeDeps || {};
6234
7035
  config.optimizeDeps.exclude = [
@@ -8116,8 +8917,88 @@ var migration16 = {
8116
8917
  }
8117
8918
  };
8118
8919
 
8920
+ // src/durable-objects/migrations/017_add_files.ts
8921
+ var migration17 = {
8922
+ version: 17,
8923
+ async up(sql) {
8924
+ await sql.exec(`
8925
+ CREATE TABLE files (
8926
+ path TEXT PRIMARY KEY,
8927
+ name TEXT NOT NULL,
8928
+ mime_type TEXT NOT NULL,
8929
+
8930
+ -- Storage backend: 'local' | 'url' | 's3' | 'r2'
8931
+ storage TEXT NOT NULL DEFAULT 'local',
8932
+ -- External reference: URL, s3://bucket/key, r2://bucket/key
8933
+ location TEXT,
8934
+
8935
+ -- Local storage (NULL if external)
8936
+ data BLOB,
8937
+ content TEXT,
8938
+
8939
+ size INTEGER NOT NULL DEFAULT 0,
8940
+ metadata TEXT,
8941
+ thumbnail BLOB,
8942
+ is_directory INTEGER NOT NULL DEFAULT 0,
8943
+ created_at INTEGER NOT NULL
8944
+ )
8945
+ `);
8946
+ await sql.exec(`
8947
+ CREATE VIRTUAL TABLE files_fts USING fts5(
8948
+ path,
8949
+ name,
8950
+ content,
8951
+ content='files',
8952
+ content_rowid='rowid'
8953
+ )
8954
+ `);
8955
+ await sql.exec(`
8956
+ CREATE TRIGGER files_ai AFTER INSERT ON files BEGIN
8957
+ INSERT INTO files_fts(rowid, path, name, content)
8958
+ VALUES (new.rowid, new.path, new.name, new.content);
8959
+ END
8960
+ `);
8961
+ await sql.exec(`
8962
+ CREATE TRIGGER files_ad AFTER DELETE ON files BEGIN
8963
+ INSERT INTO files_fts(files_fts, rowid, path, name, content)
8964
+ VALUES('delete', old.rowid, old.path, old.name, old.content);
8965
+ END
8966
+ `);
8967
+ await sql.exec(`
8968
+ CREATE TRIGGER files_au AFTER UPDATE ON files BEGIN
8969
+ INSERT INTO files_fts(files_fts, rowid, path, name, content)
8970
+ VALUES('delete', old.rowid, old.path, old.name, old.content);
8971
+ INSERT INTO files_fts(rowid, path, name, content)
8972
+ VALUES (new.rowid, new.path, new.name, new.content);
8973
+ END
8974
+ `);
8975
+ await sql.exec(`
8976
+ CREATE TABLE file_stats (
8977
+ id INTEGER PRIMARY KEY CHECK (id = 1),
8978
+ total_size INTEGER DEFAULT 0,
8979
+ file_count INTEGER DEFAULT 0
8980
+ )
8981
+ `);
8982
+ await sql.exec(`
8983
+ INSERT INTO file_stats (id, total_size, file_count) VALUES (1, 0, 0)
8984
+ `);
8985
+ await sql.exec(`
8986
+ ALTER TABLE messages ADD COLUMN attachments TEXT
8987
+ `);
8988
+ await sql.exec(`
8989
+ CREATE INDEX idx_files_created_at ON files(created_at)
8990
+ `);
8991
+ await sql.exec(`
8992
+ CREATE INDEX idx_files_mime_type ON files(mime_type)
8993
+ `);
8994
+ await sql.exec(`
8995
+ UPDATE _metadata SET value = '17' WHERE key = 'schema_version'
8996
+ `);
8997
+ }
8998
+ };
8999
+
8119
9000
  // src/durable-objects/migrations/index.ts
8120
- var migrations = [migration, migration2, migration3, migration4, migration5, migration6, migration7, migration8, migration9, migration10, migration11, migration12, migration13, migration14, migration15, migration16];
9001
+ var migrations = [migration, migration2, migration3, migration4, migration5, migration6, migration7, migration8, migration9, migration10, migration11, migration12, migration13, migration14, migration15, migration16, migration17];
8121
9002
  var LATEST_SCHEMA_VERSION = migrations.length;
8122
9003
 
8123
9004
  // src/durable-objects/DurableThread.ts
@@ -8327,7 +9208,7 @@ var DurableThread = class extends DurableObject {
8327
9208
  );
8328
9209
  break;
8329
9210
  case "processMessage":
8330
- await this.processMessage(args.threadId, args.content, args.role);
9211
+ await this.processMessage(args.threadId, args.content, args.role, args.attachments);
8331
9212
  break;
8332
9213
  case "testOperation":
8333
9214
  await this.testOperation(args.id, args.label, args.expectedOrder);
@@ -8518,9 +9399,9 @@ var DurableThread = class extends DurableObject {
8518
9399
  * Each migration is run in order, starting from the current version + 1.
8519
9400
  */
8520
9401
  async runMigrations(fromVersion) {
8521
- for (const migration18 of migrations) {
8522
- if (migration18.version > fromVersion) {
8523
- await migration18.up(this.ctx.storage.sql);
9402
+ for (const migration19 of migrations) {
9403
+ if (migration19.version > fromVersion) {
9404
+ await migration19.up(this.ctx.storage.sql);
8524
9405
  }
8525
9406
  }
8526
9407
  }
@@ -8578,7 +9459,7 @@ var DurableThread = class extends DurableObject {
8578
9459
  * Send a new message to the thread (RPC method)
8579
9460
  * Enqueues the message processing to be handled by the alarm handler
8580
9461
  */
8581
- async sendMessage(threadId, content, role = "user") {
9462
+ async sendMessage(threadId, content, role = "user", attachments) {
8582
9463
  await this.ensureMigrated();
8583
9464
  try {
8584
9465
  const queueId = await this.alarmQueue.enqueue(
@@ -8586,7 +9467,8 @@ var DurableThread = class extends DurableObject {
8586
9467
  {
8587
9468
  threadId,
8588
9469
  content,
8589
- role
9470
+ role,
9471
+ attachments
8590
9472
  },
8591
9473
  1
8592
9474
  // Execute almost immediately (1ms)
@@ -8797,7 +9679,7 @@ var DurableThread = class extends DurableObject {
8797
9679
  const total = countResult.one().total;
8798
9680
  const result = await this.ctx.storage.sql.exec(
8799
9681
  `
8800
- SELECT id, role, content, name, tool_calls, tool_call_id, tool_status, log_id, created_at, silent, parent_id, depth, status, reasoning_content, reasoning_details
9682
+ SELECT id, role, content, name, tool_calls, tool_call_id, tool_status, log_id, created_at, silent, parent_id, depth, status, reasoning_content, reasoning_details, attachments
8801
9683
  FROM messages
8802
9684
  ${whereClause}
8803
9685
  ORDER BY created_at ${order === "ASC" ? "ASC" : "DESC"}
@@ -8821,7 +9703,8 @@ var DurableThread = class extends DurableObject {
8821
9703
  depth: row.depth,
8822
9704
  status: row.status,
8823
9705
  reasoning_content: row.reasoning_content,
8824
- reasoning_details: row.reasoning_details
9706
+ reasoning_details: row.reasoning_details,
9707
+ attachments: row.attachments ? JSON.parse(row.attachments) : null
8825
9708
  }));
8826
9709
  if (order === "DESC") {
8827
9710
  messages = messages.reverse();
@@ -9526,7 +10409,7 @@ var DurableThread = class extends DurableObject {
9526
10409
  * Internal method: Process a message (called by alarm queue)
9527
10410
  * This is the actual message processing logic, separate from the public sendMessage() RPC method
9528
10411
  */
9529
- async processMessage(threadId, content, role = "user") {
10412
+ async processMessage(threadId, content, role = "user", attachments) {
9530
10413
  if (role === "user") {
9531
10414
  await this.ctx.storage.sql.exec(
9532
10415
  `UPDATE execution_state SET value = 'false' WHERE key = 'stopped'`
@@ -9576,6 +10459,7 @@ var DurableThread = class extends DurableObject {
9576
10459
  id: messageId,
9577
10460
  role,
9578
10461
  content,
10462
+ attachments: attachments || null,
9579
10463
  created_at: timestamp
9580
10464
  };
9581
10465
  const state = {
@@ -9591,16 +10475,18 @@ var DurableThread = class extends DurableObject {
9591
10475
  const { FlowEngine: FlowEngine2 } = await Promise.resolve().then(() => (init_FlowEngine(), FlowEngine_exports));
9592
10476
  message = await FlowEngine2.runBeforeCreateMessageHook(state, message);
9593
10477
  await this.ctx.storage.sql.exec(
9594
- `INSERT INTO messages (id, role, content, created_at) VALUES (?, ?, ?, ?)`,
10478
+ `INSERT INTO messages (id, role, content, attachments, created_at) VALUES (?, ?, ?, ?, ?)`,
9595
10479
  message.id,
9596
10480
  message.role,
9597
10481
  message.content,
10482
+ message.attachments,
9598
10483
  message.created_at
9599
10484
  );
9600
10485
  this.broadcastMessage({
9601
10486
  id: message.id,
9602
10487
  role: message.role,
9603
10488
  content: message.content,
10489
+ attachments: message.attachments,
9604
10490
  created_at: message.created_at
9605
10491
  });
9606
10492
  const nextSide = role === "assistant" ? "b" : "a";
@@ -9769,10 +10655,261 @@ var DurableThread = class extends DurableObject {
9769
10655
  now
9770
10656
  );
9771
10657
  }
10658
+ // ============================================================
10659
+ // File Storage Methods (RPC)
10660
+ // ============================================================
10661
+ /**
10662
+ * Get file storage helper instance
10663
+ */
10664
+ getFileStorage() {
10665
+ const { FileStorage: FileStorage2 } = (init_files(), __toCommonJS(files_exports));
10666
+ return new FileStorage2(this.ctx.storage.sql);
10667
+ }
10668
+ /**
10669
+ * Write a file to storage (RPC method)
10670
+ */
10671
+ async writeFile(path4, data, mimeType, options) {
10672
+ await this.ensureMigrated();
10673
+ try {
10674
+ const fs4 = this.getFileStorage();
10675
+ const dataBuffer = Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
10676
+ const thumbnailBuffer = options?.thumbnail ? Uint8Array.from(atob(options.thumbnail), (c) => c.charCodeAt(0)) : void 0;
10677
+ const record = await fs4.writeFile(path4, dataBuffer, mimeType, {
10678
+ metadata: options?.metadata,
10679
+ thumbnail: thumbnailBuffer
10680
+ });
10681
+ return { success: true, file: record };
10682
+ } catch (error) {
10683
+ console.error("Error in writeFile:", error);
10684
+ return { success: false, error: error.message };
10685
+ }
10686
+ }
10687
+ /**
10688
+ * Write a text file to storage (RPC method)
10689
+ */
10690
+ async writeTextFile(path4, content, mimeType = "text/plain", options) {
10691
+ await this.ensureMigrated();
10692
+ try {
10693
+ const fs4 = this.getFileStorage();
10694
+ const record = await fs4.writeFile(path4, content, mimeType, options);
10695
+ return { success: true, file: record };
10696
+ } catch (error) {
10697
+ console.error("Error in writeTextFile:", error);
10698
+ return { success: false, error: error.message };
10699
+ }
10700
+ }
10701
+ /**
10702
+ * Link to an external file (RPC method)
10703
+ */
10704
+ async linkFile(path4, location, options) {
10705
+ await this.ensureMigrated();
10706
+ try {
10707
+ const fs4 = this.getFileStorage();
10708
+ const record = await fs4.linkFile(path4, location, options);
10709
+ return { success: true, file: record };
10710
+ } catch (error) {
10711
+ console.error("Error in linkFile:", error);
10712
+ return { success: false, error: error.message };
10713
+ }
10714
+ }
10715
+ /**
10716
+ * Read a file from storage (RPC method)
10717
+ * Returns base64-encoded data
10718
+ */
10719
+ async readFile(path4) {
10720
+ await this.ensureMigrated();
10721
+ try {
10722
+ const fs4 = this.getFileStorage();
10723
+ const data = await fs4.readFile(path4);
10724
+ if (data === null) {
10725
+ return { success: false, error: "File not found or external" };
10726
+ }
10727
+ const bytes = new Uint8Array(data);
10728
+ let binary = "";
10729
+ for (let i = 0; i < bytes.length; i++) {
10730
+ binary += String.fromCharCode(bytes[i]);
10731
+ }
10732
+ const base64 = btoa(binary);
10733
+ return { success: true, data: base64 };
10734
+ } catch (error) {
10735
+ console.error("Error in readFile:", error);
10736
+ return { success: false, error: error.message };
10737
+ }
10738
+ }
10739
+ /**
10740
+ * Read a text file from storage (RPC method)
10741
+ */
10742
+ async readTextFile(path4) {
10743
+ await this.ensureMigrated();
10744
+ try {
10745
+ const fs4 = this.getFileStorage();
10746
+ const content = await fs4.readTextFile(path4);
10747
+ if (content === null) {
10748
+ return { success: false, error: "File not found or external" };
10749
+ }
10750
+ return { success: true, content };
10751
+ } catch (error) {
10752
+ console.error("Error in readTextFile:", error);
10753
+ return { success: false, error: error.message };
10754
+ }
10755
+ }
10756
+ /**
10757
+ * Get file metadata (RPC method)
10758
+ */
10759
+ async statFile(path4) {
10760
+ await this.ensureMigrated();
10761
+ try {
10762
+ const fs4 = this.getFileStorage();
10763
+ const record = await fs4.stat(path4);
10764
+ if (record === null) {
10765
+ return { success: false, error: "File not found" };
10766
+ }
10767
+ return { success: true, file: record };
10768
+ } catch (error) {
10769
+ console.error("Error in statFile:", error);
10770
+ return { success: false, error: error.message };
10771
+ }
10772
+ }
10773
+ /**
10774
+ * Check if file exists (RPC method)
10775
+ */
10776
+ async fileExists(path4) {
10777
+ await this.ensureMigrated();
10778
+ try {
10779
+ const fs4 = this.getFileStorage();
10780
+ const exists2 = await fs4.exists(path4);
10781
+ return { success: true, exists: exists2 };
10782
+ } catch (error) {
10783
+ console.error("Error in fileExists:", error);
10784
+ return { success: false, error: error.message };
10785
+ }
10786
+ }
10787
+ /**
10788
+ * Delete a file (RPC method)
10789
+ */
10790
+ async unlinkFile(path4) {
10791
+ await this.ensureMigrated();
10792
+ try {
10793
+ const fs4 = this.getFileStorage();
10794
+ await fs4.unlink(path4);
10795
+ return { success: true };
10796
+ } catch (error) {
10797
+ console.error("Error in unlinkFile:", error);
10798
+ return { success: false, error: error.message };
10799
+ }
10800
+ }
10801
+ /**
10802
+ * Create a directory (RPC method)
10803
+ */
10804
+ async mkdirFile(path4) {
10805
+ await this.ensureMigrated();
10806
+ try {
10807
+ const fs4 = this.getFileStorage();
10808
+ const record = await fs4.mkdir(path4);
10809
+ return { success: true, directory: record };
10810
+ } catch (error) {
10811
+ console.error("Error in mkdirFile:", error);
10812
+ return { success: false, error: error.message };
10813
+ }
10814
+ }
10815
+ /**
10816
+ * List directory contents (RPC method)
10817
+ */
10818
+ async readdirFile(path4) {
10819
+ await this.ensureMigrated();
10820
+ try {
10821
+ const fs4 = this.getFileStorage();
10822
+ const files = await fs4.readdir(path4);
10823
+ return { success: true, files };
10824
+ } catch (error) {
10825
+ console.error("Error in readdirFile:", error);
10826
+ return { success: false, error: error.message };
10827
+ }
10828
+ }
10829
+ /**
10830
+ * Remove empty directory (RPC method)
10831
+ */
10832
+ async rmdirFile(path4) {
10833
+ await this.ensureMigrated();
10834
+ try {
10835
+ const fs4 = this.getFileStorage();
10836
+ await fs4.rmdir(path4);
10837
+ return { success: true };
10838
+ } catch (error) {
10839
+ console.error("Error in rmdirFile:", error);
10840
+ return { success: false, error: error.message };
10841
+ }
10842
+ }
10843
+ /**
10844
+ * Get file storage statistics (RPC method)
10845
+ */
10846
+ async getFileStats() {
10847
+ await this.ensureMigrated();
10848
+ try {
10849
+ const fs4 = this.getFileStorage();
10850
+ const stats = await fs4.getFileStats();
10851
+ return { success: true, stats };
10852
+ } catch (error) {
10853
+ console.error("Error in getFileStats:", error);
10854
+ return { success: false, error: error.message };
10855
+ }
10856
+ }
10857
+ /**
10858
+ * Get thumbnail for an image (RPC method)
10859
+ * Returns base64-encoded data
10860
+ */
10861
+ async getFileThumbnail(path4) {
10862
+ await this.ensureMigrated();
10863
+ try {
10864
+ const fs4 = this.getFileStorage();
10865
+ const data = await fs4.getThumbnail(path4);
10866
+ if (data === null) {
10867
+ return { success: false, error: "Thumbnail not found" };
10868
+ }
10869
+ const bytes = new Uint8Array(data);
10870
+ let binary = "";
10871
+ for (let i = 0; i < bytes.length; i++) {
10872
+ binary += String.fromCharCode(bytes[i]);
10873
+ }
10874
+ const base64 = btoa(binary);
10875
+ return { success: true, data: base64 };
10876
+ } catch (error) {
10877
+ console.error("Error in getFileThumbnail:", error);
10878
+ return { success: false, error: error.message };
10879
+ }
10880
+ }
10881
+ /**
10882
+ * Search file contents using FTS5 (RPC method)
10883
+ */
10884
+ async grepFiles(pattern, options) {
10885
+ await this.ensureMigrated();
10886
+ try {
10887
+ const fs4 = this.getFileStorage();
10888
+ const results = await fs4.grep(pattern, options);
10889
+ return { success: true, results };
10890
+ } catch (error) {
10891
+ console.error("Error in grepFiles:", error);
10892
+ return { success: false, error: error.message };
10893
+ }
10894
+ }
10895
+ /**
10896
+ * Find files by path pattern (RPC method)
10897
+ */
10898
+ async findFiles(pattern, options) {
10899
+ await this.ensureMigrated();
10900
+ try {
10901
+ const fs4 = this.getFileStorage();
10902
+ const files = await fs4.find(pattern, options);
10903
+ return { success: true, files };
10904
+ } catch (error) {
10905
+ console.error("Error in findFiles:", error);
10906
+ return { success: false, error: error.message };
10907
+ }
10908
+ }
9772
10909
  };
9773
10910
 
9774
10911
  // src/durable-objects/agentbuilder-migrations/0001_initial.ts
9775
- var migration17 = {
10912
+ var migration18 = {
9776
10913
  version: 1,
9777
10914
  async up(sql) {
9778
10915
  sql.exec(`
@@ -9871,7 +11008,7 @@ var migration17 = {
9871
11008
  };
9872
11009
 
9873
11010
  // src/durable-objects/agentbuilder-migrations/index.ts
9874
- var migrations2 = [migration17];
11011
+ var migrations2 = [migration18];
9875
11012
  var LATEST_SCHEMA_VERSION2 = 1;
9876
11013
 
9877
11014
  // src/durable-objects/DurableAgentBuilder.ts
@@ -9961,9 +11098,9 @@ var DurableAgentBuilder = class extends DurableObject {
9961
11098
  }
9962
11099
  }
9963
11100
  async runMigrations(fromVersion) {
9964
- for (const migration18 of migrations2) {
9965
- if (migration18.version > fromVersion) {
9966
- await migration18.up(this.ctx.storage.sql);
11101
+ for (const migration19 of migrations2) {
11102
+ if (migration19.version > fromVersion) {
11103
+ await migration19.up(this.ctx.storage.sql);
9967
11104
  }
9968
11105
  }
9969
11106
  }
@@ -10731,6 +11868,12 @@ function definePrompt(options) {
10731
11868
  `Invalid reasoning.effort '${options.reasoning.effort}'. Must be one of: low, medium, high`
10732
11869
  );
10733
11870
  }
11871
+ if (options.maxImagePixels !== void 0 && (options.maxImagePixels <= 0 || !Number.isInteger(options.maxImagePixels))) {
11872
+ throw new Error("maxImagePixels must be a positive integer");
11873
+ }
11874
+ if (options.recentImageThreshold !== void 0 && (options.recentImageThreshold <= 0 || !Number.isInteger(options.recentImageThreshold))) {
11875
+ throw new Error("recentImageThreshold must be a positive integer");
11876
+ }
10734
11877
  return options;
10735
11878
  }
10736
11879
 
@@ -10923,6 +12066,165 @@ async function updateThread(thread, params) {
10923
12066
  }
10924
12067
  return result.thread;
10925
12068
  }
12069
+ async function writeFile(flow, path4, data, mimeType, options) {
12070
+ const instance = flow.thread.instance;
12071
+ let base64Data;
12072
+ if (typeof data === "string") {
12073
+ base64Data = btoa(data);
12074
+ } else {
12075
+ const bytes = new Uint8Array(data);
12076
+ let binary = "";
12077
+ for (let i = 0; i < bytes.length; i++) {
12078
+ binary += String.fromCharCode(bytes[i]);
12079
+ }
12080
+ base64Data = btoa(binary);
12081
+ }
12082
+ let base64Thumbnail;
12083
+ if (options?.thumbnail) {
12084
+ const bytes = new Uint8Array(options.thumbnail);
12085
+ let binary = "";
12086
+ for (let i = 0; i < bytes.length; i++) {
12087
+ binary += String.fromCharCode(bytes[i]);
12088
+ }
12089
+ base64Thumbnail = btoa(binary);
12090
+ }
12091
+ const result = await instance.writeFile(path4, base64Data, mimeType, {
12092
+ metadata: options?.metadata,
12093
+ thumbnail: base64Thumbnail
12094
+ });
12095
+ if (!result.success) {
12096
+ throw new Error(result.error || "Failed to write file");
12097
+ }
12098
+ return result.file;
12099
+ }
12100
+ async function writeImage(flow, path4, data, mimeType, options) {
12101
+ return writeFile(flow, path4, data, mimeType, options);
12102
+ }
12103
+ async function linkFile(flow, path4, location, options) {
12104
+ const instance = flow.thread.instance;
12105
+ const result = await instance.linkFile(path4, location, options);
12106
+ if (!result.success) {
12107
+ throw new Error(result.error || "Failed to link file");
12108
+ }
12109
+ return result.file;
12110
+ }
12111
+ async function readFile(flow, path4) {
12112
+ const instance = flow.thread.instance;
12113
+ const result = await instance.readFile(path4);
12114
+ if (!result.success) {
12115
+ return null;
12116
+ }
12117
+ const binary = atob(result.data);
12118
+ const bytes = new Uint8Array(binary.length);
12119
+ for (let i = 0; i < binary.length; i++) {
12120
+ bytes[i] = binary.charCodeAt(i);
12121
+ }
12122
+ return bytes.buffer;
12123
+ }
12124
+ async function stat(flow, path4) {
12125
+ const instance = flow.thread.instance;
12126
+ const result = await instance.statFile(path4);
12127
+ if (!result.success) {
12128
+ return null;
12129
+ }
12130
+ return result.file;
12131
+ }
12132
+ async function exists(flow, path4) {
12133
+ const instance = flow.thread.instance;
12134
+ const result = await instance.fileExists(path4);
12135
+ return result.success && result.exists;
12136
+ }
12137
+ async function unlink(flow, path4) {
12138
+ const instance = flow.thread.instance;
12139
+ const result = await instance.unlinkFile(path4);
12140
+ if (!result.success) {
12141
+ throw new Error(result.error || "Failed to delete file");
12142
+ }
12143
+ }
12144
+ async function mkdir(flow, path4) {
12145
+ const instance = flow.thread.instance;
12146
+ const result = await instance.mkdirFile(path4);
12147
+ if (!result.success) {
12148
+ throw new Error(result.error || "Failed to create directory");
12149
+ }
12150
+ return result.directory;
12151
+ }
12152
+ async function readdir(flow, path4) {
12153
+ const instance = flow.thread.instance;
12154
+ const result = await instance.readdirFile(path4);
12155
+ if (!result.success) {
12156
+ throw new Error(result.error || "Failed to read directory");
12157
+ }
12158
+ return result.files;
12159
+ }
12160
+ async function rmdir(flow, path4) {
12161
+ const instance = flow.thread.instance;
12162
+ const result = await instance.rmdirFile(path4);
12163
+ if (!result.success) {
12164
+ throw new Error(result.error || "Failed to remove directory");
12165
+ }
12166
+ }
12167
+ async function getFileStats(flow) {
12168
+ const instance = flow.thread.instance;
12169
+ const result = await instance.getFileStats();
12170
+ if (!result.success) {
12171
+ throw new Error(result.error || "Failed to get file stats");
12172
+ }
12173
+ return result.stats;
12174
+ }
12175
+ async function getThumbnail(flow, path4) {
12176
+ const instance = flow.thread.instance;
12177
+ const result = await instance.getFileThumbnail(path4);
12178
+ if (!result.success) {
12179
+ return null;
12180
+ }
12181
+ const binary = atob(result.data);
12182
+ const bytes = new Uint8Array(binary.length);
12183
+ for (let i = 0; i < binary.length; i++) {
12184
+ bytes[i] = binary.charCodeAt(i);
12185
+ }
12186
+ return bytes.buffer;
12187
+ }
12188
+ async function cat(flow, path4) {
12189
+ const instance = flow.thread.instance;
12190
+ const result = await instance.readTextFile(path4);
12191
+ if (!result.success) {
12192
+ return null;
12193
+ }
12194
+ return result.content;
12195
+ }
12196
+ async function head(flow, path4, lines = 10) {
12197
+ const content = await cat(flow, path4);
12198
+ if (content === null) return null;
12199
+ const allLines = content.split("\n");
12200
+ return allLines.slice(0, lines).join("\n");
12201
+ }
12202
+ async function tail(flow, path4, lines = 10) {
12203
+ const content = await cat(flow, path4);
12204
+ if (content === null) return null;
12205
+ const allLines = content.split("\n");
12206
+ return allLines.slice(-lines).join("\n");
12207
+ }
12208
+ async function grep(flow, pattern, options) {
12209
+ const instance = flow.thread.instance;
12210
+ const result = await instance.grepFiles(pattern, options);
12211
+ if (!result.success) {
12212
+ throw new Error(result.error || "Search failed");
12213
+ }
12214
+ return result.results.map((r) => ({
12215
+ path: r.path,
12216
+ name: r.name,
12217
+ matches: [r.snippet]
12218
+ }));
12219
+ }
12220
+ async function find(flow, pattern, options) {
12221
+ const instance = flow.thread.instance;
12222
+ const result = await instance.findFiles(pattern, options);
12223
+ if (!result.success) {
12224
+ throw new Error(result.error || "Find failed");
12225
+ }
12226
+ return result.files;
12227
+ }
10926
12228
 
10927
12229
  // src/agents/FlowStateSdk.ts
10928
12230
  init_types();
@@ -11257,6 +12559,9 @@ function enhanceFlowState(flow) {
11257
12559
  return enhanced;
11258
12560
  }
11259
12561
 
12562
+ // src/index.ts
12563
+ init_context();
12564
+
11260
12565
  // src/github/GitHubClient.ts
11261
12566
  var GITHUB_API_BASE = "https://api.github.com";
11262
12567
  var GitHubClient = class _GitHubClient {
@@ -11494,6 +12799,6 @@ var GitHubApiError = class extends Error {
11494
12799
  }
11495
12800
  };
11496
12801
 
11497
- export { DurableAgentBuilder, DurableThread, FlowStateSdk, GitHubApiError, GitHubClient, agentbuilder, authenticate, defineAgent, defineController, defineHook, defineModel, definePrompt, defineThreadEndpoint, defineTool, emitThreadEvent, enhanceFlowState, forceTurn, generateAgentFile, generateModelFile, generatePromptFile, getMessages, injectMessage, queueTool, reloadHistory, requireAdmin, requireAuth, updateThread };
12802
+ export { DurableAgentBuilder, DurableThread, FlowStateSdk, GitHubApiError, GitHubClient, agentbuilder, authenticate, buildImageDescription, cat, defineAgent, defineController, defineHook, defineModel, definePrompt, defineThreadEndpoint, defineTool, emitThreadEvent, enhanceFlowState, exists, find, forceTurn, generateAgentFile, generateImageDescription, generateModelFile, generatePromptFile, getFileStats, getMessages, getMessagesToSummarize, getThumbnail, getUnsummarizedImageAttachments, grep, hasImageAttachments, head, injectMessage, linkFile, mkdir, optimizeImageContext, queueTool, readFile, readdir, reloadHistory, replaceImagesWithDescriptions, requireAdmin, requireAuth, rmdir, stat, tail, unlink, updateThread, writeFile, writeImage };
11498
12803
  //# sourceMappingURL=index.js.map
11499
12804
  //# sourceMappingURL=index.js.map