agent build

This commit is contained in:
2026-08-28 12:21:44 +05:30
parent b6f8655909
commit f7df96c973
138 changed files with 24164 additions and 207 deletions

View File

@@ -0,0 +1,187 @@
// 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",
}}
}