( params: Record<string, unknown>, context: ExecutionContext )
| 199 | } |
| 200 | |
| 201 | export async function executeVfsRead( |
| 202 | params: Record<string, unknown>, |
| 203 | context: ExecutionContext |
| 204 | ): Promise<ToolCallResult> { |
| 205 | const path = params.path as string | undefined |
| 206 | if (!path) { |
| 207 | return { success: false, error: "Missing required parameter 'path'" } |
| 208 | } |
| 209 | |
| 210 | const workspaceId = context.workspaceId |
| 211 | if (!workspaceId) { |
| 212 | return { success: false, error: 'No workspace context available' } |
| 213 | } |
| 214 | |
| 215 | try { |
| 216 | const parseOptionalNumber = (value: unknown): number | undefined => { |
| 217 | if (typeof value === 'number' && Number.isFinite(value)) return value |
| 218 | if (typeof value === 'string' && value.trim() !== '') { |
| 219 | const parsed = Number.parseInt(value, 10) |
| 220 | return Number.isFinite(parsed) ? parsed : undefined |
| 221 | } |
| 222 | return undefined |
| 223 | } |
| 224 | const offset = parseOptionalNumber(params.offset) |
| 225 | const limit = parseOptionalNumber(params.limit) |
| 226 | const applyWindow = <T extends { content: string; totalLines: number }>(result: T): T => { |
| 227 | if (offset === undefined && limit === undefined) return result |
| 228 | const lines = result.content.split('\n') |
| 229 | const start = Math.max(0, Math.min(result.totalLines, offset ?? 0)) |
| 230 | const endRaw = limit !== undefined ? start + Math.max(0, limit) : result.totalLines |
| 231 | const end = Math.max(start, Math.min(result.totalLines, endRaw)) |
| 232 | return { |
| 233 | ...result, |
| 234 | content: lines.slice(start, end).join('\n'), |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | // Handle chat-scoped uploads via the uploads/ virtual prefix. |
| 239 | // Uploads are flat and have no metadata/content split like files/ — the upload |
| 240 | // IS the first path segment after uploads/. Any trailing segment (e.g. a |
| 241 | // /content suffix added out of habit) is ignored so the read resolves either way. |
| 242 | if (path.startsWith('uploads/')) { |
| 243 | if (!context.chatId) { |
| 244 | return { success: false, error: 'No chat context available for uploads/' } |
| 245 | } |
| 246 | const filename = path.slice('uploads/'.length).split('/')[0] |
| 247 | const uploadResult = await readChatUpload(filename, context.chatId) |
| 248 | if (uploadResult) { |
| 249 | const isAttachment = hasModelAttachment(uploadResult) |
| 250 | if ( |
| 251 | !isAttachment && |
| 252 | (isOversizedReadPlaceholder(uploadResult.content) || |
| 253 | serializedResultSize(uploadResult) > TOOL_RESULT_MAX_INLINE_CHARS) |
| 254 | ) { |
| 255 | logger.warn('Upload read result too large', { |
| 256 | path, |
| 257 | hasAttachment: isAttachment, |
| 258 | contentLength: uploadResult.content.length, |
no test coverage detected