TL;DR

Eino Components are typed capability boundaries, not interchangeable behavior guarantees. In v0.9.21, BaseChatModel provides Generate and Stream; ToolCallingChatModel.WithTools returns a tool-bound model without mutating the shared instance; Tool parameters use ToolInfo.Desc and ParamsOneOf; and Retriever exposes a common call shape while leaving ranking, filters, authorization, and consistency to the implementation and application.

Key Takeaways

  • Depend on a Component interface only after defining the semantic contract your application requires.
  • Prefer immutable WithTools over deprecated, mutating BindTools in concurrent services.
  • ToolsNode executes model-proposed calls; it does not decide whether the caller is authorized.
  • Keep retrieval relevance, source trust, freshness, and access control as separate fields.
  • Contract-test every provider adapter, including errors, streaming, options, cancellation, and upgrades.

The Component Map

The official Components documentation groups Eino capabilities by application role:

Component Input to output Application-owned contract
BaseChatModel Messages to one response or a response stream Provider identity, timeout, output validation
AgenticModel Agentic messages to structured content blocks Feature support, approval semantics, beta migration
Tool JSON or typed argument to result Authentication, authorization, side effects
Loader Source to documents SSRF controls, parsing limits, provenance
Transformer Documents to transformed documents Offset lineage, deletion propagation
Embedding Text to vectors Model revision, dimensions, normalization
Indexer Documents to stored index entries Consistency, tenancy, idempotency
Retriever Query to candidate documents Filters, score meaning, ACL before context
ChatTemplate Variables to messages Escaping, role boundaries, prompt version
Lambda Typed Go function Cancellation, bounds, deterministic errors

Concrete implementations normally come from EinoExt. Pin each imported module separately; a core tag does not automatically pin every provider adapter.

Model Interfaces in v0.9.21

The pinned components/model/interface.go defines a generic BaseModel and two message paths:

Interface Message type Tool binding
BaseChatModel *schema.Message None by itself
ToolCallingChatModel *schema.Message Immutable WithTools
AgenticModel *schema.AgenticMessage Request-level options

The old ChatModel.BindTools method remains for compatibility but is deprecated. It mutates the model instance, so two concurrent requests with different tool lists can race. WithTools returns a derived ToolCallingChatModel and is the safer default for shared services.

Types cannot normalize provider behavior. One adapter may support parallel tool calls, another may ignore a common option, and another may return usage only after a stream ends. Contract tests should cover:

  • unsupported options and feature negotiation;
  • tool-call IDs, argument fragments, and finish reasons;
  • stream errors before and after the first chunk;
  • cancellation and provider timeouts;
  • usage accounting and retry duplication;
  • structured-output and multimodal behavior.

Tool Contracts: Description Is Not Permission

The ToolsNode and Tool guide makes the execution boundary explicit: the model puts ToolCalls in an assistant message, while ToolsNode invokes configured tools. The model chooses a proposal; trusted code decides whether it may execute.

In v0.9.21, a ToolInfo uses:

  • Name for a unique callable identifier;
  • Desc to explain when and why to use it;
  • ParamsOneOf for either lightweight parameter metadata or JSON Schema 2020-12.

A safe tool execution sequence is:

sequenceDiagram participant M as Model participant N as ToolsNode participant P as Policy participant S as Source service M->>N: ToolCall name and arguments N->>P: Parsed candidate plus trusted principal P-->>N: Allow or deny exact action N->>S: Bounded idempotent request S-->>N: Typed result or typed failure N-->>M: Size-limited result with provenance

Tool aliases and schemas improve compatibility, but neither grants authority. Treat names, descriptions, aliases, arguments, and returned content as untrusted inputs.

Runnable Typed Tool

This program targets Eino Core v0.9.21. InferTool derives the argument schema from a Go struct, while the tool reads tenant identity from trusted request context rather than model-generated JSON.

go
package main

import (
	"context"
	"fmt"

	"github.com/cloudwego/eino/components/tool/utils"
)

type principalKey struct{}

type Principal struct {
	TenantID string
}

type LookupOrderInput struct {
	OrderID string `json:"order_id" jsonschema:"required" jsonschema_description:"Order identifier in the caller tenant"`
}

