* Wrap an action's run function with schema validation. * Invalid inputs get a clear error message (including what was actually passed) * so the agent can see its own mistake and correct it on the next turn.
( schema: StandardSchemaV1, run: Function, toolParameters?: ActionTool["parameters"], )
| 390 | * so the agent can see its own mistake and correct it on the next turn. |
| 391 | */ |
| 392 | function wrapWithValidation( |
| 393 | schema: StandardSchemaV1, |
| 394 | run: Function, |
| 395 | toolParameters?: ActionTool["parameters"], |
| 396 | ): (args: any) => any { |
| 397 | return async (args: any) => { |
| 398 | const result = await schema["~standard"].validate(args); |
| 399 | if (result.issues) { |
| 400 | // Split issues into "missing required field" vs other validation errors |
| 401 | // so the error message reads naturally rather than as "fieldName: Required". |
| 402 | const missing: string[] = []; |
| 403 | const other: string[] = []; |
| 404 | for (const issue of result.issues) { |
| 405 | const pathStr = issue.path |
| 406 | ? issue.path.map((p) => (typeof p === "object" ? p.key : p)).join(".") |
| 407 | : ""; |
| 408 | const msg = String(issue.message ?? ""); |
| 409 | // Zod emits "Required" for missing fields; other libraries may use |
| 410 | // similar wording. Treat any variant as "missing". |
| 411 | if ( |
| 412 | pathStr && |
| 413 | (msg === "Required" || |
| 414 | /invalid.*undefined/i.test(msg) || |
| 415 | /expected.*received undefined/i.test(msg)) |
| 416 | ) { |
| 417 | missing.push(pathStr); |
| 418 | } else { |
| 419 | other.push(pathStr ? `${pathStr}: ${msg}` : msg); |
| 420 | } |
| 421 | } |
| 422 | |
| 423 | const parts: string[] = []; |
| 424 | if (missing.length > 0) { |
| 425 | parts.push( |
| 426 | `Missing required parameter${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}`, |
| 427 | ); |
| 428 | } |
| 429 | if (other.length > 0) { |
| 430 | parts.push(other.join("; ")); |
| 431 | } |
| 432 | |
| 433 | // Echo the args that were actually passed so the caller (usually an |
| 434 | // agent) can see exactly what it sent and fix its next call. |
| 435 | let received: string; |
| 436 | try { |
| 437 | received = JSON.stringify(args); |
| 438 | if (received.length > 500) received = received.slice(0, 500) + "…"; |
| 439 | } catch { |
| 440 | received = String(args); |
| 441 | } |
| 442 | |
| 443 | // Also show the EXPECTED signature so the agent doesn't have to guess. |
| 444 | // Format: `{ deckId*: string, content*: string, slideId?: string, ... }` |
| 445 | // where `*` = required, `?` = optional. |
| 446 | let expected = ""; |
| 447 | if (toolParameters?.properties) { |
| 448 | const required = new Set(toolParameters.required ?? []); |
| 449 | const sig = Object.entries(toolParameters.properties) |
no test coverage detected