agent/new: fork_at= parameter for historical context forking

Sub-agents can now inherit parent context truncated at a specific
user turn: fork_at=5 means 'fork from the state after the 5th
user message.' Enables backtracking without losing unsummarized
context from before a failed approach.

Without fork_at (or fork_at=0), full history is inherited as before.
This commit is contained in:
Levi Neely 2026-08-17 09:04:57 +02:00
parent c34f4a1b31
commit ecb9406413
2 changed files with 30 additions and 7 deletions

View File

@ -345,13 +345,17 @@ func buildSessionChildren(
virtfs.FileNode("new", 0666,
virtfs.Doc("Create agent. With prompt=, runs as sub-agent: blocks until done, returns reply."),
virtfs.Rdwr(func(ctx context.Context, data []byte) ([]byte, error) {
params, prompt, parentID := parseAgentNewRequest(data)
params, prompt, parentID, forkAt := parseAgentNewRequest(data)
params.ParentID = parentID
// Sub-agent inherits parent's context.
// Sub-agent inherits parent's context (optionally truncated at fork_at).
if prompt != "" && parentID != "" {
if parent := s.FindAgent(parentID); parent != nil {
params.History = agent.RestoreHistoryFromMessages(parent.Messages())
msgs := parent.Messages()
if forkAt > 0 {
msgs = messagesUpToTurn(msgs, forkAt)
}
params.History = agent.RestoreHistoryFromMessages(msgs)
}
}

View File

@ -9,6 +9,7 @@ import (
"sync"
"ollie/cmd/olliesrv/internal/agent"
"ollie/cmd/olliesrv/internal/backend"
"ollie/cmd/olliesrv/internal/session"
"ollie/toolsrv/protocol"
"ollie/virtfs"
@ -90,11 +91,11 @@ var (
// - Space-separated key=value (legacy format from GUI)
//
// A bare word (no =) is treated as the profile name (legacy positional arg).
// Returns AgentParams, the prompt (empty if not a sub-agent request), and the parent agent ID.
func parseAgentNewRequest(data []byte) (session.AgentParams, string, string) {
// Returns AgentParams, prompt, parent agent ID, and fork_at (user-turn index, 0=all).
func parseAgentNewRequest(data []byte) (session.AgentParams, string, string, int) {
s := strings.TrimSpace(string(data))
if s == "" {
return session.AgentParams{Profile: "default"}, "", ""
return session.AgentParams{Profile: "default"}, "", "", 0
}
var tokens []string
@ -106,6 +107,7 @@ func parseAgentNewRequest(data []byte) (session.AgentParams, string, string) {
p := session.AgentParams{Profile: "default"}
var prompt, parent string
var forkAt int
for _, tok := range tokens {
tok = strings.TrimSpace(tok)
if tok == "" {
@ -130,10 +132,27 @@ func parseAgentNewRequest(data []byte) (session.AgentParams, string, string) {
prompt = v
case "parent":
parent = v
case "fork_at":
fmt.Sscanf(v, "%d", &forkAt)
}
} else if p.Profile == "default" {
p.Profile = tok
}
}
return p, prompt, parent
return p, prompt, parent, forkAt
}
// messagesUpToTurn returns messages up to (and including) the Nth user turn.
// Turn 1 = first user message and everything before the second user message.
func messagesUpToTurn(msgs []backend.Message, n int) []backend.Message {
userCount := 0
for i, m := range msgs {
if m.Role == "user" {
userCount++
if userCount > n {
return msgs[:i]
}
}
}
return msgs
}