"strings"
)
-// promptName stands for the name of the agent in promptText.
-const promptName = "{name}"
+const (
+ // promptName stands for the name of the agent in promptText.
+ promptName = "{name}"
+
+ // taskSentence is the sentence of promptText that gets instructionsClause
+ // when the user has instructions.
+ taskSentence = "Do the user's tasks yourself, with your tools."
+
+ // instructionsClause points to the instructions of the user at the end
+ // of the prompt.
+ instructionsClause = ", following the user's instructions\nat the end of this prompt: who they are, how their files are organized and how\nthey want work done"
+
+ // instructionsHeading starts the instructions of the user after
+ // promptText.
+ instructionsHeading = "\n\n# The user's instructions\n\n"
+)
// promptText is the system prompt of the reference design, nearly word for
// word. The reference design names its own agent, so promptText has the name of
// the agent and the kind reply in its place. It describes only what OneChat can
-// do, so it leaves out the spec text on images, subagents, computers,
-// background tasks, and the instructions of the user. It holds no date and no
-// state, so turns and compactions share its prompt cache.
+// do, so it leaves out the spec text on images, subagents, computers, and
+// background tasks. Its paragraph on persistent mode tells the model that a
+// final answer ends the turn. prompt adds the clause on the instructions of the user only
+// when there are instructions. It holds no date and no state, so turns and
+// compactions share its prompt cache.
const promptText = `You are {name}, an AI agent that works for one user in a single chat that never
ends. Each call to you is a turn or a compaction: the view below is followed by
the user's new message, or by a task starting "Compaction:".
Messages the user sends while you work reach you between tool calls.
+You are in persistent mode: a reply with no tool call is your final answer, and
+it ends your turn. You act again only when the user writes. So keep going until
+the user's task is done, or until only the user can unblock it. Answer a
+question asked in the middle of a task, then go on with the task in the same
+turn. When the user says "continue", resume the latest unfinished task from the
+view.
+
# Compactions
You write {name}'s memory: one step of the tree, compressing one message into a
text to its real author. Never make anything look further along than it was. If
told the line is too long, shorten it. Non-ASCII characters cost 2-4 bytes.`
-// prompt returns the system prompt of the agent name. Turns and compactions
-// send the same prompt and tools, so compactions read the prompt cache of the
-// turns.
-func prompt(name string) string {
- return strings.ReplaceAll(promptText, promptName, name)
+// prompt returns the system prompt of the agent name with the instructions of
+// the user at its end. Without instructions it has neither them nor the clause
+// that points to them. Turns and compactions send the same prompt and tools, so
+// compactions read the prompt cache of the turns.
+func prompt(name, instructions string) string {
+ text := strings.ReplaceAll(promptText, promptName, name)
+ instructions = strings.TrimSpace(instructions)
+ if instructions == "" {
+ return text
+ }
+
+ text = strings.Replace(text, taskSentence, strings.TrimSuffix(taskSentence, ".")+instructionsClause+".", 1)
+
+ return text + instructionsHeading + instructions
}
-// agentTools returns the function tools of every call: zoom and date. A
-// compaction sends them too and never calls them, because a change of the
-// tools ends the shared prompt cache.
-func agentTools() []Tool {
- return []Tool{
+// agentTools returns the function tools of every call: zoom, date, shell,
+// read, write, and edit. A compaction sends them too and never calls them,
+// because a change of the tools ends the shared prompt cache.
+func agentTools() []ModelTool {
+ return []ModelTool{
{
Type: "function",
Name: "zoom",
}),
Strict: true,
},
+ {
+ Type: "function",
+ Name: "shell",
+ Description: "Run a command with bash, as the user, in the working directory, with no input. Gives its output and error output, and its exit status when it fails.",
+ Parameters: objectSchema(map[string]any{
+ "command": stringSchema("The bash command."),
+ }),
+ Strict: true,
+ },
+ {
+ Type: "function",
+ Name: "read",
+ Description: "Read a text file.",
+ Parameters: objectSchema(map[string]any{
+ "path": stringSchema("The file, absolute or relative to the working directory."),
+ }),
+ Strict: true,
+ },
+ {
+ Type: "function",
+ Name: "write",
+ Description: "Write a text file whole, replacing it when it exists. Makes the missing directories.",
+ Parameters: objectSchema(map[string]any{
+ "path": stringSchema("The file, absolute or relative to the working directory."),
+ "text": stringSchema("The whole new content."),
+ }),
+ Strict: true,
+ },
+ {
+ Type: "function",
+ Name: "edit",
+ Description: "Replace the one occurrence of old in a text file with new.",
+ Parameters: objectSchema(map[string]any{
+ "path": stringSchema("The file, absolute or relative to the working directory."),
+ "old": stringSchema("The exact text to replace. It must occur once in the file."),
+ "new": stringSchema("The text that replaces old."),
+ }),
+ Strict: true,
+ },
}
}
"description": description,
}
}
+
+// stringSchema returns the JSON schema of a string.
+func stringSchema(description string) map[string]any {
+ return map[string]any{
+ "type": "string",
+ "description": description,
+ }
+}