| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:js_interop'; |
| |
| import 'package:genkit/genkit.dart'; |
| import 'package:jaspr/server.dart'; |
| import 'package:schemantic/schemantic.dart'; |
| import 'package:web/web.dart' as web; |
| |
| import '../../models/agent_tool_event.dart'; |
| import '../../models/agent_tool_id.dart'; |
| |
| export '../../models/agent_tool_metadata.dart'; |
| |
| /// Base class defining a structured tool that can be executed by the VibePad AI agent. |
| /// |
| /// Implements input/output schema typing using [SchemanticType] and maps results |
| /// to standard [AgentToolEvent] telemetry. |
| abstract class AgentTool<I, O> { |
| const AgentTool(); |
| |
| /// The type-safe identifier of this tool. |
| AgentToolId get toolId; |
| |
| /// The unique registration name of the tool (e.g. 'readFile'). |
| String get name => toolId.name; |
| |
| /// User-facing or model-facing explanation of what the tool does. |
| String get description; |
| |
| /// Schema type definitions for validating the tool's input structure. |
| SchemanticType<I> get inputSchema; |
| |
| /// Schema type definitions for validating the tool's output structure. |
| SchemanticType<O> get outputSchema; |
| |
| /// Executes the core logic of the tool with the given strongly-typed input. |
| Future<O> run(I input); |
| |
| String _formatLog(dynamic object) { |
| try { |
| final json = (object as dynamic).toJson(); |
| return const JsonEncoder.withIndent(' ').convert(json); |
| } catch (_) { |
| try { |
| return const JsonEncoder.withIndent(' ').convert(object); |
| } catch (_) { |
| return object.toString(); |
| } |
| } |
| } |
| |
| void _logToConsole(String message, {bool isError = false}) { |
| if (identical(0, 0.0)) { |
| if (isError) { |
| web.console.error(message.toJS); |
| } else { |
| web.console.log(message.toJS); |
| } |
| } else { |
| print(message); |
| } |
| } |
| |
| /// Converts this custom tool definition into a standard Genkit [Tool], |
| /// registering callbacks to track execution events for telemetry and UI visualization. |
| Tool<I, O> toGenkitTool({required void Function(AgentToolEvent) onToolEvent}) { |
| return Tool<I, O>( |
| name: name, |
| description: description, |
| inputSchema: inputSchema, |
| outputSchema: outputSchema, |
| fn: (input, _) async { |
| try { |
| onToolEvent(mapStartToToolEvent(input)); |
| if (kDebugMode) { |
| _logToConsole('[VibePad Agent Tool] $name input:'); |
| _logToConsole(_formatLog(input)); |
| } |
| |
| final output = await run(input); |
| |
| onToolEvent(mapToToolEvent(input, output)); |
| if (kDebugMode) { |
| _logToConsole('[VibePad Agent Tool] $name output:'); |
| _logToConsole(_formatLog(output)); |
| } |
| |
| return output; |
| } catch (error) { |
| if (kDebugMode) { |
| _logToConsole('[VibePad Agent Tool] $name failed: $error', isError: true); |
| } |
| final errEvent = createToolEvent( |
| status: ToolStatus.error, |
| summary: 'Tool execution failed', |
| errorMessage: error.toString(), |
| ); |
| onToolEvent(errEvent); |
| rethrow; |
| } |
| }, |
| ); |
| } |
| |
| /// Extracts the target file path from the tool's input, if applicable. |
| /// |
| /// Override this in file-based tools to avoid overriding [mapStartToToolEvent] |
| /// just to set `targetPath`. |
| String? targetPathForInput(I input) => null; |
| |
| /// Maps the tool's input to a starting [AgentToolEvent] for logging. |
| AgentToolEvent mapStartToToolEvent(I input) { |
| return createToolEvent( |
| status: ToolStatus.running, |
| summary: name, |
| targetPath: targetPathForInput(input), |
| ); |
| } |
| |
| /// Helper to create a standardized [AgentToolEvent] pre-populated with this tool's |
| /// identity. |
| AgentToolEvent createToolEvent({ |
| required ToolStatus status, |
| required String summary, |
| bool skipped = false, |
| String? subAction, |
| String? targetPath, |
| int? startLine, |
| int? endLine, |
| WriteFileChangeKind? writeFileChangeKind, |
| bool requiresPreviewRebuild = false, |
| int? patchReplacementCount, |
| String? errorMessage, |
| String? callId, |
| String? screenshotBase64, |
| }) { |
| final requestPart = Zone.current[ToolRequestPart] as ToolRequestPart?; |
| final resolvedCallId = callId ?? requestPart?.toolRequest.ref; |
| return AgentToolEvent( |
| toolId: toolId, |
| status: status, |
| summary: summary, |
| skipped: skipped, |
| subAction: subAction, |
| targetPath: targetPath, |
| startLine: startLine, |
| endLine: endLine, |
| writeFileChangeKind: writeFileChangeKind, |
| requiresPreviewRebuild: requiresPreviewRebuild, |
| patchReplacementCount: patchReplacementCount, |
| errorMessage: errorMessage, |
| callId: resolvedCallId, |
| screenshotBase64: screenshotBase64, |
| ); |
| } |
| |
| /// Maps the tool's input and execution results to an [AgentToolEvent] for logging. |
| AgentToolEvent mapToToolEvent(I input, O output); |
| } |