188 lines
7.3 KiB
Go
188 lines
7.3 KiB
Go
// Package tools is the tool layer: everything an agent can do that is not
|
|
// talking.
|
|
//
|
|
// A tool is a named, schema'd function the model may call. The contract below
|
|
// is §4's, and three parts of it are load-bearing rather than stylistic:
|
|
//
|
|
// - **Every handler authorizes on ctx.Principal, first line.** A handler that
|
|
// reads ctx for anything except authorization is wrong. This is where I1
|
|
// lives: an agent may read exactly what its caller could read directly, and
|
|
// the only way to guarantee that is for the tool — not the model, not the
|
|
// prompt — to apply the caller's own permissions.
|
|
// - **Filters are pre-filters.** A handler narrows in SQL, never in Go over a
|
|
// fetched result set. I2: post-filtering leaks through counts and ranking
|
|
// positions even when no forbidden row is ever printed.
|
|
// - **Errors are returned, not raised.** A tool that panics or returns a bare
|
|
// error takes the whole run with it. The runtime decides whether the model
|
|
// sees a failure and retries, and it can only decide that if the failure
|
|
// arrives as data.
|
|
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/krow/krow-backend/go-api/internal/authctx"
|
|
)
|
|
|
|
// Effect is whether a tool changes anything.
|
|
type Effect string
|
|
|
|
const (
|
|
// EffectRead observes. Safe to call without asking anyone.
|
|
EffectRead Effect = "read"
|
|
// EffectWrite writes, sends, deletes, charges or notifies. Never runs
|
|
// without a resolved confirmation — see Tool.RequiresConfirmation.
|
|
EffectWrite Effect = "write"
|
|
)
|
|
|
|
// DefaultMaxResultBytes caps a tool result.
|
|
//
|
|
// Not a performance guard. An unbounded result is an unbounded prompt on the
|
|
// next turn, which is an unbounded bill and eventually a context overflow that
|
|
// presents as the model ignoring the middle of its own evidence.
|
|
const DefaultMaxResultBytes = 262_144
|
|
|
|
// Context is what a handler is given about its caller.
|
|
//
|
|
// Carries the principal, the tenant, the run and what budget is left, per §4.
|
|
// Deliberately a struct and not a context.Context value: a handler must not be
|
|
// able to *forget* to read it, and a compile error is a better reminder than a
|
|
// convention.
|
|
type Context struct {
|
|
// Principal is the caller the agent is acting for. Never the agent.
|
|
Principal authctx.Identity
|
|
|
|
// RunID addresses the trajectory this call is recorded in.
|
|
RunID string
|
|
|
|
// RemainingTokens is what the run has left to spend. A handler may use it
|
|
// to decide how much to return; it must not use it to decide whether the
|
|
// caller is allowed something.
|
|
RemainingTokens int64
|
|
|
|
// Confirmation is the resolved token for a write. Empty on a read, and
|
|
// empty on a write that has not been confirmed yet — which the dispatcher
|
|
// refuses before a handler is ever reached.
|
|
Confirmation string
|
|
|
|
// AgentID is the running agent, for the record. Never for authorization —
|
|
// what a caller may do is decided by their principal, and an agent that
|
|
// could widen that by being named would be an agent that expands access.
|
|
AgentID string
|
|
|
|
// KnowledgeSources are the corpora the running agent's SPEC declares.
|
|
//
|
|
// Here rather than in a tool argument, and the difference is the whole
|
|
// security property: an argument is something a model can choose, and which
|
|
// documents an agent may read is not the model's to choose. The loop sets
|
|
// this from the agent record; nothing in the conversation can reach it.
|
|
//
|
|
// Empty means this agent has no knowledge. It does not mean "all of it" —
|
|
// retrieval refuses an empty source list for exactly that reason.
|
|
KnowledgeSources []string
|
|
}
|
|
|
|
// OrgID is the tenant this call runs inside. I5 — every handler's query
|
|
// narrows by it, and there is no path that produces a call without one.
|
|
func (c Context) OrgID() string { return c.Principal.OrgID }
|
|
|
|
// Handler runs one tool.
|
|
//
|
|
// The signature is `(inputs, ctx)` in §4's terms, with the Go context first by
|
|
// convention so cancellation and the deadline reach the query. A handler
|
|
// returns a Result and never an error: a failure is a value the runtime routes,
|
|
// not a panic that ends a run.
|
|
type Handler func(ctx context.Context, tc Context, inputs json.RawMessage) Result
|
|
|
|
// Tool is one callable capability.
|
|
type Tool struct {
|
|
Name string
|
|
Description string
|
|
|
|
// InputSchema is JSON Schema. Every field described, because the
|
|
// description is what the model reads instead of documentation — and a
|
|
// tool whose schema demands an id the model was never given is a design
|
|
// bug, not a prompt problem. Add a lookup tool instead.
|
|
InputSchema map[string]any
|
|
|
|
Effect Effect
|
|
|
|
// RequiresConfirmation is forced true for a write by Register. It is a
|
|
// field rather than a method so a read tool may opt in — some reads are
|
|
// expensive enough to be worth asking about — but a write can never opt
|
|
// out. The model does not get a say either way.
|
|
RequiresConfirmation bool
|
|
|
|
// Confirm renders, in plain language, what this tool will do if approved.
|
|
// Mandatory when RequiresConfirmation is set: Register refuses a write
|
|
// without one, because a confirmation a person cannot read is not a
|
|
// confirmation, it is a click. See confirm.go.
|
|
Confirm Confirmer
|
|
|
|
MaxResultBytes int
|
|
|
|
Handler Handler
|
|
}
|
|
|
|
// Result is what a tool returns.
|
|
//
|
|
// Structured data, never prose: formatting is the model's job, and a handler
|
|
// that returns a sentence has decided how the answer reads before the model has
|
|
// seen the question.
|
|
type Result struct {
|
|
Data any `json:"data,omitempty"`
|
|
Error *ToolError `json:"error,omitempty"`
|
|
|
|
// Truncated says the result was cut at MaxResultBytes. Set alongside the
|
|
// data that survived — never as a silent drop, because a model given a
|
|
// truncated list with no marker will reason about it as if it were whole.
|
|
Truncated bool `json:"truncated,omitempty"`
|
|
|
|
// Confirmation is set when a write was described but not performed. It is
|
|
// neither success nor error: nothing happened, and something must now be
|
|
// approved by a person before anything can. The loop reads this and ends
|
|
// the run at ConfirmationPending rather than handing it to the model —
|
|
// see I4, and the note on Registry.Dispatch.
|
|
Confirmation *Confirmation `json:"confirmation,omitempty"`
|
|
}
|
|
|
|
// ToolError is a failure a handler chose to report.
|
|
type ToolError struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// Failf builds an error result.
|
|
func Failf(code, format string, args ...any) Result {
|
|
return Result{Error: &ToolError{Code: code, Message: fmt.Sprintf(format, args...)}}
|
|
}
|
|
|
|
// OK builds a success result.
|
|
func OK(data any) Result { return Result{Data: data} }
|
|
|
|
// Standard tool error codes. A denial is deliberately one code with one
|
|
// wording — see Denied.
|
|
const (
|
|
CodeDenied = "tool.denied"
|
|
CodeInvalidInput = "tool.invalid_input"
|
|
CodeUnavailable = "tool.unavailable"
|
|
CodeFailed = "tool.failed"
|
|
)
|
|
|
|
// Denied is the single refusal every handler returns when a caller may not do
|
|
// something.
|
|
//
|
|
// One code, one message, no detail. §8: a denial must not reveal that the
|
|
// resource exists. Two different refusals — "no such venue" and "not your
|
|
// venue" — are an oracle: a caller who can tell them apart can enumerate what
|
|
// they cannot see, and the agent will happily run that enumeration for them one
|
|
// question at a time.
|
|
func Denied() Result {
|
|
return Result{Error: &ToolError{
|
|
Code: CodeDenied,
|
|
Message: "the caller does not have access to this",
|
|
}}
|
|
}
|