mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(mcp): guide agents through tool error recovery (#5470)
Add shared, typed input-contract and timeout-state recovery guidance. Normalize SDK input failures within the actual MCP wire budget, preserve full diagnostics and protocol metadata, and retain original Evidence when normalized delivery would exceed the budget. Fixes #5431.
This commit is contained in:
1 parent
c374c2fe72
commit
e50a271c72
20 files changed
+1010
-69
No files matched your search
@@ -28,6 +28,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
@@ -64,6 +91,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
@@ -100,6 +154,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
@@ -136,6 +217,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
@@ -172,6 +280,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
@@ -208,6 +343,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
@@ -244,6 +406,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
@@ -280,6 +469,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
@@ -316,6 +532,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
@@ -352,6 +595,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
@@ -388,6 +658,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -476,6 +773,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
@@ -512,6 +836,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
@@ -548,6 +899,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
@@ -584,6 +962,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
@@ -620,6 +1025,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
@@ -656,6 +1088,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
@@ -692,6 +1151,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
@@ -728,6 +1214,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
@@ -764,6 +1277,33 @@
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"recovery": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_input_contract" },
|
||||
"operation": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"required": ["kind", "operation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": { "type": "string", "const": "inspect_timeout_state" },
|
||||
"operation": { "type": "string", "minLength": 1 },
|
||||
"timeout_ms": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 9007199254740991
|
||||
}
|
||||
},
|
||||
"required": ["kind", "operation", "timeout_ms"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
|
||||
+18
-1
@@ -90,13 +90,30 @@ metadata in the supported JSON payloads (`parameters`, `raw_result`,
|
||||
|
||||
Tool execution failures return `isError: true` and the complete canonical
|
||||
`{ "error": ... }` diagnostic as JSON in text `content`. Read that diagnostic
|
||||
for the error code, target details, partial observations, and remediation.
|
||||
for the error code, target details, partial observations, remediation, and the
|
||||
optional bounded `recovery` next step. `recovery.kind=inspect_input_contract`
|
||||
names the rejected operation: re-read that operation's advertised input schema
|
||||
(call `tools/list` if the catalog may be stale), correct the listed fields, and
|
||||
do not repeat the unchanged call. `recovery.kind=inspect_timeout_state` names
|
||||
the operation and the REA server/provider deadline; inspect session/provider
|
||||
state before retrying, and raise the MCP client's tool deadline only when the
|
||||
client ended the call before REA returned this response. These hints never
|
||||
include the complete catalog or caller arguments.
|
||||
Failures omit `structuredContent`, since each advertised `outputSchema`
|
||||
describes successful results. Successful replies retain their schema-validated
|
||||
structured data and matching text projection. Oversized errors are retained
|
||||
as Evidence before delivery of a bounded diagnostic with an export reference;
|
||||
if retention is unavailable or fails, the diagnostic reports that reason.
|
||||
|
||||
SDK-generated input validation failures are normalized into the same canonical
|
||||
`invalid_request` text when the bounded diagnostic fits the response budget.
|
||||
The complete field/type diagnostic is preserved when normalization fits the
|
||||
response budget, including request-specific rejected fields and reasons. Oversized
|
||||
SDK diagnostics keep the existing retained-Evidence recovery path. An unknown
|
||||
tool remains an MCP JSON-RPC `-32602` error, but its message directs the caller
|
||||
to `tools/list` and an advertised operation instead of repeating the unknown
|
||||
name.
|
||||
|
||||
`get_call_graph` returns a `graph` keyed by traversal depth and a top-level
|
||||
`limitations` array with deduplicated execution caveats. Each node keeps its
|
||||
relationship coverage and read-specific `limitations`, alongside observed
|
||||
|
||||
@@ -17,7 +17,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
|
||||
13,src/domain/errors.ts:EvidenceIntegrityError,Evidence validation fails,Raw constructor message,May expose validation internals and lacks recovery,"Evidence is invalid or has changed. Recreate or re-import it, then try again.",implemented,focused assertions passed; full `npm run check` passed
|
||||
14,src/domain/errors.ts:UnknownRegistryError,Evidence-state mutation fails,"Residual unknown registry mutation failed: {reason}",Exposes internal subsystem and state token,"Missing IDs identify the session and retry; concurrent revision conflicts refresh current state.",implemented,focused assertions passed; full `npm run check` passed
|
||||
15,src/domain/errors.ts:ProcessCaptureError,Process capture fails,Raw constructor message,May expose process and cleanup internals,"Process capture could not complete. Check command access and limits, then try again.",implemented,focused assertions passed; full `npm run check` passed
|
||||
16,src/domain/errors.ts:AnalysisTimeoutError,Analysis deadline expires,"Analysis operation timed out after {timeoutMs}ms: {operation}",Exposes internal deadline and operation; weak recovery,"Analysis took too long. Try a smaller request, then run `rea doctor` if it continues.",implemented,focused assertions passed; full `npm run check` passed
|
||||
16,src/domain/errors.ts:AnalysisTimeoutError,Analysis deadline expires,"REA's {operation} operation reached its {timeoutMs}ms server/provider deadline; the MCP client may have ended the call earlier",Explains the two deadline owners and avoids blind retry,"Inspect session/provider state first. If the MCP client ended the call, raise its tool deadline above {timeoutMs}ms; otherwise retry with a smaller request and do not repeat an unchanged call.",implemented,verified by projection tests and real MCP/Codex CLI timeout-recovery probes
|
||||
17,src/domain/errors.ts:HopperTimeoutError,Provider deadline expires,"Hopper did not respond within {timeoutMs}ms",Exposes provider and internal deadline,"Analysis took too long. Try a smaller request, then run `rea doctor` if it continues.",implemented,focused assertions passed; full `npm run check` passed
|
||||
18,src/domain/errors.ts:ProviderSelectionError,Deep-provider selection fails,"Analysis provider selection failed ({reason}) for {requestedProviderId}; candidates: {candidateIds}",Retains non-secret local selection context that needs a safe structured projection,Reason-specific guidance with stable candidate IDs and actionable rejection diagnostics,implemented,focused registry/CLI/MCP assertions passed; full `npm run check` passed
|
||||
20,src/domain/errors.ts:AnalysisInputError,Parsed analysis input invalid,"Invalid analysis input for {operation}",Exposes operation identifier and lacks correction,"Analysis input is invalid. Check the arguments and try again.",implemented,focused assertions passed; full `npm run check` passed
|
||||
@@ -114,5 +114,5 @@ Then rerun this installer.",implemented,verified by shell branch inspection
|
||||
88,install.sh:binary absent,npm exits successfully without executable,the rea command was not installed at local path,Leaks local path and no recovery,"npm completed without installing the rea command. Check the npm global bin directory and PATH, then retry.",implemented,verified by exact absent-installed-command test
|
||||
89,install.sh:version read,Installed rea executable cannot report its version,could not read the installed REA version.,No recovery instruction,"The installed REA version could not be read. Reinstall the requested version, then retry.",implemented,verified by exact unreadable-installed-version test
|
||||
90,install.sh:version mismatch,Installed rea reports a different version,installed version ACTUAL does not match EXPECTED.,Low risk; gives exact integrity mismatch,Keep current copy,unchanged,verified by exact installer test
|
||||
90.5,@modelcontextprotocol/server:tool input validation,Caller sends arguments that violate a declared tool schema,"Input validation error: Invalid arguments for tool TOOL: FIELD: Invalid input: expected TYPE, received TYPE",Low risk; public tool and field names plus exact type correction,Keep SDK-generated copy,unchanged,verified by real in-memory MCP client/server call with wrong argument type
|
||||
90.5,@modelcontextprotocol/server:tool input validation,Caller sends arguments that violate a declared tool schema,"Canonical invalid_request with the SDK diagnostic plus recovery.kind=inspect_input_contract and a bounded instruction to read the advertised schema or call tools/list",Keeps field/type correction while telling the agent how to recover; diagnostics over 4 KiB stay on the existing retained-error path,Normalize ordinary validation failures and retain oversized diagnostics,verified by real in-memory and stdio MCP client/server calls with wrong argument types
|
||||
90.6,incur:unknown CLI command,Caller enters an unknown REA command,"'COMMAND' is not a command for 'rea'. Suggested command: rea --help",Low risk; identifies input and exact discovery action,Keep framework-generated copy,unchanged,verified by real packaged dispatcher invocation
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { analysisErrorRecoverySchema } from "../domain/analysisErrorRecovery.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import type { AnalysisErrorProjection } from "../domain/analysisErrorProjection.js";
|
||||
|
||||
const categorySchema = z.enum([
|
||||
"invalid_input",
|
||||
@@ -24,6 +26,7 @@ const common = {
|
||||
message: z.string().min(1),
|
||||
retryable: z.boolean(),
|
||||
remediation: remediationSchema,
|
||||
recovery: analysisErrorRecoverySchema.optional(),
|
||||
};
|
||||
const genericDetails = z.record(z.string(), jsonValueSchema).optional();
|
||||
const generic = <Code extends string>(code: Code) =>
|
||||
@@ -76,7 +79,7 @@ export const analysisErrorProjectionSchema = z.discriminatedUnion("code", [
|
||||
generic("configuration_invalid"),
|
||||
generic("target_unavailable"),
|
||||
generic("execution_failure"),
|
||||
]);
|
||||
]) as unknown as z.ZodType<AnalysisErrorProjection>;
|
||||
|
||||
/** CLI label and input diagnostics surrounding the strict canonical error projection. */
|
||||
export const analysisCliErrorEnvelopeSchema = z
|
||||
|
||||
@@ -82,6 +82,7 @@ export const analysisErrorRemediationAction = (
|
||||
error.remediationAction !== undefined
|
||||
)
|
||||
return error.remediationAction;
|
||||
if (error instanceof AnalysisTimeoutError) return timeoutRemediation(error);
|
||||
if (error instanceof HopperTimeoutError)
|
||||
return error.operation === undefined
|
||||
? "Review details.launcher and inspect Hopper for ongoing initial analysis or a pending loader, demo, or license dialog. For a large target, increase REA_HOPPER_STARTUP_TIMEOUT_MS in the environment that starts REA and allow at least that long in the MCP client. Resolve retained cleanup before reopening."
|
||||
@@ -130,7 +131,7 @@ export const analysisErrorRemediationAction = (
|
||||
if (error instanceof ConfigurationError && error.settings.length > 0)
|
||||
return `Correct ${[...new Set(error.settings.map(({ setting }) => setting))].join(", ")} in the environment that starts REA (your shell or the MCP client's registration), then rerun.`;
|
||||
if (error instanceof AnalysisInputError)
|
||||
return "Correct the listed arguments and retry.";
|
||||
return `Read the declared input schema or CLI usage for ${error.operation}; if using MCP and the catalog may be stale, call tools/list. Correct the listed fields, then retry. Do not repeat the unchanged call.`;
|
||||
if (
|
||||
error instanceof AnalysisOutputError &&
|
||||
error.remediationAction !== undefined
|
||||
@@ -254,6 +255,8 @@ export const analysisErrorUserMessage = (error: AnalysisError): string => {
|
||||
error.userMessage ??
|
||||
"Analysis input is invalid. Check the arguments and try again."
|
||||
);
|
||||
if (error instanceof AnalysisTimeoutError)
|
||||
return error.userMessage ?? timeoutUserMessage(error);
|
||||
const hopperMessage = hopperErrorUserMessage(error);
|
||||
if (hopperMessage !== undefined) return hopperMessage;
|
||||
if (error.userMessage !== undefined) return error.userMessage;
|
||||
@@ -383,6 +386,12 @@ const standardErrorMessage = (tag: AnalysisErrorTag): string | undefined => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const timeoutUserMessage = (error: AnalysisTimeoutError): string =>
|
||||
`REA's ${error.operation} operation reached its ${String(error.timeoutMs)}ms server/provider deadline. If the MCP client timed out before this response, increase its tool deadline above ${String(error.timeoutMs)}ms; otherwise retry with a smaller request after checking session/provider state. Do not repeat an unchanged call while work or cleanup may still be active.`;
|
||||
|
||||
const timeoutRemediation = (error: AnalysisTimeoutError): string =>
|
||||
`Inspect session/provider state for ${error.operation} before retrying. If the MCP client ended the call before REA returned this error, raise its tool deadline above ${String(error.timeoutMs)}ms; otherwise retry with a smaller request. Do not repeat an unchanged call while work or cleanup may still be active.`;
|
||||
|
||||
const UNREADABLE_OUTPUT_TAGS: ReadonlySet<AnalysisErrorTag> = new Set([
|
||||
"AnalysisProtocolError",
|
||||
"AnalysisOutputError",
|
||||
|
||||
@@ -25,6 +25,52 @@ import { ProviderSelectionError } from "./providerSelectionError.js";
|
||||
import { analysisErrorProjectionSchema } from "../contracts/errorSchemas.js";
|
||||
import { nativeCallPartialObservationSchema } from "./native/nativeCallPartialObservation.js";
|
||||
|
||||
describe("agent recovery metadata", () => {
|
||||
it("names the input contract for malformed analysis arguments", () => {
|
||||
const projected = projectAnalysisError(
|
||||
new AnalysisInputError("inspect_analysis_view", undefined, [
|
||||
{
|
||||
path: ["view", "kind"],
|
||||
reason: "invalid_value",
|
||||
expected: ["summary", "module"],
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
expect(projected).toMatchObject({
|
||||
recovery: {
|
||||
kind: "inspect_input_contract",
|
||||
operation: "inspect_analysis_view",
|
||||
},
|
||||
remediation: {
|
||||
action:
|
||||
"Read the declared input schema or CLI usage for inspect_analysis_view; if using MCP and the catalog may be stale, call tools/list. Correct the listed fields, then retry. Do not repeat the unchanged call.",
|
||||
},
|
||||
});
|
||||
expect(analysisErrorProjectionSchema.parse(projected)).toEqual(projected);
|
||||
});
|
||||
|
||||
it("separates timeout-state inspection from client deadline recovery", () => {
|
||||
const projected = projectAnalysisError(
|
||||
new AnalysisTimeoutError("inspect_binary_layout", 2_500),
|
||||
);
|
||||
|
||||
expect(projected).toMatchObject({
|
||||
code: "provider_timeout",
|
||||
recovery: {
|
||||
kind: "inspect_timeout_state",
|
||||
operation: "inspect_binary_layout",
|
||||
timeout_ms: 2_500,
|
||||
},
|
||||
message: expect.stringContaining("server/provider deadline"),
|
||||
remediation: {
|
||||
action: expect.stringContaining("MCP client"),
|
||||
},
|
||||
});
|
||||
expect(analysisErrorProjectionSchema.parse(projected)).toEqual(projected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("capability failure retry disposition", () => {
|
||||
it.each([
|
||||
{ options: {}, retryable: false, category: "unsupported_provider" },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { projectArtifactIntegrityDetails } from "./artifactIntegrity.js";
|
||||
import type { AnalysisErrorRecovery } from "./analysisErrorRecovery.js";
|
||||
import { jsonValueSchema, type JsonValue } from "./jsonValue.js";
|
||||
import { isImmutableJsonSnapshot } from "./immutableJson.js";
|
||||
|
||||
@@ -102,6 +103,7 @@ export const projectAnalysisError = (
|
||||
: jsonValueSchema.parse(partialObservation),
|
||||
}),
|
||||
};
|
||||
const recovery = errorRecovery(error);
|
||||
return {
|
||||
code,
|
||||
category: analysisErrorCategory(error),
|
||||
@@ -116,10 +118,28 @@ export const projectAnalysisError = (
|
||||
remediation: {
|
||||
action: analysisErrorRemediationAction(error),
|
||||
},
|
||||
...(recovery === undefined ? {} : { recovery }),
|
||||
...(Object.keys(details).length === 0 ? {} : { details }),
|
||||
};
|
||||
};
|
||||
|
||||
const errorRecovery = (
|
||||
error: AnalysisError,
|
||||
): AnalysisErrorRecovery | undefined => {
|
||||
if (error instanceof AnalysisInputError)
|
||||
return {
|
||||
kind: "inspect_input_contract",
|
||||
operation: error.operation,
|
||||
};
|
||||
if (error instanceof AnalysisTimeoutError)
|
||||
return {
|
||||
kind: "inspect_timeout_state",
|
||||
operation: error.operation,
|
||||
timeout_ms: error.timeoutMs,
|
||||
};
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const underlyingErrorCode = (
|
||||
error: AnalysisError,
|
||||
): AnalysisErrorProjection["code"] => {
|
||||
@@ -619,5 +639,6 @@ export interface AnalysisErrorProjection extends Readonly<
|
||||
readonly remediation: Readonly<{
|
||||
action: string;
|
||||
}>;
|
||||
readonly recovery?: AnalysisErrorRecovery;
|
||||
readonly details?: Readonly<Record<string, JsonValue>>;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Small, bounded next-step metadata for agent-facing analysis failures.
|
||||
*
|
||||
* The prose recovery remains in `remediation.action`; these fields give a
|
||||
* caller a stable way to choose the next inspection without embedding a tool
|
||||
* catalog or caller arguments in every error.
|
||||
*/
|
||||
export type AnalysisErrorRecovery =
|
||||
| {
|
||||
readonly kind: "inspect_input_contract";
|
||||
readonly operation: string;
|
||||
}
|
||||
| {
|
||||
readonly kind: "inspect_timeout_state";
|
||||
readonly operation: string;
|
||||
readonly timeout_ms: number;
|
||||
};
|
||||
|
||||
export const analysisErrorRecoverySchema: z.ZodType<AnalysisErrorRecovery> =
|
||||
z.discriminatedUnion("kind", [
|
||||
z
|
||||
.object({
|
||||
kind: z.literal("inspect_input_contract"),
|
||||
operation: z.string().min(1),
|
||||
})
|
||||
.strict(),
|
||||
z
|
||||
.object({
|
||||
kind: z.literal("inspect_timeout_state"),
|
||||
operation: z.string().min(1),
|
||||
timeout_ms: z.number().int().positive(),
|
||||
})
|
||||
.strict(),
|
||||
]);
|
||||
@@ -31,6 +31,67 @@ const firstText = (result: {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const expectFittingSdkValidationRecovery = (result: {
|
||||
readonly isError?: boolean | undefined;
|
||||
readonly content: readonly {
|
||||
readonly type: string;
|
||||
readonly text?: string;
|
||||
}[];
|
||||
}): void => {
|
||||
expect(result.isError).toBe(true);
|
||||
expect(JSON.parse(firstText(result) ?? "null")).toMatchObject({
|
||||
error: {
|
||||
code: "invalid_request",
|
||||
recovery: {
|
||||
kind: "inspect_input_contract",
|
||||
operation: "procedure_pseudo_code",
|
||||
},
|
||||
details: {
|
||||
operation: "procedure_pseudo_code",
|
||||
issues: [
|
||||
{
|
||||
path: [],
|
||||
reason: "invalid_value",
|
||||
message: expect.stringContaining("required"),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const expectEscapedSdkDiagnosticRetention = async (
|
||||
client: Client,
|
||||
records: InvestigationRecords,
|
||||
previousEvidenceId: string,
|
||||
): Promise<void> => {
|
||||
const quotedKey = '"\\'.repeat(350);
|
||||
const quoted = await client.callTool({
|
||||
name: "procedure_pseudo_code",
|
||||
arguments: { required: "present", [quotedKey]: true },
|
||||
});
|
||||
expect(quoted.isError).toBe(true);
|
||||
expect(JSON.parse(firstText(quoted) ?? "null")).toMatchObject({
|
||||
error: {
|
||||
code: "resource_constraint",
|
||||
details: {
|
||||
reported_limits: {
|
||||
evidence_reference: { kind: "retained-evidence" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(Buffer.byteLength(JSON.stringify(quoted))).toBeLessThan(4096);
|
||||
const bundle = records.exportEvidenceBundle();
|
||||
expect(bundle.records).toHaveLength(2);
|
||||
const quotedOriginal = bundle.records.find(
|
||||
(record) => record.evidence_id !== previousEvidenceId,
|
||||
);
|
||||
expect(quotedOriginal?.raw_result).toMatchObject({
|
||||
content: [{ text: expect.stringContaining(quotedKey) }],
|
||||
});
|
||||
};
|
||||
|
||||
describe("MCP request bookkeeping", () => {
|
||||
it("releases list, tool, and completion state when cancellation arrives", () => {
|
||||
const completion = new AbortController();
|
||||
@@ -55,7 +116,7 @@ describe("MCP request bookkeeping", () => {
|
||||
describe("Modern protocol error delivery", () => {
|
||||
it("preserves the modern wire envelope when replacing an oversized error", async () => {
|
||||
const records = new InvestigationRecords();
|
||||
const delivery = new ToolResultDelivery(4096);
|
||||
const delivery = new ToolResultDelivery(16384);
|
||||
const info = { name: "wire-error-recovery", version: "1" };
|
||||
const handler = createMcpHandler(() => {
|
||||
const server = new EvidenceMcpServer(
|
||||
@@ -92,16 +153,28 @@ describe("Modern protocol error delivery", () => {
|
||||
});
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const invalidField = `unexpected_${"x".repeat(5000)}_tail`;
|
||||
const fitting = await client.callTool({
|
||||
name: "procedure_pseudo_code",
|
||||
arguments: { unexpected: true },
|
||||
arguments: { [invalidField]: true },
|
||||
});
|
||||
expect(fitting.isError).toBe(true);
|
||||
expect(firstText(fitting)).toContain("Input validation error");
|
||||
expect(JSON.parse(firstText(fitting) ?? "null")).toMatchObject({
|
||||
error: {
|
||||
code: "invalid_request",
|
||||
recovery: {
|
||||
kind: "inspect_input_contract",
|
||||
operation: "procedure_pseudo_code",
|
||||
},
|
||||
details: {
|
||||
issues: [{ message: expect.stringContaining(invalidField) }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const recovered = await client.callTool({
|
||||
name: "procedure_pseudo_code",
|
||||
arguments: { selected: "value", ["x".repeat(10000)]: true },
|
||||
arguments: { selected: "value", ["x".repeat(20000)]: true },
|
||||
});
|
||||
expect(recovered.isError).toBe(true);
|
||||
expect(recovered.structuredContent).toBeUndefined();
|
||||
@@ -137,7 +210,7 @@ describe("Modern protocol error delivery", () => {
|
||||
});
|
||||
}
|
||||
expect(Buffer.byteLength(JSON.stringify(toolResults[1]))).toBeLessThan(
|
||||
4096,
|
||||
16384,
|
||||
);
|
||||
expect(toolResults[1]?._meta).toMatchObject({
|
||||
provenance: { untrusted_content: true, source: "analysis_target" },
|
||||
@@ -203,8 +276,13 @@ describe("SDK validation error delivery", () => {
|
||||
name: "procedure_pseudo_code",
|
||||
arguments: { unexpected: true },
|
||||
});
|
||||
expect(fitting.isError).toBe(true);
|
||||
expect(firstText(fitting)).toContain("Input validation error");
|
||||
expectFittingSdkValidationRecovery(fitting);
|
||||
|
||||
await expect(
|
||||
client.callTool({ name: "missing_tool", arguments: {} }),
|
||||
).rejects.toThrow(
|
||||
"Call tools/list, select an advertised tool, and retry",
|
||||
);
|
||||
|
||||
const oversized = await client.callTool({
|
||||
name: "procedure_pseudo_code",
|
||||
@@ -260,15 +338,11 @@ describe("SDK validation error delivery", () => {
|
||||
evidenceReference.evidence_id,
|
||||
);
|
||||
|
||||
const quotedKey = '"\\'.repeat(350);
|
||||
const quoted = await client.callTool({
|
||||
name: "procedure_pseudo_code",
|
||||
arguments: { required: "present", [quotedKey]: true },
|
||||
});
|
||||
expect(quoted.isError).toBe(true);
|
||||
expect(firstText(quoted)).toContain("Input validation error");
|
||||
expect(Buffer.byteLength(JSON.stringify(quoted))).toBeLessThan(4096);
|
||||
expect(records.exportEvidenceBundle().records).toHaveLength(1);
|
||||
await expectEscapedSdkDiagnosticRetention(
|
||||
client,
|
||||
records,
|
||||
evidenceReference.evidence_id,
|
||||
);
|
||||
|
||||
const recovered = await client.callTool({
|
||||
name: "procedure_pseudo_code",
|
||||
|
||||
+125
-25
@@ -2,9 +2,11 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
||||
|
||||
import {
|
||||
isCallToolResult,
|
||||
isJSONRPCErrorResponse,
|
||||
isJSONRPCRequest,
|
||||
isJSONRPCNotification,
|
||||
isJSONRPCResultResponse,
|
||||
type CallToolResult,
|
||||
type Implementation,
|
||||
type Icon,
|
||||
type JSONRPCMessage,
|
||||
@@ -27,7 +29,10 @@ import {
|
||||
import { CATALOG_IDENTITY, createCatalogIdentity } from "../catalogIdentity.js";
|
||||
import { transformAdvertisedInputJsonSchema } from "../contracts/toolSchemaMetadata.js";
|
||||
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
|
||||
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
AnalysisInputError,
|
||||
AnalysisResourceConstraintError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { createEvidence } from "../domain/evidence.js";
|
||||
import { jsonObjectSchema } from "../domain/jsonValue.js";
|
||||
@@ -65,6 +70,39 @@ const hasAdvertisedInputJsonSchema = (value: unknown): boolean => {
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
/** Recognize the SDK's argument diagnostic after its wire size is checked. */
|
||||
const sdkInputValidationDiagnostic = (
|
||||
text: string | undefined,
|
||||
tool: string | undefined,
|
||||
): string | undefined => {
|
||||
if (text === undefined || tool === undefined) return undefined;
|
||||
const prefix = `Input validation error: Invalid arguments for tool ${tool}`;
|
||||
if (!text.startsWith(prefix) || text[prefix.length] !== ":") return undefined;
|
||||
const diagnostic = text.slice(prefix.length + 1).trimStart();
|
||||
return diagnostic.length === 0 ? undefined : diagnostic;
|
||||
};
|
||||
|
||||
/** Replace the tool payload after the SDK codec while retaining its envelope. */
|
||||
const replaceToolResultPayload = (
|
||||
original: CallToolResult,
|
||||
replacement: CallToolResult,
|
||||
): CallToolResult => {
|
||||
const {
|
||||
content: _content,
|
||||
structuredContent: _structuredContent,
|
||||
isError: _isError,
|
||||
_meta,
|
||||
...envelope
|
||||
} = original;
|
||||
return {
|
||||
...envelope,
|
||||
...replacement,
|
||||
...(_meta === undefined && replacement._meta === undefined
|
||||
? {}
|
||||
: { _meta: { ..._meta, ...replacement._meta } }),
|
||||
};
|
||||
};
|
||||
|
||||
/** Remove only client-facing output schemas; SDK registrations stay canonical. */
|
||||
const omitOutputSchemasFromToolsList = (
|
||||
message: JSONRPCMessage,
|
||||
@@ -305,7 +343,7 @@ class ErrorEvidenceTransport implements Transport {
|
||||
id !== undefined &&
|
||||
this.pendingToolLists.has(id);
|
||||
try {
|
||||
const recovered = this.recoverError(message, tool);
|
||||
const recovered = this.recoverProtocolError(message, tool);
|
||||
const advertised = omitToolSchemas
|
||||
? omitOutputSchemasFromToolsList(recovered, id)
|
||||
: recovered;
|
||||
@@ -377,16 +415,45 @@ class ErrorEvidenceTransport implements Transport {
|
||||
},
|
||||
}
|
||||
: message.result;
|
||||
const encoded =
|
||||
structured !== undefined
|
||||
? encodeToolResult(structured, budget, "error-text", targetContent)
|
||||
: measureMcpResultWithinBudget(sdkResultWithMarker, budget);
|
||||
if (encoded.ok) return message;
|
||||
const operation = tool ?? "mcp_tool_error";
|
||||
const originalResult = jsonObjectSchema.parse(message.result);
|
||||
const sdkDiagnostic = !isStructured
|
||||
? message.result.content.find((item) => item.type === "text")?.text
|
||||
: undefined;
|
||||
let encoded =
|
||||
structured !== undefined
|
||||
? encodeToolResult(structured, budget, "error-text", targetContent)
|
||||
: measureMcpResultWithinBudget(sdkResultWithMarker, budget);
|
||||
const normalized = encoded.ok
|
||||
? this.normalizeSdkValidationError(
|
||||
message,
|
||||
tool,
|
||||
targetContent,
|
||||
sdkInputValidationDiagnostic(sdkDiagnostic, tool),
|
||||
)
|
||||
: undefined;
|
||||
if (
|
||||
normalized !== undefined &&
|
||||
isJSONRPCResultResponse(normalized) &&
|
||||
isCallToolResult(normalized.result)
|
||||
) {
|
||||
// The structured diagnostic is a private carrier removed before sending.
|
||||
// Measure the actual wire result, including its preserved SDK metadata.
|
||||
const { structuredContent, ...wireResult } = normalized.result;
|
||||
encoded =
|
||||
normalized.result.content.length === 0
|
||||
? encodeToolResult(
|
||||
jsonObjectSchema.parse(structuredContent),
|
||||
budget,
|
||||
"error-text",
|
||||
targetContent,
|
||||
)
|
||||
: measureMcpResultWithinBudget(wireResult, budget);
|
||||
if (encoded.ok) return normalized;
|
||||
// JSON escaping and recovery fields can exceed the budget even when
|
||||
// the original SDK result fits. Retain that original before replacing it.
|
||||
}
|
||||
if (encoded.ok) return message;
|
||||
const operation = tool ?? "mcp_tool_error";
|
||||
const originalResult = jsonObjectSchema.parse(message.result);
|
||||
const retainedResult = structured ?? originalResult;
|
||||
const originalError = isStructured
|
||||
? analysisErrorProjectionSchema.safeParse(structured?.["error"])
|
||||
@@ -449,25 +516,58 @@ class ErrorEvidenceTransport implements Transport {
|
||||
: "Restart REA with a larger REA_MCP_MAX_RESPONSE_BYTES and matching client receive buffer, then retry. The original error could not be retained by this server.",
|
||||
},
|
||||
);
|
||||
// This transport runs after the SDK's wire codec. Replace the tool payload
|
||||
// while retaining its protocol envelope, including the revision's result
|
||||
// discriminator and server identity metadata.
|
||||
const {
|
||||
content: _content,
|
||||
structuredContent: _structuredContent,
|
||||
isError: _isError,
|
||||
_meta,
|
||||
...envelope
|
||||
} = message.result;
|
||||
const replacement = this.delivery.toErrorToolResult(failure, targetContent);
|
||||
return {
|
||||
...message,
|
||||
result: {
|
||||
...envelope,
|
||||
...replacement,
|
||||
...(_meta === undefined && replacement._meta === undefined
|
||||
? {}
|
||||
: { _meta: { ..._meta, ...replacement._meta } }),
|
||||
result: replaceToolResultPayload(message.result, replacement),
|
||||
};
|
||||
}
|
||||
|
||||
private normalizeSdkValidationError(
|
||||
message: JSONRPCMessage,
|
||||
tool: string | undefined,
|
||||
targetContent: boolean,
|
||||
diagnostic: string | undefined,
|
||||
): JSONRPCMessage | undefined {
|
||||
if (
|
||||
tool === undefined ||
|
||||
diagnostic === undefined ||
|
||||
!isJSONRPCResultResponse(message) ||
|
||||
!isCallToolResult(message.result)
|
||||
)
|
||||
return undefined;
|
||||
const error = new AnalysisInputError(
|
||||
tool,
|
||||
{
|
||||
userMessage: `The arguments for MCP tool ${tool} do not match its advertised input schema.`,
|
||||
},
|
||||
[{ path: [], reason: "invalid_value", message: diagnostic }],
|
||||
);
|
||||
const result = this.delivery.toErrorToolResult(error, targetContent);
|
||||
return {
|
||||
...message,
|
||||
result: replaceToolResultPayload(message.result, result),
|
||||
};
|
||||
}
|
||||
|
||||
private recoverProtocolError(
|
||||
message: JSONRPCMessage,
|
||||
tool: string | undefined,
|
||||
): JSONRPCMessage {
|
||||
const recovered = this.recoverError(message, tool);
|
||||
if (
|
||||
!isJSONRPCErrorResponse(recovered) ||
|
||||
tool === undefined ||
|
||||
recovered.error.code !== -32602 ||
|
||||
recovered.error.message !== `Tool ${tool} not found`
|
||||
)
|
||||
return recovered;
|
||||
return {
|
||||
...recovered,
|
||||
error: {
|
||||
...recovered.error,
|
||||
message:
|
||||
"The requested MCP tool is not advertised by this REA server. Call tools/list, select an advertised tool, and retry; do not repeat the unknown tool name.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -164,7 +164,18 @@ cliTest(
|
||||
},
|
||||
});
|
||||
expect(parseMcpToolError(response)).toMatchObject({
|
||||
error: { code: "provider_timeout" },
|
||||
error: {
|
||||
code: "provider_timeout",
|
||||
message: expect.stringContaining("server/provider deadline"),
|
||||
remediation: {
|
||||
action: expect.stringContaining("MCP client"),
|
||||
},
|
||||
recovery: {
|
||||
kind: "inspect_timeout_state",
|
||||
operation: "analyze_javascript_application",
|
||||
timeout_ms: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
const bundle = await getBundle(client);
|
||||
const partial = bundle.records.find(
|
||||
|
||||
@@ -132,7 +132,13 @@ describe("browser and Inspector CLI input diagnostics", () => {
|
||||
message:
|
||||
"Analysis input is invalid. Check the arguments and try again.",
|
||||
retryable: true,
|
||||
remediation: { action: "Correct the listed arguments and retry." },
|
||||
remediation: {
|
||||
action: `Read the declared input schema or CLI usage for ${input.operation}; if using MCP and the catalog may be stale, call tools/list. Correct the listed fields, then retry. Do not repeat the unchanged call.`,
|
||||
},
|
||||
recovery: {
|
||||
kind: "inspect_input_contract",
|
||||
operation: input.operation,
|
||||
},
|
||||
details: {
|
||||
operation: input.operation,
|
||||
issues: expect.arrayContaining([input.issue]),
|
||||
|
||||
@@ -184,7 +184,14 @@ describe("process CLI input diagnostics", () => {
|
||||
category: "invalid_input",
|
||||
message: "Analysis input is invalid. Check the arguments and try again.",
|
||||
retryable: true,
|
||||
remediation: { action: "Correct the listed arguments and retry." },
|
||||
remediation: {
|
||||
action:
|
||||
"Read the declared input schema or CLI usage for capture_process_scenario; if using MCP and the catalog may be stale, call tools/list. Correct the listed fields, then retry. Do not repeat the unchanged call.",
|
||||
},
|
||||
recovery: {
|
||||
kind: "inspect_input_contract",
|
||||
operation: "capture_process_scenario",
|
||||
},
|
||||
details: expect.objectContaining({
|
||||
operation: "capture_process_scenario",
|
||||
issues: expect.arrayContaining([
|
||||
|
||||
@@ -7,6 +7,7 @@ import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { ok } from "../../../src/domain/result.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { assertAdvertisedMcpContracts } from "./mcpContractHarness.js";
|
||||
|
||||
const identity = { id: "adb", name: "Android Debug Bridge", version: null };
|
||||
@@ -391,8 +392,24 @@ it("rejects invalid tool input before reaching the provider", async () => {
|
||||
arguments: { serial: "bad serial" },
|
||||
});
|
||||
expect(rejected.isError).toBe(true);
|
||||
const text = (rejected.content as { readonly text?: string }[])
|
||||
.map((part) => part.text ?? "")
|
||||
.join("");
|
||||
expect(text).toContain("Invalid arguments for tool inspect_adb_device");
|
||||
expect(parseMcpToolError(rejected)).toMatchObject({
|
||||
error: {
|
||||
code: "invalid_request",
|
||||
recovery: {
|
||||
kind: "inspect_input_contract",
|
||||
operation: "inspect_adb_device",
|
||||
},
|
||||
details: {
|
||||
operation: "inspect_adb_device",
|
||||
issues: [
|
||||
{
|
||||
path: [],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"serial: Device serial cannot contain whitespace or quotes",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -245,6 +245,16 @@ const verifySessionAndComparisonTools = async (
|
||||
},
|
||||
});
|
||||
expect(malformedComparison.isError).toBe(true);
|
||||
expect(parseMcpToolError(malformedComparison)).toMatchObject({
|
||||
error: {
|
||||
code: "invalid_request",
|
||||
recovery: {
|
||||
kind: "inspect_input_contract",
|
||||
operation: "compare_web_captures",
|
||||
},
|
||||
details: { operation: "compare_web_captures" },
|
||||
},
|
||||
});
|
||||
expect(JSON.stringify(malformedComparison.content)).toContain("types");
|
||||
const incompleteComparison = await connected.client.callTool({
|
||||
name: "compare_web_captures",
|
||||
|
||||
@@ -101,7 +101,14 @@ cliTest(
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
retryable: true,
|
||||
remediation: { action: "Correct the listed arguments and retry." },
|
||||
remediation: {
|
||||
action:
|
||||
"Read the declared input schema or CLI usage for capture_browser_scenario; if using MCP and the catalog may be stale, call tools/list. Correct the listed fields, then retry. Do not repeat the unchanged call.",
|
||||
},
|
||||
recovery: {
|
||||
kind: "inspect_input_contract",
|
||||
operation: "capture_browser_scenario",
|
||||
},
|
||||
details: {
|
||||
operation: "capture_browser_scenario",
|
||||
issues: [
|
||||
|
||||
@@ -160,9 +160,15 @@ it("retains byte-search coverage and rejects malformed pages before creating Evi
|
||||
arguments: args,
|
||||
});
|
||||
expect(invalid.isError).toBe(true);
|
||||
expect(JSON.stringify(invalid.content)).toContain(
|
||||
"Input validation error",
|
||||
);
|
||||
expect(parseMcpToolError(invalid)).toMatchObject({
|
||||
error: {
|
||||
code: "invalid_request",
|
||||
recovery: {
|
||||
kind: "inspect_input_contract",
|
||||
operation: "search_bytes",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
expect(harness.calls).toHaveLength(calls);
|
||||
} finally {
|
||||
|
||||
@@ -8,13 +8,17 @@ const LARGE_PATTERN_BYTES = 4 * 1024 * 1024;
|
||||
|
||||
const inputValidationText = (result: CallToolResult): string => {
|
||||
expect(result.isError).toBe(true);
|
||||
const text = result.content.find((item) => item.type === "text");
|
||||
if (text?.type !== "text") throw new Error("Input error omitted text");
|
||||
expect(text.text).toMatch(
|
||||
/^Input validation error: Invalid arguments for tool search_bytes:/u,
|
||||
);
|
||||
expect(text.text).not.toMatch(/RangeError|Maximum call stack/i);
|
||||
return text.text;
|
||||
const diagnostic = parseMcpToolError(result).error;
|
||||
expect(diagnostic).toMatchObject({
|
||||
code: "invalid_request",
|
||||
recovery: {
|
||||
kind: "inspect_input_contract",
|
||||
operation: "search_bytes",
|
||||
},
|
||||
});
|
||||
const text = JSON.stringify(diagnostic);
|
||||
expect(text).not.toMatch(/RangeError|Maximum call stack/i);
|
||||
return text;
|
||||
};
|
||||
|
||||
it("validates large search_bytes inputs without breaking stdio MCP", async () => {
|
||||
|
||||
@@ -578,10 +578,26 @@ describe("json depth bounds over MCP", () => {
|
||||
right: input.right,
|
||||
},
|
||||
});
|
||||
const text = JSON.stringify(result.content);
|
||||
expect(result.isError, text).toBe(true);
|
||||
expect(text).toContain("Input validation");
|
||||
expect(text).toContain("maximum nesting depth");
|
||||
expect(result.isError, JSON.stringify(result.content)).toBe(true);
|
||||
expect(parseMcpToolError(result)).toMatchObject({
|
||||
error: {
|
||||
code: "invalid_request",
|
||||
recovery: {
|
||||
kind: "inspect_input_contract",
|
||||
operation: "compare_process_captures",
|
||||
},
|
||||
details: {
|
||||
operation: "compare_process_captures",
|
||||
issues: [
|
||||
{
|
||||
path: [],
|
||||
reason: "invalid_value",
|
||||
message: `left.parameters: JSON value exceeds maximum nesting depth of ${MAX_JSON_DEPTH}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}, 10_000);
|
||||
});
|
||||
|
||||
|
||||
@@ -77,7 +77,18 @@ it("retains timeout Evidence and recovers on the same MCP server", async () => {
|
||||
},
|
||||
});
|
||||
const timeout = parseMcpToolError(interrupted).error;
|
||||
expect(timeout.code).toBe("provider_timeout");
|
||||
expect(timeout).toMatchObject({
|
||||
code: "provider_timeout",
|
||||
message: expect.stringContaining("server/provider deadline"),
|
||||
remediation: {
|
||||
action: expect.stringContaining("MCP client"),
|
||||
},
|
||||
recovery: {
|
||||
kind: "inspect_timeout_state",
|
||||
operation: "analyze_web_bundle",
|
||||
timeout_ms: 1,
|
||||
},
|
||||
});
|
||||
const timeoutDetails = z
|
||||
.object({ partial_observation: z.unknown() })
|
||||
.passthrough()
|
||||
|
||||
Reference in new issue
Block a user