@@ -11,7 +11,7 @@ import {
1111 BetaBashCodeExecutionToolResultBlockParam ,
1212} from "@anthropic-ai/sdk/resources/beta.mjs" ;
1313import { toAcpNotifications , ToolUseCache , Logger } from "../acp-agent.js" ;
14- import { toolUpdateFromToolResult , createPostToolUseHook } from "../tools.js" ;
14+ import { toolUpdateFromToolResult , createPostToolUseHook , registerHookCallback } from "../tools.js" ;
1515
1616describe ( "rawOutput in tool call updates" , ( ) => {
1717 const mockClient = { } as AgentSideConnection ;
@@ -1234,4 +1234,173 @@ describe("Bash terminal output", () => {
12341234 expect ( hookMeta . terminal_exit ) . toBeUndefined ( ) ;
12351235 } ) ;
12361236 } ) ;
1237+
1238+ describe ( "PostToolUse hook for unregistered tool_use IDs" , ( ) => {
1239+ // The SDK fires PostToolUse for subagent tools (Task) before
1240+ // registerHookCallback() sees the tool_use block, and for
1241+ // server_tool_use (WebSearch) which never registers a callback.
1242+ // Since this is expected behavior we choose not to log it, as
1243+ // the error output can be confusing for clients.
1244+
1245+ it ( "should not call logger.error for Task (subagent) tool" , async ( ) => {
1246+ const errors : string [ ] = [ ] ;
1247+ const spyLogger : Logger = {
1248+ log : ( ) => { } ,
1249+ error : ( ...args : any [ ] ) => {
1250+ errors . push ( args . map ( String ) . join ( " " ) ) ;
1251+ } ,
1252+ } ;
1253+
1254+ const hook = createPostToolUseHook ( spyLogger ) ;
1255+ await hook (
1256+ {
1257+ hook_event_name : "PostToolUse" ,
1258+ tool_name : "Task" ,
1259+ tool_input : { prompt : "count exported functions" } ,
1260+ tool_response : "42 exported functions" ,
1261+ tool_use_id : "toolu_subagent_never_registered" ,
1262+ session_id : "test-session" ,
1263+ transcript_path : "/tmp/test" ,
1264+ cwd : "/tmp" ,
1265+ } ,
1266+ "toolu_subagent_never_registered" ,
1267+ { signal : AbortSignal . abort ( ) } ,
1268+ ) ;
1269+
1270+ expect ( errors ) . toHaveLength ( 0 ) ;
1271+ } ) ;
1272+
1273+ it ( "should not call logger.error for WebSearch (server_tool_use) tool" , async ( ) => {
1274+ const errors : string [ ] = [ ] ;
1275+ const spyLogger : Logger = {
1276+ log : ( ) => { } ,
1277+ error : ( ...args : any [ ] ) => {
1278+ errors . push ( args . map ( String ) . join ( " " ) ) ;
1279+ } ,
1280+ } ;
1281+
1282+ const hook = createPostToolUseHook ( spyLogger ) ;
1283+ await hook (
1284+ {
1285+ hook_event_name : "PostToolUse" ,
1286+ tool_name : "WebSearch" ,
1287+ tool_input : { query : "test" } ,
1288+ tool_response : "results" ,
1289+ tool_use_id : "toolu_server_tool_never_registered" ,
1290+ session_id : "test-session" ,
1291+ transcript_path : "/tmp/test" ,
1292+ cwd : "/tmp" ,
1293+ } ,
1294+ "toolu_server_tool_never_registered" ,
1295+ { signal : AbortSignal . abort ( ) } ,
1296+ ) ;
1297+
1298+ expect ( errors ) . toHaveLength ( 0 ) ;
1299+ } ) ;
1300+
1301+ it ( "should still call logger.error for genuinely unexpected unregistered tools" , async ( ) => {
1302+ const errors : string [ ] = [ ] ;
1303+ const spyLogger : Logger = {
1304+ log : ( ) => { } ,
1305+ error : ( ...args : any [ ] ) => {
1306+ errors . push ( args . map ( String ) . join ( " " ) ) ;
1307+ } ,
1308+ } ;
1309+
1310+ const hook = createPostToolUseHook ( spyLogger ) ;
1311+ await hook (
1312+ {
1313+ hook_event_name : "PostToolUse" ,
1314+ tool_name : "Read" ,
1315+ tool_input : { file_path : "/tmp/test.txt" } ,
1316+ tool_response : "file contents" ,
1317+ tool_use_id : "toolu_unexpected_missing" ,
1318+ session_id : "test-session" ,
1319+ transcript_path : "/tmp/test" ,
1320+ cwd : "/tmp" ,
1321+ } ,
1322+ "toolu_unexpected_missing" ,
1323+ { signal : AbortSignal . abort ( ) } ,
1324+ ) ;
1325+
1326+ expect ( errors ) . toHaveLength ( 1 ) ;
1327+ expect ( errors [ 0 ] ) . toContain ( "toolu_unexpected_missing" ) ;
1328+ expect ( errors [ 0 ] ) . toContain ( "Read" ) ;
1329+ } ) ;
1330+
1331+ it ( "should return { continue: true } for any unregistered tool_use ID" , async ( ) => {
1332+ const hook = createPostToolUseHook ( mockLogger ) ;
1333+ const result = await hook (
1334+ {
1335+ hook_event_name : "PostToolUse" ,
1336+ tool_name : "WebSearch" ,
1337+ tool_input : { query : "test" } ,
1338+ tool_response : "results" ,
1339+ tool_use_id : "toolu_server_tool_never_registered_2" ,
1340+ session_id : "test-session" ,
1341+ transcript_path : "/tmp/test" ,
1342+ cwd : "/tmp" ,
1343+ } ,
1344+ "toolu_server_tool_never_registered_2" ,
1345+ { signal : AbortSignal . abort ( ) } ,
1346+ ) ;
1347+
1348+ expect ( result ) . toEqual ( { continue : true } ) ;
1349+ } ) ;
1350+
1351+ it ( "should clean up the toolUseCallbacks entry even when unregistered" , async ( ) => {
1352+ // Pre-register a callback for a different ID to verify the
1353+ // unregistered path doesn't interfere with registered ones.
1354+ const hookUpdates : any [ ] = [ ] ;
1355+ const mockClientWithUpdate = {
1356+ sessionUpdate : async ( notification : any ) => {
1357+ hookUpdates . push ( notification ) ;
1358+ } ,
1359+ } as unknown as AgentSideConnection ;
1360+
1361+ registerHookCallback ( "toolu_registered_2" , {
1362+ onPostToolUseHook : async ( _id , _input , _response ) => {
1363+ await mockClientWithUpdate . sessionUpdate ( { update : { registered : true } } ) ;
1364+ } ,
1365+ } ) ;
1366+
1367+ const hook = createPostToolUseHook ( mockLogger ) ;
1368+
1369+ // Fire for unregistered Task — should silently ignore
1370+ await hook (
1371+ {
1372+ hook_event_name : "PostToolUse" ,
1373+ tool_name : "Task" ,
1374+ tool_input : { } ,
1375+ tool_response : "" ,
1376+ tool_use_id : "toolu_unregistered_2" ,
1377+ session_id : "test-session" ,
1378+ transcript_path : "/tmp/test" ,
1379+ cwd : "/tmp" ,
1380+ } ,
1381+ "toolu_unregistered_2" ,
1382+ { signal : AbortSignal . abort ( ) } ,
1383+ ) ;
1384+
1385+ expect ( hookUpdates ) . toHaveLength ( 0 ) ;
1386+
1387+ // Fire for registered ID — should work normally
1388+ await hook (
1389+ {
1390+ hook_event_name : "PostToolUse" ,
1391+ tool_name : "Read" ,
1392+ tool_input : { } ,
1393+ tool_response : "" ,
1394+ tool_use_id : "toolu_registered_2" ,
1395+ session_id : "test-session" ,
1396+ transcript_path : "/tmp/test" ,
1397+ cwd : "/tmp" ,
1398+ } ,
1399+ "toolu_registered_2" ,
1400+ { signal : AbortSignal . abort ( ) } ,
1401+ ) ;
1402+
1403+ expect ( hookUpdates ) . toHaveLength ( 1 ) ;
1404+ } ) ;
1405+ } ) ;
12371406} ) ;
0 commit comments