( toolName: string, params: Record<string, unknown> | undefined, result: ToolCallResult, context: ExecutionContext )
| 199 | } |
| 200 | |
| 201 | export async function maybeWriteOutputToFile( |
| 202 | toolName: string, |
| 203 | params: Record<string, unknown> | undefined, |
| 204 | result: ToolCallResult, |
| 205 | context: ExecutionContext |
| 206 | ): Promise<ToolCallResult> { |
| 207 | if (!result.success || !result.output) return result |
| 208 | if (!OUTPUT_PATH_TOOLS.has(toolName)) return result |
| 209 | if (!context.workspaceId || !context.userId) return result |
| 210 | |
| 211 | const outputFiles = getOutputFileDeclarations(params).filter((file) => !file.sandboxPath) |
| 212 | if (outputFiles.length === 0) return result |
| 213 | |
| 214 | const outputObject = |
| 215 | result.output && typeof result.output === 'object' && !Array.isArray(result.output) |
| 216 | ? (result.output as Record<string, unknown>) |
| 217 | : undefined |
| 218 | const resultObject = |
| 219 | outputObject?.result && |
| 220 | typeof outputObject.result === 'object' && |
| 221 | !Array.isArray(outputObject.result) |
| 222 | ? (outputObject.result as Record<string, unknown>) |
| 223 | : undefined |
| 224 | if (Array.isArray(resultObject?.files)) { |
| 225 | logger.warn('Skipping returned-value output write because sandbox export response is active', { |
| 226 | toolName, |
| 227 | outputCount: outputFiles.length, |
| 228 | }) |
| 229 | return result |
| 230 | } |
| 231 | |
| 232 | const denied = denyOutputWriteWithoutWritePermission(context) |
| 233 | if (denied) return denied |
| 234 | |
| 235 | // Only span the actual write path (where we upload to storage). Fast |
| 236 | // no-op returns above don't need a span — they'd just pad the trace |
| 237 | // with empty work. |
| 238 | return withCopilotSpan( |
| 239 | TraceSpan.CopilotToolsWriteOutputFile, |
| 240 | { |
| 241 | [TraceAttr.ToolName]: toolName, |
| 242 | [TraceAttr.WorkspaceId]: context.workspaceId, |
| 243 | }, |
| 244 | async (span) => { |
| 245 | try { |
| 246 | const writtenFiles = [] |
| 247 | for (const outputFile of outputFiles) { |
| 248 | const fileName = normalizeOutputWorkspaceFileName( |
| 249 | outputFile.formatPath ?? outputFile.path |
| 250 | ) |
| 251 | const format = resolveOutputFormat(fileName, outputFile.format) |
| 252 | const content = serializeOutputForFile(result.output, format) |
| 253 | const contentType = outputFile.mimeType || FORMAT_TO_CONTENT_TYPE[format] |
| 254 | const buffer = Buffer.from(content, 'utf-8') |
| 255 | |
| 256 | if (context.abortSignal?.aborted) { |
| 257 | throw new Error('Request aborted before tool mutation could be applied') |
| 258 | } |
no test coverage detected