X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/1e0b100b2d3d4af9f65daae7ede647ac9c388157..7cc0014244124aea59672904de65a979a8e5fb2c:/prompt.go?ds=sidebyside diff --git a/prompt.go b/prompt.go index dc0fb06..c6df057 100644 --- 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}" +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, -// 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:". @@ -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, + } +}