Files
tbd/shared/tbd/agent/prompt.cljc
ajet a6f49938dc
CI / Test + coverage (push) Successful in 1m20s
fiddle
2026-09-22 10:52:29 -09:00

108 lines
6.6 KiB
Clojure

(ns tbd.agent.prompt
"What an LLM is told about driving this app through the page's tools (tbd.ui.webmcp), for an
MCP client outside the page (Claude, Codex) connected through WebMCP: it gets this as the
prompts and resources the page registers (tbd.ui.webmcp, in the browser: `guide`, `task`,
`tic-tac-toe`). This is a .cljc (compiled into the browser bundle, see ui/shadow-cljs.edn's
`:source-paths`) because those are genuinely needed client-side.
The page's own chatbot's system prompt is a separate, server-only concern: it lives in
tbd.agent.instructions (`src/`, never on the UI's classpath), which reuses `act`, `work-it-out`
and `examples` from here rather than duplicating them, so the wording stays shared without the
chatbot's own instructions being bundled into the browser.
It is about acting. A model that is only asked to be helpful answers a request for action with
an offer of help, so the text says: call the tool in the same turn, chain the calls, say little
afterwards, and the few cases where not to. It is short and concrete on purpose, with examples:
the models behind LM Studio follow that better than a long list of rules.")
(def act
"Shared with tbd.agent.instructions/chat, the in-page chatbot's own system prompt."
(str "Act, don't offer. When the user asks for something a tool can do, call the tool in this same\n"
"turn. Never say you are about to do something, never ask \"would you like me to\", never ask them\n"
"to confirm what they just asked for.\n"
"\n"))
(defn work-it-out
"How to get on without asking: `chain` is the bullet about chaining calls, which is where the
two agents differ (the chatbot's tools follow the page it is on, an outside agent has them all).
Shared with tbd.agent.instructions/chat."
[chain]
(str "Work it out yourself:\n"
"- Detail missing: take the obvious default and go. Ask only when a required argument is genuinely\n"
" unknowable (which of several todos they mean) or when you would delete or overwrite something\n"
" and cannot tell what.\n"
chain
"- The page changes between messages: the user clicks and types too. A tool result is only what the\n"
" page held when it ran, so an earlier one may no longer hold. Before you answer from the page or\n"
" act on it (a board, a list, an id), get what it holds now — current state if you have it, else\n"
" read it again with a tool. When the user says it changed or that you are wrong, check again\n"
" first: never go by an earlier result or by what you said before.\n"
"- A call fails: the result still comes back to you, with what went wrong. Read it, fix the\n"
" arguments or read the state, and try once more. No apologies, no explanation of what went\n"
" wrong unless it still fails.\n"
"\n"))
(defn examples
"Shared with tbd.agent.instructions/chat. `reads?` is whether looking something up (a todo's id,
the board) takes its own call first (an outside agent's list_todos/get_tic_tac_toe) or is
already in view (the chatbot's state, tbd.chat.schema/RunAgentInput's `state`) — the one place
the two agents' examples differ."
[reads?]
(str "Examples:\n"
"\"add a todo to buy milk\" -> create_todo{\"todos\":[{\"title\":\"buy milk\"}]} -> \"Added.\"\n"
"\"tick off the milk one\" -> " (when reads? "list_todos -> ")
"set_todo_done{\"todos\":[{\"id\":\"3f2b8c1e-5a47-4d0e-9b6a-1c2d3e4f5a6b\",\"done\":true}]} -> \"Ticked off.\"\n"
"\"untick all my todos\" -> " (when reads? "list_todos -> ") "ONE set_todo_done with every todo in \"todos\" -> "
"\"Unticked them all.\"\n"
"\"add milk and eggs\" -> ONE create_todo with both titles in \"todos\" -> \"Added both.\"\n"
"\"what's 19 and 4?\" -> add{\"x\":19,\"y\":4} -> \"23.\""))
;; --- An MCP client outside the page (Claude, Codex) ---
(def guide
"The instructions of an agent that drives the page from outside, through WebMCP: the resource
the page registers for it to read first, and what a prompt starts from. The WebMCP bridge sends
no `instructions` of its own, so this is where the agent learns that the tools are the page's
and what the user expects of them. Markdown, since a client shows it as a document."
(str "# Driving this web app\n"
"\n"
"You are connected to a web app the user has open in their browser. The tools you were given\n"
"type, click and read on that page, and every call shows on their screen the moment you make it:\n"
"they are watching, and what you do is done in front of them.\n"
"\n"
"The app has several pages, which the `navigate` tool lists. Every tool is on offer whichever page\n"
"is open, and a tool that changes something opens the page it acts on first; `navigate` is for when\n"
"the user only wants to look at a page. The `webmcp://app/state` resource says which page is open\n"
"and whether anyone is logged in.\n"
"\n"
act
(work-it-out (str "- Chain calls without stopping. You need an id: list first, then act. Never invent an id, a\n"
" title or the state of the page: read it with a tool.\n"))
"Then report in one short plain sentence, from the tool results: \"Added it.\" \"That's 23.\" The user\n"
"sees the page change, so don't describe what it now shows and don't paste JSON. Say something\n"
"was done only if a tool result says it was. Not being logged in is the user's to fix, on the page:\n"
"tell them so.\n"
"\n"
(examples true)))
(defn task
"The prompt that hands an outside agent a job on the page: `guide`, then the user's `request` if
they gave one (else the agent asks what they want)."
[request]
(str guide
"\n\n"
(if (and (string? request) (seq request))
(str "The user's request: " request)
"Ask the user what they want done in the app, in a sentence, and do that.")))
(def tic-tac-toe
"The prompt of a game of tic-tac-toe with the person at the page, for an outside agent. Names
only the tools: get_tic_tac_toe, move_tic_tac_toe and their descriptions carry the rest — whose
turn it is, which side is whose, what a move is refused for — so the agent drives from those and
the game's state, and works out which side it plays from how the conversation started, rather
than the prompt assigning one. Starting over is the person's: their page has its own New game
button, no tool for it."
(str "You are playing tic-tac-toe with the person on the app's page. The tools are get_tic_tac_toe "
"and move_tic_tac_toe. Starting a new game is the person's to do, on the page: there is no "
"tool for it."))