You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(appkit): tool() name optional, execute returns unknown, docs accurate
Address two P0 findings from the PR #306 agentic review.
1. tool() runtime crash on the template
`npx @databricks/appkit init` produces a helper agent whose tools
call `tool({ description, schema, execute })` with no `name` field
(because the record key — `current_time`, `count_words` — already
names the tool). The previous shape required `name: string`, so
`isFunctionTool` rejected the returned FunctionTool and agent
registration failed with "Agent 'helper' tool 'current_time' has
an unrecognized shape" — the very first `pnpm dev` after init.
Make `name` optional on both ToolConfig and FunctionTool, drop the
`typeof obj.name === "string"` check from `isFunctionTool`, and
widen `execute` from `Promise<string> | string` to `unknown |
Promise<unknown>` since the template's tools naturally return
objects (`{ now: ... }`). The agents plugin already overrides
`name` with the record key at index-build time, so the original
field was functionally redundant in keyed-record contexts. Zod
error messages fall back to a generic "tool" label when the name
is omitted.
Downstream: agents plugin already normalises non-string results
via `normalizeToolResult`; DatabricksAdapter already `JSON.stringify`s
non-string returns at line 438-439. No further changes needed.
2. Level 1 docs lied about auto-inherit
`docs/docs/plugins/agents.md` Level 1 said "auto-inherits every
registered ToolProvider plugin's tools" but the Configuration
Reference correctly states `autoInheritTools` defaults to
`{ file: false, code: false }`. New users following Level 1 hit
zero tools and confusion.
Rewrite Level 1 narrative to be truthful: "The agent starts with
no tools. Tools are opt-in — declare them in frontmatter (Level 2)
or opt into auto-inherit explicitly with `agents({
autoInheritTools: { file: true } })`. See 'Auto-inherit posture'
below."
Same doc still had stale `fromPlugin(...)` examples in Level 3 +
the standalone runAgent section. Rewritten to use the
`tools(plugins) => ...` function form that v5 actually ships,
matching the rest of the codebase. The runAgent section also gets
a sentence on the new init contract (eager attachContext + setup,
shared cache across sub-agents) and the trust-boundary caveat (no
OBO, no approval gate).
Tests: +3 covering the new shape — name optional, non-string
returns, zod-error fallback label. Updated the "returns false when
name is missing" guard test to assert the new "returns true" since
the record key wins.
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Copy file name to clipboardExpand all lines: docs/docs/plugins/agents.md
+30-29Lines changed: 30 additions & 29 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -54,8 +54,9 @@ On startup the plugin:
54
54
1. Discovers `./config/agents/assistant/agent.md` and registers agent id `assistant`.
55
55
2. Parses the YAML frontmatter and markdown body as the agent's `instructions`.
56
56
3. Resolves the adapter from `endpoint` (or falls back to `DATABRICKS_AGENT_ENDPOINT`).
57
-
4. Auto-inherits every registered ToolProvider plugin's tools (`analytics.*`, `files.*`, …).
58
-
5. Mounts the agent at the default name (`assistant`).
57
+
4. Mounts the agent at the default name (`assistant`).
58
+
59
+
The agent starts with **no tools**. Tools are opt-in — declare them in frontmatter (Level 2 below) or opt into auto-inherit explicitly with `agents({ autoInheritTools: { file: true } })`. See "Auto-inherit posture" further down for what that costs and why it's off by default.
59
60
60
61
Requests land at `POST /invocations` with an OpenAI Responses-compatible body. Every tool call runs through `asUser(req)` so SQL executes as the requesting user, file access respects Unity Catalog ACLs, and telemetry spans are created automatically.
61
62
@@ -87,30 +88,23 @@ When any `tools:` is declared the auto-inherit default is turned off — the age
87
88
## Level 3: code-defined agents
88
89
89
90
```ts
90
-
import {
91
-
agents,
92
-
analytics,
93
-
createAgent,
94
-
createApp,
95
-
files,
96
-
fromPlugin,
97
-
server,
98
-
tool,
99
-
} from"@databricks/appkit";
91
+
import { agents, analytics, createApp, files, server } from"@databricks/appkit";
Code-defined agents start with no tools by default. `fromPlugin(factory)` is the primary way to pull in a plugin's tools — it returns a spread-friendly marker that the agents plugin resolves against registered `ToolProvider`s at setup time. No intermediate variable, no duplicate `plugins: [analyticsP, filesP, ...]` dance: you write the factory reference once inside `fromPlugin` and again in `plugins: [...]`.
116
+
Code-defined agents start with no tools by default. The function form `tools(plugins) => Record<string, AgentTool>` is the primary way to pull in plugin tools: each plugin registered in `createApp({ plugins: [...] })` shows up on the `plugins` parameter, and you call `.toolkit(opts?)` on it to get a spread-friendly record. The runtime invokes the function once at agent setup and caches the result — every plugin is mentioned exactly once (in `createApp`), with no held variables or marker imports.
117
+
118
+
Inline `tool({...})` calls live in the same record. `name` is optional — the agents plugin overrides it with the record key (`get_weather` above).
123
119
124
120
The asymmetry (file: auto-inherit, code: strict) matches the personas: prompt authors want zero ceremony, engineers want no surprises.
125
121
126
122
### Scoping tools in code
127
123
128
-
`fromPlugin(factory, opts?)` accepts the same `ToolkitOptions` as markdown frontmatter:
124
+
`plugins.<name>.toolkit(opts?)` accepts the same `ToolkitOptions` as markdown frontmatter:
129
125
130
126
| Option | Example | Meaning |
131
127
|---|---|---|
@@ -134,7 +130,7 @@ The asymmetry (file: auto-inherit, code: strict) matches the personas: prompt au
134
130
|`prefix`|`{ prefix: "" }`| Drop the `${pluginName}.` prefix |
135
131
|`rename`|`{ rename: { query: "q" } }`| Remap specific local names |
136
132
137
-
For plugins that don't expose a `.toolkit()` method (e.g., third-party `ToolProvider` plugins authored with plain `toPlugin`), `fromPlugin` falls back to walking `getAgentTools()` and synthesizing namespaced keys (`${pluginName}.${localName}`). The fallback respects `only` / `except` / `rename` / `prefix` the same way.
133
+
For plugins that don't expose a `.toolkit()` method (e.g., third-party `ToolProvider` plugins authored with plain `toPlugin`), the runtime falls back to walking `getAgentTools()` and synthesizing namespaced keys (`${pluginName}.${localName}`). The fallback respects `only` / `except` / `rename` / `prefix` the same way.
138
134
139
135
If a referenced plugin is not registered in `createApp({ plugins })`, the agents plugin throws at setup with an `Available: …` listing so you can fix the wiring before the first request.
140
136
@@ -190,15 +186,18 @@ for (const ticket of tickets) {
190
186
}
191
187
```
192
188
193
-
`runAgent` drives the adapter without `createApp` or HTTP. Inline `tool()` calls work standalone as shown above. To use plugin tools in standalone mode, pass the plugin factories through `RunAgentInput.plugins`— `runAgent` will resolve any `fromPlugin` markers in the def against that list:
189
+
`runAgent` drives the adapter without `createApp` or HTTP. Inline `tool()` calls work standalone as shown above. To use plugin tools in standalone mode, pass the plugin factories through `RunAgentInput.plugins`and reach into them via the `tools(plugins)` function form:
instructions: "Classify tickets. Use analytics.query for historical data.",
200
197
model: "databricks-claude-sonnet-4-5",
201
-
tools: { ...fromPlugin(analytics) },
198
+
tools(plugins) {
199
+
return { ...plugins.analytics.toolkit() };
200
+
},
202
201
});
203
202
204
203
const result =awaitrunAgent(classifier, {
@@ -207,7 +206,9 @@ const result = await runAgent(classifier, {
207
206
});
208
207
```
209
208
210
-
Hosted tools (MCP) are still `agents()`-only since they require the live MCP client. Plugin tool dispatch in standalone mode runs as the service principal (no OBO) since there is no HTTP request.
209
+
`runAgent` eagerly constructs each plugin in `RunAgentInput.plugins`, runs the standard `attachContext({})` + `await setup()` lifecycle, and shares the instances across the top-level run and every sub-agent dispatch. Plugins whose `setup()` requires `createApp`-only runtime (e.g. `WorkspaceClient`, `ServiceContext`) throw at standalone-init with a clear "use createApp instead" message rather than mid-stream.
210
+
211
+
Hosted tools (MCP) are still `agents()`-only since they require the live MCP client. Plugin tool dispatch in standalone mode runs as the service principal (no OBO) and **bypasses the agents-plugin approval gate** — treat standalone runAgent as a trusted-prompt environment (CI, batch eval, internal scripts), not as an exposed user-facing surface.
0 commit comments