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,143 @@
package runtime
import (
"context"
"encoding/json"
"errors"
"strings"
"time"
"github.com/jackc/pgx/v5"
"github.com/krow/krow-backend/go-api/internal/authctx"
"github.com/krow/krow-backend/go-api/internal/domain"
"github.com/krow/krow-backend/go-api/internal/repo"
)
// Reading a recorded run back.
//
// The write side of trajectories is PostgresSink; this is the read side, and it
// exists because §6's requirement is only worth anything if somebody can look.
// "Why did the agent say that" should be answerable by pointing at a run id.
//
// Two rules shape what comes back:
//
// - **Tenant scope is in the query.** I5. A run id from another organization
// is absent rather than forbidden, so it answers 404 and cannot be used to
// discover that a given run exists somewhere else.
// - **A talent caller sees only their own runs.** An operator sees the
// organization's, which is what an operator console is. A trajectory
// contains the caller's question and the records retrieved for them, so
// "anyone in the tenant may read any run" would be a much larger grant than
// it looks.
// RunReader loads recorded trajectories.
type RunReader struct {
db repo.Querier
}
// NewRunReader builds a reader over a pool or transaction.
func NewRunReader(db repo.Querier) *RunReader { return &RunReader{db: db} }
// RunView is a trajectory as a caller sees it.
//
// Not the Trajectory struct. That one is the internal record and gains fields
// as the runtime does; this is a response shape, and the difference is what
// keeps a new internal field from silently becoming a new public one.
type RunView struct {
RunID string `json:"runId"`
ParentRunID string `json:"parentRunId,omitempty"`
AgentID string `json:"agentId"`
AgentVersion int `json:"agentVersion"`
Tier string `json:"tier"`
Model string `json:"model,omitempty"`
StartedAt time.Time `json:"startedAt"`
EndedAt time.Time `json:"endedAt"`
Termination string `json:"termination"`
Entries []Entry `json:"entries"`
Usage RunUsage `json:"usage"`
}
// Load returns one run, if this caller may read it.
func (r *RunReader) Load(ctx context.Context, ident authctx.Identity, runID string) (*RunView, error) {
if strings.TrimSpace(runID) == "" {
return nil, domain.NotFound("run", runID)
}
if strings.TrimSpace(ident.OrgID) == "" {
// I5. No tenant, no read — and answered as absent rather than
// forbidden, on the same terms as every other row in this service.
return nil, domain.NotFound("run", runID)
}
where, args := runScope(ident, runID)
var (
view RunView
parent *string
model *string
rawEntries []byte
termination string
)
err := r.db.QueryRow(ctx, `
SELECT run_id, parent_run_id, agent_id, agent_version, tier, model,
started_at, ended_at, termination, entries,
input_tokens, output_tokens, cached_tokens, total_tokens, model_calls
FROM agent_runs
WHERE `+where, args...,
).Scan(&view.RunID, &parent, &view.AgentID, &view.AgentVersion, &view.Tier, &model,
&view.StartedAt, &view.EndedAt, &termination, &rawEntries,
&view.Usage.InputTokens, &view.Usage.OutputTokens, &view.Usage.CachedTokens,
&view.Usage.TotalTokens, &view.Usage.ModelCalls)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, domain.NotFound("run", runID)
}
return nil, domain.Internal(err)
}
view.Termination = termination
if parent != nil {
view.ParentRunID = *parent
}
if model != nil {
view.Model = *model
}
if len(rawEntries) > 0 {
if err := json.Unmarshal(rawEntries, &view.Entries); err != nil {
return nil, domain.Internal(err)
}
}
if view.Entries == nil {
view.Entries = []Entry{}
}
return &view, nil
}
// runScope builds the predicate a caller's runs are behind.
//
// Two conditions, and the second is the one that is easy to forget. Tenancy is
// obvious. The talent restriction is not: a trajectory holds the question that
// was asked and the records retrieved to answer it, so a tenant-wide read would
// let any worker read every colleague's conversation with an agent — including
// the ones about them.
//
// An unrecognised role gets `false`, so it matches nothing rather than
// everything. The safe direction, and loud enough to find.
func runScope(ident authctx.Identity, runID string) (string, []any) {
args := []any{ident.OrgID, runID}
where := "org_id = $1::uuid AND run_id = $2"
role, ok := domain.ParseRole(ident.Role)
if !ok {
return where + " AND false", args
}
if role == domain.RoleTalent {
if strings.TrimSpace(ident.UserID) == "" {
return where + " AND false", args
}
args = append(args, ident.UserID)
where += " AND user_id = $3::uuid"
}
return where, args
}