factor

module
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Aug 15, 2026 License: MIT

README ΒΆ

Factor logo

Factor

CI Coverage Go reference Latest release MIT license

A fast, reliable, lightweight desktop AI agent and companion β€” with a real memory.

Factor is a single static Go binary that lives on your machine, talks to you over the CLI or Telegram, does real work with real tools, and remembers what matters across conversations. Its long-term memory is smrti β€” Bayesian truth values, attention economics, emotional valence β€” so Factor doesn't just log what you said: it consolidates, prioritizes, and never repeats a critical mistake.

Highlights

🧠 Memory as the soul Salience-ranked recall every turn; past failures become hard constraints; consolidation decays, promotes, and prunes β€” plus deliberate remember / recall / forget / reflect tools
⚑ Never keeps you waiting Long work runs as background jobs β€” Factor acks instantly and proactively messages you when the result lands, even mid-conversation
🎯 Mid-turn steering A second message during a live turn is injected between tool iterations instead of queuing
πŸ” Provider failover that works OpenAI-compatible (OpenRouter, Ollama, LM Studio, Groq, llama.cpp, …) and native Anthropic, with error classification, per-candidate cooldowns, and overflow-triggered compaction
🧭 Reasoning, dialect-translated One provider.reasoning setting becomes reasoning (OpenRouter), reasoning_effort (OpenAI/Groq), or a thinking budget (Anthropic)
☎️ Answers the phone A real number: call it and talk to Factor out loud, or have it call and text you β€” barge-in, voicemail detection, and a fully local speech tier if you want no audio leaving the machine (Phone)
πŸ–οΈ Hands on your desktop Windows, screenshots, mouse, keyboard, clipboard, notifications β€” X11, Wayland, macOS, Windows; auto-registered when a display exists
🌐 A real browser, not just fetch CDP tools attach to your running Chrome/Chromium/Brave or launch a managed instance, visible by default so you can watch it work β€” and setup installs one when the machine has none (Browser)
🧩 Extensible everything Channel connectors, Go tools, runtime-mounted MCP servers, markdown skills, drop-in instructions β€” see Extending
πŸ”§ Self-managing Edits its own config, installs packages (apt/dnf/pip/npm/…), runs cron schedules and HEARTBEAT.md checks that cost zero LLM calls when idle
πŸ›‘οΈ Safety rails Workspace-restricted files, exec deny-patterns, sender allowlists, secrets scrubbed from every tool result β€” rails, not a sandbox (Security)

How it works

flowchart LR
    TG([Telegram]) <--> BUS
    CLI([CLI]) <--> BUS
    PH([Phone]) <--> SHELL["voice shell sidecar Β· speech Β· barge-in"]
    SHELL <-->|chat completions on loopback| LOOP
    BUS[message bus] --> LOOP["agent loop Β· one live turn per session"]
    LOOP <-->|recall Β· store| MEM[("smrti REST sidecar")]
    LOOP --> PROV["provider chain Β· failover Β· cooldowns Β· compaction"]
    LOOP --> REG[tool registry]
    REG --- SUITES["fs Β· exec Β· web Β· browser Β· desktop Β· memory Β· jobs Β· cron Β· config Β· pkg Β· skills Β· MCP"]
    BG[jobs Β· cron Β· heartbeat] -.->|proactive results| BUS

Bus + bounded workers, mid-turn steering, narrow pluggable seams, CGO-free portability β€” the architecture of PicoClaw distilled into a codebase that runs happily on an old Puppy Linux box.

Get started

go install github.com/cyqlelabs/factor/cmd/factor@latest
# or grab a release binary; linux-amd64 targets GOAMD64=v1 (no SSE4.2 needed)

factor init      # interactive setup wizard

The wizard verifies every step live: the provider with a real completion, the model picked from the endpoint's live list, the Telegram token with getMe, the carrier and voice credentials against their own APIs, the browser with a real page load. Then it installs what's missing instead of handing you a list β€” smrti (uv β†’ pipx β†’ pip --user β†’ private venv, no root needed), a browser, the helpers your desktop backend wants. It looks for that desktop on the machine rather than in this shell, so factor init over ssh still sets up the desktop the box is running. factor init -y takes the defaults for scripting; --no-install keeps it from installing anything.

