11import { existsSync } from 'node:fs' ;
22import { mkdir , rm , writeFile } from 'node:fs/promises' ;
33import { join , resolve } from 'node:path' ;
4- import type { ImageAttachment } from './channel.js' ;
4+ import type { FileAttachment , ImageAttachment } from './channel.js' ;
55import { type AgentEngine , createEngine , type DiscoveredEngine , discoverEngines , type StreamEvent } from './engine.js' ;
66import {
77 appendHistory ,
@@ -147,6 +147,8 @@ export interface ChatOpts {
147147 sessionKey ?: string ;
148148 /** Images attached to the user message. Saved to disk and referenced in the prompt. */
149149 images ?: ImageAttachment [ ] ;
150+ /** Files (non-image) attached to the user message. Saved to disk and referenced in the prompt. */
151+ files ?: FileAttachment [ ] ;
150152}
151153
152154export interface Assistant {
@@ -221,6 +223,7 @@ export function createAssistant(opts: CreateAssistantOpts): Assistant {
221223 sessionKey : string ,
222224 isRetry : boolean ,
223225 images ?: ImageAttachment [ ] ,
226+ files ?: FileAttachment [ ] ,
224227 ) : AsyncIterable < StreamEvent > {
225228 const { config, skills } = await ensureReady ( dir ) ;
226229
@@ -278,12 +281,30 @@ export function createAssistant(opts: CreateAssistantOpts): Assistant {
278281 }
279282 }
280283
284+ // Save attached files to workspace temp dir so the agent can read them
285+ const filePaths : string [ ] = [ ] ;
286+ const fileDir = join ( dir , '.golem' , 'files' ) ;
287+ if ( files && files . length > 0 ) {
288+ await mkdir ( fileDir , { recursive : true } ) ;
289+ for ( const file of files ) {
290+ const filePath = join ( fileDir , file . fileName ) ;
291+ await writeFile ( filePath , file . data ) ;
292+ filePaths . push ( filePath ) ;
293+ }
294+ }
295+
281296 // Append image file paths to the message so the agent can read/view them
282297 if ( imagePaths . length > 0 ) {
283298 const imageRefs = imagePaths . map ( ( p ) => p ) . join ( '\n' ) ;
284299 finalMessage += `\n\n[User attached ${ imagePaths . length } image(s). File paths:\n${ imageRefs } \nPlease read/view these files to see the images.]` ;
285300 }
286301
302+ // Append file paths to the message so the agent can read them
303+ if ( filePaths . length > 0 ) {
304+ const fileRefs = filePaths . map ( ( p ) => p ) . join ( '\n' ) ;
305+ finalMessage += `\n\n[User attached ${ filePaths . length } file(s). File paths:\n${ fileRefs } \nPlease read these files to see the content.]` ;
306+ }
307+
287308 // Prune once per process
288309 if ( ! pruneDone ) {
289310 pruneDone = true ;
@@ -339,8 +360,8 @@ export function createAssistant(opts: CreateAssistantOpts): Assistant {
339360 }
340361 } finally {
341362 clearTimeout ( timer ) ;
342- // Clean up temp image files
343- for ( const p of imagePaths ) {
363+ // Clean up temp image and file attachments
364+ for ( const p of [ ... imagePaths , ... filePaths ] ) {
344365 rm ( p ) . catch ( ( ) => { } ) ;
345366 }
346367 }
@@ -416,7 +437,7 @@ export function createAssistant(opts: CreateAssistantOpts): Assistant {
416437 if ( isResumeFail ) {
417438 await clearSession ( dir , sessionKey ) ;
418439 yield { type : 'warning' as const , message : 'Session could not be resumed. Starting fresh conversation.' } ;
419- yield * doChat ( message , sessionKey , true , images ) ;
440+ yield * doChat ( message , sessionKey , true , images , files ) ;
420441 }
421442 }
422443 }
@@ -425,6 +446,7 @@ export function createAssistant(opts: CreateAssistantOpts): Assistant {
425446 message : string ,
426447 sessionKey : string ,
427448 images ?: ImageAttachment [ ] ,
449+ files ?: FileAttachment [ ] ,
428450 ) : AsyncIterable < StreamEvent > {
429451 // Rate limits use opts values directly — no file I/O before acquiring the mutex,
430452 // so same-key serialization order is preserved (first caller wins the lock).
@@ -455,7 +477,7 @@ export function createAssistant(opts: CreateAssistantOpts): Assistant {
455477 }
456478
457479 try {
458- yield * doChat ( message , sessionKey , false , images ) ;
480+ yield * doChat ( message , sessionKey , false , images , files ) ;
459481 } finally {
460482 activeChatCount -- ;
461483 mutex . release ( sessionKey ) ;
@@ -465,7 +487,7 @@ export function createAssistant(opts: CreateAssistantOpts): Assistant {
465487 return {
466488 chat ( message : string , chatOpts ?: ChatOpts ) : AsyncIterable < StreamEvent > {
467489 const key = chatOpts ?. sessionKey || DEFAULT_SESSION_KEY ;
468- return chatImpl ( message , key , chatOpts ?. images ) ;
490+ return chatImpl ( message , key , chatOpts ?. images , chatOpts ?. files ) ;
469491 } ,
470492
471493 async init ( initOpts : { engine : string ; name : string ; role ?: string } ) : Promise < void > {
0 commit comments