agent/new: sub-agent support via rdwr with prompt=
Writing to session/{s}/agent/new with a prompt= key now blocks
until the agent completes its task, then returns the reply and
destroys the transient agent. Without prompt=, behaves as before
(creates agent, returns ID).
Also:
- Move ParsePayload/UnescapeValue to shared ollie/toolsrv package
- Remove duplicate implementations from cmd/toolsrv/internal/server
- Add session.CreateAgentWithParams for direct AgentParams usage
- Eliminate flattenParams/unescapeValue redundancy in fs package
This commit is contained in:
parent
ab4668cb3f
commit
250ad4b233
|
|
@ -288,13 +288,32 @@ func buildSessionChildren(
|
|||
),
|
||||
virtfs.DirNode("agent",
|
||||
virtfs.FileNode("new", 0666,
|
||||
virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) {
|
||||
ag, err := session.CreateAgent(s.Name(), strings.Fields(string(data)))
|
||||
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 := parseAgentNewRequest(data)
|
||||
|
||||
ag, err := session.CreateAgentWithParams(s.Name(), params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create agent: %w", err)
|
||||
}
|
||||
wireAgentEvents(s.ID, ag)
|
||||
return []byte(ag.ID() + "\n"), nil
|
||||
|
||||
if prompt == "" {
|
||||
return []byte(ag.ID() + "\n"), nil
|
||||
}
|
||||
|
||||
// Sub-agent mode: submit, block until idle, return reply, destroy.
|
||||
session.PublishEvent("session."+s.ID+".agent."+ag.ID()+".new", "")
|
||||
ag.Submit(ctx, prompt)
|
||||
ag.EnsureTrailingNewline()
|
||||
reply := ag.Reply()
|
||||
s.RemoveAgent(ag.ID())
|
||||
session.PublishEvent("session."+s.ID+".agent."+ag.ID()+".kill", "")
|
||||
|
||||
if reply == "" {
|
||||
return []byte("(no reply)\n"), nil
|
||||
}
|
||||
return []byte(reply), nil
|
||||
}),
|
||||
),
|
||||
virtfs.FileNode("idx", 0444,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"ollie/cmd/olliesrv/internal/agent"
|
||||
"ollie/cmd/olliesrv/internal/session"
|
||||
"ollie/toolsrv"
|
||||
"ollie/virtfs"
|
||||
)
|
||||
|
||||
|
|
@ -77,3 +78,54 @@ var (
|
|||
return helpText
|
||||
}
|
||||
)
|
||||
|
||||
// parseAgentNewRequest parses agent/new payloads in both formats:
|
||||
// - Newline-separated key=value (new format, supports multi-line values via \n escape)
|
||||
// - Space-separated key=value (legacy format from GUI)
|
||||
//
|
||||
// A bare word (no =) is treated as the profile name (legacy positional arg).
|
||||
// Returns AgentParams and the prompt (empty if not a sub-agent request).
|
||||
func parseAgentNewRequest(data []byte) (session.AgentParams, string) {
|
||||
s := strings.TrimSpace(string(data))
|
||||
if s == "" {
|
||||
return session.AgentParams{Profile: "default"}, ""
|
||||
}
|
||||
|
||||
var tokens []string
|
||||
if strings.Contains(s, "\n") {
|
||||
tokens = strings.Split(s, "\n")
|
||||
} else {
|
||||
tokens = strings.Fields(s)
|
||||
}
|
||||
|
||||
p := session.AgentParams{Profile: "default"}
|
||||
var prompt string
|
||||
for _, tok := range tokens {
|
||||
tok = strings.TrimSpace(tok)
|
||||
if tok == "" {
|
||||
continue
|
||||
}
|
||||
if k, v, ok := strings.Cut(tok, "="); ok {
|
||||
v = toolsrv.UnescapeValue(v)
|
||||
switch k {
|
||||
case "cwd":
|
||||
p.CWD = v
|
||||
case "backend":
|
||||
p.Backend = v
|
||||
case "model":
|
||||
p.Model = v
|
||||
case "name":
|
||||
p.Name = v
|
||||
case "remote":
|
||||
p.Remote = v
|
||||
case "profile":
|
||||
p.Profile = v
|
||||
case "prompt":
|
||||
prompt = v
|
||||
}
|
||||
} else if p.Profile == "default" {
|
||||
p.Profile = tok
|
||||
}
|
||||
}
|
||||
return p, prompt
|
||||
}
|
||||
|
|
|
|||
|
|
@ -457,12 +457,7 @@ func buildAgent(sess *Session, p AgentParams) (*agent.Agent, error) {
|
|||
// CreateAgent creates a new agent within an existing session.
|
||||
// Args: [profile] [key=value...] where keys include name, backend, model, remote, cwd.
|
||||
func CreateAgent(sessName string, args []string) (*agent.Agent, error) {
|
||||
sess := Lookup(sessName)
|
||||
if sess == nil {
|
||||
return nil, fmt.Errorf("session not found: %s", sessName)
|
||||
}
|
||||
|
||||
// Parse args
|
||||
// Parse args into AgentParams.
|
||||
p := AgentParams{Profile: "default"}
|
||||
for _, arg := range args {
|
||||
if k, v, ok := strings.Cut(arg, "="); ok {
|
||||
|
|
@ -482,6 +477,20 @@ func CreateAgent(sessName string, args []string) (*agent.Agent, error) {
|
|||
p.Profile = arg
|
||||
}
|
||||
}
|
||||
return CreateAgentWithParams(sessName, p)
|
||||
}
|
||||
|
||||
// CreateAgentWithParams creates a new agent within an existing session using
|
||||
// a pre-built AgentParams struct.
|
||||
func CreateAgentWithParams(sessName string, p AgentParams) (*agent.Agent, error) {
|
||||
sess := Lookup(sessName)
|
||||
if sess == nil {
|
||||
return nil, fmt.Errorf("session not found: %s", sessName)
|
||||
}
|
||||
|
||||
if p.Profile == "" {
|
||||
p.Profile = "default"
|
||||
}
|
||||
|
||||
if p.CWD == "" {
|
||||
return nil, fmt.Errorf("cwd= is required when creating an agent")
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ func (st *State) allocID() int {
|
|||
// If background is true, returns immediately with pid.
|
||||
func (st *State) NewProc(ctx context.Context, payload string, background bool) (result string, pid int, err error) {
|
||||
// Parse payload
|
||||
args := ParsePayload(payload)
|
||||
args := toolsrv.ParsePayload(payload)
|
||||
toolName := args["tool"]
|
||||
if toolName == "" {
|
||||
return "", 0, fmt.Errorf("missing 'tool' in payload")
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"ollie/toolsrv"
|
||||
)
|
||||
|
||||
func TestState_Basic(t *testing.T) {
|
||||
|
|
@ -189,10 +191,10 @@ func TestParsePayload(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := ParsePayload(tt.input)
|
||||
got := toolsrv.ParsePayload(tt.input)
|
||||
for k, v := range tt.want {
|
||||
if got[k] != v {
|
||||
t.Errorf("ParsePayload(%q)[%q] = %q, want %q", tt.input, k, got[k], v)
|
||||
t.Errorf("toolsrv.ParsePayload(%q)[%q] = %q, want %q", tt.input, k, got[k], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// parse.go - Key=value payload parsing for toolsrv.
|
||||
package server
|
||||
// parse.go — Shared key=value payload parsing.
|
||||
package toolsrv
|
||||
|
||||
import "strings"
|
||||
|
||||
// ParsePayload parses key=value lines into a map.
|
||||
// ParsePayload parses newline-separated key=value pairs into a map.
|
||||
// Values are unescaped: \n → newline, \\ → backslash.
|
||||
func ParsePayload(payload string) map[string]string {
|
||||
args := make(map[string]string)
|
||||
|
|
@ -24,8 +24,11 @@ func ParsePayload(payload string) map[string]string {
|
|||
return args
|
||||
}
|
||||
|
||||
// UnescapeValue reverses escape sequences: \n → newline, \\ → backslash.
|
||||
// UnescapeValue reverses wire-format escape sequences: \n → newline, \\ → backslash.
|
||||
func UnescapeValue(s string) string {
|
||||
if !strings.ContainsRune(s, '\\') {
|
||||
return s
|
||||
}
|
||||
result := make([]byte, 0, len(s))
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '\\' && i+1 < len(s) {
|
||||
Loading…
Reference in New Issue