sysagent

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 25, 2026 License: AGPL-3.0 Imports: 50 Imported by: 0

Documentation

Overview

Package sysagent is the in-airlock chat agent that lets operators manage agents, bridges, connections, members, A2A, runs, and (later) other tenant resources through tool calls.

No JS VM, no per-agent connections, no Sol — sysagent runs inside airlock and its tools are typed Go functions wrapping existing service.{domain} methods. Every tool call uses the caller's authz.Principal; the service layer's authz.Authorize gates by action.

One sysagent per Airlock instance, per-user multi-conversation chat history. Schema lives in migrations/002_a2a.sql (system_conversations, system_messages, system_audit).

Index

Constants

View Source
const MaxAuditSummaryBytes = 512

MaxAuditSummaryBytes caps the human-readable result_summary stored in system_audit. Audit rows aren't shown to the LLM and don't need the full 8 KiB tool budget — a one-line gist is enough for forensics.

View Source
const MaxToolOutputBytes = 8 * 1024

MaxToolOutputBytes caps every tool result (success and error) at 8 KiB. Tools that hand back larger payloads get truncated with a "[truncated: total=N bytes; refine query or paginate]" suffix, so one runaway list_runs call can't blow through the LLM's context budget. Sized to match the agentsdk JS-binding spill threshold so the LLM sees one consistent "8 KiB-ish" budget across surfaces.

Variables

This section is empty.

Functions

func SystemPrompt

func SystemPrompt(env promptEnv, availableTools tool.Set) string

SystemPrompt renders the sysagent chat system prompt: the conventions preamble, the per-turn <env> block, and a tool catalogue generated from the registered (tenant-filtered) tool set so it can't drift from the actual Execute surface. Only the first description line is shown — the full schema reaches the model via the tool envelope.

Types

type Conversation

type Conversation struct {
	dbq.SystemConversation
}

Conversation is one row from ListConversations / CreateConversation.

type ConversationDetail

type ConversationDetail struct {
	Conversation dbq.SystemConversation
	Messages     []dbq.SystemMessage
}

