Prompts & Messages
Prompts in Sema are composable data structures — not string templates. They are built from message expressions, and can be inspected, transformed, and composed before being sent to an LLM.
The core idea: build small prompt pieces, compose them together, fill in template slots, and send the result. Everything is a value you can pass around, store, and introspect.
;; Build reusable prompt pieces
(define safety
(prompt (system "Follow policy. Refuse unsafe requests.")))
(define domain
(prompt (system "You are a senior Lisp developer.")))
(define task
(prompt (user "Review this function:\n\n{{code}}")))
;; Compose, fill, and send
(define p (prompt/concat safety domain task))
(define ready (prompt/fill p {:code "(define (f x) (+ x 1))"}))
(llm/send ready {:max-tokens 300})Messages
A message is a role–content pair. The role is a keyword: :system, :user, or :assistant.
message
Create a message with a role and content.
(message :system "You are a helpful assistant.")
(message :user "What is Lisp?")
(message :assistant "Lisp is a family of programming languages.")message/role
Get the role of a message as a keyword.
(message/role (message :user "hi")) ; => :usermessage/content
Get the text content of a message.
(message/content (message :user "hi")) ; => "hi"Building Prompts
prompt
Build a prompt from message expressions. Inside prompt, use the shorthand constructors (system ...), (user ...), and (assistant ...) — these are equivalent to (message :system ...), etc.
(define review-prompt
(prompt
(system "You are a code reviewer. Be concise.")
(user "Review this function.")))prompt/messages
Get the list of messages from a prompt.
(prompt/messages my-prompt) ; => list of message values
(length (prompt/messages my-prompt)) ; => 2Composing Prompts
prompt/append
Compose prompts by appending their messages together. Variadic — accepts 2 or more prompts.
(define base (prompt (system "You are helpful.")))
(define question (prompt (user "What is 2+2?")))
(define full (prompt/append base question))
;; Three or more prompts
(define safety (prompt (system "Be safe.")))
(define full (prompt/append base safety question))
(llm/send full)prompt/concat
Alias for prompt/append. Use whichever name reads better in context.
(define full (prompt/concat base-prompt safety-prompt domain-prompt))Templating
prompt/fill
Substitute placeholders in all message contents using a map. Unfilled slots are left as-is, so you can partially fill a template and fill the rest later.
(define template
(prompt
(system "You are a {{role}} reviewing {{language}} code.")
(user "{{query}}")))
;; Full fill
(define filled (prompt/fill template {:role "expert" :language "Rust" :query "Explain this."}))
;; Partial fill — unfilled slots remain as {{...}}
(define partial (prompt/fill template {:role "code reviewer"}))
;; partial still has {{language}} and {{query}} unfilledprompt/slots
Return a list of unfilled names as keywords. Duplicates are removed.
(prompt/slots template) ; => (:role :language :query)
;; After partial fill, only unfilled slots remain
(prompt/slots (prompt/fill template {:role "expert"}))
;; => (:language :query)
;; After full fill, no slots remain
(prompt/slots filled) ; => ()Use prompt/slots to validate that all required slots are filled before sending:
(when (not (null? (prompt/slots my-prompt)))
(error "unfilled slots remain"))Modifying Prompts
prompt/set-system
Replace all system messages with a single new one. Non-system messages are preserved.
(define p (prompt (system "old system") (user "hello")))
(define p2 (prompt/set-system p "new system instructions"))
;; p2 has: [(system "new system instructions"), (user "hello")]Type Predicates
prompt?
Check if a value is a prompt.
(prompt? review-prompt) ; => #t
(prompt? 42) ; => #fmessage?
Check if a value is a message.
(message? (message :user "hi")) ; => #t
(message? "not a message") ; => #f