Skip to content

feat(appkit): add fromPlugin() for referencing plugin tools in code-defined agents#298

Closed
MarioCadenas wants to merge 1 commit intoagent/5e-prepare-plugins-forward-instancefrom
agent/5f-from-plugin-api
Closed

feat(appkit): add fromPlugin() for referencing plugin tools in code-defined agents#298
MarioCadenas wants to merge 1 commit intoagent/5e-prepare-plugins-forward-instancefrom
agent/5f-from-plugin-api

Conversation

@MarioCadenas
Copy link
Copy Markdown
Collaborator

@MarioCadenas MarioCadenas commented Apr 21, 2026

Summary

Introduces fromPlugin(factory, opts?) — a spread-friendly, symbol-keyed marker for pulling a plugin's tools into a code-defined agent without needing an intermediate variable or an eager .toolkit() call.

Before

const analyticsP = analytics();
const filesP = files();
const support = createAgent({
  tools: {
    ...analyticsP.toolkit(),
    ...filesP.toolkit({ only: ["uploads.read"] }),
  },
});
await createApp({ plugins: [server(), analyticsP, filesP, agents({ agents: { support } })] });

After

const support = createAgent({
  tools: {
    ...fromPlugin(analytics),
    ...fromPlugin(files, { only: ["uploads.read"] }),
  },
});
await createApp({ plugins: [server(), analytics(), files(), agents({ agents: { support } })] });

How it works

  • fromPlugin(factory) reads factory.pluginName (stamped onto every toPlugin-produced factory) and returns a single symbol-keyed entry: { [Symbol()]: FromPluginMarker }. Each call generates a fresh Symbol(), so multiple spreads of the same plugin coexist safely.
  • AgentsPlugin.buildToolIndex walks Object.getOwnPropertySymbols(def.tools), resolves each marker against context.getToolProviders(), and calls the plugin's .toolkit(opts) method. Missing plugins throw at setup with an Available: ... listing — wiring errors surface on boot, not mid-request.
  • For ToolProviders without a .toolkit() method (e.g. third-party plugins built with plain toPlugin), the resolver falls back to walking getAgentTools() and synthesizing namespaced keys (${pluginName}.${localName}) while still honoring only / except / rename / prefix.

Type plumbing

  • NamedPluginFactory type + pluginName stamp on the factory returned by toPlugin / toPluginWithInstance.
  • AgentDefinition.tools widened to { [key: string]: AgentTool } & { [key: symbol]: FromPluginMarker } so spread compiles under strict TS while preserving string-key autocomplete.
  • hasExplicitTools in buildToolIndex now counts symbol keys, so a tools: { ...fromPlugin(x) } record correctly disables auto-inherit on code-defined agents.

runAgent support

RunAgentInput gains an optional plugins?: PluginData[] argument so fromPlugin markers work in standalone mode too. Plugin tool dispatch in standalone mode runs as the service principal (no OBO, no HTTP request). If a def contains markers and the argument is absent, runAgent throws with guidance.

Migration

  • agent-app (apps/agent-app/server.ts): rewritten to spread fromPlugin(analytics) / fromPlugin(files, ...) instead of the .toolkit() dance.
  • dev-playground (apps/dev-playground/server/index.ts): gains a new code-defined helper agent demonstrating the API alongside the markdown-driven autocomplete agent.
  • Docs updated: new "Scoping tools in code" section in agents.md; new fromPlugin section in the migration guide with before/after.

PR Stack

  1. Shared types + Adapters — feat(appkit): add shared agent types and LLM adapter implementations #282
  2. Tool types + MCP client — feat(appkit): add FunctionTool, HostedTool types and MCP client #283
  3. Agent plugin core + ToolProvider implementations — feat(appkit): add agent plugin core and ToolProvider implementations #284
  4. PluginContext mediator — feat(appkit): add PluginContext mediator for inter-plugin communication #285
  5. createAgent + agent-app + docs (v1) — feat(appkit): add createAgent wrapper, agent-app, and API docs #286
  6. Plugin context-binding separation — refactor(appkit): separate plugin context binding from plugin construction #293
  7. agents() plugin + createAgent(def) + .toolkit()feat(appkit): add agents() plugin, createAgent() factory, and .toolkit() #294
  8. agent-app + docs migrated to agents()feat(appkit): migrate agent-app and docs to the new agents() plugin #295
  9. Relocate shared agent utilities — refactor(appkit): relocate shared agent utilities into plugins/agents #296
  10. preparePlugins forwards eager instance — refactor(appkit): forward eager plugin instance through preparePlugins #297
  11. fromPlugin() API (this PR)
  12. Retire deprecated agent() + createAgentAppchore(appkit): remove deprecated agent() plugin and createAgentApp shortcut #299
  13. Retire toPluginWithInstance + bug fixes — refactor(appkit): retire toPluginWithInstance; consolidate on fromPlugin + fix schema/routing bugs #300