ConversationDetail bundles a conversation with its full message history. The caller decides whether to also request paginated tails separately for very long conversations (we don't paginate on the initial Get because chat conversations rarely grow beyond a few dozen messages — agent chat pages from the same shape).

type Deps

type Deps struct {
	DB        *db.DB
	Encryptor secrets.Store
	PubSub    *realtime.PubSub
	PublicURL string // base URL for deep-link tools (no trailing slash)
	Logger    *zap.Logger

	Agents      *agentssvc.Service
	Bridges     *bridgessvc.Service
	Catalog     *catalogsvc.Service
	Conns       *connsvc.Service
	Execs       *execsvc.Service
	GitCreds    *gitcredssvc.Service
	ManagedBots *managedbotssvc.Service
	Members     *memberssvc.Service
	Runs        *runssvc.Service
	Siblings    *siblingssvc.Service
	Users       *userssvc.Service
}

Deps bundles the dependencies New requires. Pulled out as a struct so the call site stays readable as the service set grows.

type ListRunsResult

type ListRunsResult struct {
	Runs       []RunSummary
	NextCursor time.Time
}

ListRunsResult bundles a page of runs with the cursor for the next page (zero StartedAt when no further pages exist).

type PromptInput

type PromptInput struct {
	Message string
	// Platform is the channel for the <env> block ("web"/"telegram"), set
	// explicitly by the caller — never inferred. Empty omits the line.
	Platform string
	Approved *bool
	// ResumeRunID on an approve/deny names the run whose
	// confirmation is being resolved. RunPrompt waits for that run to suspend
	// before dispatching the resume, so an approval that beats the async
	// suspend write isn't rejected as a state mismatch. It is required for a
	// confirmation response.
	ResumeRunID string
}

PromptInput is the per-call payload the HTTP handler hands to RunPrompt. Exactly one of Message / Approved should be set per the /system/conversations/{id}/prompt API:

  • Message non-empty + Approved nil = fresh operator turn.
  • Approved non-nil + Message empty = resolution of a pending confirmation (true=execute, false=synthesize denial).
  • Both empty = auto-resume after an injected system event (see Service.resumeConversation).

type RunSummary

type RunSummary struct {
	ID                uuid.UUID
	ConversationID    uuid.UUID
	ConversationTitle string
	Status            string // 'running' | 'suspended' | 'complete' | 'error' | 'cancelled'
	TriggerType       string // 'prompt' | 'bridge' | 'event'
	MessagePreview    string // first linked message for the turn (” when no message is bound)
	ErrorMessage      string
	CostEstimate      float64
	StartedAt         time.Time
	FinishedAt        *time.Time
}

RunSummary is one row from the caller's sysagent activity view — run lifecycle (status + timestamps) plus the parent conversation's title so the UI can render without a per-row fetch.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service is the central sysagent hub. Owns the dependency set every tool body needs (db, encryptor, all the per-domain service handles, logger) plus the public URL used to mint deep-link tool results.

One instance per Airlock; constructed in api/router.go and shared across HTTP requests. Per-request state (Principal, conversation, message history) lives on the chat loop, not here.

func New

func New(d Deps) *Service

New wires the sysagent Service. Fail-loud on nil deps — every field is required (AGENTS.md rule).

func (*Service) CancelRun

func (s *Service) CancelRun(runID uuid.UUID) bool

CancelRun aborts an in-flight chat goroutine. Returns true if a matching run was found and cancelled. The runChat goroutine sees ctx.Done() through sol.Runner and exits with status='cancelled' via the existing sol.RunCancelled branch.

func (*Service) Compact

func (s *Service) Compact(ctx context.Context, p authz.Principal, conversationID uuid.UUID) (string, error)

Compact runs sol's user-triggered compaction on a sysagent conversation in-process. Loads the conversation's post-checkpoint messages, asks the LLM to summarize, persists the summary as a new system_messages row, and advances context_checkpoint_message_id.

Unlike RunPrompt this is synchronous from the caller's perspective: returns once compaction completes with the summary text the user will see in chat. Mirrors the agent path's /compact (which forwards to the agent container and lets Sol.Runner.Compact emit the summary as a normal assistant message), just executed locally because sysagent has no agent container.

func (*Service) CreateConversation

func (s *Service) CreateConversation(ctx context.Context, p authz.Principal, title string) (dbq.SystemConversation, error)

CreateConversation inserts a fresh conversation for the caller. title is optional; the DB default is "New chat".

func (*Service) DeleteConversation

func (s *Service) DeleteConversation(ctx context.Context, p authz.Principal, conversationID uuid.UUID) error

DeleteConversation removes a conversation (and via ON DELETE CASCADE its messages, runs, and any pending checkpoint state). audit rows for the conversation stay (ON DELETE SET NULL preserves forensics — what happened in a now-deleted conversation is still recoverable).

func (*Service) GetConversation

func (s *Service) GetConversation(ctx context.Context, p authz.Principal, conversationID uuid.UUID) (ConversationDetail, error)

GetConversation returns the conversation + its full message history (including pre-checkpoint rows + checkpoint markers — the UI's full timeline, not the LLM context filter). 404 for a missing conversation, 404 (not 403) for someone else's — exposing existence to non-owners would leak metadata.

func (*Service) ListConversations

func (s *Service) ListConversations(ctx context.Context, p authz.Principal) ([]dbq.SystemConversation, error)

ListConversations returns the caller's own conversations, newest-active first. Ownership is enforced at the query level — conversations aren't shared.

func (*Service) ListRuns

func (s *Service) ListRuns(ctx context.Context, p authz.Principal, cursor time.Time, limit int32) (ListRunsResult, error)

ListRuns returns the caller's recent sysagent runs across all their conversations, newest first. Cursor is the started_at of the last row from the previous page; zero fetches the newest page. Caller must be authenticated; the query is owner-scoped (user_id = p.UserID) so no cross-user leak is possible.

Gating is uniform with the other sysagent reads: any authenticated user can list their OWN runs, which is what the policy table already expresses via TenantUserView for the user directory. We don't add a new Action here because the data is owner-scoped at the query level — the WHERE clause IS the gate.

func (*Service) Logger

func (s *Service) Logger() *zap.Logger

Logger exposes the package logger for callers that want to surface sysagent events through the same channel (e.g. the API handler logging request failures).

func (*Service) NotifyBotCreated

func (s *Service) NotifyBotCreated(ctx context.Context, conversationID uuid.UUID, botUsername string) error

NotifyBotCreated announces a freshly-created managed Telegram bot into the system-agent conversation that requested it (create_tg_bot) and auto-resumes, so the agent hands the operator the open link in-character. Same inject-then- resume mechanism as NotifyBuildComplete; agentID is nil (the event is about a bot, not an agent build — the WS envelope's AgentId is unused on bridges).

func (*Service) NotifyBuildComplete

func (s *Service) NotifyBuildComplete(ctx context.Context, agentID, conversationID uuid.UUID, status, message string) error

NotifyBuildComplete satisfies builder.PostBuildSystemNotifier — the initial-build counterpart of NotifyUpgradeComplete. Triggered after a build kicked off from the system-agent create_agent tool finishes; same inject-then-resume mechanism, build-flavored prefix.

func (*Service) NotifyUpgradeComplete

func (s *Service) NotifyUpgradeComplete(ctx context.Context, agentID, conversationID uuid.UUID, status, message string) error

NotifyUpgradeComplete satisfies builder.PostUpgradeSystemNotifier for the sysagent surface. Triggered by the builder after a build initiated from a sysagent-conversation upgrade tool finishes (success, failure, or out-of-scope refusal). Mirrors agent chat's conversationsHandler.NotifyUpgradeComplete (api/conversations.go) so the operator sees the outcome through the same render path the LLM-driving "user message" pattern uses.

What lands in the conversation: a single user-role system_messages row prefixed with [Upgrade succeeded] / [Upgrade failed] / [Request declined] — the prefix is what tells the LLM this is a system-injected event, not a real operator statement. Following agentsdk's deliberate choice not to write a separate assistant bubble: keeping the LLM's read of the situation unambiguous matters more than dual rendering.

agentID is the agent that was being upgraded; the message body references it so the LLM has context when responding. conversationID is the sysagent conversation that triggered the upgrade.

func (*Service) RunPrompt

func (s *Service) RunPrompt(ctx context.Context, p authz.Principal, conversationID uuid.UUID, input PromptInput) (uuid.UUID, error)

RunPrompt creates the run row, hands off the chat to a background goroutine, and returns the new run id immediately. The HTTP handler returns {runId, conversationId} to the caller and the frontend subscribes to the conversation topic via WS to receive the streamed events (same pattern agent chat uses).

The synchronous part is intentionally small (create row, return id). Everything else — model resolve, message build, sol.Runner.Run, suspension handling, persistence — runs in the goroutine so a slow LLM doesn't tie up the HTTP request.

func (*Service) RunPromptInline

func (s *Service) RunPromptInline(ctx context.Context, p authz.Principal, conversationID uuid.UUID, text, platform string, approved *bool, resumeRunID string, extraSink eventstream.Sink, onStart func(runID uuid.UUID)) (uuid.UUID, error)

RunPromptInline is RunPrompt's synchronous sibling: the chat loop runs on the caller's goroutine, and an additional eventstream.Sink is fanned every bus event alongside the WS pubsubSink. Used by the bridge path so a system-bridge poller can block on a turn (its poller is single-threaded per bridge → natural per-thread serialization) and translate sysagent events into bridge ResponseEvents.

Takes a full PromptInput so the bridge path can resolve pending confirmations (Approve/Reject button taps) the same way the web UI does — set Approved + ResumeRunID. A plain user message leaves both nil/empty. When extraSink is non-nil the WS pubsub is bypassed entirely; only the bridge sees events. Returns once the chat loop exits — RunCompleted / RunFailed / RunSuspended / RunCancelled all reach this.

func (*Service) SetBridgeResumer

func (s *Service) SetBridgeResumer(r bridgeResumer)

SetBridgeResumer wires the bridge resume path. Called once at startup; nil leaves bridge follow-up delivery disabled (web notifications still work).

Directories

Path Synopsis
Package agentview projects the public API protos down to a compact, LLM-friendly shape for the system agent's tool outputs.
Package agentview projects the public API protos down to a compact, LLM-friendly shape for the system agent's tool outputs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL