# Tool vs Talent in Solon AI: When a Function Is Not Enough

> Source: <https://dev.to/solonjava/tool-vs-talent-in-solon-ai-when-a-function-is-not-enough-47bh>
> Published: 2026-07-22 00:02:20+00:00

Most agent tutorials stop at tools: give the model a function schema, hope it calls the right one. That works for `get_time`

and `hash_string`

. It falls apart when the model skips a knowledge search and opens a ticket, or when eighty APIs all land in one context window.

Solon AI keeps tools as the execution unit, then adds **Talent** as the product unit: tools plus SOP plus activation rules. Think of it this way:

This post is a practical map of when to stay on tools, when to wrap them in a talent, and how registration actually works in Solon v4.0.3.

Bare tools only answer two questions for the model:

They do **not** answer:

That gap shows up as:

Talent is Solon’s answer: a reusable package of **awareness + instruction + tools**, with automatic **coloring** so tools keep their domain identity.

From the official comparison:

| Dimension | Tool (`FunctionTool` ) |
Talent (`Talent` ) |
|---|---|---|
| Unit | Single function / method | Instruction + tool set + state |
| Abstraction | Physical: how
|
Logical: when and under which SOP
|
| Context awareness | Passive |
`isSupported(Prompt)` can activate or hide |
| Injected content | Tool schema (JSON) | System prompt fragment + tool list |
| Constraint strength | Weak — model freestyles from description | Strong — SOP via `getInstruction`
|
| Registration |
`defaultToolAdd` / `toolAdd`
|
`defaultTalentAdd` / `talentAdd`
|

They are not rivals. A talent **contains** tools. Registering a talent also registers its tools; you do not need a second `defaultToolAdd`

for the same set.

When a request starts, Solon walks registered talents:

`isSupported(prompt)`

filters inactive ones`onAttach(prompt)`

for warm-up / audit / context prep`getInstruction`

is merged into the system message; tools get talent metadata (coloring)That is why talents can cut tokens: inactive domains never enter the tool table.

Use a plain tool when the capability is:

```
public class ClockTools extends AbsToolProvider {
    @ToolMapping(description = "Return the current server time in ISO-8601")
    public String now() {
        return Instant.now().toString();
    }
}

ChatModel chatModel = ChatModel.of(apiUrl)
        .apiKey(apiKey)
        .defaultModel(model)
        .defaultToolAdd(new ClockTools())
        .build();
```

Also fine for request-scoped options when the branching is tiny:

``` php
chatModel.prompt("Weather in Hangzhou?")
        .options(o -> {
            o.systemPrompt("You are a weather assistant.");
            if ("admin".equals(role)) {
                o.toolAdd(new UserTool());
                o.toolAdd(new AdminTool());
            } else {
                o.toolAdd(new UserTool());
            }
        })
        .call();
```

Good for spikes. Painful when the same role rules repeat across controllers.

Wrap tools in a talent when you need any of:

`TalentDesc`

```
TalentDesc orderTalent = new TalentDesc("order_expert")
        .description("Order assistant")
        .isSupported(prompt -> prompt.getUserContent().contains("order"))
        .instruction(prompt -> {
            if ("VIP".equals(prompt.attr("user_level"))) {
                return "VIP customer: prefer fast_track_tool when eligible.";
            }
            return "Follow the standard order lookup flow.";
        })
        .toolAdd(new OrderTools());
```

Fast for local, lambda-friendly definitions.

`AbsTalent`

+ `@ToolMapping`

```
public class TechSupportTalent extends AbsTalent {
    @Override
    public String name() {
        return "tech_support";
    }

    @Override
    public String description() {
        return "Technical support: diagnose before opening tickets";
    }

    @Override
    public boolean isSupported(Prompt prompt) {
        String content = prompt.getUserContent();
        return content != null && (
                content.contains("error")
                        || content.contains("故障")
                        || content.contains("报错"));
    }

    @Override
    public String getInstruction(Prompt prompt) {
        return """
                You are a tech support specialist. Follow this SOP:
                1. Search the knowledge base first (search_kb).
                2. Confirm the runtime version before any fix.
                3. Only open a ticket after diagnosis fails.
                """;
    }

    @ToolMapping(name = "search_kb", description = "Search the tech knowledge base")
    public String searchKb(@Param("query") String query) {
        return kbService.search(query);
    }

    @ToolMapping(name = "open_ticket", description = "Open a support ticket after diagnosis")
    public String openTicket(@Param("summary") String summary) {
        return ticketService.create(summary);
    }
}
```

`AbsTalent`

scans `@ToolMapping`

methods via `MethodToolProvider`

, same family of tool registration you already use for agents.

```
public class AuthControlTalent extends AbsTalent {
    private final UserTool userTool = new UserTool();
    private final AdminTool adminTool = new AdminTool();

    @Override
    public String getInstruction(Prompt prompt) {
        return "You are a weather assistant. Respect the caller's role.";
    }

    @Override
    public boolean isSupported(Prompt prompt) {
        return prompt.getUserContent() != null
                && prompt.getUserContent().contains("weather");
    }

    @Override
    public Collection<FunctionTool> getTools(Prompt prompt) {
        String role = prompt.attrAs("role");
        if ("admin".equals(role)) {
            return Arrays.asList(userTool, adminTool);
        }
        return Collections.singletonList(userTool);
    }
}
```

Call site stays thin:

```
ChatModel chatModel = ChatModel.of(apiUrl)
        .apiKey(apiKey)
        .defaultModel(model)
        .defaultTalentAdd(new AuthControlTalent())
        .build();

chatModel.prompt(Prompt.of("Weather in Hangzhou today?")
                .attrPut("role", role))
        .call();
```

Or per request:

``` php
chatModel.prompt(Prompt.of("...").attrPut("role", role))
        .options(o -> o.talentAdd(new AuthControlTalent()))
        .call();
```

| Scope | API |
|---|---|
| Every request on a model | `ChatModel.of(...).defaultTalentAdd(talent)` |
| One request | `prompt(...).options(o -> o.talentAdd(talent))` |
| Ordered injection |
`defaultTalentAdd(index, talent)` / `talentAdd(index, talent)`
|

Multiple talents inject instructions in registration order. Their tools are colored with talent metadata so the model can align SOP text with the right tool group.

Same pattern works on **ChatModel**, **SimpleAgent**, **ReActAgent**, and **TeamAgent**.

| Signal | Prefer |
|---|---|
| Single pure function, no policy | Tool |
| Same branching copy-pasted at call sites | Talent |
| Must force order: search → confirm → mutate |
Talent (`getInstruction` ) |
| Hide whole domains by intent / tenant |
Talent (`isSupported` + dynamic `getTools` ) |
| Huge OpenAPI / MCP surface |
Gateway Talent (staged discovery) — still a talent, not a flat tool dump |
| Prototype only | Tool first; promote when the model mis-orders or over-calls |

Official guidance in one line: **start with tools; wrap in a talent when the model needs a playbook or a gate.**

If your agent keeps “knowing the tools” but still fails the business path, you usually do not need more tools. You need a talent that owns the path.