Test plan

  • New test file packages/appkit/src/plugins/agents/tests/from-plugin.test.ts (marker shape, symbol uniqueness, type guard, factory without pluginName)
  • Extended agents-plugin.test.ts with: marker resolution, mixed inline + marker, structural error assertions, symbol-only disables auto-inherit, fallback-to-getAgentTools
  • Extended run-agent.test.ts with: standalone marker resolution via plugins arg + guidance error when missing
  • Full vitest suite passing
  • Typecheck clean

…efined agents

Introduces `fromPlugin(factory, opts?)` — a spread-friendly symbol-keyed
marker that references a plugin's tools inside an `AgentDefinition.tools`
record without needing an intermediate `const analyticsP = analytics()`
variable or a `.toolkit()` call.

```ts
const support = createAgent({
  instructions: "…",
  tools: {
    ...fromPlugin(analytics),
    ...fromPlugin(files, { only: ["uploads.read"] }),
    get_weather: tool({ … }),
  },
});

await createApp({
  plugins: [server(), analytics(), files(), agents({ agents: { support } })],
});
```

At setup time `AgentsPlugin.buildToolIndex` walks
`Object.getOwnPropertySymbols(def.tools)`, resolves each marker against
`PluginContext.getToolProviders()`, and calls the provider's `.toolkit(opts)`
method. For providers without `.toolkit()` (third-party ToolProviders or
plugins built with plain `toPlugin`), the resolver falls back to walking
`getAgentTools()` and synthesizing namespaced keys
(`\${pluginName}.\${localName}`), respecting `only` / `except` / `rename` /
`prefix` the same way. Missing plugins throw at setup with an
`Available: …` listing so wiring errors surface on boot, not mid-request.

`runAgent` gains an optional `plugins?: PluginData[]` argument so
`fromPlugin` markers work in standalone mode too (plugin tools dispatch
as the service principal since there is no HTTP request).

Factory identity is carried via a new `pluginName` field stamped onto
the returned function inside `toPlugin` / `toPluginWithInstance`, which
`fromPlugin` reads synchronously — no throwaway instance construction.

`.toolkit()` is not deprecated — it remains the power-user path for
renaming or combining tools in ways `fromPlugin`'s options can't express.
Docs lead with `fromPlugin` as the primary shape.

Also:

- Widens `AgentDefinition.tools` to
  `{ [key: string]: AgentTool } & { [key: symbol]: FromPluginMarker }`
  so spread compiles under strict TS while preserving string-key
  autocomplete.
- `hasExplicitTools` now includes symbol keys, so a `tools: { ...fromPlugin(x) }`
  record correctly disables auto-inherit on code-defined agents.
- Shared `resolveToolkitFromProvider` helper between auto-inherit and
  fromPlugin resolution paths.
- Migrates apps/agent-app/server.ts to spread `fromPlugin` instead of
  hand-writing the support agent's tool list.
- Adds a new code-defined `helper` agent to apps/dev-playground/server/index.ts
  showing the API alongside the markdown-driven `autocomplete` agent.
- Docs updated: new "Scoping tools in code" section in agents.md; new
  fromPlugin section in the migration guide with before/after.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas
Copy link
Copy Markdown
Collaborator Author

Superseded by the v2 6-PR stack:

  1. Shared agent types + LLM adapters — feat(appkit): shared agent types and LLM adapter implementations #301
  2. Tool primitives + ToolProvider surfaces — feat(appkit): tool primitives and ToolProvider surfaces on core plugins #302
  3. Plugin infrastructure (attachContext + PluginContext) — feat(appkit): plugin infrastructure — attachContext + PluginContext mediator #303
  4. agents() plugin + createAgent(def) + markdown-driven agents — feat(appkit): agents() plugin, createAgent(def), and markdown-driven agents #304
  5. fromPlugin() DX + runAgent plugins arg + toolkit-resolver — feat(appkit): fromPlugin() DX, runAgent plugins arg, shared toolkit-resolver #305
  6. Reference app + dev-playground + docs — feat(appkit): reference agent-app, dev-playground chat UI, docs, and template #306

The v2 stack reorganizes the same work so no PR ships API that a later PR deletes. Start at #301 for the new entry point. Branches from this older stack are preserved unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant