fix(core): Keep abandoned tool-call suspensions visible in AI Assistant thread history (#36737)

This commit is contained in:
oleg
2026-08-26 07:22:13 +00:00
committed by GitHub
parent 0dbff64652
commit 052c30eac3
12 changed files with 484 additions and 98 deletions
@@ -46,30 +46,30 @@
}
},
"status": 200,
"response": "{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_01EnqFT8EPmZWjHrWYgoh19p\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_01Uro8PcgjCZT5hFmxDqmt29\",\"name\":\"delete_file\",\"input\":{\"path\":\"/tmp/interim-test.txt\"},\"caller\":{\"type\":\"direct\"}}],\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":629,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":61,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}",
"response": "{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_011CeE6ytibZSyMKdtSdxJuT\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_01CGnHG2Mgi14kX9YptML9tR\",\"name\":\"delete_file\",\"input\":{\"path\":\"/tmp/interim-test.txt\"},\"caller\":{\"type\":\"direct\"}}],\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":629,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":61,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}",
"rawHeaders": {
"anthropic-ratelimit-input-tokens-limit": "10000000",
"anthropic-ratelimit-input-tokens-remaining": "10000000",
"anthropic-ratelimit-input-tokens-reset": "2026-07-03T09:47:07Z",
"anthropic-ratelimit-input-tokens-reset": "2026-08-20T13:47:07Z",
"anthropic-ratelimit-output-tokens-limit": "2000000",
"anthropic-ratelimit-output-tokens-remaining": "2000000",
"anthropic-ratelimit-output-tokens-reset": "2026-07-03T09:47:07Z",
"anthropic-ratelimit-output-tokens-reset": "2026-08-20T13:47:07Z",
"anthropic-ratelimit-requests-limit": "20000",
"anthropic-ratelimit-requests-remaining": "19999",
"anthropic-ratelimit-requests-reset": "2026-07-03T09:47:06Z",
"anthropic-ratelimit-requests-reset": "2026-08-20T13:47:06Z",
"anthropic-ratelimit-tokens-limit": "12000000",
"anthropic-ratelimit-tokens-remaining": "12000000",
"anthropic-ratelimit-tokens-reset": "2026-07-03T09:47:07Z",
"anthropic-ratelimit-tokens-reset": "2026-08-20T13:47:07Z",
"cf-cache-status": "DYNAMIC",
"cf-ray": "a154f7852daa2f95-VIE",
"cf-ray": "a2e1d9163d4abb94-PRG",
"connection": "keep-alive",
"content-security-policy": "default-src 'none'; frame-ancestors 'none'",
"content-type": "application/json",
"date": "Fri, 03 Jul 2026 09:47:07 GMT",
"request-id": "req_011CceurR4NhauetyZxvhpup",
"date": "Thu, 20 Aug 2026 13:47:07 GMT",
"request-id": "req_011CeE6ys5cygHXyVLtaV79K",
"server": "cloudflare",
"strict-transport-security": "max-age=31536000; includeSubDomains; preload",
"traceresponse": "00-1d7bd32e330bf09b94f07aac753ddd62-50c7378e7bb49636-01",
"traceresponse": "00-85861d820196f1f3917c4b11c6f381a4-49a1f4935e264fbf-01",
"vary": "Accept-Encoding",
"x-robots-tag": "none"
},
@@ -95,6 +95,33 @@
{
"type": "text",
"text": "Please delete /tmp/interim-test.txt"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_01CGnHG2Mgi14kX9YptML9tR",
"name": "delete_file",
"input": {
"path": "/tmp/interim-test.txt"
},
"caller": {
"type": "direct"
}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01CGnHG2Mgi14kX9YptML9tR",
"content": "INTERRUPTED — this delete_file call never completed. The user was shown a confirmation (\"Delete \"/tmp/interim-test.txt\"?\") and never answered it. Its action did NOT take effect: nothing was saved, executed, or applied by this call. Do not describe this action as done. If it is still needed, explain to the user that it requires their approval and what approving will do, then re-issue it once.",
"is_error": true
},
{
"type": "text",
@@ -105,30 +132,30 @@
]
},
"status": 200,
"response": "{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_01MmT2fvAKbKoS36sivZ87nv\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"I can't delete files from your system. I'm an AI assistant without access to your file system or the ability to execute commands on your computer.\\n\\nTo delete the file yourself, you can use:\\n- **Linux/Mac**: `rm /tmp/interim-test.txt`\\n- **Windows**: `del C:\\\\temp\\\\interim-test.txt`\\n\\nTo answer your math question: **1 + 1 = 2**\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":36,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":97,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}",
"response": "{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_011CeE6yyAF9B9x9tGAFpKFn\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"1 + 1 = 2\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":212,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":13,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}",
"rawHeaders": {
"anthropic-ratelimit-input-tokens-limit": "10000000",
"anthropic-ratelimit-input-tokens-remaining": "10000000",
"anthropic-ratelimit-input-tokens-reset": "2026-07-03T09:47:07Z",
"anthropic-ratelimit-input-tokens-reset": "2026-08-20T13:47:08Z",
"anthropic-ratelimit-output-tokens-limit": "2000000",
"anthropic-ratelimit-output-tokens-remaining": "2000000",
"anthropic-ratelimit-output-tokens-reset": "2026-07-03T09:47:08Z",
"anthropic-ratelimit-output-tokens-reset": "2026-08-20T13:47:08Z",
"anthropic-ratelimit-requests-limit": "20000",
"anthropic-ratelimit-requests-remaining": "19999",
"anthropic-ratelimit-requests-reset": "2026-07-03T09:47:07Z",
"anthropic-ratelimit-requests-reset": "2026-08-20T13:47:08Z",
"anthropic-ratelimit-tokens-limit": "12000000",
"anthropic-ratelimit-tokens-remaining": "12000000",
"anthropic-ratelimit-tokens-reset": "2026-07-03T09:47:07Z",
"anthropic-ratelimit-tokens-reset": "2026-08-20T13:47:08Z",
"cf-cache-status": "DYNAMIC",
"cf-ray": "a154f78ba8a42f95-VIE",
"cf-ray": "a2e1d91e6b8bbb94-PRG",
"connection": "keep-alive",
"content-security-policy": "default-src 'none'; frame-ancestors 'none'",
"content-type": "application/json",
"date": "Fri, 03 Jul 2026 09:47:08 GMT",
"request-id": "req_011CceurVNq5VeCS4aeUBgTx",
"date": "Thu, 20 Aug 2026 13:47:08 GMT",
"request-id": "req_011CeE6yxeEvTbEzMdSpFE9f",
"server": "cloudflare",
"strict-transport-security": "max-age=31536000; includeSubDomains; preload",
"traceresponse": "00-e264c50d366748e6df27a951c442956d-2dd42bce2c8f7ba3-01",
"traceresponse": "00-e8c9ac1407f70a985d587bd485676792-7e97abd826c26818-01",
"vary": "Accept-Encoding",
"x-robots-tag": "none"
},
@@ -162,7 +189,7 @@
"content": [
{
"type": "tool_use",
"id": "toolu_01Uro8PcgjCZT5hFmxDqmt29",
"id": "toolu_01CGnHG2Mgi14kX9YptML9tR",
"name": "delete_file",
"input": {
"path": "/tmp/interim-test.txt"
@@ -178,7 +205,7 @@
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01Uro8PcgjCZT5hFmxDqmt29",
"tool_use_id": "toolu_01CGnHG2Mgi14kX9YptML9tR",
"content": "{\"deleted\":true,\"path\":\"/tmp/interim-test.txt\"}"
}
]
@@ -207,30 +234,30 @@
}
},
"status": 200,
"response": "{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_017piTttFSVzw2R5YDHk4FRj\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Done. The file `/tmp/interim-test.txt` has been deleted.\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":718,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":20,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}",
"response": "{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_011CeE6z1m1dLDrpwKeW41Wo\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"The file `/tmp/interim-test.txt` has been successfully deleted.\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":718,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":19,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}",
"rawHeaders": {
"anthropic-ratelimit-input-tokens-limit": "10000000",
"anthropic-ratelimit-input-tokens-remaining": "10000000",
"anthropic-ratelimit-input-tokens-reset": "2026-07-03T09:47:09Z",
"anthropic-ratelimit-input-tokens-reset": "2026-08-20T13:47:09Z",
"anthropic-ratelimit-output-tokens-limit": "2000000",
"anthropic-ratelimit-output-tokens-remaining": "2000000",
"anthropic-ratelimit-output-tokens-reset": "2026-07-03T09:47:09Z",
"anthropic-ratelimit-output-tokens-reset": "2026-08-20T13:47:10Z",
"anthropic-ratelimit-requests-limit": "20000",
"anthropic-ratelimit-requests-remaining": "19999",
"anthropic-ratelimit-requests-reset": "2026-07-03T09:47:08Z",
"anthropic-ratelimit-requests-reset": "2026-08-20T13:47:08Z",
"anthropic-ratelimit-tokens-limit": "12000000",
"anthropic-ratelimit-tokens-remaining": "12000000",
"anthropic-ratelimit-tokens-reset": "2026-07-03T09:47:09Z",
"anthropic-ratelimit-tokens-reset": "2026-08-20T13:47:09Z",
"cf-cache-status": "DYNAMIC",
"cf-ray": "a154f793b86e2f95-VIE",
"cf-ray": "a2e1d9226f0cbb94-PRG",
"connection": "keep-alive",
"content-security-policy": "default-src 'none'; frame-ancestors 'none'",
"content-type": "application/json",
"date": "Fri, 03 Jul 2026 09:47:09 GMT",
"request-id": "req_011Cceuratie1LZarVJwEhi9",
"date": "Thu, 20 Aug 2026 13:47:10 GMT",
"request-id": "req_011CeE6z1MTSYiWHpmf41RJY",
"server": "cloudflare",
"strict-transport-security": "max-age=31536000; includeSubDomains; preload",
"traceresponse": "00-c6b192b6cc3982fdcd2b61a74f332396-9270fc23470f0b1a-01",
"traceresponse": "00-b4ee5f81bdcd05ebd86bf058ee408dc2-6c0f7158ee2700ed-01",
"vary": "Accept-Encoding",
"x-robots-tag": "none"
},
@@ -46,30 +46,30 @@
}
},
"status": 200,
"response": "{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_01PAKgMWTmRgjHRK3ak6LCtQ\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_01GQeR8FTAmiKSVej51UTLjH\",\"name\":\"delete_file\",\"input\":{\"path\":\"/tmp/order-test.txt\"},\"caller\":{\"type\":\"direct\"}}],\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":628,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":61,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}",
"response": "{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_011CeE6zLuwds7KqLD8WmGWT\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_019mUUAatkj38qZAt6KzskKE\",\"name\":\"delete_file\",\"input\":{\"path\":\"/tmp/order-test.txt\"},\"caller\":{\"type\":\"direct\"}}],\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":628,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":61,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}",
"rawHeaders": {
"anthropic-ratelimit-input-tokens-limit": "10000000",
"anthropic-ratelimit-input-tokens-remaining": "10000000",
"anthropic-ratelimit-input-tokens-reset": "2026-07-03T09:47:13Z",
"anthropic-ratelimit-input-tokens-reset": "2026-08-20T13:47:13Z",
"anthropic-ratelimit-output-tokens-limit": "2000000",
"anthropic-ratelimit-output-tokens-remaining": "2000000",
"anthropic-ratelimit-output-tokens-reset": "2026-07-03T09:47:13Z",
"anthropic-ratelimit-output-tokens-reset": "2026-08-20T13:47:13Z",
"anthropic-ratelimit-requests-limit": "20000",
"anthropic-ratelimit-requests-remaining": "19999",
"anthropic-ratelimit-requests-reset": "2026-07-03T09:47:12Z",
"anthropic-ratelimit-requests-reset": "2026-08-20T13:47:13Z",
"anthropic-ratelimit-tokens-limit": "12000000",
"anthropic-ratelimit-tokens-remaining": "12000000",
"anthropic-ratelimit-tokens-reset": "2026-07-03T09:47:13Z",
"anthropic-ratelimit-tokens-reset": "2026-08-20T13:47:13Z",
"cf-cache-status": "DYNAMIC",
"cf-ray": "a154f7ab59ab2f95-VIE",
"cf-ray": "a2e1d93e4cc9bb94-PRG",
"connection": "keep-alive",
"content-security-policy": "default-src 'none'; frame-ancestors 'none'",
"content-type": "application/json",
"date": "Fri, 03 Jul 2026 09:47:13 GMT",
"request-id": "req_011Cceurs6J3Hi618SWaanik",
"date": "Thu, 20 Aug 2026 13:47:13 GMT",
"request-id": "req_011CeE6zLUQECSHDZ4Pamd2R",
"server": "cloudflare",
"strict-transport-security": "max-age=31536000; includeSubDomains; preload",
"traceresponse": "00-1018d5a7613260b4899eada070e88d00-9b4ca326b046fbf2-01",
"traceresponse": "00-f9239d3b2a5b10bb36be146c4ba8ec6f-7e26229d67ad8f88-01",
"vary": "Accept-Encoding",
"x-robots-tag": "none"
},
@@ -95,6 +95,33 @@
{
"type": "text",
"text": "Delete /tmp/order-test.txt"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_019mUUAatkj38qZAt6KzskKE",
"name": "delete_file",
"input": {
"path": "/tmp/order-test.txt"
},
"caller": {
"type": "direct"
}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_019mUUAatkj38qZAt6KzskKE",
"content": "INTERRUPTED — this delete_file call never completed. The user was shown a confirmation (\"Delete \"/tmp/order-test.txt\"?\") and never answered it. Its action did NOT take effect: nothing was saved, executed, or applied by this call. Do not describe this action as done. If it is still needed, explain to the user that it requires their approval and what approving will do, then re-issue it once.",
"is_error": true
},
{
"type": "text",
@@ -105,30 +132,30 @@
]
},
"status": 200,
"response": "{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_01YLL8CSJZBgYsLkXQSWNKCQ\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"I can't actually execute commands on your system. I can't delete files or access your filesystem.\\n\\nIf you want to delete that file, you can run:\\n\\n```bash\\nrm /tmp/order-test.txt\\n```\\n\\nHi! 👋\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":24,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":57,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}",
"response": "{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_011CeE6zQCtBM8T7tSEL7SGg\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Hi! 👋\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":199,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":8,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}",
"rawHeaders": {
"anthropic-ratelimit-input-tokens-limit": "10000000",
"anthropic-ratelimit-input-tokens-remaining": "10000000",
"anthropic-ratelimit-input-tokens-reset": "2026-07-03T09:47:13Z",
"anthropic-ratelimit-input-tokens-reset": "2026-08-20T13:47:14Z",
"anthropic-ratelimit-output-tokens-limit": "2000000",
"anthropic-ratelimit-output-tokens-remaining": "2000000",
"anthropic-ratelimit-output-tokens-reset": "2026-07-03T09:47:14Z",
"anthropic-ratelimit-output-tokens-reset": "2026-08-20T13:47:14Z",
"anthropic-ratelimit-requests-limit": "20000",
"anthropic-ratelimit-requests-remaining": "19999",
"anthropic-ratelimit-requests-reset": "2026-07-03T09:47:13Z",
"anthropic-ratelimit-requests-reset": "2026-08-20T13:47:13Z",
"anthropic-ratelimit-tokens-limit": "12000000",
"anthropic-ratelimit-tokens-remaining": "12000000",
"anthropic-ratelimit-tokens-reset": "2026-07-03T09:47:13Z",
"anthropic-ratelimit-tokens-reset": "2026-08-20T13:47:14Z",
"cf-cache-status": "DYNAMIC",
"cf-ray": "a154f7b0d8e02f95-VIE",
"cf-ray": "a2e1d94319d5bb94-PRG",
"connection": "keep-alive",
"content-security-policy": "default-src 'none'; frame-ancestors 'none'",
"content-type": "application/json",
"date": "Fri, 03 Jul 2026 09:47:14 GMT",
"request-id": "req_011Cceurvo3NtBmtL4dwVT1t",
"date": "Thu, 20 Aug 2026 13:47:14 GMT",
"request-id": "req_011CeE6zPjrNAdVmZHDna22u",
"server": "cloudflare",
"strict-transport-security": "max-age=31536000; includeSubDomains; preload",
"traceresponse": "00-1c4015f4ddb3ee68df66fdba1a2bd642-43f23952779c8045-01",
"traceresponse": "00-539542eb755e977ee5f3e3a6da757e6e-de12a4df504895cd-01",
"vary": "Accept-Encoding",
"x-robots-tag": "none"
},
@@ -162,7 +189,7 @@
"content": [
{
"type": "tool_use",
"id": "toolu_01GQeR8FTAmiKSVej51UTLjH",
"id": "toolu_019mUUAatkj38qZAt6KzskKE",
"name": "delete_file",
"input": {
"path": "/tmp/order-test.txt"
@@ -178,7 +205,7 @@
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01GQeR8FTAmiKSVej51UTLjH",
"tool_use_id": "toolu_019mUUAatkj38qZAt6KzskKE",
"content": "{\"deleted\":true,\"path\":\"/tmp/order-test.txt\"}"
}
]
@@ -207,30 +234,30 @@
}
},
"status": 200,
"response": "{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_01SyCT4wuK36YHQLeCSbXD14\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Done. The file `/tmp/order-test.txt` has been deleted.\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":717,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":20,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}",
"response": "{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_011CeE6zTx7CB4A1dFQvfce9\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"File deleted successfully.\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":717,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":7,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}",
"rawHeaders": {
"anthropic-ratelimit-input-tokens-limit": "10000000",
"anthropic-ratelimit-input-tokens-remaining": "10000000",
"anthropic-ratelimit-input-tokens-reset": "2026-07-03T09:47:15Z",
"anthropic-ratelimit-input-tokens-reset": "2026-08-20T13:47:15Z",
"anthropic-ratelimit-output-tokens-limit": "2000000",
"anthropic-ratelimit-output-tokens-remaining": "2000000",
"anthropic-ratelimit-output-tokens-reset": "2026-07-03T09:47:15Z",
"anthropic-ratelimit-output-tokens-reset": "2026-08-20T13:47:15Z",
"anthropic-ratelimit-requests-limit": "20000",
"anthropic-ratelimit-requests-remaining": "19999",
"anthropic-ratelimit-requests-reset": "2026-07-03T09:47:14Z",
"anthropic-ratelimit-requests-reset": "2026-08-20T13:47:14Z",
"anthropic-ratelimit-tokens-limit": "12000000",
"anthropic-ratelimit-tokens-remaining": "12000000",
"anthropic-ratelimit-tokens-reset": "2026-07-03T09:47:15Z",
"anthropic-ratelimit-tokens-reset": "2026-08-20T13:47:15Z",
"cf-cache-status": "DYNAMIC",
"cf-ray": "a154f7b84db52f95-VIE",
"cf-ray": "a2e1d947addcbb94-PRG",
"connection": "keep-alive",
"content-security-policy": "default-src 'none'; frame-ancestors 'none'",
"content-type": "application/json",
"date": "Fri, 03 Jul 2026 09:47:15 GMT",
"request-id": "req_011Cceus2159kBBXhfqdCWx3",
"date": "Thu, 20 Aug 2026 13:47:15 GMT",
"request-id": "req_011CeE6zSxaN8jj2pVZkyeJv",
"server": "cloudflare",
"strict-transport-security": "max-age=31536000; includeSubDomains; preload",
"traceresponse": "00-40fc288030ad26dc01a3b2458bb3663d-2b1b2075b53226c8-01",
"traceresponse": "00-d79cf3755cbadf040132eb98d80b3632-71c7d3b5bbb39b4f-01",
"vary": "Accept-Encoding",
"x-robots-tag": "none"
},
@@ -46,30 +46,30 @@
}
},
"status": 200,
"response": "{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_01Q4ikLq8n4cbc4ZgHqszwz1\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_017RwPnNVuN8WkzFkDUp1Ruu\",\"name\":\"delete_file\",\"input\":{\"path\":\"/tmp/interim-test.txt\"},\"caller\":{\"type\":\"direct\"}}],\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":629,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":61,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}",
"response": "{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_011CeE6z8ovT2Z3MCC8dDWF5\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_01LC4A1xXP3auktG1rpfcuk9\",\"name\":\"delete_file\",\"input\":{\"path\":\"/tmp/interim-test.txt\"},\"caller\":{\"type\":\"direct\"}}],\"stop_reason\":\"tool_use\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":629,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":61,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}",
"rawHeaders": {
"anthropic-ratelimit-input-tokens-limit": "10000000",
"anthropic-ratelimit-input-tokens-remaining": "10000000",
"anthropic-ratelimit-input-tokens-reset": "2026-07-03T09:47:10Z",
"anthropic-ratelimit-input-tokens-reset": "2026-08-20T13:47:11Z",
"anthropic-ratelimit-output-tokens-limit": "2000000",
"anthropic-ratelimit-output-tokens-remaining": "2000000",
"anthropic-ratelimit-output-tokens-reset": "2026-07-03T09:47:10Z",
"anthropic-ratelimit-output-tokens-reset": "2026-08-20T13:47:11Z",
"anthropic-ratelimit-requests-limit": "20000",
"anthropic-ratelimit-requests-remaining": "19999",
"anthropic-ratelimit-requests-reset": "2026-07-03T09:47:09Z",
"anthropic-ratelimit-requests-reset": "2026-08-20T13:47:10Z",
"anthropic-ratelimit-tokens-limit": "12000000",
"anthropic-ratelimit-tokens-remaining": "12000000",
"anthropic-ratelimit-tokens-reset": "2026-07-03T09:47:10Z",
"anthropic-ratelimit-tokens-reset": "2026-08-20T13:47:11Z",
"cf-cache-status": "DYNAMIC",
"cf-ray": "a154f7987d002f95-VIE",
"cf-ray": "a2e1d92c6ef3bb94-PRG",
"connection": "keep-alive",
"content-security-policy": "default-src 'none'; frame-ancestors 'none'",
"content-type": "application/json",
"date": "Fri, 03 Jul 2026 09:47:10 GMT",
"request-id": "req_011Cceure9g2zGrzVb1Z6z32",
"date": "Thu, 20 Aug 2026 13:47:11 GMT",
"request-id": "req_011CeE6z8CThGAD2RxASg9c3",
"server": "cloudflare",
"strict-transport-security": "max-age=31536000; includeSubDomains; preload",
"traceresponse": "00-8aa902ee1e7eb6f58b5e2878d75a119a-635b89f5d10d5738-01",
"traceresponse": "00-b6e65748653ea3f1605720b9cfe7a338-cca2c7b55c120740-01",
"vary": "Accept-Encoding",
"x-robots-tag": "none"
},
@@ -95,6 +95,33 @@
{
"type": "text",
"text": "Please delete /tmp/interim-test.txt"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_01LC4A1xXP3auktG1rpfcuk9",
"name": "delete_file",
"input": {
"path": "/tmp/interim-test.txt"
},
"caller": {
"type": "direct"
}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01LC4A1xXP3auktG1rpfcuk9",
"content": "INTERRUPTED — this delete_file call never completed. The user was shown a confirmation (\"Delete \"/tmp/interim-test.txt\"?\") and never answered it. Its action did NOT take effect: nothing was saved, executed, or applied by this call. Do not describe this action as done. If it is still needed, explain to the user that it requires their approval and what approving will do, then re-issue it once.",
"is_error": true
},
{
"type": "text",
@@ -105,30 +132,30 @@
]
},
"status": 200,
"response": "{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_012B2ryS5wEuCVHWhmdP76TG\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"I can't delete files. I'm an AI assistant without access to your file system.\\n\\nHowever, I can answer your math question: **1 + 1 = 2**\\n\\nIf you need to delete that file, you can do it yourself using:\\n- Command line: `rm /tmp/interim-test.txt`\\n- File manager: Navigate to `/tmp/` and delete it manually\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":36,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":89,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}",
"response": "{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_011CeE6zD2g1JHbkoCqf5gzi\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"1 + 1 = 2\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":212,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":13,\"service_tier\":\"standard\",\"inference_geo\":\"not_available\"}}",
"rawHeaders": {
"anthropic-ratelimit-input-tokens-limit": "10000000",
"anthropic-ratelimit-input-tokens-remaining": "10000000",
"anthropic-ratelimit-input-tokens-reset": "2026-07-03T09:47:10Z",
"anthropic-ratelimit-input-tokens-reset": "2026-08-20T13:47:11Z",
"anthropic-ratelimit-output-tokens-limit": "2000000",
"anthropic-ratelimit-output-tokens-remaining": "2000000",
"anthropic-ratelimit-output-tokens-reset": "2026-07-03T09:47:11Z",
"anthropic-ratelimit-output-tokens-reset": "2026-08-20T13:47:11Z",
"anthropic-ratelimit-requests-limit": "20000",
"anthropic-ratelimit-requests-remaining": "19999",
"anthropic-ratelimit-requests-reset": "2026-07-03T09:47:10Z",
"anthropic-ratelimit-requests-reset": "2026-08-20T13:47:11Z",
"anthropic-ratelimit-tokens-limit": "12000000",
"anthropic-ratelimit-tokens-remaining": "12000000",
"anthropic-ratelimit-tokens-reset": "2026-07-03T09:47:10Z",
"anthropic-ratelimit-tokens-reset": "2026-08-20T13:47:11Z",
"cf-cache-status": "DYNAMIC",
"cf-ray": "a154f79d4b0c2f95-VIE",
"cf-ray": "a2e1d932ac29bb94-PRG",
"connection": "keep-alive",
"content-security-policy": "default-src 'none'; frame-ancestors 'none'",
"content-type": "application/json",
"date": "Fri, 03 Jul 2026 09:47:11 GMT",
"request-id": "req_011CceurhQdb8mFHcFMpoT65",
"date": "Thu, 20 Aug 2026 13:47:11 GMT",
"request-id": "req_011CeE6zCYQJhwwxZ1XgEct2",
"server": "cloudflare",
"strict-transport-security": "max-age=31536000; includeSubDomains; preload",
"traceresponse": "00-db38b2887ddda7cad3d7fd296fc30f9f-daacc71bf36111e6-01",
"traceresponse": "00-66d0cb732117d7b6d3001270957fc73e-cc90527a425940cc-01",
"vary": "Accept-Encoding",
"x-robots-tag": "none"
},
@@ -162,7 +189,7 @@
"content": [
{
"type": "tool_use",
"id": "toolu_017RwPnNVuN8WkzFkDUp1Ruu",
"id": "toolu_01LC4A1xXP3auktG1rpfcuk9",
"name": "delete_file",
"input": {
"path": "/tmp/interim-test.txt"
@@ -178,7 +205,7 @@
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_017RwPnNVuN8WkzFkDUp1Ruu",
"tool_use_id": "toolu_01LC4A1xXP3auktG1rpfcuk9",
"content": "{\"deleted\":true,\"path\":\"/tmp/interim-test.txt\"}"
}
]
@@ -209,33 +236,33 @@
},
"status": 200,
"response": [
"1f8b0800000000000013bd54c18ed33014bcf72bac774ebb4dd50ac80d690f5c910a1220e475e36963d5b183fd52ba5de5df51ba099b9416b8402e51669e3df3e6e9050738ce448918d50e32b20a3cd18a55269e881f2b5046239292fe9bb2272abd86a58c72ab6a8d69a1ccbe9e2ea7abe962be58a5f3794a0919ddde1177729ebef97ec269f3eec33dc78f6f4febf7cb75f8f47949c985122514bc6d0115a389ac5c2b9b7bc7704cd997af0945f6950c50d13bca5c6d6d07457cabe1728c400d56c6c61eab7bf7c655354bf67bb848d9abf47542b9ca0bc83c40b1f14e8e2be63d1fa0f42dae3fdb0aa02a5022282b57e5aff52f6c5a5cb24d42bee621942614110e268764834019b5c16815749bb1db22b47dcb1d3c65e43c4b7550c6aa8d05358d183ccd6482e7a97789ca8df5f9fefaecaf949ce5348ecf3d0ff9b6e5ee1ce3d8169e5f19d1c8c0c04165dcee4252500bd22d971a96d5ef5d9e4b462e9f91b1bb9f659dc77befd0f9fcefda33b12e20b6c6423cdc7159dd19c708a69c3222cff8c80fa250516c0027342c185ac43acf11e3b6b6f671f612f0ede9faea4fc3f5d5c0b9b831b2fe6f70358c114983e647fb4a705a721d1cfdf5d636ff6e6d2ff66c9136d7dabd96df90a32ea51f3d23137a51050000"
"1f8b0800000000000013b553b16edb3010ddfd15c4cd5262a5759a726b8ba24b870e4551a02898b378b65853a4429e0ca781febda06dc5962b2759a285d07b8f7cefee70b426c752d414232e4945c6c0138d8c523c00df370412062464fd3fc807a8bd260b124a8bada6bc42b36af3b7f92cbf9a5ecd8ae9b4800c8c4e6fc4a59a16c527fa7cfdf7cbddcd879f1fe77ffc8fafdfdc0aaf213b71820c82b709c0184d6474c9b6f48ec931c85fbf3388ec1b1508a377205d6bed1e8a74d7922b69006a623436f658dba737ae6959b15f918b20df153719945856a4ca40c8c63b35544c7b3e10ea735c7f37195053514d01ad9ad5ffeb0f6c519db25d06bee563e84d0691c2da94a4d8500009a9311a834e3d760b0aa96eb5240f129c67856b3416e796a0eb8410dd6442bb61ef1ba9e6d697abf1918f48b62e9a36bb528ff954e9fe1ed32609b78704e80ea68d71cb131701098473c13459c6a7836d2583603b6418e851b68ff5bd22b13096c4ed2574e71bf36afe5c3797c6310553e74c912f78c3b7a2c228e6444ec4b62c29c6456bedbdd06489495fa4a04f0ed137cfcdd0374749c5e03b3cdbeffa68f103128e8a1d6c2390d38adbe0e0c53bd9bdde529e6c51f1be1bad77ac81c71cec6efd03e59ea04b2f050000"
],
"rawHeaders": {
"anthropic-ratelimit-input-tokens-limit": "10000000",
"anthropic-ratelimit-input-tokens-remaining": "10000000",
"anthropic-ratelimit-input-tokens-reset": "2026-07-03T09:47:11Z",
"anthropic-ratelimit-input-tokens-reset": "2026-08-20T13:47:12Z",
"anthropic-ratelimit-output-tokens-limit": "2000000",
"anthropic-ratelimit-output-tokens-remaining": "2000000",
"anthropic-ratelimit-output-tokens-reset": "2026-07-03T09:47:11Z",
"anthropic-ratelimit-output-tokens-reset": "2026-08-20T13:47:12Z",
"anthropic-ratelimit-requests-limit": "20000",
"anthropic-ratelimit-requests-remaining": "19999",
"anthropic-ratelimit-requests-reset": "2026-07-03T09:47:11Z",
"anthropic-ratelimit-requests-reset": "2026-08-20T13:47:12Z",
"anthropic-ratelimit-tokens-limit": "12000000",
"anthropic-ratelimit-tokens-remaining": "12000000",
"anthropic-ratelimit-tokens-reset": "2026-07-03T09:47:11Z",
"anthropic-ratelimit-tokens-reset": "2026-08-20T13:47:12Z",
"cache-control": "no-cache",
"cf-cache-status": "DYNAMIC",
"cf-ray": "a154f7a65ac52f95-VIE",
"cf-ray": "a2e1d936ff53bb94-PRG",
"connection": "keep-alive",
"content-encoding": "gzip",
"content-security-policy": "default-src 'none'; frame-ancestors 'none'",
"content-type": "text/event-stream; charset=utf-8",
"date": "Fri, 03 Jul 2026 09:47:12 GMT",
"request-id": "req_011CceurocwFnEVw1JGBfKDc",
"date": "Thu, 20 Aug 2026 13:47:12 GMT",
"request-id": "req_011CeE6zGRp97hjGxRk4t93h",
"server": "cloudflare",
"strict-transport-security": "max-age=31536000; includeSubDomains; preload",
"traceresponse": "00-962f3f29592dd0795f03734fd76bd982-2df7b75546da1124-01",
"traceresponse": "00-65548e4e6f77edda0b239804578b882d-ed4f8ab94b6ff6a3-01",
"transfer-encoding": "chunked",
"vary": "Accept-Encoding",
"x-robots-tag": "none"
@@ -515,3 +515,37 @@ describe('AgentMessageList — setToolCallError', () => {
expect((block as unknown as { output?: unknown }).output).toBeUndefined();
});
});
describe('AgentMessageList — markToolCallSuspended', () => {
it('stamps suspension info on a pending tool-call block', () => {
const list = new AgentMessageList();
list.addResponse([makePendingToolCallMsg('id-1')]);
list.markToolCallSuspended('id-1', { message: 'Edit My Workflow (ID: abc)?', requestId: 'r1' });
const host = list
.turnDelta()
.find((m) => 'content' in m && m.content.some((c) => c.type === 'tool-call')) as Message;
const block = host.content.find((c) => c.type === 'tool-call') as ContentToolCall & {
state: 'pending';
};
expect(block.state).toBe('pending');
expect(block.suspension).toEqual({ message: 'Edit My Workflow (ID: abc)?', requestId: 'r1' });
});
it('is a no-op for settled blocks and unknown ids', () => {
const list = new AgentMessageList();
list.addResponse([makePendingToolCallMsg('id-1')]);
list.setToolCallResult('id-1', { ok: true });
list.markToolCallSuspended('id-1', { message: 'stale' });
list.markToolCallSuspended('missing', { message: 'stale' });
const host = list
.turnDelta()
.find((m) => 'content' in m && m.content.some((c) => c.type === 'tool-call')) as Message;
const block = host.content.find((c) => c.type === 'tool-call') as ContentToolCall;
expect(block.state).toBe('resolved');
expect('suspension' in block).toBe(false);
});
});
@@ -1,5 +1,8 @@
import type { AgentMessage, Message } from '../../types/sdk/message';
import { stripOrphanedToolMessages } from '../memory/strip-orphaned-tool-messages';
import type { AgentMessage, ContentToolCall, Message } from '../../types/sdk/message';
import {
settleOrphanedToolMessages,
stripOrphanedToolMessages,
} from '../memory/strip-orphaned-tool-messages';
describe('stripOrphanedToolMessages', () => {
it('returns messages unchanged when all tool-calls are settled', () => {
@@ -135,3 +138,168 @@ describe('stripOrphanedToolMessages', () => {
expect(result[0]).toBe(customMsg);
});
});
describe('settleOrphanedToolMessages', () => {
const settledBlock = (result: AgentMessage[], msgIndex: number, blockIndex: number) =>
(result[msgIndex] as Message).content[blockIndex] as ContentToolCall;
it('returns messages unchanged when all tool-calls are settled', () => {
const messages: AgentMessage[] = [
{ role: 'user', content: [{ type: 'text', text: 'Hello' }] },
{
role: 'assistant',
content: [
{
type: 'tool-call',
toolCallId: 'c1',
toolName: 'lookup',
input: {},
state: 'resolved',
output: 42,
},
],
},
];
const result = settleOrphanedToolMessages(messages);
expect(result).toEqual(messages);
// Untouched messages are passed through by reference, not rebuilt.
expect(result[1]).toBe(messages[1]);
});
it('rejects a pending block with a "did not take effect" record instead of dropping it', () => {
const messages: AgentMessage[] = [
{ role: 'user', content: [{ type: 'text', text: 'Save it' }] },
{
role: 'assistant',
content: [
{ type: 'text', text: 'Saving...' },
{
type: 'tool-call',
toolCallId: 'c1',
toolName: 'build-workflow',
input: { workflowId: 'wf1' },
state: 'pending',
},
],
},
];
const result = settleOrphanedToolMessages(messages);
expect(result).toHaveLength(2);
const msg = result[1] as Message;
expect(msg.content).toHaveLength(2);
const block = settledBlock(result, 1, 1);
expect(block.state).toBe('rejected');
expect(block.toolCallId).toBe('c1');
expect(block.input).toEqual({ workflowId: 'wf1' });
const error = (block as Extract<ContentToolCall, { state: 'rejected' }>).error;
expect(error).toContain('build-workflow call never completed');
expect(error).toContain('did NOT take effect');
});
it('quotes the confirmation message when the pending block carries suspension info', () => {
const messages: AgentMessage[] = [
{
role: 'assistant',
content: [
{
type: 'tool-call',
toolCallId: 'c1',
toolName: 'build-workflow',
input: {},
state: 'pending',
suspension: { message: 'Edit My Workflow (ID: abc123)?', requestId: 'req1' },
},
],
},
];
const result = settleOrphanedToolMessages(messages);
const block = settledBlock(result, 0, 0);
expect(block.state).toBe('rejected');
const error = (block as Extract<ContentToolCall, { state: 'rejected' }>).error;
expect(error).toContain('Edit My Workflow (ID: abc123)?');
expect(error).toContain('never answered');
// The suspension marker is consumed into the error, not carried on the settled block.
expect('suspension' in block).toBe(false);
});
it('keeps a message whose only content is a pending block (unlike strip)', () => {
const messages: AgentMessage[] = [
{ role: 'user', content: [{ type: 'text', text: 'Do it' }] },
{
role: 'assistant',
content: [
{ type: 'tool-call', toolCallId: 'c1', toolName: 'action', input: {}, state: 'pending' },
],
},
];
const result = settleOrphanedToolMessages(messages);
expect(result).toHaveLength(2);
expect(settledBlock(result, 1, 0).state).toBe('rejected');
});
it('settles only pending blocks in a mixed message', () => {
const messages: AgentMessage[] = [
{
role: 'assistant',
content: [
{
type: 'tool-call',
toolCallId: 'c1',
toolName: 'lookup',
input: {},
state: 'resolved',
output: 99,
},
{
type: 'tool-call',
toolCallId: 'c2',
toolName: 'delete',
input: {},
state: 'pending',
},
{
type: 'tool-call',
toolCallId: 'c3',
toolName: 'create',
input: {},
state: 'rejected',
error: 'boom',
},
],
},
];
const result = settleOrphanedToolMessages(messages);
const blocks = (result[0] as Message).content as ContentToolCall[];
expect(blocks).toHaveLength(3);
expect(blocks[0].state).toBe('resolved');
expect(blocks[1].state).toBe('rejected');
expect((blocks[1] as Extract<ContentToolCall, { state: 'rejected' }>).error).toContain(
'never completed',
);
expect(blocks[2].state).toBe('rejected');
expect((blocks[2] as Extract<ContentToolCall, { state: 'rejected' }>).error).toBe('boom');
});
it('preserves custom (non-LLM) messages by reference', () => {
const customMsg: AgentMessage = {
id: 'custom-1',
type: 'custom',
messageType: 'notification',
data: { info: 'hello' },
} as unknown as AgentMessage;
const result = settleOrphanedToolMessages([customMsg]);
expect(result).toHaveLength(1);
expect(result[0]).toBe(customMsg);
});
});
@@ -1067,6 +1067,21 @@ export class AgentRuntime {
const executionOptions: PersistedExecutionOptions | undefined =
resolvedMaxIterations !== undefined ? { maxIterations: resolvedMaxIterations } : undefined;
// Record what confirmation each suspended call showed the user, so an
// abandoned suspension can be settled with that context on a later
// history load instead of vanishing from the transcript.
for (const pending of Object.values(pendingToolCalls)) {
if (!pending.suspended) continue;
const payload =
typeof pending.suspendPayload === 'object' && pending.suspendPayload !== null
? (pending.suspendPayload as { message?: unknown; requestId?: unknown })
: undefined;
list.markToolCallSuspended(pending.toolCallId, {
...(typeof payload?.message === 'string' ? { message: payload.message } : {}),
...(typeof payload?.requestId === 'string' ? { requestId: payload.requestId } : {}),
});
}
const state: SerializableAgentState = {
persistence: options?.persistence,
status: 'suspended',
@@ -14,7 +14,7 @@ import { runObservationLogReflector } from './observation-log-reflector';
import { renderObservationLog } from './observation-log-renderer';
import { hasObservationLogStore, hasObservationLogTaskLockStore } from './observation-log-store';
import { ScopedMemoryTaskRunner } from './scoped-memory-task-runner';
import { stripOrphanedToolMessages } from './strip-orphaned-tool-messages';
import { settleOrphanedToolMessages } from './strip-orphaned-tool-messages';
import type {
AgentExecutionCounter,
BuiltMemory,
@@ -155,7 +155,9 @@ export class MemoryOrchestrator {
const memMessages = await this.loadHistoryMessages(options.persistence, telemetry);
if (memMessages.length > 0) {
list.addHistory(stripOrphanedToolMessages(memMessages));
// Settle (not strip) so an abandoned suspension stays visible as an
// explicit "never completed" record instead of vanishing (INS-1223).
list.addHistory(settleOrphanedToolMessages(memMessages));
}
}
@@ -262,8 +264,8 @@ export class MemoryOrchestrator {
* / title jobs that `saveToMemory` schedules; those stay at end-of-turn. Idempotent
* with the end-of-turn save: ids are stable across serialize/deserialize, so both
* writes target the same rows and TypeORM upserts (pending tool-call → resolved in
* place). A still-pending tool-call left by an abort is stripped on the next load
* (`stripOrphanedToolMessages`), so persisting an incomplete turn can't malform history.
* place). A still-pending tool-call left by an abort is settled on the next load
* (`settleOrphanedToolMessages`), so persisting an incomplete turn can't malform history.
*/
async persistTurnDelta(
list: AgentMessageList,
@@ -1,5 +1,56 @@
import { isLlmMessage } from '../../sdk/message';
import type { AgentMessage, MessageContent } from '../../types/sdk/message';
import type { AgentMessage, ContentToolCall, MessageContent } from '../../types/sdk/message';
/**
* Settle pending tool-call blocks in loaded thread history by rejecting them
* with an explanation that the call never completed.
*
* A pending block in *history* means a previous turn suspended on the call
* (usually awaiting a user confirmation) and was never resumed — the user
* typed a new message instead, the confirmation timed out, or the process
* restarted. Silently dropping the block (what `stripOrphanedToolMessages`
* does for the mid-run path) erases the only evidence the action was ever
* attempted, so the model may describe the action as done or blindly re-fire
* it. Rejecting it with an explicit "did not take effect" record keeps the
* model's view of the world truthful (INS-1223).
*
* Only for history loads. The mid-run serialization path must keep using
* `stripOrphanedToolMessages`: there a pending block is the *current* turn's
* live suspension, which receives its real result on resume.
*/
export function settleOrphanedToolMessages<T extends AgentMessage>(messages: T[]): T[] {
return messages.map((msg) => {
if (!isLlmMessage(msg)) return msg;
if (!msg.content.some((block) => block.type === 'tool-call' && block.state === 'pending')) {
return msg;
}
const content = msg.content.map((block: MessageContent) => {
if (block.type !== 'tool-call' || block.state !== 'pending') return block;
const { suspension, ...rest } = block;
return {
...rest,
state: 'rejected' as const,
error: buildAbandonedSuspensionError(block),
};
});
return { ...msg, content };
});
}
function buildAbandonedSuspensionError(
block: Extract<ContentToolCall, { state: 'pending' }>,
): string {
const card = block.suspension?.message
? ` The user was shown a confirmation ("${block.suspension.message}") and never answered it.`
: " It was awaiting the user's confirmation (or the session was interrupted) and was never resumed.";
return (
`INTERRUPTED — this ${block.toolName} call never completed.${card}` +
' Its action did NOT take effect: nothing was saved, executed, or applied by this call.' +
' Do not describe this action as done. If it is still needed, explain to the user that it' +
' requires their approval and what approving will do, then re-issue it once.'
);
}
/**
* Strip pending tool-call blocks from a message list before sending to the LLM.
@@ -4,7 +4,12 @@ import type { ModelMessage, SystemModelMessage } from 'ai';
import { toAiMessages } from './messages';
import { filterLlmMessages, getCreatedAt } from '../../sdk/message';
import type { SerializedMessageList } from '../../types/runtime/message-list';
import type { AgentDbMessage, AgentMessage, ContentToolCall } from '../../types/sdk/message';
import type {
AgentDbMessage,
AgentMessage,
ContentToolCall,
ToolCallSuspensionInfo,
} from '../../types/sdk/message';
import type { JSONValue } from '../../types/utils/json';
import { stringifyError } from '../loop/runtime-helpers';
import { stripOrphanedToolMessages } from '../memory/strip-orphaned-tool-messages';
@@ -255,6 +260,20 @@ export class AgentMessageList {
return host;
}
/**
* Record on a pending tool-call block what confirmation the user was shown
* (HITL suspension). No-op when the toolCallId is unknown or the block is
* already settled. Lets a later history load of an abandoned suspension
* explain the unanswered confirmation instead of silently dropping it.
*/
markToolCallSuspended(toolCallId: string, suspension: ToolCallSuspensionInfo): void {
const host = this.findToolCallHost(toolCallId);
if (!host) return;
const block = this.findToolCallBlock(host, toolCallId);
if (!block || block.state !== 'pending') return;
block.suspension = suspension;
}
private findToolCallHost(toolCallId: string): AgentDbMessage | undefined {
// Start from the last message and go backwards to find the host message
for (let i = this.all.length - 1; i >= 0; i--) {
+13 -1
View File
@@ -134,11 +134,23 @@ export type ContentToolCall = ContentMetadata & {
providerExecuted?: boolean;
} & (
| { state: 'pending' }
| { state: 'pending'; suspension?: ToolCallSuspensionInfo }
| { state: 'resolved'; output: JSONValue; canceled?: boolean }
| { state: 'rejected'; error: string }
);
/**
* Present on a pending block when the call suspended for user confirmation
* (HITL). Records what was asked of the user, so a later history load can
* settle an abandoned confirmation with an explanation instead of silently
* dropping the call.
*/
export interface ToolCallSuspensionInfo {
/** Human-readable confirmation message shown to the user (e.g. the approval card text). */
message?: string;
requestId?: string;
}
export type ContentInvalidToolCall = ContentMetadata & {
type: 'invalid-tool-call';
@@ -156,7 +156,8 @@ ${getToolDiscoverySection(toolSearchEnabled, mcpToolSearchEnabled)}
- No emojis unless the user explicitly requests them.
- At the beginning of a normal user-visible turn, before your first tool call, write one short sentence explaining what you are about to do or what decision you need. Keep it tied to the user's goal, not the tool name. For system-generated background or checkpoint follow-up turns, follow the follow-up instructions.
- Never let an empty assistant message or a \`[Calling tools: ...]\` placeholder be the first visible response.
- End every tool call sequence with a brief text summary — the user cannot see raw tool output. Do not end your turn silently after tool calls. Exception: after calling \`create-tasks\`, or during planned-task build/checkpoint follow-ups, the task card, approval card, or checklist replaces your reply — do not write text.
- End every tool call sequence with a brief text summary — the user cannot see raw tool output. Do not end your turn silently after tool calls. Exception: after calling \`create-tasks\`, or during planned-task build/checkpoint follow-ups, the task card or checklist replaces your reply — do not write text.
- Approval cards are never a reply on their own. Before a tool call that will show an approval card (e.g. saving changes to an existing workflow, publishing, or a live run), write one short sentence saying what the card asks and that nothing happens until they respond to it. If the user seems confused or asks what is happening while an approval is pending, explain in words that the action is waiting for their approval and what approving or denying does — never answer with only a re-issued card.
## Capability Honesty
@@ -599,7 +599,10 @@ export function createBuildWorkflowTool(context: InstanceAiContext) {
category: 'blocked',
shouldEdit: false,
reason: 'user_denied',
guidance: 'The user denied permission to edit this workflow.',
guidance:
'The user declined the save approval card — nothing was saved. Do not re-issue ' +
'the same save unprompted: acknowledge the denial, tell the user what remains ' +
'unsaved, and ask how they want to proceed.',
});
trackWorkflowSourceBuild(context, {
result: 'denied',