#18860: feat(agents): expose tools and their schemas via new after_tools_resolved hook [AI-assisted]
agents
size: S
Cluster:
Subagent Enhancements and Features
## 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
#21035: feat: agent hardening — modifying after_tool_call hook, cron contex...
by roelven · 2026-02-19
78.5%
#20580: feat(hooks): bridge after_tool_call to internal hook handler system
by CryptoKrad · 2026-02-19
77.0%
#17667: feat: tool-hooks extension — run shell commands on tool calls
by FaradayHunt · 2026-02-16
76.5%
#18810: feat(hooks): wire before_tool_call/after_tool_call with veto support
by vjranagit · 2026-02-17
75.2%
#10678: feat(hooks): wire after_tool_call hook into tool execution pipeline
by yassinebkr · 2026-02-06
75.0%
#19422: fix: pass session context to plugin tool hooks in toToolDefinitions
by namabile · 2026-02-17
73.8%
#7771: Hooks: wire lifecycle events and tests
by rabsef-bicrym · 2026-02-03
73.1%
#16028: feat/before-tool-result
by ambushalgorithm · 2026-02-14
72.8%
#21588: fix(security): decouple external hook trust from session key format
by nabbilkhan · 2026-02-20
72.3%
#18889: feat(hooks): add agent and tool lifecycle boundaries
by vincentkoc · 2026-02-17
71.3%