]> git.giacomolli.uk - onechat/blobdiff - prompt.go
Name and order the memory core code
[onechat] / prompt.go
index e3c52c48262381ba4bfbe7230362121c6db75f88..c6df0575f5686ef8708b099ddf6d0046180690f0 100644 (file)
--- a/prompt.go
+++ b/prompt.go
@@ -5,15 +5,31 @@ import (
        "strings"
 )
 
-// promptName stands for the name of the agent in promptText.
-const promptName = "{name}"
-
-// promptText is the system prompt of the UniiChat design, nearly word for word,
-// with the name of the agent in place of Unii and the kind reply in place of
-// unii. 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.
+const (
+       // promptName stands for the name of the agent in promptText.
+       promptName = "{name}"
+
+       // promptTaskSentence is the sentence of promptText that gets
+       // promptInstructionsClause when the user has instructions.
+       promptTaskSentence = "Do the user's tasks yourself, with your tools."
+
+       // promptInstructionsClause points to the instructions of the user at
+       // the end of the prompt.
+       promptInstructionsClause = ", 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"
+
+       // promptInstructionsHeading starts the instructions of the user after
+       // promptText.
+       promptInstructionsHeading = "\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, 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:".
@@ -57,6 +73,13 @@ say in your reply what you learned that will matter later.
 
 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
@@ -92,25 +115,34 @@ exactly. Tag each item with its kind ("user: ...; echo: ..."), and credit quoted
 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, promptTaskSentence, strings.TrimSuffix(promptTaskSentence, ".")+promptInstructionsClause+".", 1)
+
+       return text + promptInstructionsHeading + 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{
+// promptTools 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 promptTools() []ModelTool {
+       return []ModelTool{
                {
                        Type:        "function",
                        Name:        "zoom",
                        Description: "Open the line id+n of the view into the two lines of n/2 under it; n = 1 gives the message whole.",
-                       Parameters: objectSchema(map[string]any{
-                               "id": integerSchema("The first message of the line: a multiple of n."),
-                               "n":  integerSchema("The number of messages of the line: a power of 2."),
+                       Parameters: promptObjectSchema(map[string]any{
+                               "id": promptIntegerSchema("The first message of the line: a multiple of n."),
+                               "n":  promptIntegerSchema("The number of messages of the line: a power of 2."),
                        }),
                        Strict: true,
                },
@@ -118,17 +150,56 @@ func agentTools() []Tool {
                        Type:        "function",
                        Name:        "date",
                        Description: "The date and time of message id.",
-                       Parameters: objectSchema(map[string]any{
-                               "id": integerSchema("The message."),
+                       Parameters: promptObjectSchema(map[string]any{
+                               "id": promptIntegerSchema("The message."),
+                       }),
+                       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: promptObjectSchema(map[string]any{
+                               "command": promptStringSchema("The bash command."),
+                       }),
+                       Strict: true,
+               },
+               {
+                       Type:        "function",
+                       Name:        "read",
+                       Description: "Read a text file.",
+                       Parameters: promptObjectSchema(map[string]any{
+                               "path": promptStringSchema("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: promptObjectSchema(map[string]any{
+                               "path": promptStringSchema("The file, absolute or relative to the working directory."),
+                               "text": promptStringSchema("The whole new content."),
+                       }),
+                       Strict: true,
+               },
+               {
+                       Type:        "function",
+                       Name:        "edit",
+                       Description: "Replace the one occurrence of old in a text file with new.",
+                       Parameters: promptObjectSchema(map[string]any{
+                               "path": promptStringSchema("The file, absolute or relative to the working directory."),
+                               "old":  promptStringSchema("The exact text to replace. It must occur once in the file."),
+                               "new":  promptStringSchema("The text that replaces old."),
                        }),
                        Strict: true,
                },
        }
 }
 
-// objectSchema returns a strict JSON schema of an object whose properties are
-// all required.
-func objectSchema(properties map[string]any) map[string]any {
+// promptObjectSchema returns a strict JSON schema of an object whose properties
+// are all required.
+func promptObjectSchema(properties map[string]any) map[string]any {
        required := make([]string, 0, len(properties))
        for name := range properties {
                required = append(required, name)
@@ -144,10 +215,18 @@ func objectSchema(properties map[string]any) map[string]any {
        }
 }
 
-// integerSchema returns the JSON schema of an integer.
-func integerSchema(description string) map[string]any {
+// promptIntegerSchema returns the JSON schema of an integer.
+func promptIntegerSchema(description string) map[string]any {
        return map[string]any{
                "type":        "integer",
                "description": description,
        }
 }
+
+// promptStringSchema returns the JSON schema of a string.
+func promptStringSchema(description string) map[string]any {
+       return map[string]any{
+               "type":        "string",
+               "description": description,
+       }
+}