@wingman-ai/gateway 0.1.5 → 0.2.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.
Files changed (66) hide show
  1. package/.wingman/agents/README.md +7 -0
  2. package/.wingman/agents/coding/agent.md +1 -0
  3. package/.wingman/agents/main/agent.md +1 -0
  4. package/.wingman/agents/researcher/agent.md +1 -0
  5. package/.wingman/agents/stock-trader/agent.md +1 -0
  6. package/dist/agent/config/agentConfig.cjs +12 -1
  7. package/dist/agent/config/agentConfig.d.ts +14 -0
  8. package/dist/agent/config/agentConfig.js +12 -1
  9. package/dist/agent/config/agentLoader.cjs +37 -1
  10. package/dist/agent/config/agentLoader.d.ts +2 -1
  11. package/dist/agent/config/agentLoader.js +37 -1
  12. package/dist/agent/config/modelFactory.cjs +2 -1
  13. package/dist/agent/config/modelFactory.js +2 -1
  14. package/dist/agent/config/toolRegistry.cjs +6 -4
  15. package/dist/agent/config/toolRegistry.d.ts +1 -0
  16. package/dist/agent/config/toolRegistry.js +6 -4
  17. package/dist/agent/middleware/additional-messages.cjs +8 -1
  18. package/dist/agent/middleware/additional-messages.d.ts +1 -0
  19. package/dist/agent/middleware/additional-messages.js +8 -1
  20. package/dist/agent/tests/agentConfig.test.cjs +25 -0
  21. package/dist/agent/tests/agentConfig.test.js +25 -0
  22. package/dist/agent/tests/agentLoader.test.cjs +18 -0
  23. package/dist/agent/tests/agentLoader.test.js +18 -0
  24. package/dist/agent/tests/modelFactory.test.cjs +13 -0
  25. package/dist/agent/tests/modelFactory.test.js +14 -1
  26. package/dist/agent/tests/toolRegistry.test.cjs +15 -0
  27. package/dist/agent/tests/toolRegistry.test.js +15 -0
  28. package/dist/agent/tools/code_search.cjs +1 -1
  29. package/dist/agent/tools/code_search.js +1 -1
  30. package/dist/agent/tools/command_execute.cjs +1 -1
  31. package/dist/agent/tools/command_execute.js +1 -1
  32. package/dist/agent/tools/ui_registry.d.ts +3 -3
  33. package/dist/cli/core/agentInvoker.cjs +212 -21
  34. package/dist/cli/core/agentInvoker.d.ts +55 -20
  35. package/dist/cli/core/agentInvoker.js +197 -21
  36. package/dist/cli/core/sessionManager.cjs +93 -4
  37. package/dist/cli/core/sessionManager.d.ts +1 -1
  38. package/dist/cli/core/sessionManager.js +93 -4
  39. package/dist/gateway/http/agents.cjs +121 -10
  40. package/dist/gateway/http/agents.js +121 -10
  41. package/dist/gateway/index.cjs +2 -2
  42. package/dist/gateway/server.cjs +55 -17
  43. package/dist/gateway/server.js +55 -17
  44. package/dist/gateway/types.d.ts +9 -1
  45. package/dist/tests/additionalMessageMiddleware.test.cjs +26 -0
  46. package/dist/tests/additionalMessageMiddleware.test.js +26 -0
  47. package/dist/tests/agentInvokerAttachments.test.cjs +123 -0
  48. package/dist/tests/agentInvokerAttachments.test.js +123 -0
  49. package/dist/tests/agentInvokerWorkdir.test.cjs +100 -0
  50. package/dist/tests/agentInvokerWorkdir.test.d.ts +1 -0
  51. package/dist/tests/agentInvokerWorkdir.test.js +72 -0
  52. package/dist/tests/agents-api.test.cjs +232 -0
  53. package/dist/tests/agents-api.test.d.ts +1 -0
  54. package/dist/tests/agents-api.test.js +226 -0
  55. package/dist/tests/gateway.test.cjs +21 -0
  56. package/dist/tests/gateway.test.js +21 -0
  57. package/dist/tests/sessionMessageAttachments.test.cjs +59 -0
  58. package/dist/tests/sessionMessageAttachments.test.js +59 -0
  59. package/dist/types/agents.d.ts +5 -0
  60. package/dist/webui/assets/index-BytPznA_.css +1 -0
  61. package/dist/webui/assets/index-u_5qlVip.js +176 -0
  62. package/dist/webui/index.html +2 -2
  63. package/package.json +3 -3
  64. package/.wingman/agents/wingman/agent.json +0 -12
  65. package/dist/webui/assets/index-CyE7T5pV.js +0 -162
  66. package/dist/webui/assets/index-DMEHdune.css +0 -1
