logos

package module
v1.7.0 Latest Latest
Warning

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

Go to latest
Published: Apr 12, 2026 License: MIT Imports: 16 Imported by: 1

README

logos

Stateless agent loop for Go. LLMs think in plain text and act via shell commands inside <cmd> blocks — no tool schemas, no JSON.

prompt → LLM → scan <cmd> blocks → execute → feed <result> back → repeat

Backends

logos supports two command execution backends, selected by Config.Sandbox:

  • Sandbox: false — local exec via /bin/bash. No daemon required. Commands run directly on the host. Useful for development and environments where a sandbox daemon is unavailable.
  • Sandbox: true — sandboxed exec via temenos. Commands run in a restricted environment. Requires a running temenos daemon. Set SandboxAddr to override the socket path (empty uses TEMENOS_LISTEN_ADDRTEMENOS_SOCKET_PATH~/.temenos/daemon.sock).

Library Mode

For callers that already have an assistant message in hand and want to run its <cmd> blocks without driving a full agent loop, use the library API:

import (
    "context"

    "github.com/tta-lab/logos"
)

func dispatch(ctx context.Context, assistantMsg string) (string, error) {
    cmds := logos.ParseCmdBlocks(assistantMsg)
    if len(cmds) == 0 {
        return "", nil
    }
    cfg, err := logos.NewExecConfig(logos.Config{
        Sandbox: true, // or false for local exec
        Env: map[string]string{"MY_VAR": "value"},
        AllowedPaths: []client.AllowedPath{
            {Path: "/ro/project", ReadOnly: true},
            {Path: "/rw/workspace", ReadOnly: false},
        },
        TimeoutSec: 120,
    })
    if err != nil {
        return "", err
    }
    results := logos.ExecuteBlocks(ctx, cfg, cmds)
    return logos.FormatResults(results), nil
}

Use logos.StripCmdBlocks to get the prose portion of the message when you want to display the assistant text to a human without the tool calls.

Install

go get github.com/tta-lab/logos

Usage

result, err := logos.Run(ctx, logos.Config{
    Provider:     provider,     // fantasy.Provider (LLM abstraction)
    Model:        "claude-sonnet-4-6",
    SystemPrompt: systemPrompt,
    Sandbox:      true,        // true = temenos sandbox, false = local exec
    SandboxAddr:  "",         // optional; empty uses env fallback
    SandboxEnv:   map[string]string{"HOME": "/app"},
    AllowedPaths: []client.AllowedPath{
        {Path: "/app", ReadOnly: false},
    },
}, history, "read main.go and explain what it does", logos.Callbacks{
    OnDelta: func(text string) {
        fmt.Print(text) // stream to terminal
    },
})

The LLM responds in plain text. When it wants to act, it wraps commands in a <cmd> block:

Let me check the file structure first.

<cmd>
ls -la /app
</cmd>

logos detects the commands, executes them in the configured backend, and feeds the output back wrapped in <result>. The loop continues until the LLM responds without any <cmd> blocks.

How it works

  1. Run() takes config, conversation history, a prompt, and streaming callbacks
  2. Each turn, the LLM streams a response
  3. ParseCmdBlocks() extracts the contents of each <cmd> block from an assistant message
  4. Commands run via the configured backend (localRunner or temenos)
  5. Output wrapped in <result> becomes the next user message; loop repeats
  6. When the LLM responds with no <cmd> blocks, the loop ends and returns RunResult

Key types

Type Purpose
Config Provider, model, Sandbox/SandboxAddr, sandbox env, allowed paths
RunResult Final response text + all step messages
StepMessage One message in the loop (assistant text, with optional reasoning, or command output)
Callbacks Optional OnDelta and OnCommandResult streaming hooks
ParseCmdBlocks Extract <cmd> block contents from a complete assistant message
ExecuteBlocks Run parsed commands concurrently, return []Result
FormatResults Render []Result as a <result> wrap for the model
NewExecConfig Create an ExecConfig from Config (selects runner)
Result One command's execution outcome (Command, Stdout, Stderr, ExitCode, Err)
ExecConfig Execution knobs: Env, AllowedPaths, TimeoutSec

System prompt

BuildSystemPrompt() renders an embedded template with runtime context (working dir, platform, date, available commands). Consumers typically append their own instructions after the base prompt:

base, _ := logos.BuildSystemPrompt(logos.PromptData{
    WorkingDir: "/app",
    Platform:   "linux",
    Date:       "2026-03-16",
    Commands:   availableCommands,
})
systemPrompt := base + "\n\n" + customInstructions

Design

  • StatelessRun() takes history in, returns steps out. The caller owns persistence.
  • Single-cmd protocol — each LLM turn emits at most one <cmd> block; chain commands with &&, ;, or | inside one block.
  • Dual backend — sandbox (temenos) or local exec (/bin/bash), selected via Config.Sandbox.
  • Provider-agnostic — uses fantasy for LLM abstraction.
  • Reasoning round-trip — thinking blocks (Anthropic extended thinking) captured in StepMessage.Reasoning and ReasoningSignature for conversation restoration.

