← Back to PRs

#18860: feat(agents): expose tools and their schemas via new after_tools_resolved hook [AI-assisted]

by lan17 open 2026-02-17 04:32 View on GitHub →
agents size: S
## Summary - **Problem:** Plugins have no way to discover which tools are available to an agent or inspect their input schemas. The plugin API exposes `registerTool()` but no `listTools()` or `getTools()`. Built-in tools (exec, read, write, web_fetch, etc.) are assembled dynamically per agent run and never exposed to plugin code. - **Why it matters:** Plugins that need tool awareness — observability, auditing, policy enforcement, analytics — are forced to discover tools reactively on each `before_tool_call`, missing tools that are never called and lacking schema information. - **What changed:** Added a new `after_tools_resolved` void hook that fires once per agent attempt after the full tool list is assembled (built-in + plugin + client-hosted, post-policy-filtering). The event includes tool name, label, description, and deep-cloned JSON Schema parameters. - **What did NOT change (scope boundary):** No existing hooks or tool assembly logic modified. Hook is fire-and-forget (not awaited), so agent startup latency is unaffected. Not fired during compaction runs. ## Change Type (select all) - [ ] Bug fix - [x] Feature - [ ] Refactor - [ ] Docs - [ ] Security hardening - [ ] Chore/infra ## Scope (select all touched areas) - [x] Gateway / orchestration - [x] Skills / tool execution - [ ] Auth / tokens - [ ] Memory / storage - [x] Integrations - [x] API / contracts - [ ] UI / DX - [ ] CI/CD / infra ## Linked Issue/PR None ## User-visible / Behavior Changes - New plugin hook `after_tools_resolved` available via `api.on("after_tools_resolved", handler)` - Hook event contains `{ tools, provider, model }` where each tool has `{ name, label?, description?, parameters? }` - Parameters are deep-cloned via `structuredClone` — plugins cannot mutate runtime tool schemas - Hook context includes `agentId`, `sessionKey`, `sessionId`, `workspaceDir` - Fire-and-forget: plugin handler errors are caught and logged, never block agent startup ## Security Impact (required) - New permissions/capabilities? `No` — read-only observability hook, no new capabilities granted - Secrets/tokens handling changed? `No` - New/changed network calls? `No` - Command/tool execution surface changed? `No` — void hook, cannot modify or block tool execution - Data access scope changed? `No` — exposes tool metadata (names, schemas) that plugins could already observe via `before_tool_call` - If any `Yes`, explain risk + mitigation: N/A ## Repro + Verification ### Environment - OS: Ubuntu (remote VM) - Runtime/container: Node.js 22 - Model/provider: Anthropic / claude-opus-4-6 - Integration/channel: WhatsApp - Relevant config: Default agent with full tool profile ### Steps 1. Create a workspace plugin at `{workspaceDir}/.openclaw/extensions/test-tools-hook/`: `openclaw.plugin.json`: ```json { "id": "test-tools-hook", "configSchema": { "type": "object", "additionalProperties": false, "properties": {} } } ``` `index.ts`: ```typescript export default function testToolsHook(api) { api.on("after_tools_resolved", async (event, ctx) => { const toolNames = event.tools.map((t) => t.name).join(", "); console.log(`[test-tools-hook] agent=${ctx.agentId} provider=${event.provider} model=${event.model}`); console.log(`[test-tools-hook] ${event.tools.length} tools resolved: ${toolNames}`); for (const tool of event.tools) { const params = tool.parameters?.properties ? Object.keys(tool.parameters.properties).join(", ") : "(no schema)"; console.log(` - ${tool.name}: ${params}`); } }); } ``` 2. Restart the gateway: `openclaw gateway --force --verbose` 3. Send a WhatsApp message to trigger an agent run ### Expected - Gateway logs show `[test-tools-hook]` lines with agent ID, provider, model, full tool list, and per-tool parameter names - Agent startup is not delayed by the hook ### Actual - Hook fired with 24 tools resolved including all built-in, plugin, and skill tools - Each tool listed with its full parameter schema (parameter names extracted from JSON Schema `properties`) - Agent responded normally with no added latency ## Evidence - [x] Failing test/log before + passing after - [x] Trace/log snippets - [ ] Screenshot/recording - [ ] Perf numbers (if relevant) **Live gateway log output (WhatsApp inbound → hook fires → agent responds):** ``` 04:55:20 [test-tools-hook] agent=main provider=anthropic model=claude-opus-4-6 04:55:20 [test-tools-hook] 24 tools resolved: read, edit, write, exec, process, browser, canvas, nodes, cron, message, tts, gateway, agents_list, sessions_list, sessions_history, sessions_send, sessions_spawn, subagents, session_status, web_search, web_fetch, image, memory_search, memory_get 04:55:20 - read: path, offset, limit, file_path 04:55:20 - edit: path, oldText, newText, file_path, old_string, new_string 04:55:20 - write: path, content, file_path 04:55:20 - exec: command, workdir, env, yieldMs, background, timeout, pty, elevated, host, security, ask, node 04:55:20 - process: action, sessionId, data, keys, hex, literal, text, bracketed, eof, offset, limit, timeout 04:55:20 - browser: action, target, node, profile, targetUrl, targetId, limit, maxChars, mode, snapshotFormat, refs, interactive, compact, depth, selector, frame, labels, fullPage, ref, element, type, level, paths, inputRef, timeoutMs, accept, promptText, request 04:55:20 - canvas: action, gatewayUrl, gatewayToken, timeoutMs, node, target, x, y, width, height, url, javaScript, outputFormat, maxWidth, quality, delayMs, jsonl, jsonlPath 04:55:20 - nodes: action, gatewayUrl, gatewayToken, timeoutMs, node, requestId, title, body, sound, priority, delivery, facing, maxWidth, quality, delayMs, deviceId, duration, durationMs, includeAudio, fps, screenIndex, outPath, maxAgeMs, locationTimeoutMs, desiredAccuracy, command, cwd, env, commandTimeoutMs, invokeTimeoutMs, needsScreenRecording, invokeCommand, invokeParamsJson 04:55:20 - cron: action, gatewayUrl, gatewayToken, timeoutMs, includeDisabled, job, jobId, id, patch, text, mode, runMode, contextMessages 04:55:20 - message: action, channel, target, targets, accountId, dryRun, message, effectId, effect, media, filename, buffer, contentType, mimeType, caption, path, filePath, replyTo, threadId, asVoice, silent, quoteText, bestEffort, gifPlayback, messageId, emoji, remove, targetAuthor, targetAuthorUuid, groupId, limit, before, after, around, fromMe, includeArchived, ... 04:55:20 - tts: text, channel 04:55:20 - gateway: action, delayMs, reason, gatewayUrl, gatewayToken, timeoutMs, raw, baseHash, sessionKey, note, restartDelayMs 04:55:20 - agents_list: 04:55:20 - sessions_list: kinds, limit, activeMinutes, messageLimit 04:55:20 - sessions_history: sessionKey, limit, includeTools 04:55:20 - sessions_send: sessionKey, label, agentId, message, timeoutSeconds 04:55:20 - sessions_spawn: task, label, agentId, model, thinking, runTimeoutSeconds, timeoutSeconds, cleanup 04:55:20 - subagents: action, target, message, recentMinutes 04:55:20 - session_status: sessionKey, model 04:55:20 - web_search: query, count, country, search_lang, ui_lang, freshness 04:55:20 - web_fetch: url, extractMode, maxChars 04:55:20 - image: prompt, image, images, model, maxBytesMb, maxImages 04:55:20 - memory_search: query, maxResults, minScore 04:55:20 - memory_get: path, from, lines ``` **New unit tests pass:** - `buildAfterToolsResolvedToolMetadata`: deep clone mutation safety, undefined parameters handling - `wired-hooks-after-tools-resolved.test.ts`: hook runner invocation, `hasHooks` registration ## Human Verification (required) - Verified scenarios: Hook fires with correct tool metadata on live WhatsApp agent run (24 tools with full parameter schemas), agent startup and response unaffected - Edge cases checked: Undefined parameters (client tools), empty tool names filtered out, empty label/description omitted, `agents_list` tool with no parameters shown correctly - What you did **not** verify: Retry emission deduplication (consumer-side concern), compaction path (deliberately excluded) ## Compatibility / Migration - Backward compatible? `Yes` — purely additive, no existing behavior changed - Config/env changes? `No` - Migration needed? `No` - If yes, exact upgrade steps: N/A ## Failure Recovery (if this breaks) - How to disable/revert this change quickly: Revert this commit. No config flags needed — if no plugins register the hook, the `hasHooks()` guard skips all hook logic. - Files/config to restore: N/A - Known bad symptoms reviewers should watch for: Unhandled promise rejections in gateway logs (mitigated by explicit `.catch()` on the fire-and-forget call) ## Risks and Mitigations - Risk: Hook fires per attempt, not per user request (retries cause repeated emissions) - Mitigation: By design — each attempt is a fresh tool resolution. Consumers should dedupe if needed (e.g., hash-based caching). Documented in hook description. - Risk: `structuredClone` failure on exotic parameter shapes - Mitigation: `cloneToolParametersForHook` wraps in try/catch, falls back to `undefined`. Tool metadata still emitted without parameters. <!-- greptile_comment --> <h3>Greptile Summary</h3> Adds a new `after_tools_resolved` hook that fires once per agent attempt after the full tool list is assembled (built-in + plugin + client-hosted tools). The hook provides read-only observability of tool metadata (name, label, description, JSON Schema parameters) and is fire-and-forget (non-blocking). Tool parameters are deep-cloned via `structuredClone` to prevent plugin mutations from affecting runtime schemas. The implementation is clean, well-tested, and purely additive with no changes to existing behavior. <h3>Confidence Score: 5/5</h3> - This PR is safe to merge with minimal risk - The implementation is purely additive (new hook, no existing logic modified), ...

Most Similar PRs