Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,19 @@ input, so scrolling, resize, and type-ahead all work while tokens stream in, and

## Slash commands

Built-ins: `/help`, `/model [name]`, `/clear`, `/tools`, `/mcp`, `/resume`,
`/cwd`, `/config`, `/reload`, `/quit`, `/exit`. In the TUI, type `/` to open a
fuzzy command palette. Add your own in config (see below).
Built-ins: `/help`, `/model [name]`, `/effort [level]`, `/clear`, `/tools`,
`/mcp`, `/resume`, `/cwd`, `/config`, `/reload`, `/quit`, `/exit`. In the TUI,
type `/` to open a fuzzy command palette; once you type `/model ` the same
palette fuzzy-completes the **model argument** from the config's `:models`
list, shown as `Anthropic: Claude Opus 4.6` (Tab inserts the selection, Enter
runs it — any model id typed by hand still works). The active model is marked
`●` and providers whose API key isn't set are annotated `· no key`. `/effort`
sets Sema's portable reasoning-effort level the same way (`none` / `minimal` /
`low` / `medium` / `high` / `xhigh`; `default` resets) — models without
reasoning support simply ignore it, and a `(model … {:effort "high"})` record
sets a per-model default. `/resume <id>` restores a saved session directly
(completing from your session list) and brings back the model and effort it
ran with. Add your own commands in config (see below).

## Configuration

Expand All @@ -111,9 +121,27 @@ A complete `init.sema`:
(configure!
(coder-config
{:model "" ; "" = auto-detect from API keys; or e.g. "claude-sonnet-5"
:effort "" ; reasoning effort (none…xhigh); "" = provider default
:max-turns 50 ; max tool-use rounds in a single turn
:tool-preview-lines 5 ; result lines shown under each tool call

;; Models offered by the /model autocomplete, grouped by provider.
;; Only a picker list — any model id typed by hand still works. A model
;; may carry per-model defaults: (model id label {:effort "high"}).
:models
(list
(provider "Anthropic"
(list
(model "claude-opus-4-6" "Claude Opus 4.6")
(model "claude-sonnet-5" "Claude Sonnet 5")
(model "claude-haiku-4-5" "Claude Haiku 4.5")))
(provider "OpenAI"
(list
(model "gpt-5.6-sol" "GPT-5.6 Sol")
(model "gpt-5.6-terra" "GPT-5.6 Terra")
(model "gpt-5.6-luna" "GPT-5.6 Luna")
(model "gpt-5.5" "GPT-5.5"))))

;; MCP servers — each is a value; manage connections in the /mcp modal (⌃O).
:mcp-servers
(list
Expand All @@ -138,8 +166,10 @@ A complete `init.sema`:
| Key | Default | Meaning |
| --- | --- | --- |
| `:model` | `""` | LLM model; `""` auto-detects from `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` |
| `:effort` | `""` | Reasoning effort (`none`/`minimal`/`low`/`medium`/`high`/`xhigh`); `""` = provider default |
| `:max-turns` | `50` | Max agent tool-use rounds per user turn |
| `:tool-preview-lines` | `5` | Result lines shown under each tool call in the TUI |
| `:models` | Anthropic + OpenAI flagships | `(provider …)` groups of `(model …)` records driving the `/model` autocomplete |
| `:mcp-servers` | `'()` | List of `(mcp-server …)` records |
| `:commands` | `'()` | List of `(command …)` records |
| `:keys` | `{}` | Action → key overrides |
Expand Down Expand Up @@ -184,6 +214,18 @@ can also register commands at runtime from Sema, after loading `src/commands.sem
(lambda (state args) (emit :info "hi!") state))
```

A command can also register **argument completions** — the palette switches to
them once you type `/name ` (this is how `/model`, `/effort`, `/resume`, and
`/config` offer theirs). The function receives the live state map (`:config`,
`:model`, `:effort`, …); an entry with `:active #t` is marked `●` in the palette:

```sema
(register-completions! "hello"
(lambda (state)
(list {:value "world" :label "the whole world" :active #t}
{:value "mom" :label "hi mom"})))
```

### Keybindings

The keymap is data, merged from four layers (weakest first): the built-in
Expand Down
29 changes: 18 additions & 11 deletions coder.sema
Original file line number Diff line number Diff line change
Expand Up @@ -21,38 +21,42 @@
(when (:help cli) (show-usage) (exit 0))
(when (:version cli) (println f"sema-coder ${VERSION}") (exit 0))

(define provider (llm/auto-configure))
(when (nil? provider)
;; not `provider`/`model` — those names would shadow the config constructors
;; init.sema calls on every load/hot-reload (same trap as `agent` below).
(define llm-provider (llm/auto-configure))
(when (nil? llm-provider)
(io/println-error "Error: No API key found. Set ANTHROPIC_API_KEY or OPENAI_API_KEY.")
(exit 1))

(define cfg (boot-config!)) ;; ensure + load init.sema, reconcile commands + servers
(define cwd (sys/cwd))
(define model (:model cli))
(define cfg (boot-config!)) ;; ensure + load init.sema, reconcile commands + servers
(define cwd (sys/cwd))
(define cli-model (:model cli))
(mcp-autostart!) ;; connect :autostart servers before the agent is built
;; not `agent` — that name would shadow the (agent {…}) builtin constructor.
(define sema-coder-agent (create-agent cwd model cfg))
(define sema-coder-agent (create-agent cwd cli-model cfg))

;; One-shot mode — prose to stdout, nothing else.
(when (:prompt cli)
(let ((result (run-turn sema-coder-agent (:prompt cli) '() on-tool-call)))
(let ((result (run-turn sema-coder-agent (:prompt cli) '() on-tool-call
(pick-effort "" (choose-model cli-model cfg) cfg))))
(println (:response result))
(exit 0)))

;; Interactive, on a TTY → the full-screen TUI.
(when (sys/tty)
(tui-run cwd model cfg sema-coder-agent)
(tui-run cwd cli-model cfg sema-coder-agent)
(exit 0))

;; Not a TTY (piped/dumb terminal) → the plain line-based REPL below.
(show-banner VERSION)
(show-welcome cwd model)
(show-welcome cwd cli-model)
(let ((na (mcp-needs-auth)))
(unless (null? na)
(show-info f"${(length na)} MCP server(s) need auth: ${(string/join na ", ")}")))

(define state
{:messages '() :model model :cwd cwd :agent sema-coder-agent :config cfg})
{:messages '() :model cli-model :effort ""
:cwd cwd :agent sema-coder-agent :config cfg})

(defun handle-input (input)
(cond
Expand All @@ -72,7 +76,10 @@
(set! state next))))
(else
(let ((result (try
(run-turn (:agent state) (string/trim input) (:messages state) on-tool-call)
(run-turn (:agent state) (string/trim input) (:messages state) on-tool-call
(let ((c (get state :config {})))
(pick-effort (get state :effort "")
(choose-model (get state :model "") c) c)))
(catch e
(show-error f"agent error: ${(get e :message (str e))}")
#f))))
Expand Down
33 changes: 24 additions & 9 deletions src/agent.sema
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
(load "util.sema")
(load "tools.sema")
(load "mcp.sema")
(load "config.sema") ;; model-effort — per-model :effort defaults

(defun agent-tools ()
"The agent's full tool set: the app's own tools plus every connected MCP
Expand Down Expand Up @@ -46,15 +47,15 @@ Platform: ${platform}
</environment>")
"\n\n"))

(defun choose-model (model cfg)
"Model precedence: explicit MODEL > config :model > \"\" (auto-detect)."
(if (not (= model "")) model (get cfg :model "")))

(defun create-agent (cwd model cfg)
"Build the coding agent. Model precedence: explicit arg > config > auto."
(set-workspace-root! cwd)
(let* ((platform f"${(sys/os)} (${(sys/arch)})")
(chosen
(cond
((not (= model "")) model)
((not (= (get cfg :model "") "")) (get cfg :model))
(else "")))
(chosen (choose-model model cfg))
(prompt (build-system-prompt cwd platform)))
;; Anonymous constructor, not defagent — this is called on every model
;; switch / config reload / MCP connect, and defagent would rebind a global
Expand All @@ -65,13 +66,27 @@ Platform: ${platform}
:max-turns (get cfg :max-turns 50)
:model chosen})))

(defun run-turn (agent input messages on-tool)
(defun pick-effort (override model-id cfg)
"Effort precedence: session OVERRIDE (/effort) → the model's own :effort in
the config's :models entry for MODEL-ID → the config :effort. \"\" means
the provider default."
(let ((m-eff (model-effort cfg model-id)))
(cond ((and override (not (= override ""))) override)
((not (= m-eff "")) m-eff)
(else (get cfg :effort "")))))

(defun turn-opts (opts effort)
"Merge a non-empty reasoning EFFORT into an agent/run OPTS map. Effort is
portable — providers/models without it treat the option as a no-op."
(if (and effort (not (= effort ""))) (assoc opts :reasoning-effort effort) opts))

(defun run-turn (agent input messages on-tool effort)
"Blocking turn used by the non-TTY / one-shot path (no live streaming)."
(agent/run agent input {:on-tool-call on-tool :messages messages}))
(agent/run agent input (turn-opts {:on-tool-call on-tool :messages messages} effort)))

(defun run-turn-streaming (agent input messages on-tool on-text)
(defun run-turn-streaming (agent input messages on-tool on-text effort)
"Turn used by the TUI: :on-text streams assistant deltas, :on-tool-call streams
tool events, so the front-end can render the reply live."
(agent/run agent
input
{:on-tool-call on-tool :on-text on-text :messages messages}))
(turn-opts {:on-tool-call on-tool :on-text on-text :messages messages} effort)))
Loading
Loading