export FACTOR_PROVIDER_API_KEY=sk-or-...   # OpenRouter by default
factor                                     # interactive chat
factor -m "what's on my disk?"             # one-shot
factor gateway                             # daemon: Telegram, phone, cron, heartbeat, jobs
factor status                              # daemon / provider / memory / phone / desktop health

Factor spawns and supervises the smrti sidecar automatically, restarts it with backoff, and degrades gracefully (empty recalls, dropped writes) when it's down. Point memory.mode: "external" + memory.url at a shared smrti if you run one.

Configuration

~/.factor/config.json β€” every key optional, defaults work. FACTOR_* env overrides: FACTOR_PROVIDER_API_KEY, FACTOR_PROVIDER_MODEL, FACTOR_MEMORY_MODE, …

Annotated example
{
  "provider": {
    "type": "openrouter",                    // openrouter|openai|groq|ollama|lmstudio|llamacpp|anthropic|custom
    "api_key": "sk-or-...",
    "model": "google/gemini-3.1-pro-preview",
    "reasoning": { "effort": "xhigh" },      // or {"max_tokens": 12000}; "none" turns it off
    "fallbacks": [{ "type": "ollama", "model": "qwen3:8b" }]
  },
  "memory": {
    "mode": "sidecar",                       // sidecar | external | off
    "auto_install": true,                    // install smrti when it is missing
    "personality": "balanced",               // analytical | curious | empathetic | maverick | deterministic
    "space": "main"
  },
  "channels": {
    "telegram": { "token": "123:ABC", "allow_from": ["your-telegram-id"] },
    "phone": {                                 // optional; absent = nothing runs
      "user_number": "+15550001111",           // you: the only one who may call in
      "phone_number": "+15550002222",          // the number you bought
      "twilio_account_sid": "AC...",
      "twilio_auth_token": "...",
      "elevenlabs_api_key": "...",
      "stt_api_key": "...",                    // Deepgram
      "language": "en",
      "stt": { "provider": "deepgram" },       // deepgram | whisper | local-openai
      "tts": { "provider": "elevenlabs" },     // elevenlabs | local-openai
      "proactive": "sms",                      // sms | call | off
      "max_call_minutes": 15
    }
  },
  "mcp": {
    "servers": { "github": { "command": "github-mcp-server", "args": ["stdio"] } }
  },
  "tools": { "disabled": [], "restrict_to_workspace": true },
  "desktop": { "enabled": null },            // null = on when a display exists
  "browser": {
    "enabled": true,
    "command": "",                           // "" = find one; init records what it installed
    "headless": false,
    "fast_path": false                       // opt in to the lightweight read-only engine
  },
  "heartbeat": { "enabled": true, "interval_minutes": 30 }
}

The workspace (~/.factor/workspace) is the agent's home: AGENT.md, SOUL.md, USER.md shape its identity; HEARTBEAT.md lists proactive tasks; instructions/, skills/, sessions/, cron/ do what they say.

Browser

Factor drives a real browser over DevTools: it attaches to your running Chrome/Chromium/Brave, or launches a managed instance that stays visible so you can watch it work. A machine with no browser gets one β€” factor init installs Helium under ~/.factor/engine, from a portable tarball that needs no package manager and no root. Helium is ungoogled-chromium with the telemetry stripped, the anti-fingerprinting patches in, and uBlock Origin bundled, which is what actually keeps a tab's memory down on a small box.

Reading a page and driving a page cost wildly different amounts, so you can add a second engine for the cheap half:

Engine Tools Renders Good for
Chromium β€” Helium, or the browser you already run browser_navigate Β· _read Β· _click Β· _fill Β· _screenshot Β· _eval Β· _back yes anything interactive
Lightpanda β€” opt-in, browser.fast_path browser_fetch β€” title, text, links never reading a page for a fraction of the memory

Lightpanda runs the same JavaScript against a DOM and never starts a renderer, a GPU process, or a compositor. It cannot click, fill, or screenshot and it keeps no session, so it supplements the real browser instead of replacing it β€” the agent picks whichever the job needs. The wizard offers it only where it runs: its builds need glibc 2.34, and the check happens before the 150 MB download, not after.

Phone calls and SMS

Give Factor a phone number and it picks up: you talk, it answers out loud, with the same memory, tools, and session history it has everywhere else. It can also call or text you β€” a finished job, a cron result, or because you asked it to ring someone and report back.

factor init        # the Channels step walks through the carrier and the speech tier
factor gateway     # brings the line up
factor status      # number, speech tier, voice-shell health

The voice shell is Patter running as a supervised sidecar β€” exactly like the smrti memory engine, into its own virtualenv, installed on demand. It terminates the carrier's media stream and owns the parts of a phone call that are hard: turn-taking, barge-in, voice activity detection, answering-machine detection, transcoding. Factor is its brain, plugged in over loopback as an OpenAI-compatible endpoint that never leaves 127.0.0.1.

Speech tiers β€” the one decision with real trade-offs, asked plainly by the wizard:

Tier Speech-to-text Text-to-speech Extra RAM When
1 Β· cloud (default) Deepgram nova-3 ElevenLabs flash v2.5 (Β΅-law 8 kHz, no transcode) ~150–300 MB any machine; lowest latency, least to go wrong
2 Β· local STT faster-whisper ElevenLabs +0.5–2 GB transcription stays home; wants a GPU
3 Β· local TTS Deepgram Piper +0.3 GB Piper's ~100 ms render beats the cloud, on any CPU
4 Β· fully local faster-whisper Piper +1–2 GB no audio leaves the machine, no per-minute audio cost

A tier picks who serves each half of the speech pipeline. Everything else is the same call:

flowchart LR
    SHELL["voice shell Β· Patter"] -->|hears with| STT{{speech-to-text}}
    SHELL -->|speaks with| TTS{{text-to-speech}}
    subgraph LOCAL ["Factor's own speech server Β· 127.0.0.1"]
        WH["faster-whisper"]
        PI[Piper]
    end
    subgraph CLOUD [audio leaves the machine]
        DG[Deepgram]
        EL[ElevenLabs]
    end
    STT -.->|tiers 2, 4| WH
    TTS -.->|tiers 3, 4| PI
    STT -.->|tiers 1, 3| DG
    TTS -.->|tiers 1, 2| EL

Pick a local tier and Factor installs it. The engines go into their own virtualenv and your language's models download before setup finishes β€” so the first call finds everything on disk. No server to start, no model names to choose. factor status reports what it built.

Languages. Transcription covers everything Whisper does, about 99 languages. Voices come from Piper's catalogue: 49 languages, resolved from your language setting. The exact locale wins where it exists β€” es-MX gets a Mexican voice, not a Castilian one β€” then the language at large. Spanish is first-class on every tier.

Why a GPU changes which tier to pick

Whisper decodes a fixed 30-second window however little audio it gets, and the phone pipeline feeds it about a second at a time. Cost is therefore per chunk, not per second of speech. Measured on this design:

Model Device Per 1 s chunk Verdict
small CUDA ~0.14 s keeps up comfortably
base CPU ~0.9 s keeps up, mishears more
small CPU ~2.4 s falls behind; the backlog grows while you talk

So local transcription runs small on a GPU and drops to base on a CPU, and the wizard says so when it does. On a machine with no GPU, tier 3 is the better trade: Piper renders in ~100 ms on any CPU, and transcription stays in the cloud.

You can still point stt.base_url / tts.base_url at a speech server you run yourself β€” Speaches, or anything else OpenAI-compatible β€” and Factor will leave it alone and use it. Either way it probes at startup. If the server is not answering, Factor falls back to the cloud tier and says so rather than failing calls; set local_audio_fallback: false to have the channel report itself down instead. Silero voice-activity detection runs locally in every tier.

Roughly $0.04–0.06 per talk-minute on tier 1 plus your model's tokens, and about 1.3Β’ per SMS segment. Turns are not streamed yet, so a tool-using turn leans on the spoken filler while it works; simple questions land in the normal 1.5–3 s range.

Getting the line up, and the guardrails on it

Buy a number at a supported carrier (Twilio today; Patter also speaks Telnyx and Plivo), then run factor init. The wizard verifies the carrier credentials and the voice key live before writing anything.

The carrier has to reach the voice shell, so it needs a public URL. tunnel: "quick" (the default) uses Patter's built-in Cloudflare quick tunnel β€” fine for trying it out, not for daily use: the hostname rotates and first legs occasionally drop. For real use set tunnel: "none" and a stable webhook_url from a named tunnel or a reverse proxy. Factor's own endpoints β€” the brain bridge and the shell's control API β€” always bind 127.0.0.1 and share a bearer secret regenerated every boot.

Because a phone number is dialable by anyone and a phone call costs money, the rails are closed by default: only user_number may call in (add more with allow_from, or "*" for anyone, which logs a security warning), only user_number may be dialed (add more with allow_call_to β€” there is deliberately no wildcard), calls are cut off at max_call_minutes, and call transfer is off. A caller who is not allowed is hung up at the carrier and refused by the bridge.

Two tools appear only when the channel is configured: phone_sms sends a text, and phone_call dials β€” returning immediately, then reporting the outcome (answered, no answer, busy, voicemail) with a transcript tail back into whichever conversation asked for the call.

Extending Factor

Seam What it takes
Connector One package: channel.Register(name, factory) in init(), with its own config section
Tool Four methods β€” Name, Description, Parameters, Execute β€” and one registry.Register(t) line
MCP server mcp_add (or the mcp.servers config section) mounts its tools at runtime β€” no Go required
Skill Drop workspace/skills/<name>/SKILL.md β€” catalog in prompt, full text on demand, skill_install from git
Wiring in a connector
func init() {
    channel.Register("mychat", func(raw json.RawMessage, b *bus.MessageBus) (channel.Channel, error) {
        var cfg MyConfig
        _ = json.Unmarshal(raw, &cfg)          // your own config section
        return New(cfg, b), nil                // implement Name/Start/Stop/Send/MaxMessageLength
    })
}

Security model

Factor is a personal agent, not a multi-tenant service. The guardrails (workspace restriction, exec deny-patterns, allowlists, secret redaction) protect against accidents and casual prompt-injection β€” they are not a security boundary. Run it under your own account for yourself; set channels.telegram.allow_from; keep restrict_to_workspace on unless you know why you're turning it off. The phone channel is the one place where the default is closed rather than open β€” a number anyone can dial, and a bill attached to every minute, earn stricter rails.

Development

make check        # gofmt + vet + race tests + coverage gate (β‰₯90%, what CI runs)
make build        # local binary
make build-all    # release cross-compile (incl. GOAMD64=v1 for old x86-64)
make build-tiny   # -tags nobrowser: smallest binary

The suite runs against fakes β€” scripted providers, a fake smrti sidecar and a fake voice shell (both spawned by re-execing the test binary), a fake Telegram API, a fake carrier, a fake MCP server over real stdio JSON-RPC, a scripted desktop β€” plus live headless-Chrome and desktop round-trip tests that auto-skip where the machine can't host them.

License

MIT Β© CyqleLabs

Directories ΒΆ

Path Synopsis
cmd
factor command
Command factor is a fast, reliable desktop AI agent and companion with smrti long-term memory.
Command factor is a fast, reliable desktop AI agent and companion with smrti long-term memory.
internal
agent
Package agent implements the turn loop: one live turn per session, mid-turn steering for overflow messages, bounded worker concurrency, and a system prompt assembled from identity, workspace bootstrap files, drop-in instructions, the skills catalog, and smrti memory recall.
Package agent implements the turn loop: one live turn per session, mid-turn steering for overflow messages, bounded worker concurrency, and a system prompt assembled from identity, workspace bootstrap files, drop-in instructions, the skills catalog, and smrti memory recall.
app
Package app is the composition root: it wires config, provider chain, smrti memory, tools, skills, sessions, and the agent loop into one unit shared by the CLI and the gateway daemon.
Package app is the composition root: it wires config, provider chain, smrti memory, tools, skills, sessions, and the agent loop into one unit shared by the CLI and the gateway daemon.
browser
Package browser gives the agent a real browser via the Chrome DevTools Protocol (chromedp): it attaches to the user's running Chrome/Chromium/ Brave when a DevTools port is open, otherwise launches a managed instance β€” visible by default, so the user can watch the agent work.
Package browser gives the agent a real browser via the Chrome DevTools Protocol (chromedp): it attaches to the user's running Chrome/Chromium/ Brave when a DevTools port is open, otherwise launches a managed instance β€” visible by default, so the user can watch the agent work.
bus
Package bus decouples channels from the agent loop with bounded queues.
Package bus decouples channels from the agent loop with bounded queues.
channel
Package channel defines the connector seam.
Package channel defines the connector seam.
channel/phone
Package phone is the voice connector: the user talks to Factor on a real phone call, and Factor can call or text back.
Package phone is the voice connector: the user talks to Factor on a real phone call, and Factor can call or text back.
channel/telegram
Package telegram is the reference connector: raw Bot API over HTTP long-polling, no SDK.
Package telegram is the reference connector: raw Bot API over HTTP long-polling, no SDK.
config
Package config loads, defaults, persists, and redacts Factor's configuration.
Package config loads, defaults, persists, and redacts Factor's configuration.
cron
Package cron schedules recurring agent tasks.
Package cron schedules recurring agent tasks.
desktop
Package desktop gives the agent hands on the graphical session: listing and controlling windows, taking screenshots, moving the mouse, typing, the clipboard, notifications, and opening files or URLs.
Package desktop gives the agent hands on the graphical session: listing and controlling windows, taking screenshots, moving the mouse, typing, the clipboard, notifications, and opening files or URLs.
gateway
Package gateway runs Factor as a daemon: channels, cron, heartbeat, background jobs, and a local health endpoint.
Package gateway runs Factor as a daemon: channels, cron, heartbeat, background jobs, and a local health endpoint.
heartbeat
Package heartbeat periodically checks HEARTBEAT.md for user-defined tasks.
Package heartbeat periodically checks HEARTBEAT.md for user-defined tasks.
jobs
Package jobs runs long work in the background so the agent can reply immediately and report back when the work finishes.
Package jobs runs long work in the background so the agent can reply immediately and report back when the work finishes.
mcp
Package mcp implements a minimal MCP client (JSON-RPC 2.0 over stdio, newline-delimited) so external MCP servers' tools mount directly into Factor's tool registry as <server>__<tool>.
Package mcp implements a minimal MCP client (JSON-RPC 2.0 over stdio, newline-delimited) so external MCP servers' tools mount directly into Factor's tool registry as <server>__<tool>.
memory
Package memory is the soul of Factor: long-term memory backed by smrti (github.com/cyqlelabs/smrti), an AtomSpace-inspired engine with Bayesian truth values, attention economics, and emotional valence, reached over a localhost REST sidecar.
Package memory is the soul of Factor: long-term memory backed by smrti (github.com/cyqlelabs/smrti), an AtomSpace-inspired engine with Bayesian truth values, attention economics, and emotional valence, reached over a localhost REST sidecar.
provider
Package provider defines the LLM provider seam: a minimal Chat interface, wire-format adapters (OpenAI-compatible, Anthropic), error classification, and a failover chain with per-candidate cooldowns.
Package provider defines the LLM provider seam: a minimal Chat interface, wire-format adapters (OpenAI-compatible, Anthropic), error classification, and a failover chain with per-candidate cooldowns.
session
Package session persists conversation history as append-only JSONL files with a meta sidecar carrying the summary and logical truncation offset.
Package session persists conversation history as append-only JSONL files with a meta sidecar carrying the summary and logical truncation offset.
skills
Package skills implements progressive-disclosure markdown skills: only name + description enter the system prompt; the model reads the full SKILL.md with read_file when it decides a skill applies.
Package skills implements progressive-disclosure markdown skills: only name + description enter the system prompt; the model reads the full SKILL.md with read_file when it decides a skill applies.
tools
Package tools defines the tool seam and the built-in arsenal.
Package tools defines the tool seam and the built-in arsenal.
tui
Package tui draws Factor's interactive chat.
Package tui draws Factor's interactive chat.
version
Package version holds build metadata injected at link time.
Package version holds build metadata injected at link time.
wizard
Package wizard is Factor's interactive setup: a terminal front-end for `factor init` that picks a provider and model (probing them live), installs the smrti memory engine, wires up channels, and checks the desktop tools.
Package wizard is Factor's interactive setup: a terminal front-end for `factor init` that picks a provider and model (probing them live), installs the smrti memory engine, wires up channels, and checks the desktop tools.

Jump to

Keyboard shortcuts

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