agent

package
v0.0.0-...-50da398 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2026 License: MIT Imports: 46 Imported by: 0

Documentation

Index

Constants

View Source
const (
	DefaultCommandTimeoutSeconds = 900
	EnvCommandTimeoutSeconds     = "WACKYPUB_COMMAND_TIMEOUT_SECONDS"
)
View Source
const (
	ScratchpadDirName         = "scratchpad"
	MaxScratchpadEntries      = 300
	MaxExpandedArgBytes       = 500000
	ScratchpadOutputThreshold = 4000
	MediaDetectionHeaderBytes = 262
	BinaryCheckPrefixBytes    = 24
)
View Source
const (
	SkillsDirName = "skills"
	SkillFileName = "SKILL.md"
)
View Source
const (
	RootMarkerFile    = "WACKYPUB_ROOT"
	AllowedAgentsFile = "WACKYPUB_ALLOWED_AGENTS"
	CallChainEnvVar   = "WACKYPUB_CALL_CHAIN"
	ToolsDirName      = "tools"
)
View Source
const (
	Agent2AgentEnvVar = "AGENT2AGENT"
)
View Source
const DefaultAgentGitignoreContent = `` /* 298-byte string literal not displayed */
View Source
const DefaultCompactionPct = 50.0
View Source
const DefaultHTTPTimeoutSeconds = 900

DefaultHTTPTimeoutSeconds is the default timeout (15 minutes) for HTTP client calls to LLM backends.

View Source
const DefaultJPEGQuality = 85
View Source
const DefaultMaxToolTurns = 300

DefaultMaxToolTurns is the default cap on consecutive tool-call turns within a single GenerateTurn call, used wherever a caller doesn't specify one explicitly (the --max-tool-turns CLI flag, AgentSDK.NewSDK, and BuildADKAgent/LoadFolderAgent's own <= 0 fallback).

View Source
const DefaultWorkspaceDomain = "wackypub.local"
View Source
const DefaultWorkspaceRootGitignoreContent = `` /* 199-byte string literal not displayed */
View Source
const SessionFileName = "session.jsonl"

Variables

View Source
var DefaultCompactMD string

DefaultCompactMD holds examples/COMPACT.md's content, in the same append-only/compact-pct frontmatter + body shape a real <agentDir>/COMPACT.md has - parsed through the exact same ParseCompactConfig path, according to D44.

Set from main.go (D45), which embeds examples/COMPACT.md and assigns it here before cmd.Execute() runs - mirrors cmd.BundledA2ASkill/BundledWSSkill (D34), required because examples/ isn't reachable by a //go:embed directive living in pkg/agent (embed patterns can't use ".." to leave their own package directory, and a symlink pointing back into pkg/agent doesn't work either - confirmed live, embed refuses to read a symlink at all: "cannot embed irregular file"). Tests populate this themselves (see TestMain) rather than relying on main.go ever running.

Functions

func AppendSessionContent

func AppendSessionContent(agentDir string, content *genai.Content) error

AppendSessionContent appends a genai.Content turn to <agent_dir>/session.jsonl.

func AppendSessionTurn

func AppendSessionTurn(agentDir string, role string, text string) error

AppendSessionTurn is a convenience wrapper that appends a simple text turn.

func BuildADKAgent

func BuildADKAgent(agentID string, renderedPrompt string, maxToolTurns int, llmModel model.LLM, tools ...tool.Tool) (agent.Agent, error)

BuildADKAgent constructs a Google ADK LLMAgent for an agent directory. Name is agentID (unique within workspace), renderedPrompt is AGENTS.md system prompt, maxToolTurns caps tool executions.

func BuildADKAgentWithConfig

func BuildADKAgentWithConfig(agentID string, renderedPrompt string, maxToolTurns int, runtimeCfg *RuntimeConfig, llmModel model.LLM, tools ...tool.Tool) (agent.Agent, error)

BuildADKAgentWithConfig constructs a Google ADK LLMAgent for an agent directory, applying RuntimeConfig settings.

func BuildFolderAgentTools

func BuildFolderAgentTools(agentDir string, commandTimeoutSeconds ...int) (map[string]tool.Tool, []*genai.FunctionDeclaration, error)

BuildFolderAgentTools constructs ADK functiontool instances for built-in tools (create_scratchpad, get_scratchpad, list_scratchpads, search_scratchpad, delete_scratchpad) and a single generic run_command tool covering executables discovered under <agent_dir>/tools/.

func CheckAndCompactSession

func CheckAndCompactSession(ctx context.Context, agentDir string, runtimeCfg *RuntimeConfig, adkAgent agent.Agent, force bool) (bool, error)

CheckAndCompactSession checks if the session exceeds contextWindow and performs compaction, preserving the exact session prefix to optimize prompt caching according to D38/D45. force skips the contextWindow/token-estimate gate checks below (D44) - still refuses on a genuinely empty session regardless, since forcing compaction with nothing to compact isn't a testing use case, it's a no-op either way.