Dependencies

  • fantasy — LLM provider abstraction (streaming, messages)
  • temenos — sandboxed command execution daemon

License

MIT

Documentation

Overview

Package logos provides a reusable stateless agent loop.

Run() executes one agent loop iteration: prompt → LLM → <cmd> blocks → response. The caller provides conversation history, a system prompt, and streaming callbacks. No persistence — the caller receives StepMessages and handles storage.

Library mode: for callers that already have an assistant message in hand and want to run its <cmd> blocks without driving a full agent loop, use ParseCmdBlocks + ExecuteBlocks + FormatResults. NewExecConfig creates an ExecConfig from Config, selecting the appropriate runner (local or sandbox) based on Config.Sandbox.

Plane: shared

Index

Constants

View Source
const CmdBlockClose = "</cmd>"

CmdBlockClose is the closing tag for command blocks: <cmd>...</cmd>.

View Source
const CmdBlockOpen = "<cmd>"

CmdBlockOpen is the opening tag for command blocks: <cmd>...</cmd>.

View Source
const DefaultMaxSteps = 30

DefaultMaxSteps is the fallback max steps when Config.MaxSteps is 0.

View Source
const DefaultMaxTokens = 16384

DefaultMaxTokens is the fallback max output tokens when Config.MaxTokens is 0.

View Source
const MaxHallucinationRetries = 3

MaxHallucinationRetries is the maximum number of tool call hallucination retries before Run() returns an error.

Variables

This section is empty.

Functions

func BuildSystemPrompt

func BuildSystemPrompt(data PromptData) (string, error)

BuildSystemPrompt renders the default system prompt with runtime context. The result is the base prompt — consumers append their own instructions after this.

func ContainsBlockedCommand added in v1.6.0

func ContainsBlockedCommand(cmd string) bool

ContainsBlockedCommand returns true if cmd contains a blocked in-place editing command (sed -i or perl -i).

func ContainsToolCallHallucination added in v0.8.0

func ContainsToolCallHallucination(text string) bool

ContainsToolCallHallucination returns true if text contains tool call patterns produced by models that hallucinate structured formats — XML tags (e.g. <tool_call>) or bracket delimiters (e.g. [TOOL_CALL]...[/TOOL_CALL]). Standalone utility — internal detection is handled by streamFilter during streaming.

func FormatResults added in v1.4.0

func FormatResults(results []Result) string

FormatResults renders a slice of Results as the single outer <result>...</result> wrap that ExecuteBlocks callers feed back to the model as a user message. Format matches what streamOneTurn produces internally:

<result>
<cmd-1-verbatim>
<stdout-1>
STDERR:                        ← only if stderr non-empty
<stderr-1>
(exit code: N)                 ← only if exit != 0 AND exit != -1

<cmd-2-verbatim>
...
</result>

Entries are joined with a single "\n" between them. Empty results slice returns an empty string (no outer wrap).

func ParseCmdBlocks added in v1.4.0

func ParseCmdBlocks(text string) []string

ParseCmdBlocks extracts the content of each <cmd>...</cmd> block from a complete assistant message. Returns contents in document order with surrounding whitespace trimmed. Nested blocks are not supported — the first </cmd> after a <cmd> closes the block. Unclosed blocks are silently dropped (an unclosed block at the end of the message is ignored with no error — callers who care can detect via strings.Contains themselves).

This is the non-streaming sibling of the internal cmdBlockBuffer used by streamOneTurn. Use this when you have the full text; use cmdBlockBuffer when you have a stream of deltas.

func StepsToMessages added in v1.7.0

func StepsToMessages(steps []StepMessage) []fantasy.Message

StepsToMessages converts StepMessages back to fantasy.Messages for conversation round-tripping. Assistant steps produce ReasoningPart-first ordering (required by Anthropic). Result steps pass through the pre-formatted <result> envelope as a user message.

Used by fn-agent and lenos to rehydrate a conversation history from persisted StepMessages when resuming an agent session.

func StripCmdBlocks added in v1.4.0

func StripCmdBlocks(text string) string

StripCmdBlocks returns text with all <cmd>...</cmd> blocks (including the tags themselves) removed. Runs of blank lines left behind are collapsed to a single blank line. Unclosed blocks are stripped from their opening tag to the end of the string.

Use this to prepare prose for display when the raw assistant message contains cmd blocks you don't want the human to see (e.g. forwarding to chat without showing the tool calls).

Types

type Callbacks