type LookupOrderOutput struct {
	OrderID string `json:"order_id"`
	Status  string `json:"status"`
}

func main() {
	orders := map[string]struct {
		TenantID string
		Status   string
	}{
		"A-42": {TenantID: "tenant-a", Status: "paid"},
	}

	lookup, err := utils.InferTool(
		"lookup_order",
		"Read one order already authorized for the authenticated tenant.",
		func(ctx context.Context, input LookupOrderInput) (LookupOrderOutput, error) {
			principal, ok := ctx.Value(principalKey{}).(Principal)
			if !ok {
				return LookupOrderOutput{}, fmt.Errorf("missing principal")
			}
			order, ok := orders[input.OrderID]
			if !ok || order.TenantID != principal.TenantID {
				return LookupOrderOutput{}, fmt.Errorf("order not found")
			}
			return LookupOrderOutput{OrderID: input.OrderID, Status: order.Status}, nil
		},
	)
	if err != nil {
		panic(err)
	}

	info, err := lookup.Info(context.Background())
	if err != nil {
		panic(err)
	}
	ctx := context.WithValue(context.Background(), principalKey{}, Principal{TenantID: "tenant-a"})
	result, err := lookup.InvokableRun(ctx, `{"order_id":"A-42"}`)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%s => %s\n", info.Name, result)
}

Expected output:

text
lookup_order => {"order_id":"A-42","status":"paid"}

Use an unexported typed key for request context. Never accept tenant_id, roles, approval state, or an authorization boolean from model arguments as the source of truth.

Retriever Is a Candidate Interface

A Retriever returns candidate documents. It does not establish that a document is true, current, complete, or visible to the caller. Normalize these properties before a Generator sees evidence:

Field Why it matters
document_id and revision Stable citation and invalidation
source_uri and span offsets Claim-level provenance
tenant_id and object scope Authorization before ranking
score_type and direction Comparable ranking semantics
valid_from / valid_to Freshness and temporal questions
parser / chunker / embedding revision Reproducible retrieval

When swapping Elasticsearch, VikingDB, or another adapter, test the same query set and verify filter semantics, tie ordering, duplicate behavior, score calibration, pagination, timeout, partial failure, and deletion.

Component Contract Tests

For each implementation, build tests at three levels:

  1. Interface: accepted types, options, nil values, cancellation, and error classes.
  2. Semantics: provider-specific feature support, score meaning, tool schema, and metadata.
  3. System: authorization, budgets, retries, idempotency, redaction, and observable outcomes.

A successful compile proves type compatibility. A passing provider call proves connectivity. Neither proves task quality or policy compliance.

Common Failure Modes

Failure Consequence Control
Mutating a shared model with BindTools Cross-request tool leakage or race Use WithTools; test concurrency
Trusting generated tool arguments Cross-tenant read or unsafe write Derive principal server-side; authorize object/action
Treating Schema as business validation Valid shape, invalid operation Enforce invariants after parsing
Swapping Retriever by interface alone Ranking or filter regression Adapter contract suite
Logging complete payloads in callbacks Secret or personal-data exposure Redaction, sampling, retention
Retrying every error Duplicate effects and higher cost Typed retry policy and idempotency

Frequently Asked Questions

Does Eino automatically execute every tool a model requests?

ToolsNode executes calls present in its input against configured tools. Your application must still restrict the available tool set and enforce authorization and side-effect policy around execution.

Is AgenticModel a drop-in replacement for BaseChatModel?

No. It uses AgenticMessage content blocks and different tool-binding behavior. Treat migration as a contract change and test every provider and downstream consumer.

Should a Tool return plain text or structured data?

Return the narrowest machine-readable result the next step needs. Bound size, preserve provenance, and avoid embedding secrets. Eino also defines enhanced Tool interfaces for multimodal results.

Can a Retriever enforce access control?

An implementation may support filters, but the application must derive them from trusted identity and verify their semantics. Authorization should happen before unauthorized candidates affect ranking, caches, logs, or model context.

What belongs in a Lambda?

Small, bounded transformations with clear input, output, cancellation, and error behavior. Hidden network calls or policy decisions make a Lambda harder to test and operate.

Primary Sources