55
66use anyhow:: { Result , bail} ;
77use log:: { debug, error, info, warn} ;
8+ use serde:: de:: DeserializeOwned ;
89use serde_json:: Value ;
910use std:: collections:: HashMap ;
1011use std:: path:: Path ;
@@ -213,6 +214,35 @@ pub async fn execute_safe_outputs(
213214 Ok ( results)
214215}
215216
217+ /// Parse a JSON entry as `T` and run it through `execute_sanitized`.
218+ ///
219+ /// This is the common path for all tools that implement `Executor`. The tool name
220+ /// is used only for the error message so callers don't have to repeat it.
221+ async fn dispatch_tool < T > (
222+ tool_name : & str ,
223+ entry : & Value ,
224+ ctx : & ExecutionContext ,
225+ ) -> Result < ExecutionResult >
226+ where
227+ T : DeserializeOwned + Executor ,
228+ {
229+ debug ! ( "Parsing {} payload" , tool_name) ;
230+ let mut output: T = serde_json:: from_value ( entry. clone ( ) )
231+ . map_err ( |e| anyhow:: anyhow!( "Failed to parse {}: {}" , tool_name, e) ) ?;
232+ output. execute_sanitized ( ctx) . await
233+ }
234+
235+ macro_rules! dispatch_executor_tools {
236+ ( $tool_name: expr, $entry: expr, $ctx: expr, { $( $name: literal => $ty: ty) ,+ $( , ) ? } ) => {
237+ match $tool_name {
238+ $(
239+ $name => dispatch_tool:: <$ty>( $tool_name, $entry, $ctx) . await . map( Some ) ,
240+ ) +
241+ _ => Ok ( None ) ,
242+ }
243+ } ;
244+ }
245+
216246/// Execute a single safe output entry, returning the tool name and result
217247pub async fn execute_safe_output (
218248 entry : & Value ,
@@ -226,191 +256,49 @@ pub async fn execute_safe_output(
226256
227257 debug ! ( "Dispatching tool: {}" , tool_name) ;
228258
229- // Dispatch based on tool name
230- let result = match tool_name {
231- "create-work-item" => {
232- debug ! ( "Parsing create-work-item payload" ) ;
233- let mut output: CreateWorkItemResult = serde_json:: from_value ( entry. clone ( ) )
234- . map_err ( |e| anyhow:: anyhow!( "Failed to parse create-work-item: {}" , e) ) ?;
235- debug ! (
236- "create-work-item: title='{}', description length={}" ,
237- output. title,
238- output. description. len( )
239- ) ;
240- output. execute_sanitized ( ctx) . await ?
241- }
242- "comment-on-work-item" => {
243- debug ! ( "Parsing comment-on-work-item payload" ) ;
244- let mut output: CommentOnWorkItemResult = serde_json:: from_value ( entry. clone ( ) )
245- . map_err ( |e| anyhow:: anyhow!( "Failed to parse comment-on-work-item: {}" , e) ) ?;
246- debug ! (
247- "comment-on-work-item: work_item_id={}, body length={}" ,
248- output. work_item_id,
249- output. body. len( )
250- ) ;
251- output. execute_sanitized ( ctx) . await ?
252- }
253- "update-work-item" => {
254- debug ! ( "Parsing update-work-item payload" ) ;
255- let mut output: UpdateWorkItemResult = serde_json:: from_value ( entry. clone ( ) )
256- . map_err ( |e| anyhow:: anyhow!( "Failed to parse update-work-item: {}" , e) ) ?;
257- debug ! ( "update-work-item: id={}" , output. id) ;
258- output. execute_sanitized ( ctx) . await ?
259- }
260- "create-pull-request" => {
261- debug ! ( "Parsing create-pull-request payload" ) ;
262- let mut output: CreatePrResult = serde_json:: from_value ( entry. clone ( ) )
263- . map_err ( |e| anyhow:: anyhow!( "Failed to parse create-pull-request: {}" , e) ) ?;
264- debug ! (
265- "create-pull-request: title='{}', repo='{}', branch='{}', patch='{}'" ,
266- output. title, output. repository, output. source_branch, output. patch_file
267- ) ;
268- output. execute_sanitized ( ctx) . await ?
269- }
270- "update-wiki-page" => {
271- debug ! ( "Parsing update-wiki-page payload" ) ;
272- let mut output: UpdateWikiPageResult = serde_json:: from_value ( entry. clone ( ) )
273- . map_err ( |e| anyhow:: anyhow!( "Failed to parse update-wiki-page: {}" , e) ) ?;
274- debug ! (
275- "update-wiki-page: path='{}', content length={}" ,
276- output. path,
277- output. content. len( )
278- ) ;
279- output. execute_sanitized ( ctx) . await ?
280- }
281- "create-wiki-page" => {
282- debug ! ( "Parsing create-wiki-page payload" ) ;
283- let mut output: CreateWikiPageResult = serde_json:: from_value ( entry. clone ( ) )
284- . map_err ( |e| anyhow:: anyhow!( "Failed to parse create-wiki-page: {}" , e) ) ?;
285- debug ! (
286- "create-wiki-page: path='{}', content length={}" ,
287- output. path,
288- output. content. len( )
289- ) ;
290- output. execute_sanitized ( ctx) . await ?
291- }
292- "add-pr-comment" => {
293- debug ! ( "Parsing add-pr-comment payload" ) ;
294- let mut output: AddPrCommentResult = serde_json:: from_value ( entry. clone ( ) )
295- . map_err ( |e| anyhow:: anyhow!( "Failed to parse add-pr-comment: {}" , e) ) ?;
296- debug ! (
297- "add-pr-comment: pr_id={}, content length={}" ,
298- output. pull_request_id,
299- output. content. len( )
300- ) ;
301- output. execute_sanitized ( ctx) . await ?
302- }
303- "link-work-items" => {
304- debug ! ( "Parsing link-work-items payload" ) ;
305- let mut output: LinkWorkItemsResult = serde_json:: from_value ( entry. clone ( ) )
306- . map_err ( |e| anyhow:: anyhow!( "Failed to parse link-work-items: {}" , e) ) ?;
307- debug ! (
308- "link-work-items: source={}, target={}, type={}" ,
309- output. source_id, output. target_id, output. link_type
310- ) ;
311- output. execute_sanitized ( ctx) . await ?
312- }
313- "queue-build" => {
314- debug ! ( "Parsing queue-build payload" ) ;
315- let mut output: QueueBuildResult = serde_json:: from_value ( entry. clone ( ) )
316- . map_err ( |e| anyhow:: anyhow!( "Failed to parse queue-build: {}" , e) ) ?;
317- debug ! ( "queue-build: pipeline_id={}" , output. pipeline_id) ;
318- output. execute_sanitized ( ctx) . await ?
319- }
320- "create-git-tag" => {
321- debug ! ( "Parsing create-git-tag payload" ) ;
322- let mut output: CreateGitTagResult = serde_json:: from_value ( entry. clone ( ) )
323- . map_err ( |e| anyhow:: anyhow!( "Failed to parse create-git-tag: {}" , e) ) ?;
324- debug ! ( "create-git-tag: tag_name='{}'" , output. tag_name) ;
325- output. execute_sanitized ( ctx) . await ?
326- }
327- "add-build-tag" => {
328- debug ! ( "Parsing add-build-tag payload" ) ;
329- let mut output: AddBuildTagResult = serde_json:: from_value ( entry. clone ( ) )
330- . map_err ( |e| anyhow:: anyhow!( "Failed to parse add-build-tag: {}" , e) ) ?;
331- debug ! ( "add-build-tag: build_id={}, tag='{}'" , output. build_id, output. tag) ;
332- output. execute_sanitized ( ctx) . await ?
333- }
334- "create-branch" => {
335- debug ! ( "Parsing create-branch payload" ) ;
336- let mut output: CreateBranchResult = serde_json:: from_value ( entry. clone ( ) )
337- . map_err ( |e| anyhow:: anyhow!( "Failed to parse create-branch: {}" , e) ) ?;
338- debug ! ( "create-branch: branch_name='{}'" , output. branch_name) ;
339- output. execute_sanitized ( ctx) . await ?
340- }
341- "update-pr" => {
342- debug ! ( "Parsing update-pr payload" ) ;
343- let mut output: UpdatePrResult = serde_json:: from_value ( entry. clone ( ) )
344- . map_err ( |e| anyhow:: anyhow!( "Failed to parse update-pr: {}" , e) ) ?;
345- debug ! (
346- "update-pr: pr_id={}, operation='{}'" ,
347- output. pull_request_id, output. operation
348- ) ;
349- output. execute_sanitized ( ctx) . await ?
350- }
351- "upload-attachment" => {
352- debug ! ( "Parsing upload-attachment payload" ) ;
353- let mut output: UploadAttachmentResult = serde_json:: from_value ( entry. clone ( ) )
354- . map_err ( |e| anyhow:: anyhow!( "Failed to parse upload-attachment: {}" , e) ) ?;
355- debug ! (
356- "upload-attachment: work_item_id={}, file_path='{}'" ,
357- output. work_item_id, output. file_path
358- ) ;
359- output. execute_sanitized ( ctx) . await ?
360- }
361- "submit-pr-review" => {
362- debug ! ( "Parsing submit-pr-review payload" ) ;
363- let mut output: SubmitPrReviewResult = serde_json:: from_value ( entry. clone ( ) )
364- . map_err ( |e| anyhow:: anyhow!( "Failed to parse submit-pr-review: {}" , e) ) ?;
365- debug ! (
366- "submit-pr-review: pr_id={}, event='{}'" ,
367- output. pull_request_id, output. event
368- ) ;
369- output. execute_sanitized ( ctx) . await ?
370- }
371- "reply-to-pr-review-comment" => {
372- debug ! ( "Parsing reply-to-pr-review-comment payload" ) ;
373- let mut output: ReplyToPrCommentResult = serde_json:: from_value ( entry. clone ( ) )
374- . map_err ( |e| anyhow:: anyhow!( "Failed to parse reply-to-pr-review-comment: {}" , e) ) ?;
375- debug ! (
376- "reply-to-pr-review-comment: pr_id={}, thread_id={}" ,
377- output. pull_request_id, output. thread_id
378- ) ;
379- output. execute_sanitized ( ctx) . await ?
380- }
381- "resolve-pr-thread" => {
382- debug ! ( "Parsing resolve-pr-thread payload" ) ;
383- let mut output: ResolvePrThreadResult = serde_json:: from_value ( entry. clone ( ) )
384- . map_err ( |e| anyhow:: anyhow!( "Failed to parse resolve-pr-thread: {}" , e) ) ?;
385- debug ! (
386- "resolve-pr-thread: pr_id={}, thread_id={}, status='{}'" ,
387- output. pull_request_id, output. thread_id, output. status
388- ) ;
389- output. execute_sanitized ( ctx) . await ?
390- }
391- "noop" => {
392- debug ! ( "Skipping noop entry" ) ;
393- ExecutionResult :: success ( "Skipped informational output: noop" )
259+ // Dispatch based on tool name. All standard tools go through `dispatch_tool` which
260+ // handles deserialization and sanitized execution uniformly. Special cases (informational
261+ // outputs and report-incomplete) are handled inline.
262+ let result = if let Some ( dispatched_result) = dispatch_executor_tools ! ( tool_name, entry, ctx, {
263+ "create-work-item" => CreateWorkItemResult ,
264+ "comment-on-work-item" => CommentOnWorkItemResult ,
265+ "update-work-item" => UpdateWorkItemResult ,
266+ "create-pull-request" => CreatePrResult ,
267+ "update-wiki-page" => UpdateWikiPageResult ,
268+ "create-wiki-page" => CreateWikiPageResult ,
269+ "add-pr-comment" => AddPrCommentResult ,
270+ "link-work-items" => LinkWorkItemsResult ,
271+ "queue-build" => QueueBuildResult ,
272+ "create-git-tag" => CreateGitTagResult ,
273+ "add-build-tag" => AddBuildTagResult ,
274+ "create-branch" => CreateBranchResult ,
275+ "update-pr" => UpdatePrResult ,
276+ "upload-attachment" => UploadAttachmentResult ,
277+ "submit-pr-review" => SubmitPrReviewResult ,
278+ "reply-to-pr-review-comment" => ReplyToPrCommentResult ,
279+ "resolve-pr-thread" => ResolvePrThreadResult ,
280+ } ) ? {
281+ dispatched_result
282+ } else {
283+ match tool_name {
284+ // Informational outputs — no side effects, always succeed
285+ "noop" | "missing-tool" | "missing-data" => {
286+ debug ! ( "Skipping informational entry: {}" , tool_name) ;
287+ ExecutionResult :: success ( format ! ( "Skipped informational output: {}" , tool_name) )
394288 }
289+ // report-incomplete does not implement Executor; Stage 3 surfaces its reason as a failure
395290 "report-incomplete" => {
396291 let mut output: ReportIncompleteResult = serde_json:: from_value ( entry. clone ( ) )
397292 . map_err ( |e| anyhow:: anyhow!( "Failed to parse report-incomplete: {}" , e) ) ?;
398293 output. sanitize_content_fields ( ) ;
399294 debug ! ( "report-incomplete: {}" , output. reason) ;
400295 ExecutionResult :: failure ( format ! ( "Agent reported task incomplete: {}" , output. reason) )
401296 }
402- "missing-tool" => {
403- debug ! ( "Skipping missing-tool entry" ) ;
404- ExecutionResult :: success ( "Skipped informational output: missing-tool" )
405- }
406- "missing-data" => {
407- debug ! ( "Skipping missing-data entry" ) ;
408- ExecutionResult :: success ( "Skipped informational output: missing-data" )
409- }
410297 other => {
411298 error ! ( "Unknown tool type: {}" , other) ;
412299 bail ! ( "Unknown tool type: {}. No executor registered." , other)
413300 }
301+ }
414302 } ;
415303
416304 Ok ( ( tool_name. to_string ( ) , result) )
0 commit comments