type Callbacks struct {
	// OnDelta is called with each text delta as the LLM streams its response.
	OnDelta func(text string)
	// OnCommandResult is called after a command executes with the command string,
	// raw combined stdout+stderr output (no exit code suffix), and the exit code.
	OnCommandResult func(command string, output string, exitCode int)
	// OnRetry is called when a tool call hallucination (XML or bracket) is detected
	// and an "unprocessed" directive is injected. reason is "tool_call".
	OnRetry func(reason string, step int)
}

Callbacks holds optional streaming callbacks for the agent loop. All fields are nil-safe — unset callbacks are simply not called.

type CommandDoc added in v0.9.0

type CommandDoc struct {
	Name    string // command name, e.g. "url", "web", "rg"
	Summary string // one-line description shown under the heading
	Help    string // full help text (flags, examples, caveats)
}

CommandDoc describes a command available to the agent. Callers provide these to control which commands appear in the system prompt.

type Config

type Config struct {
	Provider     fantasy.Provider
	Model        string
	SystemPrompt string
	MaxSteps     int               // 0 means use default (DefaultMaxSteps)
	MaxTokens    int               // 0 means use default (DefaultMaxTokens)
	Sandbox      bool              // true = require temenos sandbox; false = use local exec
	SandboxAddr  string            // temenos socket/address; empty = env fallback chain
	SandboxEnv   map[string]string // env vars passed to sandbox per-request
	// AllowedPaths lists filesystem paths accessible during command execution.
	// Path validation (non-empty, absolute) is enforced by the temenos daemon.
	// Note: localRunner (Sandbox=false) uses AllowedPaths[0].Path as the working
	// directory; additional entries are ignored (no RO enforcement).
	AllowedPaths []client.AllowedPath
	// contains filtered or unexported fields
}

Config holds everything needed to run one agent loop iteration.

type ExecConfig added in v1.4.0

type ExecConfig struct {
	Env          map[string]string
	AllowedPaths []client.AllowedPath
	TimeoutSec   int // maps to RunRequest.Timeout; 0 = daemon default (seconds)
	// contains filtered or unexported fields
}

ExecConfig holds the knobs ExecuteBlocks needs to dispatch cmds to a runner. runner is required and set by NewExecConfig. Env, AllowedPaths, TimeoutSec are optional and map directly to temenos client.RunRequest fields.

func NewExecConfig added in v1.5.0

func NewExecConfig(cfg Config) (ExecConfig, error)

NewExecConfig creates an ExecConfig by resolving the runner from cfg. Uses localRunner if cfg.Sandbox is false; uses temenos client if true. Returns an error only if Sandbox is true but temenos is unreachable.

type PromptData

type PromptData struct {
	WorkingDir string
	Platform   string
	Date       string
	Commands   []CommandDoc // caller-provided command documentation
}

PromptData holds the runtime context used to render the default system prompt.

type Result added in v1.4.0

type Result struct {
	Command  string
	Stdout   string
	Stderr   string
	ExitCode int
	Err      error
}

Result is one command's execution outcome, returned by ExecuteBlocks. Command is the raw bash content that was run (verbatim from the <cmd> block). Stdout/Stderr/ExitCode come from the sandbox runner. Err is non-nil only on runner-level failures (sandbox refusal, daemon unreachable, timeout) — a non-zero exit code from bash is NOT an Err, it's reported via ExitCode. ExitCode == -1 means the runner did not return a normal exit (timeout, sandbox error); Err will be non-nil in that case.

func ExecuteBlocks added in v1.4.0

func ExecuteBlocks(ctx context.Context, cfg ExecConfig, cmds []string) []Result

ExecuteBlocks runs each cmd concurrently against cfg.runner and returns results in the original order of cmds. Worker pool is capped at 8. Ctx cancellation stops new task submissions; already-submitted tasks run to completion (their Results will contain ctx.Err() on premature exit). Empty cmds returns nil.

ExecuteBlocks panics if cfg.runner is nil.

type RunResult

type RunResult struct {
	Response string        // final text response (accumulated assistant text)
	Steps    []StepMessage // all messages generated (for persistence by caller)
}

RunResult contains the agent's output after a loop completes.

func Run

func Run(
	ctx context.Context,
	cfg Config,
	history []fantasy.Message,
	prompt string,
	cbs Callbacks,
) (*RunResult, error)

Run executes the agent loop: prompt → LLM → <cmd> blocks → repeat. Stateless — the caller handles conversation persistence.

type StepMessage

type StepMessage struct {
	Role               StepRole
	Content            string
	Reasoning          string // thinking block text (empty if no reasoning)
	ReasoningSignature string // provider signature for round-trip
	Timestamp          time.Time
}

StepMessage represents one message generated during the agent loop.

type StepRole

type StepRole string

StepRole represents the role of a message step in the agent loop.

const (
	StepRoleAssistant StepRole = "assistant" // LLM turn (with or without commands)
	StepRoleUser      StepRole = "user"      // human input
	StepRoleResult    StepRole = "result"    // command output fed back to LLM
)

Jump to

Keyboard shortcuts

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