@@ -253,27 +253,217 @@ function hermesToolTitle(name: string): string {
253253 return titles [ name ] ?? name . replaceAll ( "_" , " " ) . replace ( / ^ \w / , ( value ) => value . toUpperCase ( ) ) ;
254254}
255255
256- function hermesToolItemType (
257- name : string ,
258- ) :
256+ type HermesToolItemType =
259257 | "command_execution"
260258 | "file_change"
261259 | "mcp_tool_call"
260+ | "dynamic_tool_call"
262261 | "collab_agent_tool_call"
263262 | "web_search"
264- | "image_view" {
263+ | "image_view" ;
264+
265+ type HermesToolStatus = "inProgress" | "completed" | "failed" ;
266+
267+ function asRecord ( value : unknown ) : Readonly < Record < string , unknown > > | undefined {
268+ return typeof value === "object" && value !== null && ! Array . isArray ( value )
269+ ? ( value as Readonly < Record < string , unknown > > )
270+ : undefined ;
271+ }
272+
273+ function stringField (
274+ record : Readonly < Record < string , unknown > > | undefined ,
275+ ...keys : ReadonlyArray < string >
276+ ) : string | undefined {
277+ for ( const key of keys ) {
278+ const value = record ?. [ key ] ;
279+ if ( typeof value === "string" && value . trim ( ) ) return value ;
280+ }
281+ return undefined ;
282+ }
283+
284+ function hermesMcpIdentity (
285+ name : string ,
286+ input : unknown ,
287+ ) : { readonly server : string ; readonly tool : string } | undefined {
288+ const record = asRecord ( input ) ;
289+ const explicitServer = stringField ( record , "server" ) ;
290+ const explicitTool = stringField ( record , "tool" , "operation" ) ;
291+ if ( explicitServer && explicitTool ) return { server : explicitServer , tool : explicitTool } ;
292+
293+ const parts = name . split ( "__" ) . filter ( Boolean ) ;
294+ if ( parts [ 0 ] ?. toLowerCase ( ) === "mcp" && parts . length >= 3 ) {
295+ return {
296+ server : parts [ 1 ] ?? "hermes" ,
297+ tool : parts . slice ( 2 ) . join ( "__" ) ,
298+ } ;
299+ }
300+ if ( name . toLowerCase ( ) . startsWith ( "mcp" ) ) {
301+ return { server : explicitServer ?? "hermes" , tool : explicitTool ?? name } ;
302+ }
303+ return undefined ;
304+ }
305+
306+ function hermesToolItemType ( name : string , input : unknown ) : HermesToolItemType {
265307 if ( name === "terminal" || name === "execute_code" || name === "process" ) {
266308 return "command_execution" ;
267309 }
268- if ( name === "write_file" || name === "patch" ) return "file_change" ;
310+ if (
311+ name === "write_file" ||
312+ name === "patch" ||
313+ name === "apply_patch" ||
314+ name === "edit_file" ||
315+ name === "delete_file" ||
316+ name === "move_file"
317+ ) {
318+ return "file_change" ;
319+ }
320+ if ( hermesMcpIdentity ( name , input ) ) return "mcp_tool_call" ;
269321 if ( name === "web_search" || name === "web_extract" ) return "web_search" ;
270322 if ( name === "computer_use" || name === "image_view" ) return "image_view" ;
271323 if ( name === "delegate_task" ) return "collab_agent_tool_call" ;
272- return "mcp_tool_call " ;
324+ return "dynamic_tool_call " ;
273325}
274326
275- function toolResultData ( result : unknown ) : unknown {
276- return typeof result === "string" ? { output : result } : result ;
327+ function resultText ( result : unknown ) : string | undefined {
328+ if ( typeof result === "string" ) return result ;
329+ if ( result === undefined ) return undefined ;
330+ try {
331+ return JSON . stringify ( result ) ;
332+ } catch {
333+ return String ( result ) ;
334+ }
335+ }
336+
337+ function patchChanges ( input : unknown ) : ReadonlyArray < {
338+ readonly path : string ;
339+ readonly kind :
340+ | { readonly type : "add" }
341+ | { readonly type : "delete" }
342+ | { readonly type : "update" } ;
343+ readonly diff : string ;
344+ } > {
345+ const record = asRecord ( input ) ;
346+ const patch = stringField ( record , "patch" , "diff" ) ?? "" ;
347+ const changes : Array < {
348+ readonly path : string ;
349+ readonly kind :
350+ | { readonly type : "add" }
351+ | { readonly type : "delete" }
352+ | { readonly type : "update" } ;
353+ readonly diff : string ;
354+ } > = [ ] ;
355+ const seen = new Set < string > ( ) ;
356+ const add = ( path : string , kind : { readonly type : "add" | "delete" | "update" } ) : void => {
357+ const normalized = path . trim ( ) ;
358+ if ( ! normalized || normalized === "/dev/null" || seen . has ( normalized ) ) return ;
359+ seen . add ( normalized ) ;
360+ changes . push ( { path : normalized , kind, diff : patch } ) ;
361+ } ;
362+
363+ for ( const line of patch . split ( / \r ? \n / u) ) {
364+ const marker = / ^ \* \* \* ( A d d | U p d a t e | D e l e t e ) F i l e : ( .+ ) $ / u. exec ( line ) ;
365+ if ( marker ) {
366+ const operation = marker [ 1 ] ;
367+ const path = marker [ 2 ] ;
368+ if ( path ) {
369+ add ( path , {
370+ type : operation === "Add" ? "add" : operation === "Delete" ? "delete" : "update" ,
371+ } ) ;
372+ }
373+ continue ;
374+ }
375+ const unified = / ^ \+ \+ \+ (?: b \/ ) ? ( .+ ) $ / u. exec ( line ) ;
376+ if ( unified ?. [ 1 ] ) add ( unified [ 1 ] , { type : "update" } ) ;
377+ }
378+
379+ if ( changes . length === 0 ) {
380+ const path = stringField ( record , "path" , "file_path" , "filePath" , "filename" ) ;
381+ if ( path ) add ( path , { type : "update" } ) ;
382+ }
383+ return changes ;
384+ }
385+
386+ function hermesToolItem (
387+ itemType : HermesToolItemType ,
388+ callback : Extract < HermesBridgeHermesToT3Request , { type : "tool.started" | "tool.completed" } > ,
389+ status : HermesToolStatus ,
390+ ) : unknown {
391+ const input = asRecord ( callback . input ) ?? { } ;
392+ const completed = callback . type === "tool.completed" ;
393+ const output = completed ? resultText ( callback . result ) : undefined ;
394+ switch ( itemType ) {
395+ case "command_execution" : {
396+ const command =
397+ stringField ( input , "command" , "code" , "process" ) ?? hermesToolTitle ( callback . name ) ;
398+ return {
399+ type : "commandExecution" ,
400+ id : callback . toolCallId ,
401+ command,
402+ cwd : stringField ( input , "cwd" , "workdir" ) ?? "" ,
403+ commandActions : [ { type : "unknown" , command } ] ,
404+ status,
405+ ...( output !== undefined ? { aggregatedOutput : output } : { } ) ,
406+ } ;
407+ }
408+ case "file_change" :
409+ return {
410+ type : "fileChange" ,
411+ id : callback . toolCallId ,
412+ status,
413+ changes : patchChanges ( input ) ,
414+ } ;
415+ case "mcp_tool_call" : {
416+ const identity = hermesMcpIdentity ( callback . name , input ) ?? {
417+ server : "hermes" ,
418+ tool : callback . name ,
419+ } ;
420+ return {
421+ type : "mcpToolCall" ,
422+ id : callback . toolCallId ,
423+ server : identity . server ,
424+ tool : identity . tool ,
425+ arguments : input ,
426+ status,
427+ ...( completed ? { result : callback . result } : { } ) ,
428+ } ;
429+ }
430+ case "web_search" :
431+ return {
432+ type : "webSearch" ,
433+ id : callback . toolCallId ,
434+ query : stringField ( input , "query" , "q" , "url" ) ?? "" ,
435+ ...( completed ? { results : callback . result } : { } ) ,
436+ } ;
437+ case "image_view" :
438+ return {
439+ type : "imageView" ,
440+ id : callback . toolCallId ,
441+ path : stringField ( input , "path" , "image_path" ) ?? "" ,
442+ } ;
443+ case "collab_agent_tool_call" :
444+ return {
445+ type : "collabAgentToolCall" ,
446+ id : callback . toolCallId ,
447+ tool : callback . name ,
448+ arguments : input ,
449+ status,
450+ ...( completed ? { result : callback . result } : { } ) ,
451+ } ;
452+ case "dynamic_tool_call" :
453+ return {
454+ type : "dynamicToolCall" ,
455+ id : callback . toolCallId ,
456+ tool : callback . name ,
457+ arguments : input ,
458+ status,
459+ ...( completed
460+ ? {
461+ success : ! callback . isError ,
462+ result : callback . result ,
463+ }
464+ : { } ) ,
465+ } ;
466+ }
277467}
278468
279469function toolResultDetail ( result : unknown ) : string | undefined {
@@ -528,28 +718,26 @@ export const makeHermesAdapter = Effect.fn("makeHermesAdapter")(function* (
528718 threadId ,
529719 callback . type === "tool.started" ? "tool-started" : "tool-completed" ,
530720 ) ;
531- const itemType = hermesToolItemType ( callback . name ) ;
532- const title = hermesToolTitle ( callback . name ) ;
533- const item = {
534- toolCallId : callback . toolCallId ,
535- name : callback . name ,
536- input : callback . input ,
537- ...( callback . type === "tool.completed" ? { result : toolResultData ( callback . result ) } : { } ) ,
538- } ;
721+ const itemType = hermesToolItemType ( callback . name , callback . input ) ;
722+ const status : HermesToolStatus =
723+ callback . type === "tool.started" ? "inProgress" : callback . isError ? "failed" : "completed" ;
724+ const mcpIdentity =
725+ itemType === "mcp_tool_call" ? hermesMcpIdentity ( callback . name , callback . input ) : undefined ;
726+ const title = mcpIdentity
727+ ? `${ mcpIdentity . server } · ${ mcpIdentity . tool } `
728+ : hermesToolTitle ( callback . name ) ;
729+ const item = hermesToolItem ( itemType , callback , status ) ;
539730 const detail =
540- callback . type === "tool.completed" ? toolResultDetail ( callback . result ) : undefined ;
731+ callback . type === "tool.completed" && itemType !== "file_change"
732+ ? toolResultDetail ( callback . result )
733+ : undefined ;
541734 yield * publish ( {
542735 ...base ,
543736 type : callback . type === "tool.started" ? "item.started" : "item.completed" ,
544737 itemId : RuntimeItemId . make ( `hermes-tool:${ callback . toolCallId } ` ) ,
545738 payload : {
546739 itemType,
547- status :
548- callback . type === "tool.started"
549- ? "inProgress"
550- : callback . isError
551- ? "failed"
552- : "completed" ,
740+ status,
553741 title,
554742 ...( detail ? { detail } : { } ) ,
555743 data : {
0 commit comments