adkAgent is the calling FolderAgent's real ADK agent (fa.ADKAgent) - already carries the agent's system instruction and tool declarations, so routing the compaction call through it (via a disposable in-memory session + one runner.Run call, D45) sends a request whose shared prefix - system instruction, tools, memory turn, the archived turns - is structurally identical to a real generation call, unlike the hand-built request this used to send directly to an *model.LLM (no Tools, system prompt glued into turn 1's text - see D45 for the full trace).

func CleanSessionTurns

func CleanSessionTurns(contents []*genai.Content) []*genai.Content

CleanSessionTurns sanitizes a sequence of conversation turns for model requests by: 1. Removing dangling FunctionResponse parts (responses without a matching FunctionCall in the preceding model turn). 2. Pruning empty turns (turns with zero parts remaining after filtering). 3. Merging consecutive "user"-role turns into a single user turn per run, concatenating their parts in order.

session.jsonl intentionally allows consecutive user turns to accumulate — multiple `add` calls without an intervening `generate`, and, on every generation, the injected system-prompt+memory turn landing immediately before whatever the first real turn happens to be (itself usually "user"). That's fine for storage, but many OpenAI-compatible chat templates reject or silently mishandle non-alternating roles. Furthermore, LLM backends (OpenAI, Anthropic, Gemini) reject requests with a 400 Bad Request error if a FunctionResponse appears without a matching FunctionCall in the immediately preceding assistant message (e.g. if compaction cut history mid-exchange). This normalizes the sequence right before it's sent to a model, without touching what's stored on disk; callers should apply it to the Contents slice built for a model.LLMRequest, not to what gets persisted via AppendSessionContent/WriteSessionTurns.

func CommitWorkspaceEvent

func CommitWorkspaceEvent(wsDir, agentID, eventType string) error

CommitWorkspaceEvent creates a new git commit for a workspace or agent state-mutating event according to D35. If git tracking is not enabled for the agent or workspace, this is a silent no-op.

func ContentText

func ContentText(c *genai.Content) string

ContentText extracts the concatenated final-answer text from a genai.Content's parts, excluding any parts marked as Thought (reasoning/thinking output).

func CountLines

func CountLines(text string) int

CountLines counts the number of lines in text according to D39. Matches TailFile's convention: splits on \n, drops trailing empty segment if text ends in \n.

func CreateGeminiModel

func CreateGeminiModel(ctx context.Context, modelName string, apiKey string) (model.LLM, error)

CreateGeminiModel instantiates a native Gemini LLM model using Google ADK model package.

func CreateWorkspaceSnapshot

func CreateWorkspaceSnapshot(wsDir string) (string, error)

CreateWorkspaceSnapshot creates/updates <wsDir>/MANIFEST.md with a list of all agent directories and their HEAD commit SHAs according to D35.

func CurrentAgentIDFromCWD

func CurrentAgentIDFromCWD() (string, bool)

CurrentAgentIDFromCWD returns the agent ID whose directory the current working directory IS - not contains, not a subdirectory of - and whether one was detected at all, according to D41. Same looksLikeAgentDir + filepath.Base pattern ValidateAgentTarget already uses for its own sendingAgentID computation, applied directly rather than via an upward walk: run_command always sets a spawned tool's cmd.Dir to the calling agent's directory exactly, never a subdirectory of it, so there's no case in the actual call path a direct check misses.

func DeleteScratchpad

func DeleteScratchpad(agentDir string, id string) error

DeleteScratchpad removes a scratchpad entry (.txt or .dat) by ID per D48.

func DetectMediaType

func DetectMediaType(data []byte) (bool, string)

DetectMediaType returns whether data is binary and its MIME type using a 2-stage heuristic per D48.

func DiscoverAgentSkills

func DiscoverAgentSkills(agentDir string) (map[string]*Skill, []*Skill, []*Skill, error)

DiscoverAgentSkills walks <agentDir>/skills/ recursively looking for SKILL.md files according to D20. Returns:

  • skillsMap: map of skillName -> *Skill
  • onDemandSkills: sorted slice of *Skill where AlwaysLoad is false
  • alwaysLoadedSkills: sorted slice of *Skill where AlwaysLoad is true
  • error

func DiscoverAgentSkillsMap

func DiscoverAgentSkillsMap(agentDir string) (map[string]*Skill, []*Skill, []*Skill, []string, error)

DiscoverAgentSkillsMap walks <agentDir>/skills/ recursively looking for SKILL.md files according to D20. Resolves directory and file symlinks and follows them, preventing infinite symlink cycles. Returns:

  • skillsMap: map of skillName -> *Skill
  • onDemandSkills: sorted slice of *Skill where AlwaysLoad is false
  • alwaysLoadedSkills: sorted slice of *Skill where AlwaysLoad is true
  • shadowed: shadowing warning messages
  • error

func DiscoverAgentTools

func DiscoverAgentTools(agentDir string) ([]string, []string, error)

DiscoverAgentTools walks <agentDir>/tools/ recursively for executable files according to D14. Returns discovered unique tool names and shadowing warning messages.

func DiscoverAgentToolsMap

func DiscoverAgentToolsMap(agentDir string) (map[string]string, []string, []string, error)

DiscoverAgentToolsMap walks <agentDir>/tools/ recursively for executable files according to D14. Resolves directory and file symlinks and follows them, preventing infinite symlink cycles. Returns a map of tool name -> file path, discovered unique tool names, shadowing warning messages, and error.

func EnsureAgentGitignore

func EnsureAgentGitignore(agentDir string) error

EnsureAgentGitignore creates <agentDir>/.gitignore if it does not already exist.

func EnsureWorkspaceGitignore

func EnsureWorkspaceGitignore(wsDir string) error

EnsureWorkspaceGitignore creates <wsDir>/.gitignore if it does not already exist.

func EstimateTokens

func EstimateTokens(turns []*genai.Content, includeThinking bool) int

EstimateTokens calculates an approximate token count for session turns. includeThinking should match RuntimeConfig.PreserveThinking: when true, Thought-marked part text is counted too, since it's actually replayed to the model on every subsequent request for backends that preserve thinking.

func EvictOldestScratchpad

func EvictOldestScratchpad(spDir string, maxCap int)

func ExpandMacros

func ExpandMacros(content string, agentDir string) (string, error)

ExpandMacros processes text content and replaces any @<FILE_PATH> directives with the content of the referenced file relative to agentDir.

func ExpandScratchpadMacros

func ExpandScratchpadMacros(agentDir string, text string) (string, error)

ExpandScratchpadMacros replaces any inline <SCRATCHPAD_DATA id="X" skip_lines="N" num_lines="M" json_escape="true" /> macros in text with the corresponding scratchpad text content according to D18/D28/D30/D37.

func ExtractTextFromEvent

func ExtractTextFromEvent(event *session.Event) string

ExtractTextFromEvent parses plain text output from an ADK session event, excluding reasoning/thinking parts - mirrors ContentText's behavior.

func FindWorkspaceRootDir

func FindWorkspaceRootDir(startDir string) string

FindWorkspaceRootDir walks up from startDir looking for WACKYPUB_ROOT marker file. If not found, falls back to startDir's parent directory.

func FormatCompactionNotice

func FormatCompactionNotice(notice string) string

FormatCompactionNotice wraps a compaction-notice string in <COMPACTION_NOTICE> tags, mirroring FormatPersistentMemoryTurn (D46).

func FormatPersistentMemoryTurn

func FormatPersistentMemoryTurn(memoryContent string) string

FormatPersistentMemoryTurn constructs User Turn 1 wrapping MEMORY.md in <PERSISTENT_MEMORY> tags.

func FormatTraceResult

func FormatTraceResult(wsDir string, res *TraceResult, opts TraceOptions) string

FormatTraceResult formats a TraceResult to a readable string based on opts.Verbosity (0..4).

func GenerateTraceID

func GenerateTraceID() string

GenerateTraceID generates a random correlation trace ID with "a2a-" prefix.

func GetScratchpad

func GetScratchpad(agentDir string, id string, skipLines *int, numLines *int) (string, error)

GetScratchpad retrieves stored text by entry ID, optionally paginated by line range. Rejects binary (.dat) entries outright per D48.

func GetWorkspaceHeadCommit

func GetWorkspaceHeadCommit(repoDir string) (string, error)

GetWorkspaceHeadCommit returns the current HEAD commit SHA string for the target repository. Returns "" if the repository does not exist or has no commits yet.

func InitAgentGit

func InitAgentGit(wsDir, agentID string) error

InitAgentGit initializes an isolated per-agent git repository at <wsDir>/<agentID>/.git according to D35.

func InitWorkspaceGit

func InitWorkspaceGit(wsDir string) error

InitWorkspaceGit initializes a new git repository at wsDir according to D35.

func IsBinaryContent

func IsBinaryContent(data []byte) bool

IsBinaryContent checks the first min(BinaryCheckPrefixBytes, len(data)) bytes for any byte <= 8 (NUL, etc.) according to D48.

func IsWorkspaceGitRepo

func IsWorkspaceGitRepo(dir string) bool

IsWorkspaceGitRepo returns true if <dir>/.git exists.

func ListAgentIDs

func ListAgentIDs(wsDir string) ([]string, error)

ListAgentIDs returns the names of subdirectories of wsDir that look like agent directories - see agentDirSignals. Returned in sorted order. Returns an empty (nil) slice without error if wsDir does not exist.

func LoadAgentDotEnv

func LoadAgentDotEnv(agentDir string) (map[string]string, error)

LoadAgentDotEnv loads workspace root .env (<ws_dir>/.env) followed by per-agent .env (<agentDir>/.env). Applies loaded key-values into process environment (os.Setenv) and returns the combined map.

func MergeConsecutiveUserTurns

func MergeConsecutiveUserTurns(contents []*genai.Content) []*genai.Content

MergeConsecutiveUserTurns is a backwards-compatible alias for CleanSessionTurns.

func NewAnthropicModel

func NewAnthropicModel(runtimeCfg *RuntimeConfig) model.LLM

NewAnthropicModel instantiates an Anthropic model adapter for ADK, backed by official Anthropic SDK via achetronic/adk-utils-go.

func NewOpenAIModel

func NewOpenAIModel(runtimeCfg *RuntimeConfig) model.LLM

NewOpenAIModel instantiates an OpenAI-compatible model adapter for ADK, backed by the official OpenAI Go SDK via achetronic/adk-utils-go. Works against OpenAI itself and OpenAI-compatible providers (Ollama, vLLM, llama.cpp, DeepSeek, OpenRouter, etc.), including ones that emit the non-standard reasoning_content field for chain-of-thought, which the adapter surfaces as a Thought-marked genai.Part.

go.mod currently replaces achetronic/adk-utils-go with github.com/colinrgodsey/adk-utils-go (master, currently commit ee4a5294, "fix: treat `reasoning_content` appropriately" — this commit hash supersedes earlier ones referenced in prior history/commit messages here, since the fork's master was squashed/rewritten upstream). The fork re-emits Thought parts as a proper reasoning_content field on egress instead of merging them into plain "content" — required by DeepSeek V4 thinking mode and Kimi K2 Thinking, which 400 without it — preserves OpenRouter's structured reasoning_details blocks (needed for encrypted/signed reasoning), fixes streamed turns losing reasoning on the terminal response, and adds Config.ExtraBody for provider-specific request extensions Chat Completions doesn't define (e.g. OpenRouter's `{"reasoning": {"effort": "high"}}` to request extended thinking from models like Claude that don't emit it by default). See ADK_UTILS_GO_REASONING_EGRESS_BUG.md at the repo root for the original bug writeup. Once the fix lands upstream and is tagged, drop the replace directive and go back to depending on achetronic/adk-utils-go directly.

func NormalizeAndResizeImage

func NormalizeAndResizeImage(r io.Reader, maxDimension int) ([]byte, string, error)

NormalizeAndResizeImage reads an image from r, detects format, flattens any transparency onto white, resizes so the longest side does not exceed maxDimension (downscale only), and re-encodes as JPEG.

func ParseDotEnv

func ParseDotEnv(dotenvPath string) (map[string]string, error)

ParseDotEnv reads a .env file from the specified path and returns a key-value map. If the file does not exist, it returns an empty map and a nil error.

func PushWorkspaceAndAgents

func PushWorkspaceAndAgents(wsDir, remoteName string) error

PushWorkspaceAndAgents pushes the workspace root repo and each agent folder repo to <remoteName> according to D35. Reads the remote URL from the root workspace repo, and applies it to each per-agent repo pushing to branch <agent_id>.

func ReadMediaHeader

func ReadMediaHeader(filePath string) ([]byte, error)

ReadMediaHeader reads up to MediaDetectionHeaderBytes from filePath. Closes file properly and verifies errors from both read and close without swallowing.

func ReadMemoryFile

func ReadMemoryFile(agentDir string) (string, error)

ReadMemoryFile reads the contents of <agent_dir>/MEMORY.md. If the file does not exist, returns empty string without error.

func ReadSessionTurns

func ReadSessionTurns(agentDir string) ([]*genai.Content, error)

ReadSessionTurns reads all turns from <agent_dir>/session.jsonl as genai.Content objects. If the file does not exist, returns an empty list without error.

func RenderAgentSystemPrompt

func RenderAgentSystemPrompt(wsDir, agentID string) (string, error)

RenderAgentSystemPrompt reads <wsDir>/<agentID>/AGENTS.md (falling back to a generic "You are agent <id>." prompt if it doesn't exist, matching LoadFolderAgent) and expands @<FILE_PATH> macros. Unlike LoadFolderAgent, it does not touch runtime.json and does not construct a model - useful for validating AGENTS.md/macro output independently of backend configuration.

func RenderAutoloadedSkills

func RenderAutoloadedSkills(agentDir string) (string, error)

RenderAutoloadedSkills formats always-loaded skills into the <AUTOLOADED_SKILLS> block.

func ResolveCommitHash

func ResolveCommitHash(repo *git.Repository, spec string) (plumbing.Hash, error)

ResolveCommitHash resolves a commit specifier (SHA, prefix, suffix, branch, tag, HEAD~N) in repo.

func ResolveGitRepoDir

func ResolveGitRepoDir(wsDir, agentID string) string

ResolveGitRepoDir resolves whether the agent has its own repository (<wsDir>/<agentID>/.git) or (for workspace-level commands when agentID is "") uses the workspace root repository (<wsDir>/.git). Returns "" if git tracking is not enabled for the target.

func ResolveWorkspaceDir

func ResolveWorkspaceDir(wsFlag string, isExplicit bool) (string, error)

ResolveWorkspaceDir resolves the workspace directory according to D15: - If isExplicit is true (--ws was explicitly specified), wsFlag must contain RootMarkerFile directly. - If isExplicit is false (default), walk up from CWD looking for RootMarkerFile. Error if not found.

func StripSessionSignatures

func StripSessionSignatures(agentDir string) (int, error)

StripSessionSignatures rewrites <agentDir>/session.jsonl in place, removing provider-specific opaque reasoning/thought signatures (OpenRouter reasoning_details block metadata and Gemini's ThoughtSignature field — see StripSignatures) from every turn. Readable plain-text reasoning (Thought parts with text) is left untouched.

Useful when permanently moving an agent off a model/provider that emitted signed reasoning: those signatures would otherwise sit in session.jsonl as a landmine, either as a stale, unreplayable blob if SupportsReasoningDetails is toggled back on for a different backend that can't decrypt them (see ADK_UTILS_GO_REASONING_EGRESS_BUG.md), or - as confirmed live - an outright 400 the moment a session carrying another provider's thought signatures is replayed against a new provider (e.g. switching an agent's runtime.json from Gemini to Anthropic).

Returns the number of turns that were actually modified. Does not acquire the session lock; callers must hold it (see AgentSDK.StripSignatures).

func StripSignatures

func StripSignatures(c *genai.Content) *genai.Content

StripSignatures returns a copy of c with provider-specific opaque reasoning/thought signatures removed from every part: adk-utils-go's OpenRouter reasoning_details block metadata, and Gemini's ThoughtSignature field. Both are opaque blobs a specific backend issues to let its own thinking be replayed in a later request; neither means anything to a different provider, and replaying one to the wrong provider gets the request rejected outright (confirmed live: an Anthropic request 400s with "Invalid `signature` in `thinking` block" when it receives a Gemini ThoughtSignature carried over from an earlier session).

Ingest captures reasoning_details blocks unconditionally, regardless of SupportsReasoningDetails — so a block (including an opaque encrypted one from e.g. an OpenAI model routed through OpenRouter) ends up in session.jsonl even when the runtime config has egress disabled. Left in place, it's dead weight at best; at worst, it becomes a stale, unreplayable blob if SupportsReasoningDetails is later toggled back on for the same session but the routed endpoint has changed (see ADK_UTILS_GO_REASONING_EGRESS_BUG.md for the underlying encrypted-payload endpoint-pinning issue). Callers should apply this before persisting a turn when RuntimeConfig.SupportsReasoningDetails is false.

A thought Part that carries nothing but a signature (no readable text) is dropped entirely once stripped, since nothing would remain to preserve.

func TagWorkspaceAndAgents

func TagWorkspaceAndAgents(wsDir, tagName string) error

TagWorkspaceAndAgents creates a git tag <tagName> in the workspace root repo (if present), and tags each agent repository with "tag-<agent_id>" according to D35.

func ValidateAgentTarget

func ValidateAgentTarget(targetAgentID string) (func(), error)

ValidateAgentTarget performs cross-agent authorization (AllowedAgentsFile against CWD) and deadlock prevention (CallChainEnvVar) according to D16. Returns a cleanup function that restores CallChainEnvVar to its previous state.

func WriteMemoryFile

func WriteMemoryFile(agentDir string, memoryContent string) error

WriteMemoryFile updates the contents of <agent_dir>/MEMORY.md.

func WriteSessionTurns

func WriteSessionTurns(agentDir string, turns []*genai.Content) error

WriteSessionTurns overwrites <agent_dir>/session.jsonl with a new list of turns.

Types

type A2AMetadata

type A2AMetadata struct {
	CallerID  string            `json:"caller_id,omitempty"`
	CallChain []string          `json:"call_chain,omitempty"`
	TraceID   string            `json:"trace_id,omitempty"`
	Metadata  map[string]string `json:"metadata,omitempty"`
}

A2AMetadata defines the minified Agent2Agent context payload passed between agent calls via AGENT2AGENT env var according to D33.

func ParseA2AMetadata

func ParseA2AMetadata() (*A2AMetadata, error)

ParseA2AMetadata parses the AGENT2AGENT environment variable if present. If AGENT2AGENT is empty or absent, it falls back to parsing legacy WACKYPUB_CALL_CHAIN CSV string.

func (*A2AMetadata) Encode

func (m *A2AMetadata) Encode() (string, error)

Encode serializes A2AMetadata into a minified (dense) JSON string.

type AgentInspection

type AgentInspection struct {
	AgentID  string
	AgentDir string

	// AgentDirExists is false when AgentDir doesn't exist yet - every other
	// field is zero-valued in that case.
	AgentDirExists bool

	AgentsMDExists bool
	MemoryMDExists bool
	DotEnvExists   bool

	RuntimeJSONExists    bool
	RuntimeJSONIsSymlink bool
	// RuntimeJSONResolved is the symlink target's real path, only set when
	// RuntimeJSONIsSymlink is true and it resolves.
	RuntimeJSONResolved string
	RuntimeJSONValid    bool
	// RuntimeJSONError holds LoadRuntimeConfig's error message when
	// RuntimeJSONExists is true but RuntimeJSONValid is false.
	RuntimeJSONError string
	// RuntimeConfig is non-nil only when RuntimeJSONValid is true.
	RuntimeConfig *RuntimeConfig

	SessionJSONLExists bool
	// SessionTurnCount is the number of turns ReadSessionTurns successfully
	// parsed.
	SessionTurnCount int
	// SessionCorruptLines is the number of non-empty lines in session.jsonl
	// that ReadSessionTurns silently skipped because they didn't parse as a
	// genai.Content - see .agents/AGENTS.md's session.jsonl corruption
	// gotcha. Zero in the common case.
	SessionCorruptLines int
	AllowedAgentsExists bool
	AllowedAgents       []string

	ToolsDirExists  bool
	DiscoveredTools []string
	ShadowedTools   []string

	SkillsDirExists  bool
	DiscoveredSkills []string
	ShadowedSkills   []string
}

AgentInspection reports the on-disk state of a single agent directory: which expected files are present, whether runtime.json parses, and basic session/memory stats. Intended for diagnosing what a workspace still needs without requiring prior knowledge of the file layout - see the `workspace` CLI command and AgentSDK.InspectAgent.

func InspectAgentDir

func InspectAgentDir(wsDir, agentID string) (*AgentInspection, error)

InspectAgentDir builds an AgentInspection for <wsDir>/<agentID> without acquiring the session lock - callers that need a consistent snapshot alongside concurrent writers should hold the lock themselves (see AgentSDK.InspectAgent, which does). Safe to call even if the agent directory or any of its expected files don't exist.

type AgentSDK

type AgentSDK struct {
	WorkspaceDir          string
	MaxToolTurns          int
	CommandTimeoutSeconds int
}

AgentSDK provides a clean, programmatic Go API for orchestrating folder-based agents.

func NewSDK

func NewSDK(workspaceDir string) *AgentSDK

NewSDK creates an SDK instance bound to a workspace directory.

func (*AgentSDK) AddAndGenerateTurn

func (s *AgentSDK) AddAndGenerateTurn(ctx context.Context, agentID string, userMessage string) (string, error)

AddAndGenerateTurn atomically appends a user message and generates the assistant response under a single lock.

func (*AgentSDK) AddMedia

func (s *AgentSDK) AddMedia(agentID string, reader io.Reader) (*genai.Content, error)

AddMedia appends a normalized, resized JPEG image turn read from reader to <ws_dir>/<agent_id>/session.jsonl according to D47. Gated by runtime.json's maxImageDimension field — returns an error if maxImageDimension is absent or <= 0.

func (*AgentSDK) AddUserTurn

func (s *AgentSDK) AddUserTurn(agentID string, message string) error

AddUserTurn appends a user message to <ws_dir>/<agent_id>/session.jsonl. Creates the agent directory automatically if it does not exist yet.

func (*AgentSDK) AgentDir

func (s *AgentSDK) AgentDir(agentID string) string

AgentDir returns the absolute or relative path for an agent folder (<ws_dir>/<agent_id>).

func (*AgentSDK) CompactSession

func (s *AgentSDK) CompactSession(ctx context.Context, agentID string, force bool) (bool, error)

CompactSession manually triggers session compaction evaluation for an agent. force bypasses the contextWindow/token-estimate gate checks (D44) - only this manual path can force; the automatic pre-generation check never does.

func (*AgentSDK) CreateScratchpad

func (s *AgentSDK) CreateScratchpad(agentID string, text string, createdBy string) (*ScratchpadEntry, error)

CreateScratchpad creates a new persistent scratchpad entry for an agent (<ws_dir>/<agent_id>/scratchpad/). Atomic and collision-safe across processes without requiring the session lock.

func (*AgentSDK) DeleteScratchpad

func (s *AgentSDK) DeleteScratchpad(agentID string, entryID string) error

DeleteScratchpad removes a scratchpad entry from <ws_dir>/<agent_id>/scratchpad/ by entry ID.

func (*AgentSDK) GenerateTurn

func (s *AgentSDK) GenerateTurn(ctx context.Context, agentID string) (string, error)

GenerateTurn loads the folder agent, checks for compaction, generates the next assistant turn, prints to output if configured, and appends the assistant turn to session.jsonl.

func (*AgentSDK) GetAgent

func (s *AgentSDK) GetAgent(agentID string) (*FolderAgent, error)

GetAgent loads and returns the FolderAgent object for low-level ADK runner interactions.

func (*AgentSDK) GetScratchpad

func (s *AgentSDK) GetScratchpad(agentID string, entryID string, skipLines *int, numLines *int) (string, error)

GetScratchpad retrieves stored text from <ws_dir>/<agent_id>/scratchpad.json by entry ID. Does not acquire the session lock (read-only against atomic temp-file replace).

func (*AgentSDK) InspectAgent

func (s *AgentSDK) InspectAgent(agentID string) (*AgentInspection, error)

InspectAgent reports the on-disk state of <ws_dir>/<agent_id>: which expected files are present, whether runtime.json parses, and session/memory stats. Safe to call on an agent that doesn't exist yet or is only partially set up - see AgentInspection.

Deliberately does not go through ValidateAgentTarget's WACKYPUB_ALLOWED_AGENTS check (D16): that authorization boundary exists to gate cross-agent tool invocation/generation, not read-only diagnostic visibility - InspectAgent has no side effects and can't cause another agent to do anything. Gating it the same way surfaces an "unauthorized" failure as a generic parse/ config error in wackypub workspace's summary table, which is actively misleading (see D16).

Does not create the agent directory as a side effect (unlike most other AgentSDK methods) - if it doesn't exist, returns an AgentInspection with AgentDirExists false and every other field zero-valued.

Deliberately does not acquire the session lock. AcquireSessionLock blocks until the lock is free, and InspectAgent is exactly the kind of call an agent's own tool loop can make against itself mid-generation (directly, or via wackypub workspace's no-arg summary, which inspects every agent including the caller) - since GenerateTurn already holds that same lock for the whole call, that blocking acquire deadlocks forever. Reading without the lock is safe: ReadSessionTurns already tolerates a torn read gracefully (see AgentInspection.SessionCorruptLines), and the lock's real job is serializing concurrent writers, not protecting readers.

func (*AgentSDK) ListAgents

func (s *AgentSDK) ListAgents() ([]string, error)

ListAgents returns the IDs of agent directories found directly under the workspace directory (see ListAgentIDs for how a directory is recognized as an agent). Does not acquire any lock - it only reads directory names.

func (*AgentSDK) ListScratchpads

func (s *AgentSDK) ListScratchpads(agentID string) ([]ScratchpadItem, int, int, error)

ListScratchpads returns metadata items for all live scratchpad entries in <ws_dir>/<agent_id>/scratchpad.json. Does not acquire the session lock (read-only against atomic temp-file replace).

func (*AgentSDK) ReadMemory

func (s *AgentSDK) ReadMemory(agentID string) (string, error)

ReadMemory returns the current contents of <ws_dir>/<agent_id>/MEMORY.md.

func (*AgentSDK) ReadSession

func (s *AgentSDK) ReadSession(agentID string) ([]*genai.Content, error)

ReadSession returns all conversation turns logged in <ws_dir>/<agent_id>/session.jsonl.

func (*AgentSDK) RenderSystemPrompt

func (s *AgentSDK) RenderSystemPrompt(agentID string) (string, error)

RenderSystemPrompt returns the fully rendered system prompt for an agent - AGENTS.md (or the generic fallback if it doesn't exist) after @<FILE_PATH> macro expansion. Does not construct a model and does not require runtime.json to exist or be valid - useful for validating AGENTS.md/macro output independently of backend configuration.

func (*AgentSDK) SearchScratchpad

func (s *AgentSDK) SearchScratchpad(agentID string, entryID string, query string, caseSensitive *bool, useRegex bool, maxResults int) (*SearchScratchpadResult, error)

SearchScratchpad searches a specific scratchpad entry in <ws_dir>/<agent_id>/scratchpad.json for matching lines. Does not acquire the session lock (read-only against atomic temp-file replace).

func (*AgentSDK) StripSignatures

func (s *AgentSDK) StripSignatures(agentID string) (int, error)

StripSignatures permanently removes provider-specific opaque reasoning/thought signatures (OpenRouter reasoning_details block metadata, e.g. encrypted/signed reasoning tied to a specific backend endpoint, and Gemini's ThoughtSignature field) from every turn in <ws_dir>/<agent_id>/session.jsonl, rewriting the file in place. Readable plain-text reasoning is left untouched. Useful when switching an agent from one model/provider to another, since a replayed signature from the old provider is rejected outright by the new one. Returns the number of turns that were modified.

func (*AgentSDK) Trace

func (s *AgentSDK) Trace(agentID string, commitSpec string, traceID string, opts TraceOptions) (*TraceResult, error)

Trace performs backward causal tracing starting from an agent commit specifier or global trace ID according to D36.

type CompactConfig

type CompactConfig struct {
	AppendOnly       bool
	CompactPct       float64
	CompactionNotice string
	Prompt           string
}

func LoadCompactConfig

func LoadCompactConfig(agentDir string) (*CompactConfig, error)

LoadCompactConfig loads per-agent COMPACT.md from <agentDir>/COMPACT.md if present according to D38. Falls back to the embedded default (DefaultCompactMD) if absent, according to D44.

func ParseCompactConfig

func ParseCompactConfig(content string) (*CompactConfig, error)

ParseCompactConfig parses COMPACT.md's YAML frontmatter + body from an in-memory string - either read from an agent's own <agentDir>/COMPACT.md or the embedded DefaultCompactMD - mirroring ParseSkillFile/ParseSkillContent's split (D40). Fields left unset in the frontmatter keep cfg's zero-value defaults (AppendOnly=false, CompactPct=0) - callers seed cfg with real defaults before calling if that matters, the way LoadCompactConfig does.

type CompactFrontmatter

type CompactFrontmatter struct {
	AppendOnly       *bool    `yaml:"append-only"`
	CompactPct       *float64 `yaml:"compact-pct"`
	CompactionNotice *string  `yaml:"compaction-notice"`
}

type CreateScratchpadArgs

type CreateScratchpadArgs struct {
	Text string `json:"text" jsonschema_description:"Text content to store in a persistent scratchpad entry"`
}

type CreateScratchpadResult

type CreateScratchpadResult struct {
	ID   string `json:"id"`
	Size int    `json:"size"`
}

type DeleteScratchpadArgs

type DeleteScratchpadArgs struct {
	ID string `json:"id" jsonschema_description:"4-character ID of the scratchpad entry to delete"`
}

type DeleteScratchpadResult

type DeleteScratchpadResult struct {
	Status string `json:"status"`
}

type ExecToolArgs

type ExecToolArgs struct {
	Args  []string          `` /* 167-byte string literal not displayed */
	Env   map[string]string `` /* 143-byte string literal not displayed */
	Stdin string            `` /* 157-byte string literal not displayed */
}

type FileSessionService

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

func NewFileSessionService

func NewFileSessionService(wsDir string) *FileSessionService

func (*FileSessionService) AppendEvent

func (s *FileSessionService) AppendEvent(ctx context.Context, sess session.Session, evt *session.Event) error

func (*FileSessionService) Create

func (*FileSessionService) Delete

func (*FileSessionService) Get

func (*FileSessionService) List

type FolderAgent

type FolderAgent struct {
	AgentID               string
	AgentDir              string
	DotEnv                map[string]string
	RuntimeConfig         *RuntimeConfig
	SystemPrompt          string
	MemoryPrompt          string
	Model                 model.LLM
	ADKAgent              agent.Agent
	MaxToolTurns          int
	CommandTimeoutSeconds int
}

FolderAgent encapsulates an agent loaded from a folder environment (<ws_dir>/<agent_id>).

func LoadFolderAgent

func LoadFolderAgent(wsDir string, agentID string, maxToolTurns int, commandTimeoutSeconds ...int) (*FolderAgent, error)

LoadFolderAgent loads and initializes an agent from <wsDir>/<agentID>.

func (*FolderAgent) GenerateTurn

func (fa *FolderAgent) GenerateTurn(ctx context.Context) (string, error)

GenerateTurn performs the agent generation turn for the current session using Google ADK runner.Runner. Uses FileSessionService to read and write session history directly to session.jsonl.

func (*FolderAgent) RunWithRunner

func (fa *FolderAgent) RunWithRunner(ctx context.Context, sessionID string, prompt string) ([]*session.Event, error)

Helper to run ADK runner session for folder agent

type GetScratchpadArgs

type GetScratchpadArgs struct {
	ID        string `json:"id" jsonschema_description:"4-character ID of the scratchpad entry to read"`
	SkipLines *int   `json:"skip_lines,omitempty" jsonschema_description:"Optional number of lines to skip from the beginning"`
	NumLines  *int   `json:"num_lines,omitempty" jsonschema_description:"Optional maximum number of lines to retrieve"`
}

type GetScratchpadResult

type GetScratchpadResult struct {
	Output       string `json:"output"`
	Deferred     bool   `json:"deferred,omitempty"`
	ScratchpadID string `json:"scratchpad_id,omitempty"`
}

type ListScratchpadsArgs

type ListScratchpadsArgs struct{}

type ListScratchpadsResult

type ListScratchpadsResult struct {
	Entries []ScratchpadItem `json:"entries"`
	Count   int              `json:"count"`
	Cap     int              `json:"cap"`
}

type LoadSkillArgs

type LoadSkillArgs struct {
	Name string `json:"name" jsonschema_description:"Name of the skill to load into conversation context"`
}

type LoadSkillResult

type LoadSkillResult struct {
	Output string `json:"output"`
}

type RunCommandArgs

type RunCommandArgs struct {
	Command string            `json:"command" jsonschema_description:"Name of the command executable to run from the discovered tools list"`
	Args    []string          `` /* 157-byte string literal not displayed */
	Env     map[string]string `` /* 143-byte string literal not displayed */
	Stdin   string            `` /* 157-byte string literal not displayed */
}

type RunCommandResult

type RunCommandResult struct {
	Output string `json:"output"`
}

type RuntimeConfig

type RuntimeConfig struct {
	// Provider selects the model provider: "openai" (default when Endpoint is set),
	// "gemini" (default when Endpoint is empty), or "anthropic".
	Provider string `json:"provider,omitempty"`

	Endpoint      string `json:"endpoint"`
	Model         string `json:"model"`
	APIKey        string `json:"apiKey"`
	ContextWindow int    `json:"contextWindow"`

	// TimeoutSeconds sets the HTTP client timeout in seconds for API calls to the LLM backend.
	// Defaults to DefaultHTTPTimeoutSeconds (900s / 15 minutes) when unset or <= 0.
	TimeoutSeconds int `json:"timeoutSeconds,omitempty"`

	// Anthropic-specific thinking fields:
	AnthropicThinkingBudgetTokens *int   `json:"anthropicThinkingBudgetTokens,omitempty"`
	AnthropicThinkingEffort       string `json:"anthropicThinkingEffort,omitempty"`
	AnthropicThinkingMode         string `json:"anthropicThinkingMode,omitempty"`

	// Gemini-specific thinking fields:
	GeminiThinkingBudget  *int   `json:"geminiThinkingBudget,omitempty"`
	GeminiThinkingLevel   string `json:"geminiThinkingLevel,omitempty"`
	GeminiIncludeThoughts *bool  `json:"geminiIncludeThoughts,omitempty"`

	// OpenAI / OpenRouter-specific reasoning fields:
	ReasoningEffort          string         `json:"reasoningEffort,omitempty"`
	ReasoningEgress          string         `json:"reasoningEgress,omitempty"`
	ReasoningField           string         `json:"reasoningField,omitempty"`
	SupportsReasoningDetails bool           `json:"supportsReasoningDetails,omitempty"`
	ExtraBody                map[string]any `json:"extraBody,omitempty"`

	// ExtraHeaders overrides the default identifying HTTP headers
	// (X-Title, HTTP-Referer) sent on every request - a key present here
	// replaces the default of the same name. See D43.
	ExtraHeaders map[string]string `json:"extraHeaders,omitempty"`

	// Generic thinking aliases (fallback if provider-specific fields are unset):
	ThinkingBudgetTokens *int   `json:"thinkingBudgetTokens,omitempty"`
	ThinkingEffort       string `json:"thinkingEffort,omitempty"`
	ThinkingMode         string `json:"thinkingMode,omitempty"`

	// PreserveThinking should be set for backends that resend and bill for
	// prior reasoning/thinking text on every turn (e.g. Kimi K2 Thinking,
	// DeepSeek V4 thinking mode, or any provider used with reasoning egress
	// enabled). When true, EstimateTokens includes Thought-marked part text
	// in its count, since that text is actually replayed to the model on
	// every subsequent request and consumes real context budget. Leave false
	// for backends that drop or ignore reasoning_content in history by
	// default (e.g. Qwen3), where thinking never counts toward future
	// requests' token usage.
	PreserveThinking bool `json:"preserveThinking,omitempty"`

	// MaxImageDimension gates "wackypub agent <id> add-media" (D47): the
	// longer side, in pixels, an attached image is downscaled to fit (never
	// upscaled). Absent or <= 0 means image attachments are rejected outright
	// - image support is opt-in per agent, not on by default.
	MaxImageDimension int `json:"maxImageDimension,omitempty"`
}

RuntimeConfig represents the agent's runtime.json configuration.

func LoadRuntimeConfig

func LoadRuntimeConfig(agentDir string) (*RuntimeConfig, error)

LoadRuntimeConfig reads and unmarshals runtime.json for an agent. Loads workspace root and per-agent .env files, expands environment variables (${VAR} / $VAR) in runtime.json data, and handles symlinks transparently using os.ReadFile / filepath.EvalSymlinks.

type ScratchpadEntry

type ScratchpadEntry struct {
	ID        string `json:"id"`
	Size      int    `json:"size"`
	Lines     int    `json:"lines"`
	CreatedBy string `json:"created_by"`
	Text      string `json:"text,omitempty"`
	IsBinary  bool   `json:"is_binary,omitempty"`
	MIMEType  string `json:"mime_type,omitempty"`
}

func CreateBinaryScratchpad

func CreateBinaryScratchpad(agentDir string, data []byte, createdBy string, mimeType string) (*ScratchpadEntry, error)

CreateBinaryScratchpad creates a new binary scratchpad entry in <agentDir>/scratchpad/<id>-0-<createdBy>.dat per D48.

func CreateScratchpad

func CreateScratchpad(agentDir string, text string, createdBy string) (*ScratchpadEntry, error)

CreateScratchpad creates a new text scratchpad entry in <agentDir>/scratchpad/<id>-<lines>-<createdBy>.txt according to D30/D39. Automatically expands inline <SCRATCHPAD_DATA id="X" ... /> macros before storing. Atomic and collision-safe across separate OS processes via O_CREATE|O_EXCL. Automatically evicts the entry with the oldest mtime when live entries exceed cap (300).

type ScratchpadItem

type ScratchpadItem struct {
	ID        string `json:"id"`
	Size      int    `json:"size"`
	Lines     int    `json:"lines"`
	CreatedBy string `json:"created_by"`
	IsBinary  bool   `json:"is_binary,omitempty"`
	MIMEType  string `json:"mime_type,omitempty"`
}

func ListScratchpads

func ListScratchpads(agentDir string) ([]ScratchpadItem, int, int, error)

ListScratchpads returns metadata items for all live entries in <agentDir>/scratchpad/ ordered by mtime ascending.

type ScratchpadMatch

type ScratchpadMatch struct {
	Line      int    `json:"line"`
	SkipLines int    `json:"skip_lines"`
	Text      string `json:"text"`
}

type SearchScratchpadArgs

type SearchScratchpadArgs struct {
	ID            string `json:"id" jsonschema_description:"Required scratchpad entry ID to search"`
	Query         string `json:"query" jsonschema_description:"Search query string"`
	CaseSensitive *bool  `json:"case_sensitive,omitempty" jsonschema_description:"Whether search is case-sensitive (default: true)"`
	Regex         bool   `json:"regex,omitempty" jsonschema_description:"Opt-in to treat query as a regular expression (default: false)"`
	MaxResults    int    `json:"max_results,omitempty" jsonschema_description:"Maximum number of matching lines to return (default: 50)"`
}

type SearchScratchpadResult

type SearchScratchpadResult struct {
	ID           string            `json:"id"`
	Query        string            `json:"query"`
	TotalMatches int               `json:"total_matches"`
	MaxResults   int               `json:"max_results"`
	Matches      []ScratchpadMatch `json:"matches"`
}

func SearchScratchpad

func SearchScratchpad(agentDir string, id string, query string, caseSensitive *bool, useRegex bool, maxResults int) (*SearchScratchpadResult, error)

SearchScratchpad searches a specific scratchpad entry (by ID) for query according to D25/D30. Rejects binary (.dat) entries outright per D48.

type SessionLock

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

SessionLock provides process-level exclusive locking for an agent session.

func AcquireSessionLock

func AcquireSessionLock(agentDir string) (*SessionLock, error)

AcquireSessionLock acquires an exclusive POSIX lock (flock) on <agent_dir>/session.lock. It writes the current process PID to the lock file for diagnostic visibility.

func (*SessionLock) Release

func (l *SessionLock) Release()

Release unlocks and closes the session lock file.

type Skill

type Skill struct {
	Name        string
	Description string
	AlwaysLoad  bool
	Body        string
	Path        string
}

func ParseSkillContent

func ParseSkillContent(content string, fallbackName string) (*Skill, error)

ParseSkillContent parses a SKILL.md document's optional YAML frontmatter and body from an in-memory string (e.g. one embedded via go:embed) rather than a file on disk. fallbackName is used as Name when the frontmatter doesn't specify one.

func ParseSkillFile

func ParseSkillFile(filePath string) (*Skill, error)

ParseSkillFile reads SKILL.md at filePath and parses optional YAML frontmatter.

type SkillFrontmatter

type SkillFrontmatter struct {
	Name        string `yaml:"name"`
	Description string `yaml:"description"`
	AlwaysLoad  bool   `yaml:"always_load"`
}

type TraceOptions

type TraceOptions struct {
	MaxSteps  int
	Verbosity int
}

TraceOptions configures causal graph traversal and formatting according to D36.

func DefaultTraceOptions

func DefaultTraceOptions() TraceOptions

DefaultTraceOptions returns standard defaults for tracing.

type TraceResult

type TraceResult struct {
	TargetAgentID string      `json:"target_agent_id,omitempty"`
	TargetCommit  string      `json:"target_commit,omitempty"`
	TraceID       string      `json:"trace_id,omitempty"`
	Steps         []TraceStep `json:"steps"`
}

TraceResult holds the ordered steps of a causal trace.

func TraceAgentCommit

func TraceAgentCommit(wsDir, agentID, commitSpec string, opts TraceOptions) (*TraceResult, error)

TraceAgentCommit traces backward starting from <agentID> at <commitSpec>.

func TraceByTraceID

func TraceByTraceID(wsDir, traceID string, opts TraceOptions) (*TraceResult, error)

TraceByTraceID searches all agent repos for commits matching <traceID> and builds the trace chain.

type TraceStep

type TraceStep struct {
	StepIndex        int              `json:"step_index"`
	AgentID          string           `json:"agent_id"`
	CommitSHA        string           `json:"commit_sha"`
	ShortSHA         string           `json:"short_sha"`
	EventType        string           `json:"event_type"`
	A2AMetadata      *A2AMetadata     `json:"a2a_metadata,omitempty"`
	RawCommitMessage string           `json:"raw_commit_message"`
	TurnContent      *genai.Content   `json:"turn_content,omitempty"`
	TurnContents     []*genai.Content `json:"turn_contents,omitempty"`
}

TraceStep represents a single hop in the causal trace chain.

Jump to

Keyboard shortcuts

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