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

@@ -26,11 +26,55 @@ var allowedDefinitionFilters = map[string]bool{
// DefinitionsService manages authored Agent and Skill definitions.
type DefinitionsService struct {
repo *repo.DefinitionsRepo
// versions is the append-only history beside the editable definition.
// Written on publish, never on save — see snapshotIfPublished.
versions *repo.VersionsRepo
// unknownTools reports which of a spec's tool names are not registered.
// nil means no check — the shipped importer path, which has its own.
unknownTools func([]string) []string
}
// NewDefinitions builds a definitions service over a repository.
func NewDefinitions(db repo.Querier) *DefinitionsService {
return &DefinitionsService{repo: repo.NewDefinitionsRepo(db)}
return &DefinitionsService{
repo: repo.NewDefinitionsRepo(db),
versions: repo.NewVersionsRepo(db),
}
}
// WithToolCheck teaches the service which tool names exist.
//
// §3 requires an unknown tool name to fail validation at PUBLISH. Without it
// the runtime records the name and drops it, so a typo produces an agent that
// is silently missing a capability its author believes it has — and the author
// finds out by watching it fail to answer.
//
// Injected rather than imported so this package does not depend on the tool
// registry, and so a test can supply its own vocabulary.
func (s *DefinitionsService) WithToolCheck(unknown func([]string) []string) *DefinitionsService {
s.unknownTools = unknown
return s
}
// rejectUnknownTools fails a definition that names a tool that does not exist.
func (s *DefinitionsService) rejectUnknownTools(markdown string) error {
if s.unknownTools == nil {
return nil
}
agent, err := definition.ParseAgent(markdown, definition.Options{})
if err != nil || agent == nil {
// ValidateAgent has already run and reported anything real; a parse
// failure here is not a second opinion worth raising.
return nil
}
if bad := s.unknownTools(agent.Tools); len(bad) > 0 {
return domain.Validation(
fmt.Sprintf("unknown tool(s): %s", strings.Join(bad, ", ")),
map[string]string{"tools": strings.Join(bad, ", ")})
}
return nil
}
// ParseListParams validates query parameters for listing definitions.
@@ -148,6 +192,9 @@ func (s *DefinitionsService) CreateAgent(ctx context.Context, ident authctx.Iden
if err := definition.ValidateAgent(markdown); err != nil {
return nil, domain.Validation(err.Error(), nil)
}
if err := s.rejectUnknownTools(markdown); err != nil {
return nil, err
}
visibility := "personal"
if visRaw, ok := body["visibility"]; ok && visRaw != nil {
@@ -187,7 +234,15 @@ func (s *DefinitionsService) CreateAgent(ctx context.Context, ident authctx.Iden
input.OwnerUserID = &ident.UserID
}
return s.repo.InsertAgent(ctx, ident, input)
rec, err := s.repo.InsertAgent(ctx, ident, input)
if err != nil {
return nil, err
}
// Recorded after the row exists, and never allowed to fail the save: the
// author's work is already stored, and losing it to protect a record of it
// would be the wrong trade.
_ = s.snapshotIfPublished(ctx, ident, repo.KindAgent, rec)
return rec, nil
}
// UpdateAgent validates and applies updates to an authored agent definition.
@@ -227,6 +282,9 @@ func (s *DefinitionsService) UpdateAgent(ctx context.Context, ident authctx.Iden
if err := definition.ValidateAgent(markdown); err != nil {
return nil, domain.Validation(err.Error(), nil)
}
if err := s.rejectUnknownTools(markdown); err != nil {
return nil, err
}
agent, err := definition.ParseAgent(markdown, definition.Options{})
if err != nil {
return nil, domain.Validation("That definition could not be parsed. "+err.Error(), nil)
@@ -246,7 +304,12 @@ func (s *DefinitionsService) UpdateAgent(ctx context.Context, ident authctx.Iden
input.Status = &status
}
return s.repo.UpdateAgent(ctx, ident, id, input)
rec, err := s.repo.UpdateAgent(ctx, ident, id, input)
if err != nil {
return nil, err
}
_ = s.snapshotIfPublished(ctx, ident, repo.KindAgent, rec)
return rec, nil
}
// DeleteAgent removes an agent definition following idempotent delete semantics.
@@ -449,3 +512,87 @@ func (s *DefinitionsService) DeleteSkill(ctx context.Context, ident authctx.Iden
}
return domain.Record{"id": id}, nil
}
/* ── Publishing ─────────────────────────────────────────────────────────── */
// snapshotIfPublished records an immutable copy when a definition is published.
//
// §3: editing publishes a NEW version, and a published version never changes.
// This is the half that records it. The half that enforces it is a trigger on
// the table, because the repository is not the only thing that can reach it.
//
// ONLY ON PUBLISH. A draft is a work in progress and snapshotting every save
// would fill the history with keystrokes — the version number would stop
// meaning "a thing somebody decided to ship" and start meaning "a time somebody
// pressed save", which is the number a run records and a person has to
// recognise.
//
// A failure here does NOT fail the save. The definition is already written;
// refusing the whole operation because its history could not be recorded would
// lose the author's work to protect a record of it. It is returned so the
// caller can log it, and the missing version shows up as a gap rather than as
// a wrong answer.
func (s *DefinitionsService) snapshotIfPublished(ctx context.Context, ident authctx.Identity,
kind repo.VersionKind, rec domain.Record) error {
if s.versions == nil || rec == nil {
return nil
}
status, _ := rec["status"].(string)
if status != "published" {
return nil
}
markdown, _ := rec["markdown"].(string)
definitionID, _ := rec["definition_id"].(string)
if markdown == "" || definitionID == "" {
return nil
}
version := 1
switch v := rec["version"].(type) {
case int:
version = v
case int32:
version = int(v)
case int64:
version = int(v)
case float64:
version = int(v)
}
name, _ := rec["name"].(string)
description, _ := rec["description"].(string)
var pages []string
if raw, ok := rec["pages"].([]string); ok {
pages = raw
} else if raw, ok := rec["pages"].([]any); ok {
for _, p := range raw {
if str, ok := p.(string); ok {
pages = append(pages, str)
}
}
}
return s.versions.Snapshot(ctx, ident, repo.SnapshotInput{
Kind: kind,
DefinitionID: definitionID,
Version: version,
Markdown: markdown,
Name: name,
Description: description,
Pages: pages,
})
}
// AgentHistory lists an agent's published versions, newest first.
func (s *DefinitionsService) AgentHistory(ctx context.Context, ident authctx.Identity,
definitionID string, limit int) ([]repo.Version, error) {
return s.versions.History(ctx, ident, repo.KindAgent, definitionID, limit)
}
// AgentVersion loads one published version of an agent, as it was.
func (s *DefinitionsService) AgentVersion(ctx context.Context, ident authctx.Identity,
definitionID string, version int) (*repo.Version, error) {
return s.versions.Load(ctx, ident, repo.KindAgent, definitionID, version)
}