@@ -1,6 +1,6 @@
1
1
  import { CompositeBackend, FilesystemBackend, createDeepAgent } from "deepagents";
2
2
  import { existsSync } from "node:fs";
3
- import { join } from "node:path";
3
+ import { isAbsolute, join, normalize, sep } from "node:path";
4
4
  import { v4 } from "uuid";
5
5
  import { AgentLoader } from "../../agent/config/agentLoader.js";
6
6
  import { WingmanConfigLoader } from "../config/loader.js";
@@ -20,6 +20,40 @@ function _define_property(obj, key, value) {
20
20
  else obj[key] = value;
21
21
  return obj;
22
22
  }
23
+ const WORKDIR_VIRTUAL_PATH = "/workdir/";
24
+ const OUTPUT_VIRTUAL_PATH = "/output/";
25
+ const isPathWithinRoot = (targetPath, rootPath)=>{
26
+ const normalizedTarget = normalize(targetPath);
27
+ const normalizedRoot = normalize(rootPath);
28
+ return normalizedTarget === normalizedRoot || normalizedTarget.startsWith(normalizedRoot + sep);
29
+ };
30
+ const resolveExecutionWorkspace = (workspace, workdir)=>{
31
+ if (!workdir) return normalize(workspace);
32
+ if (isAbsolute(workdir)) return normalize(workdir);
33
+ return normalize(join(workspace, workdir));
34
+ };
35
+ const toWorkspaceAliasVirtualPath = (absolutePath)=>{
36
+ const normalized = normalize(absolutePath);
37
+ if (!isAbsolute(normalized)) return null;
38
+ const posixPath = normalized.replace(/\\/g, "/");
39
+ const trimmed = posixPath.replace(/^\/+/, "").replace(/\/+$/, "");
40
+ if (!trimmed) return null;
41
+ return `/${trimmed}/`;
42
+ };
43
+ const resolveExternalOutputMount = (workspace, workdir, defaultOutputDir)=>{
44
+ if (workdir && !isPathWithinRoot(workdir, workspace)) return {
45
+ virtualPath: WORKDIR_VIRTUAL_PATH,
46
+ absolutePath: workdir
47
+ };
48
+ if (!workdir && defaultOutputDir && !isPathWithinRoot(defaultOutputDir, workspace)) return {
49
+ virtualPath: OUTPUT_VIRTUAL_PATH,
50
+ absolutePath: defaultOutputDir
51
+ };
52
+ return {
53
+ virtualPath: null,
54
+ absolutePath: null
55
+ };
56
+ };
23
57
  class AgentInvoker {
24
58
  findAllAgents() {
25
59
  const agentConfigs = this.loader.loadAllAgentConfigs();
@@ -30,7 +64,10 @@ class AgentInvoker {
30
64
  }
31
65
  async invokeAgent(agentName, prompt, sessionId, attachments) {
32
66
  try {
33
- const targetAgent = await this.findAgent(agentName);
67
+ const executionWorkspace = resolveExecutionWorkspace(this.workspace, this.workdir);
68
+ const effectiveWorkdir = this.workdir ? executionWorkspace : null;
69
+ const loader = normalize(executionWorkspace) === normalize(this.workspace) ? this.loader : new AgentLoader(this.configDir, this.workspace, this.wingmanConfig, executionWorkspace);
70
+ const targetAgent = await loader.loadAgent(agentName);
34
71
  if (!targetAgent) throw new Error(`Agent "${agentName}" not found`);
35
72
  this.logger.info(`Invoking agent: ${agentName}`);
36
73
  const preview = prompt.trim() || (attachments && attachments.length > 0 ? buildAttachmentPreview(attachments) : "");
@@ -58,43 +95,65 @@ class AgentInvoker {
58
95
  }
59
96
  }
60
97
  const skillsDirectory = this.wingmanConfig?.skills?.skillsDirectory || "skills";
98
+ const normalizedSkillsDirectory = skillsDirectory.replace(/^\/+|\/+$/g, "");
99
+ const skillsVirtualPath = `/${normalizedSkillsDirectory}/`;
100
+ const outputMount = resolveExternalOutputMount(executionWorkspace, effectiveWorkdir, this.defaultOutputDir);
61
101
  const middleware = [
62
102
  mediaCompatibilityMiddleware({
63
103
  model: targetAgent.model
64
104
  }),
65
105
  additionalMessageMiddleware({
66
- workspaceRoot: this.workspace,
67
- workdir: this.workdir,
106
+ workspaceRoot: executionWorkspace,
107
+ workdir: effectiveWorkdir,
68
108
  defaultOutputDir: this.defaultOutputDir,
109
+ outputVirtualPath: outputMount.virtualPath,
69
110
  dynamicUiEnabled: this.wingmanConfig?.gateway?.dynamicUiEnabled !== false,
70
111
  skillsDirectory
71
112
  })
72
113
  ];
73
114
  if (mergedHooks) {
74
115
  this.logger.debug(`Adding hooks middleware with ${mergedHooks.PreToolUse?.length || 0} PreToolUse hooks, ${mergedHooks.PostToolUse?.length || 0} PostToolUse hooks, and ${mergedHooks.Stop?.length || 0} Stop hooks`);
75
- middleware.push(createHooksMiddleware(mergedHooks, this.workspace, hookSessionId, this.logger));
116
+ middleware.push(createHooksMiddleware(mergedHooks, executionWorkspace, hookSessionId, this.logger));
76
117
  }
77
118
  const checkpointer = this.sessionManager?.getCheckpointer();
78
119
  const bundledSkillsPath = getBundledSkillsPath();
79
120
  const skillsSources = [];
80
121
  if (existsSync(bundledSkillsPath)) skillsSources.push("/skills-bundled/");
81
- skillsSources.push(`/${skillsDirectory.replace(/^\/+|\/+$/g, "")}/`);
122
+ skillsSources.push(skillsVirtualPath);
82
123
  const backendOverrides = {
83
124
  "/memories/": new FilesystemBackend({
84
125
  rootDir: join(this.workspace, this.configDir, "memories"),
85
126
  virtualMode: true
86
127
  })
87
128
  };
129
+ const executionWorkspaceAlias = toWorkspaceAliasVirtualPath(executionWorkspace);
130
+ if (executionWorkspaceAlias) backendOverrides[executionWorkspaceAlias] = new FilesystemBackend({
131
+ rootDir: executionWorkspace,
132
+ virtualMode: true
133
+ });
134
+ if (effectiveWorkdir) backendOverrides[WORKDIR_VIRTUAL_PATH] = new FilesystemBackend({
135
+ rootDir: executionWorkspace,
136
+ virtualMode: true
137
+ });
138
+ const workspaceSkillsPath = join(this.workspace, normalizedSkillsDirectory);
139
+ if (existsSync(workspaceSkillsPath)) backendOverrides[skillsVirtualPath] = new FilesystemBackend({
140
+ rootDir: workspaceSkillsPath,
141
+ virtualMode: true
142
+ });
88
143
  if (existsSync(bundledSkillsPath)) backendOverrides["/skills-bundled/"] = new FilesystemBackend({
89
144
  rootDir: bundledSkillsPath,
90
145
  virtualMode: true
91
146
  });
147
+ if (outputMount.virtualPath && outputMount.absolutePath) backendOverrides[outputMount.virtualPath] = new FilesystemBackend({
148
+ rootDir: outputMount.absolutePath,
149
+ virtualMode: true
150
+ });
92
151
  const standaloneAgent = createDeepAgent({
93
152
  systemPrompt: targetAgent.systemPrompt,
94
153
  tools: targetAgent.tools,
95
154
  model: targetAgent.model,
96
155
  backend: ()=>new CompositeBackend(new FilesystemBackend({
97
- rootDir: this.workspace,
156
+ rootDir: executionWorkspace,
98
157
  virtualMode: true
99
158
  }), backendOverrides),
100
159
  middleware: middleware,
@@ -103,7 +162,7 @@ class AgentInvoker {
103
162
  checkpointer: checkpointer
104
163
  });
105
164
  this.logger.debug("Agent created, sending message");
106
- const userContent = buildUserContent(prompt, attachments);
165
+ const userContent = buildUserContent(prompt, attachments, targetAgent.model);
107
166
  if (this.sessionManager && sessionId) {
108
167
  this.logger.debug(`Using streaming with session: ${sessionId}`);
109
168
  const stream = await standaloneAgent.streamEvents({
@@ -187,7 +246,7 @@ class AgentInvoker {
187
246
  this.loader = new AgentLoader(this.configDir, this.workspace, this.wingmanConfig);
188
247
  }
189
248
  }
190
- function buildUserContent(prompt, attachments) {
249
+ function buildUserContent(prompt, attachments, model) {
191
250
  const text = prompt?.trim() ?? "";
192
251
  if (!attachments || 0 === attachments.length) return text;
193
252
  const parts = [];
@@ -195,18 +254,32 @@ function buildUserContent(prompt, attachments) {
195
254
  type: "text",
196
255
  text
197
256
  });
198
- for (const attachment of attachments)if (attachment?.dataUrl) {
199
- if (isAudioAttachment(attachment)) {
200
- const audioPart = buildAudioPart(attachment);
201
- if (audioPart) parts.push(audioPart);
257
+ for (const attachment of attachments)if (attachment) {
258
+ if (isFileAttachment(attachment)) {
259
+ const nativePdfPart = buildNativePdfPart(attachment, model);
260
+ if (nativePdfPart) {
261
+ parts.push(nativePdfPart);
262
+ continue;
263
+ }
264
+ parts.push({
265
+ type: "text",
266
+ text: buildFileAttachmentText(attachment)
267
+ });
202
268
  continue;
203
269
  }
204
- parts.push({
205
- type: "image_url",
206
- image_url: {
207
- url: attachment.dataUrl
270
+ if (attachment.dataUrl) {
271
+ if (isAudioAttachment(attachment)) {
272
+ const audioPart = buildAudioPart(attachment);
273
+ if (audioPart) parts.push(audioPart);
274
+ continue;
208
275
  }
209
- });
276
+ parts.push({
277
+ type: "image_url",
278
+ image_url: {
279
+ url: attachment.dataUrl
280
+ }
281
+ });
282
+ }
210
283
  }
211
284
  if (0 === parts.length) {
212
285
  if (!text) throw new Error("Attachment payload is empty or invalid.");
@@ -214,12 +287,93 @@ function buildUserContent(prompt, attachments) {
214
287
  }
215
288
  return parts;
216
289
  }
290
+ function supportsNativePdfInputs(model) {
291
+ if (!model || "object" != typeof model) return false;
292
+ try {
293
+ const profile = model.profile;
294
+ if (!profile || "object" != typeof profile) return false;
295
+ return true === profile.pdfInputs;
296
+ } catch {
297
+ return false;
298
+ }
299
+ }
300
+ function isPdfName(name) {
301
+ return (name || "").trim().toLowerCase().endsWith(".pdf");
302
+ }
303
+ function resolveFileMimeType(attachment) {
304
+ const direct = attachment.mimeType?.trim().toLowerCase();
305
+ if (direct) return direct.split(";")[0] || "";
306
+ const parsed = parseDataUrl(attachment.dataUrl);
307
+ return (parsed.mimeType || "").trim().toLowerCase().split(";")[0] || "";
308
+ }
309
+ function buildFileMetadata(attachment, defaultName) {
310
+ const filename = attachment.name?.trim() || defaultName;
311
+ return {
312
+ filename,
313
+ name: filename,
314
+ title: filename
315
+ };
316
+ }
317
+ function buildNativePdfPart(attachment, model) {
318
+ if (!supportsNativePdfInputs(model)) return null;
319
+ const mimeType = resolveFileMimeType(attachment);
320
+ const isPdf = "application/pdf" === mimeType || isPdfName(attachment.name);
321
+ if (!isPdf) return null;
322
+ const metadata = buildFileMetadata(attachment, "document.pdf");
323
+ const parsed = parseDataUrl(attachment.dataUrl);
324
+ const useResponsesInputFile = shouldUseResponsesInputFile(model);
325
+ if (useResponsesInputFile) {
326
+ if (parsed.data) {
327
+ const fileDataMime = parsed.mimeType || mimeType || "application/pdf";
328
+ return {
329
+ type: "input_file",
330
+ file_data: `data:${fileDataMime};base64,${parsed.data}`,
331
+ filename: metadata.filename
332
+ };
333
+ }
334
+ const fileDataUrl = attachment.dataUrl?.trim();
335
+ if (!fileDataUrl || !fileDataUrl.startsWith("data:")) return null;
336
+ return {
337
+ type: "input_file",
338
+ file_data: fileDataUrl,
339
+ filename: metadata.filename
340
+ };
341
+ }
342
+ if (parsed.data) return {
343
+ type: "file",
344
+ source_type: "base64",
345
+ mime_type: parsed.mimeType || mimeType || "application/pdf",
346
+ data: parsed.data,
347
+ metadata
348
+ };
349
+ const url = attachment.dataUrl?.trim();
350
+ if (!url || !url.startsWith("data:")) return null;
351
+ return {
352
+ type: "file",
353
+ source_type: "url",
354
+ mime_type: mimeType || "application/pdf",
355
+ url,
356
+ metadata
357
+ };
358
+ }
359
+ function shouldUseResponsesInputFile(model) {
360
+ if (!model || "object" != typeof model) return false;
361
+ try {
362
+ const flag = model.useResponsesApi;
363
+ if ("boolean" == typeof flag) return flag;
364
+ } catch {}
365
+ return false;
366
+ }
217
367
  function isAudioAttachment(attachment) {
218
368
  if ("audio" === attachment.kind) return true;
219
369
  if (attachment.mimeType?.startsWith("audio/")) return true;
220
370
  if (attachment.dataUrl?.startsWith("data:audio/")) return true;
221
371
  return false;
222
372
  }
373
+ function isFileAttachment(attachment) {
374
+ if ("file" === attachment.kind) return true;
375
+ return "string" == typeof attachment.textContent;
376
+ }
223
377
  function buildAudioPart(attachment) {
224
378
  const parsed = parseDataUrl(attachment.dataUrl);
225
379
  const mimeType = attachment.mimeType || parsed.mimeType;
@@ -245,14 +399,36 @@ function parseDataUrl(dataUrl) {
245
399
  data: match[2]
246
400
  };
247
401
  }
402
+ function buildFileAttachmentText(attachment) {
403
+ const name = attachment.name?.trim() || "file";
404
+ const mime = attachment.mimeType?.trim();
405
+ const sizeLabel = "number" == typeof attachment.size && attachment.size >= 0 ? `, ${attachment.size} bytes` : "";
406
+ const meta = mime || sizeLabel ? ` (${[
407
+ mime,
408
+ sizeLabel.replace(/^, /, "")
409
+ ].filter(Boolean).join(", ")})` : "";
410
+ const header = `[Attached file: ${name}${meta}]`;
411
+ const text = attachment.textContent?.trim();
412
+ if (!text) return `${header}\n[No extractable text content provided.]`;
413
+ return `${header}\n${text}`;
414
+ }
248
415
  function buildAttachmentPreview(attachments) {
416
+ let hasFile = false;
249
417
  let hasAudio = false;
250
418
  let hasImage = false;
251
- for (const attachment of attachments)if (isAudioAttachment(attachment)) hasAudio = true;
252
- else hasImage = true;
419
+ for (const attachment of attachments){
420
+ if (isFileAttachment(attachment)) {
421
+ hasFile = true;
422
+ continue;
423
+ }
424
+ if (isAudioAttachment(attachment)) hasAudio = true;
425
+ else hasImage = true;
426
+ }
427
+ if (hasFile && (hasAudio || hasImage)) return "[files and media]";
253
428
  if (hasAudio && hasImage) return "[attachments]";
429
+ if (hasFile) return "[file]";
254
430
  if (hasAudio) return "[audio]";
255
431
  if (hasImage) return "[image]";
256
432
  return "";
257
433
  }
258
- export { AgentInvoker, buildUserContent };
434
+ export { AgentInvoker, OUTPUT_VIRTUAL_PATH, WORKDIR_VIRTUAL_PATH, buildUserContent, resolveExecutionWorkspace, resolveExternalOutputMount, toWorkspaceAliasVirtualPath };
@@ -480,10 +480,15 @@ function extractAttachments(blocks) {
480
480
  continue;
481
481
  }
482
482
  const audioUrl = extractAudioUrl(block);
483
- if (audioUrl) attachments.push({
484
- kind: "audio",
485
- dataUrl: audioUrl
486
- });
483
+ if (audioUrl) {
484
+ attachments.push({
485
+ kind: "audio",
486
+ dataUrl: audioUrl
487
+ });
488
+ continue;
489
+ }
490
+ const fileAttachment = extractFileAttachment(block);
491
+ if (fileAttachment) attachments.push(fileAttachment);
487
492
  }
488
493
  return attachments;
489
494
  }
@@ -538,6 +543,90 @@ function extractAudioUrl(block) {
538
543
  }
539
544
  return null;
540
545
  }
546
+ function parseDataUrlMime(dataUrl) {
547
+ if ("string" != typeof dataUrl || !dataUrl.startsWith("data:")) return;
548
+ const match = dataUrl.match(/^data:([^;,]+)[;,]/i);
549
+ return match?.[1];
550
+ }
551
+ function extractString(...values) {
552
+ for (const value of values)if ("string" == typeof value && value.trim().length > 0) return value.trim();
553
+ }
554
+ function extractFileAttachment(block) {
555
+ if (!block || "object" != typeof block) return null;
556
+ if ("file" === block.type) {
557
+ const sourceType = block.source_type || block.sourceType;
558
+ const metadata = block.metadata && "object" == typeof block.metadata ? block.metadata : {};
559
+ const name = extractString(block.name, block.filename, metadata.filename, metadata.name, metadata.title);
560
+ const declaredMime = extractString(block.mime_type, block.mimeType, block.media_type, block.mediaType);
561
+ if ("base64" === sourceType && "string" == typeof block.data) {
562
+ const mimeType = declaredMime || "application/octet-stream";
563
+ return {
564
+ kind: "file",
565
+ dataUrl: `data:${mimeType};base64,${block.data}`,
566
+ name,
567
+ mimeType
568
+ };
569
+ }
570
+ if ("url" === sourceType && "string" == typeof block.url) {
571
+ const mimeType = declaredMime || parseDataUrlMime(block.url);
572
+ return {
573
+ kind: "file",
574
+ dataUrl: block.url,
575
+ name,
576
+ mimeType
577
+ };
578
+ }
579
+ const openAiFile = block.file;
580
+ if (openAiFile && "object" == typeof openAiFile) {
581
+ const fileData = extractString(openAiFile.file_data, openAiFile.data);
582
+ const fileUrl = extractString(openAiFile.file_url, openAiFile.url);
583
+ const fileName = extractString(openAiFile.filename, name);
584
+ if (fileData) return {
585
+ kind: "file",
586
+ dataUrl: fileData,
587
+ name: fileName,
588
+ mimeType: parseDataUrlMime(fileData)
589
+ };
590
+ if (fileUrl) return {
591
+ kind: "file",
592
+ dataUrl: fileUrl,
593
+ name: fileName,
594
+ mimeType: parseDataUrlMime(fileUrl)
595
+ };
596
+ }
597
+ }
598
+ if ("input_file" === block.type) {
599
+ const dataUrl = extractString(block.file_data, block.file_url);
600
+ if (!dataUrl) return null;
601
+ return {
602
+ kind: "file",
603
+ dataUrl,
604
+ name: extractString(block.filename),
605
+ mimeType: parseDataUrlMime(dataUrl)
606
+ };
607
+ }
608
+ if ("document" === block.type && block.source && "object" == typeof block.source) {
609
+ const source = block.source;
610
+ const sourceType = extractString(source.type);
611
+ const name = extractString(block.title);
612
+ if ("base64" === sourceType && "string" == typeof source.data) {
613
+ const mimeType = extractString(source.media_type, "application/pdf");
614
+ return {
615
+ kind: "file",
616
+ dataUrl: `data:${mimeType};base64,${source.data}`,
617
+ name,
618
+ mimeType
619
+ };
620
+ }
621
+ if ("url" === sourceType && "string" == typeof source.url) return {
622
+ kind: "file",
623
+ dataUrl: source.url,
624
+ name,
625
+ mimeType: parseDataUrlMime(source.url)
626
+ };
627
+ }
628
+ return null;
629
+ }
541
630
  function resolveAudioMimeType(format) {
542
631
  const normalized = format.toLowerCase();
543
632
  switch(normalized){
@@ -30,7 +30,7 @@ export interface SessionMessage {
30
30
  uiTextFallback?: string;
31
31
  }
32
32
  export interface SessionAttachment {
33
- kind: "image" | "audio";
33
+ kind: "image" | "audio" | "file";
34
34
  dataUrl: string;
35
35
  name?: string;
36
36
  mimeType?: string;
@@ -447,10 +447,15 @@ function extractAttachments(blocks) {
447
447
  continue;
448
448
  }
449
449
  const audioUrl = extractAudioUrl(block);
450
- if (audioUrl) attachments.push({
451
- kind: "audio",
452
- dataUrl: audioUrl
453
- });
450
+ if (audioUrl) {
451
+ attachments.push({
452
+ kind: "audio",
453
+ dataUrl: audioUrl
454
+ });
455
+ continue;
456
+ }
457
+ const fileAttachment = extractFileAttachment(block);
458
+ if (fileAttachment) attachments.push(fileAttachment);
454
459
  }
455
460
  return attachments;
456
461
  }
@@ -505,6 +510,90 @@ function extractAudioUrl(block) {
505
510
  }
506
511
  return null;
507
512
  }
513
+ function parseDataUrlMime(dataUrl) {
514
+ if ("string" != typeof dataUrl || !dataUrl.startsWith("data:")) return;
515
+ const match = dataUrl.match(/^data:([^;,]+)[;,]/i);
516
+ return match?.[1];
517
+ }
518
+ function extractString(...values) {
519
+ for (const value of values)if ("string" == typeof value && value.trim().length > 0) return value.trim();
520
+ }
521
+ function extractFileAttachment(block) {
522
+ if (!block || "object" != typeof block) return null;
523
+ if ("file" === block.type) {
524
+ const sourceType = block.source_type || block.sourceType;
525
+ const metadata = block.metadata && "object" == typeof block.metadata ? block.metadata : {};
526
+ const name = extractString(block.name, block.filename, metadata.filename, metadata.name, metadata.title);
527
+ const declaredMime = extractString(block.mime_type, block.mimeType, block.media_type, block.mediaType);
528
+ if ("base64" === sourceType && "string" == typeof block.data) {
529
+ const mimeType = declaredMime || "application/octet-stream";
530
+ return {
531
+ kind: "file",
532
+ dataUrl: `data:${mimeType};base64,${block.data}`,
533
+ name,
534
+ mimeType
535
+ };
536
+ }
537
+ if ("url" === sourceType && "string" == typeof block.url) {
538
+ const mimeType = declaredMime || parseDataUrlMime(block.url);
539
+ return {
540
+ kind: "file",
541
+ dataUrl: block.url,
542
+ name,
543
+ mimeType
544
+ };
545
+ }
546
+ const openAiFile = block.file;
547
+ if (openAiFile && "object" == typeof openAiFile) {
548
+ const fileData = extractString(openAiFile.file_data, openAiFile.data);
549
+ const fileUrl = extractString(openAiFile.file_url, openAiFile.url);
550
+ const fileName = extractString(openAiFile.filename, name);
551
+ if (fileData) return {
552
+ kind: "file",
553
+ dataUrl: fileData,
554
+ name: fileName,
555
+ mimeType: parseDataUrlMime(fileData)
556
+ };
557
+ if (fileUrl) return {
558
+ kind: "file",
559
+ dataUrl: fileUrl,
560
+ name: fileName,
561
+ mimeType: parseDataUrlMime(fileUrl)
562
+ };
563
+ }
564
+ }
565
+ if ("input_file" === block.type) {
566
+ const dataUrl = extractString(block.file_data, block.file_url);
567
+ if (!dataUrl) return null;
568
+ return {
569
+ kind: "file",
570
+ dataUrl,
571
+ name: extractString(block.filename),
572
+ mimeType: parseDataUrlMime(dataUrl)
573
+ };
574
+ }
575
+ if ("document" === block.type && block.source && "object" == typeof block.source) {
576
+ const source = block.source;
577
+ const sourceType = extractString(source.type);
578
+ const name = extractString(block.title);
579
+ if ("base64" === sourceType && "string" == typeof source.data) {
580
+ const mimeType = extractString(source.media_type, "application/pdf");
581
+ return {
582
+ kind: "file",
583
+ dataUrl: `data:${mimeType};base64,${source.data}`,
584
+ name,
585
+ mimeType
586
+ };
587
+ }
588
+ if ("url" === sourceType && "string" == typeof source.url) return {
589
+ kind: "file",
590
+ dataUrl: source.url,
591
+ name,
592
+ mimeType: parseDataUrlMime(source.url)
593
+ };
594
+ }
595
+ return null;
596
+ }
508
597
  function resolveAudioMimeType(format) {
509
598
  const normalized = format.toLowerCase();
510
599
  switch(normalized){