human-friendliness: self-describing ctl, structured errors, status, overview
Make the namespace explain itself instead of requiring prior knowledge. - ctl is self-describing: reading it (empty write) or writing 'help' returns the valid verbs with one-line descriptions; an unknown verb errors with the valid list. Refactor dispatch to an ordered []ctlCmd carrying descriptions; drop the undocumented '.' alias and the drift-prone hardcoded help verb. Keep 'i' (drop 'inject') for fast injects. o's ctl usage now reads the live listing. - Errors carry severity + remediation. backend.ClassifyError maps the typed errors to transient/config/fatal with a one-line fix; the error event renders [[[error:<severity>]]] and a 'remediation:' line so a human knows whether to wait or intervene. - Add a human status file: 'thinking · 12s', 'calling shell · 3s', 'idle' — distinct from the machine-facing raw state. Wire the TUI bar to it. - Bare 'o' shows an overview of running sessions/agents with status, so you don't need to know any names to get oriented. Tests: dispatch help/unknown/routing, ClassifyError severity table.
This commit is contained in:
parent
f3933e21e7
commit
42cae9ee2e
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
"9fans.net/go/plan9"
|
"9fans.net/go/plan9"
|
||||||
p9client "9fans.net/go/plan9/client"
|
p9client "9fans.net/go/plan9/client"
|
||||||
|
|
@ -41,6 +42,7 @@ type Agent struct {
|
||||||
|
|
||||||
// Execution state — owned by the agent, protected by stateMu.
|
// Execution state — owned by the agent, protected by stateMu.
|
||||||
state string // "idle", "thinking", "calling: <tool>"
|
state string // "idle", "thinking", "calling: <tool>"
|
||||||
|
stateSince time.Time // when the current state began
|
||||||
reply string // last assistant response
|
reply string // last assistant response
|
||||||
getCwd func() string // returns session working directory
|
getCwd func() string // returns session working directory
|
||||||
id string // agent identity (unique principal)
|
id string // agent identity (unique principal)
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ func (ag *Agent) run(ctx context.Context) error {
|
||||||
|
|
||||||
_, compactErr := ag.autoCompact(ctx)
|
_, compactErr := ag.autoCompact(ctx)
|
||||||
if compactErr != nil {
|
if compactErr != nil {
|
||||||
ag.emit(Event{Role: "error", Content: compactErr.Error()})
|
ag.emit(Event{Role: "error", Name: backend.SeverityFatal, Content: compactErr.Error()})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ func (ag *Agent) trackErrors(ctx context.Context, results []toolResult, es *erro
|
||||||
}
|
}
|
||||||
|
|
||||||
if es.consecutive >= consecutiveErrorHardLimit {
|
if es.consecutive >= consecutiveErrorHardLimit {
|
||||||
ag.emit(Event{Role: "error", Content: fmt.Sprintf("%d consecutive tool errors — aborting", es.consecutive)})
|
ag.emit(Event{Role: "error", Name: backend.SeverityFatal, Content: fmt.Sprintf("%d consecutive tool errors — aborting\nremediation: the model is stuck; rephrase the task or intervene", es.consecutive)})
|
||||||
return fmt.Errorf("step %d: %d consecutive tool errors", step, es.consecutive)
|
return fmt.Errorf("step %d: %d consecutive tool errors", step, es.consecutive)
|
||||||
}
|
}
|
||||||
if es.consecutive == consecutiveErrorSoftLimit {
|
if es.consecutive == consecutiveErrorSoftLimit {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,13 @@
|
||||||
|
|
||||||
package agent
|
package agent
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// WatchField names supported by Agent.WaitChange.
|
// WatchField names supported by Agent.WaitChange.
|
||||||
const (
|
const (
|
||||||
|
|
@ -26,6 +32,7 @@ func (ag *Agent) State() string {
|
||||||
func (ag *Agent) SetState(state string) {
|
func (ag *Agent) SetState(state string) {
|
||||||
ag.stateMu.Lock()
|
ag.stateMu.Lock()
|
||||||
ag.state = state
|
ag.state = state
|
||||||
|
ag.stateSince = time.Now()
|
||||||
ag.stateMu.Unlock()
|
ag.stateMu.Unlock()
|
||||||
ag.notifyChange()
|
ag.notifyChange()
|
||||||
if ag.onStateChange != nil {
|
if ag.onStateChange != nil {
|
||||||
|
|
@ -33,6 +40,38 @@ func (ag *Agent) SetState(state string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Status renders a human-readable one-line status: the current state and how
|
||||||
|
// long it has been in that state, e.g. "thinking · 12s" or "calling shell · 3s".
|
||||||
|
// The raw state string is machine-facing; this is the human-facing view.
|
||||||
|
func (ag *Agent) Status() string {
|
||||||
|
ag.stateMu.RLock()
|
||||||
|
state := ag.state
|
||||||
|
since := ag.stateSince
|
||||||
|
ag.stateMu.RUnlock()
|
||||||
|
|
||||||
|
if state == "" {
|
||||||
|
state = "idle"
|
||||||
|
}
|
||||||
|
// "calling: shell" reads better as "calling shell".
|
||||||
|
state = strings.Replace(state, "calling: ", "calling ", 1)
|
||||||
|
if state == "idle" || since.IsZero() {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
return state + " · " + humanDuration(time.Since(since))
|
||||||
|
}
|
||||||
|
|
||||||
|
// humanDuration renders a short elapsed duration: "3s", "2m10s", "1h04m".
|
||||||
|
func humanDuration(d time.Duration) string {
|
||||||
|
secs := int(d.Seconds())
|
||||||
|
if secs < 60 {
|
||||||
|
return strconv.Itoa(secs) + "s"
|
||||||
|
}
|
||||||
|
if secs < 3600 {
|
||||||
|
return strconv.Itoa(secs/60) + "m" + strconv.Itoa(secs%60) + "s"
|
||||||
|
}
|
||||||
|
return strconv.Itoa(secs/3600) + "h" + fmt.Sprintf("%02dm", (secs%3600)/60)
|
||||||
|
}
|
||||||
|
|
||||||
// Reply returns the agent's last assistant response.
|
// Reply returns the agent's last assistant response.
|
||||||
func (ag *Agent) Reply() string {
|
func (ag *Agent) Reply() string {
|
||||||
ag.stateMu.RLock()
|
ag.stateMu.RLock()
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ func (ag *Agent) Submit(ctx context.Context, input string) {
|
||||||
a.cancel(fmt.Errorf("%v", r))
|
a.cancel(fmt.Errorf("%v", r))
|
||||||
}
|
}
|
||||||
ag.SetState("idle")
|
ag.SetState("idle")
|
||||||
ag.emit(Event{Role: "error", Content: fmt.Sprintf("%v", r)})
|
ag.emit(Event{Role: "error", Name: backend.SeverityFatal, Content: fmt.Sprintf("%v", r)})
|
||||||
// Re-submit next FIFO item in a new goroutine so queued
|
// Re-submit next FIFO item in a new goroutine so queued
|
||||||
// prompts aren't orphaned by the panic.
|
// prompts aren't orphaned by the panic.
|
||||||
if next, ok := ag.fifo.Pop(); ok {
|
if next, ok := ag.fifo.Pop(); ok {
|
||||||
|
|
@ -122,7 +122,7 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
|
||||||
ti, listErr := ag.runtime.ToolServer.ListTools()
|
ti, listErr := ag.runtime.ToolServer.ListTools()
|
||||||
if listErr != nil {
|
if listErr != nil {
|
||||||
ag.log.Error("tool server unavailable: %v", listErr)
|
ag.log.Error("tool server unavailable: %v", listErr)
|
||||||
ag.emit(Event{Role: "error", Content: fmt.Sprintf("tool server unavailable: %v", listErr)})
|
ag.emit(Event{Role: "error", Name: backend.SeverityFatal, Content: fmt.Sprintf("tool server unavailable: %v\nremediation: the tool server process is not responding; kill and recreate the session", listErr)})
|
||||||
ag.SetState("idle")
|
ag.SetState("idle")
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -354,7 +354,12 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
|
||||||
ag.save()
|
ag.save()
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
ag.emit(Event{Role: "error", Content: err.Error()})
|
severity, remediation := backend.ClassifyError(err)
|
||||||
|
content := err.Error()
|
||||||
|
if remediation != "" {
|
||||||
|
content += "\nremediation: " + remediation
|
||||||
|
}
|
||||||
|
ag.emit(Event{Role: "error", Name: severity, Content: content})
|
||||||
// Drain one FIFO item.
|
// Drain one FIFO item.
|
||||||
if next, ok := ag.fifo.Pop(); ok {
|
if next, ok := ag.fifo.Pop(); ok {
|
||||||
return next
|
return next
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -141,6 +142,41 @@ type ToolUnsupportedError struct {
|
||||||
|
|
||||||
func (e *ToolUnsupportedError) Error() string { return e.Message }
|
func (e *ToolUnsupportedError) Error() string { return e.Message }
|
||||||
|
|
||||||
|
// Error severity levels for the human-facing error surface.
|
||||||
|
const (
|
||||||
|
// SeverityTransient: temporary, the runtime is retrying. Wait.
|
||||||
|
SeverityTransient = "transient"
|
||||||
|
// SeverityConfig: the user's configuration or request is wrong. Act.
|
||||||
|
SeverityConfig = "config"
|
||||||
|
// SeverityFatal: the turn aborted and won't recover on its own.
|
||||||
|
SeverityFatal = "fatal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClassifyError maps an error to a human-facing severity and a one-line
|
||||||
|
// remediation hint. The remediation is empty when there's nothing useful to
|
||||||
|
// suggest. Classification keys off the typed backend errors; anything
|
||||||
|
// unrecognised is fatal with no remediation.
|
||||||
|
func ClassifyError(err error) (severity, remediation string) {
|
||||||
|
if err == nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
var rl *RateLimitError
|
||||||
|
var tr *TransientError
|
||||||
|
var co *ContextOverflowError
|
||||||
|
var tu *ToolUnsupportedError
|
||||||
|
switch {
|
||||||
|
case errors.As(err, &rl):
|
||||||
|
return SeverityTransient, "rate limited by the provider; the runtime will retry automatically"
|
||||||
|
case errors.As(err, &tr):
|
||||||
|
return SeverityTransient, "temporary provider/network error; the runtime will retry automatically"
|
||||||
|
case errors.As(err, &co):
|
||||||
|
return SeverityConfig, "context window exceeded; run 'ctl compact' or start a fresh session"
|
||||||
|
case errors.As(err, &tu):
|
||||||
|
return SeverityConfig, "this model does not support tools; switch models or use an agent with no tools"
|
||||||
|
}
|
||||||
|
return SeverityFatal, ""
|
||||||
|
}
|
||||||
|
|
||||||
// GenerationParams controls sampling behaviour for a single ChatStream call.
|
// GenerationParams controls sampling behaviour for a single ChatStream call.
|
||||||
// Zero values mean "use the API default".
|
// Zero values mean "use the API default".
|
||||||
type GenerationParams struct {
|
type GenerationParams struct {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClassifyError(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
err error
|
||||||
|
wantSeverity string
|
||||||
|
wantRemed bool
|
||||||
|
}{
|
||||||
|
{nil, "", false},
|
||||||
|
{&RateLimitError{}, SeverityTransient, true},
|
||||||
|
{&TransientError{}, SeverityTransient, true},
|
||||||
|
{&ContextOverflowError{}, SeverityConfig, true},
|
||||||
|
{&ToolUnsupportedError{}, SeverityConfig, true},
|
||||||
|
{errors.New("something else"), SeverityFatal, false},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
sev, remed := ClassifyError(c.err)
|
||||||
|
if sev != c.wantSeverity {
|
||||||
|
t.Errorf("ClassifyError(%v) severity = %q, want %q", c.err, sev, c.wantSeverity)
|
||||||
|
}
|
||||||
|
if (remed != "") != c.wantRemed {
|
||||||
|
t.Errorf("ClassifyError(%v) remediation = %q, wantNonEmpty=%v", c.err, remed, c.wantRemed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClassifyError_Wrapped(t *testing.T) {
|
||||||
|
// Classification must see through fmt.Errorf %w wrapping.
|
||||||
|
err := fmt.Errorf("step 2: %w", &ContextOverflowError{Message: "too long"})
|
||||||
|
sev, remed := ClassifyError(err)
|
||||||
|
if sev != SeverityConfig {
|
||||||
|
t.Errorf("severity = %q, want %q", sev, SeverityConfig)
|
||||||
|
}
|
||||||
|
if remed == "" {
|
||||||
|
t.Error("expected remediation for context overflow")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testCmds() []ctlCmd {
|
||||||
|
ok := func(_ []string) ([]byte, error) { return []byte("ok\n"), nil }
|
||||||
|
return []ctlCmd{
|
||||||
|
{"stop", "interrupt the current turn", ok},
|
||||||
|
{"compact", "compact context now", ok},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDispatch_HelpListsCommands(t *testing.T) {
|
||||||
|
// Empty input and "help" both return the self-describing listing.
|
||||||
|
for _, in := range []string{"", "help", " "} {
|
||||||
|
out, err := dispatch(testCmds(), []byte(in))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dispatch(%q) error: %v", in, err)
|
||||||
|
}
|
||||||
|
s := string(out)
|
||||||
|
if !strings.Contains(s, "stop") || !strings.Contains(s, "interrupt the current turn") {
|
||||||
|
t.Errorf("help listing missing stop/description: %q", s)
|
||||||
|
}
|
||||||
|
if !strings.Contains(s, "compact") {
|
||||||
|
t.Errorf("help listing missing compact: %q", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDispatch_UnknownVerbListsValid(t *testing.T) {
|
||||||
|
_, err := dispatch(testCmds(), []byte("stpo"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for unknown verb")
|
||||||
|
}
|
||||||
|
msg := err.Error()
|
||||||
|
if !strings.Contains(msg, "stpo") {
|
||||||
|
t.Errorf("error should name the bad verb: %q", msg)
|
||||||
|
}
|
||||||
|
if !strings.Contains(msg, "stop") || !strings.Contains(msg, "compact") {
|
||||||
|
t.Errorf("error should list valid verbs: %q", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDispatch_RoutesToHandler(t *testing.T) {
|
||||||
|
out, err := dispatch(testCmds(), []byte("stop"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dispatch error: %v", err)
|
||||||
|
}
|
||||||
|
if string(out) != "ok\n" {
|
||||||
|
t.Errorf("got %q, want ok", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -220,8 +220,8 @@ func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
|
||||||
virtfs.FileNode("ctl", 0666,
|
virtfs.FileNode("ctl", 0666,
|
||||||
virtfs.Doc("Server control. Write: 'invalidate', 'kill'"),
|
virtfs.Doc("Server control. Write: 'invalidate', 'kill'"),
|
||||||
virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) {
|
virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) {
|
||||||
return dispatch(map[string]func([]string) ([]byte, error){
|
return dispatch([]ctlCmd{
|
||||||
"invalidate": func(_ []string) ([]byte, error) {
|
{"invalidate", "clear the models cache", func(_ []string) ([]byte, error) {
|
||||||
if mc != nil {
|
if mc != nil {
|
||||||
mc.Invalidate()
|
mc.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
@ -229,13 +229,13 @@ func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
|
||||||
cfg.Invalidate()
|
cfg.Invalidate()
|
||||||
}
|
}
|
||||||
return []byte("ok\n"), nil
|
return []byte("ok\n"), nil
|
||||||
},
|
}},
|
||||||
"kill": func(_ []string) ([]byte, error) {
|
{"kill", "shut down the server", func(_ []string) ([]byte, error) {
|
||||||
if cfg.Shutdown != nil {
|
if cfg.Shutdown != nil {
|
||||||
cfg.Shutdown()
|
cfg.Shutdown()
|
||||||
}
|
}
|
||||||
return []byte("ok\n"), nil
|
return []byte("ok\n"), nil
|
||||||
},
|
}},
|
||||||
}, data)
|
}, data)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -478,30 +478,29 @@ func buildSessionChildren(
|
||||||
),
|
),
|
||||||
virtfs.FileNode("ctl", 0666,
|
virtfs.FileNode("ctl", 0666,
|
||||||
virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) {
|
virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) {
|
||||||
return dispatch(map[string]func([]string) ([]byte, error){
|
return dispatch([]ctlCmd{
|
||||||
"kill": func(_ []string) ([]byte, error) { return []byte("ok\n"), removeFn() },
|
{"kill", "destroy this session", func(_ []string) ([]byte, error) { return []byte("ok\n"), removeFn() }},
|
||||||
".": func(_ []string) ([]byte, error) { return []byte("ok\n"), removeFn() },
|
{"save", "persist session state to disk", func(_ []string) ([]byte, error) {
|
||||||
"save": func(_ []string) ([]byte, error) {
|
|
||||||
s.Save()
|
s.Save()
|
||||||
return []byte("ok\n"), nil
|
return []byte("ok\n"), nil
|
||||||
},
|
}},
|
||||||
"invalidate": func(_ []string) ([]byte, error) {
|
{"invalidate", "clear the models cache", func(_ []string) ([]byte, error) {
|
||||||
s.InvalidateModelsCache()
|
s.InvalidateModelsCache()
|
||||||
return []byte("ok\n"), nil
|
return []byte("ok\n"), nil
|
||||||
},
|
}},
|
||||||
"pause": func(_ []string) ([]byte, error) {
|
{"pause", "pause all agents in this session", func(_ []string) ([]byte, error) {
|
||||||
if err := s.Pause(); err != nil {
|
if err := s.Pause(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return []byte("ok\n"), nil
|
return []byte("ok\n"), nil
|
||||||
},
|
}},
|
||||||
"resume": func(_ []string) ([]byte, error) {
|
{"resume", "resume all agents in this session", func(_ []string) ([]byte, error) {
|
||||||
if err := s.Resume(); err != nil {
|
if err := s.Resume(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return []byte("ok\n"), nil
|
return []byte("ok\n"), nil
|
||||||
},
|
}},
|
||||||
"run": func(args []string) ([]byte, error) {
|
{"run", "run a workflow: run [workflow] [variant]", func(args []string) ([]byte, error) {
|
||||||
workflow := s.Workflow()
|
workflow := s.Workflow()
|
||||||
variant := s.Variant()
|
variant := s.Variant()
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
|
|
@ -512,7 +511,7 @@ func buildSessionChildren(
|
||||||
}
|
}
|
||||||
runWorkflow(s, workflow, variant)
|
runWorkflow(s, workflow, variant)
|
||||||
return []byte("ok\n"), nil
|
return []byte("ok\n"), nil
|
||||||
},
|
}},
|
||||||
}, data)
|
}, data)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -837,6 +836,12 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
||||||
return []byte(st + "\n"), st, nil
|
return []byte(st + "\n"), st, nil
|
||||||
}, a.SignalCh),
|
}, a.SignalCh),
|
||||||
),
|
),
|
||||||
|
virtfs.FileNode("status", 0444,
|
||||||
|
virtfs.Doc("Human-readable status: current activity and elapsed time"),
|
||||||
|
virtfs.Read(func() ([]byte, error) {
|
||||||
|
return []byte(a.Status() + "\n"), nil
|
||||||
|
}),
|
||||||
|
),
|
||||||
virtfs.FileNode("log", 0444,
|
virtfs.FileNode("log", 0444,
|
||||||
virtfs.Read(func() ([]byte, error) {
|
virtfs.Read(func() ([]byte, error) {
|
||||||
const maxWindow = 64 * 1024
|
const maxWindow = 64 * 1024
|
||||||
|
|
@ -936,17 +941,17 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
||||||
}),
|
}),
|
||||||
virtfs.FileNode("ctl", 0666,
|
virtfs.FileNode("ctl", 0666,
|
||||||
virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) {
|
virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) {
|
||||||
return dispatch(map[string]func([]string) ([]byte, error){
|
return dispatch([]ctlCmd{
|
||||||
"kill": func(_ []string) ([]byte, error) {
|
{"kill", "destroy this agent", func(_ []string) ([]byte, error) {
|
||||||
s.RemoveAgent(a.ID())
|
s.RemoveAgent(a.ID())
|
||||||
session.PublishEvent("session."+s.ID+".agent."+a.ID()+".kill", "")
|
session.PublishEvent("session."+s.ID+".agent."+a.ID()+".kill", "")
|
||||||
return []byte("ok\n"), nil
|
return []byte("ok\n"), nil
|
||||||
},
|
}},
|
||||||
"stop": func(_ []string) ([]byte, error) {
|
{"stop", "interrupt the current turn", func(_ []string) ([]byte, error) {
|
||||||
a.Interrupt(agent.ErrInterrupted)
|
a.Interrupt(agent.ErrInterrupted)
|
||||||
return []byte("ok\n"), nil
|
return []byte("ok\n"), nil
|
||||||
},
|
}},
|
||||||
"detach": func(_ []string) ([]byte, error) {
|
{"detach", "detach the foreground process to background", func(_ []string) ([]byte, error) {
|
||||||
ts := a.ToolServer()
|
ts := a.ToolServer()
|
||||||
if ts == nil {
|
if ts == nil {
|
||||||
return nil, fmt.Errorf("no tool server")
|
return nil, fmt.Errorf("no tool server")
|
||||||
|
|
@ -955,30 +960,29 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
||||||
return nil, fmt.Errorf("no foreground process to detach")
|
return nil, fmt.Errorf("no foreground process to detach")
|
||||||
}
|
}
|
||||||
return []byte("ok\n"), nil
|
return []byte("ok\n"), nil
|
||||||
},
|
}},
|
||||||
"compact": func(_ []string) ([]byte, error) {
|
{"compact", "compact context now", func(_ []string) ([]byte, error) {
|
||||||
if err := a.Compact(s.Ctx); err != nil {
|
if err := a.Compact(s.Ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return []byte("ok\n"), nil
|
return []byte("ok\n"), nil
|
||||||
},
|
}},
|
||||||
"compactionmodel": func(args []string) ([]byte, error) {
|
{"compactionmodel", "get/set the compaction model", func(args []string) ([]byte, error) {
|
||||||
model := strings.TrimSpace(strings.Join(args, " "))
|
model := strings.TrimSpace(strings.Join(args, " "))
|
||||||
if model == "" {
|
if model == "" {
|
||||||
return []byte(a.CompactionModel() + "\n"), nil
|
return []byte(a.CompactionModel() + "\n"), nil
|
||||||
}
|
}
|
||||||
a.SetCompactionModel(model)
|
a.SetCompactionModel(model)
|
||||||
return []byte(model + "\n"), nil
|
return []byte(model + "\n"), nil
|
||||||
},
|
}},
|
||||||
"clear": func(_ []string) ([]byte, error) {
|
{"clear", "clear chat history", func(_ []string) ([]byte, error) {
|
||||||
if err := a.Clear(); err != nil {
|
if err := a.Clear(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return []byte("ok\n"), nil
|
return []byte("ok\n"), nil
|
||||||
},
|
}},
|
||||||
"inject": inject,
|
{"i", "inject text into the running turn", inject},
|
||||||
"i": inject,
|
{"agent", "get/switch agent profile: agent [name]", func(args []string) ([]byte, error) {
|
||||||
"agent": func(args []string) ([]byte, error) {
|
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return []byte(a.Profile() + "\n"), nil
|
return []byte(a.Profile() + "\n"), nil
|
||||||
}
|
}
|
||||||
|
|
@ -992,8 +996,8 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
||||||
session.LoadTools(cfg, ts, s.ID, a.ID(), nil)
|
session.LoadTools(cfg, ts, s.ID, a.ID(), nil)
|
||||||
}
|
}
|
||||||
return []byte(args[0] + "\n"), nil
|
return []byte(args[0] + "\n"), nil
|
||||||
},
|
}},
|
||||||
"model": func(args []string) ([]byte, error) {
|
{"model", "get/set model: model [name]", func(args []string) ([]byte, error) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
if be := a.Backend(); be != nil {
|
if be := a.Backend(); be != nil {
|
||||||
return []byte(be.Model() + "\n"), nil
|
return []byte(be.Model() + "\n"), nil
|
||||||
|
|
@ -1006,11 +1010,11 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
||||||
return []byte(modelPricingBlock(be, name)), nil
|
return []byte(modelPricingBlock(be, name)), nil
|
||||||
}
|
}
|
||||||
return []byte(name + "\n"), nil
|
return []byte(name + "\n"), nil
|
||||||
},
|
}},
|
||||||
"models": func(_ []string) ([]byte, error) {
|
{"models", "list available models", func(_ []string) ([]byte, error) {
|
||||||
return []byte(s.CachedListModels() + "\n"), nil
|
return []byte(s.CachedListModels() + "\n"), nil
|
||||||
},
|
}},
|
||||||
"tools": func(_ []string) ([]byte, error) {
|
{"tools", "list loaded tools", func(_ []string) ([]byte, error) {
|
||||||
ts := a.ToolServer()
|
ts := a.ToolServer()
|
||||||
if ts == nil {
|
if ts == nil {
|
||||||
return []byte("(no tool server)\n"), nil
|
return []byte("(no tool server)\n"), nil
|
||||||
|
|
@ -1028,8 +1032,8 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []byte(sb.String()), nil
|
return []byte(sb.String()), nil
|
||||||
},
|
}},
|
||||||
"tool_load": func(args []string) ([]byte, error) {
|
{"tool_load", "load a tool: tool_load <name>", func(args []string) ([]byte, error) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return nil, fmt.Errorf("tool_load requires a tool name")
|
return nil, fmt.Errorf("tool_load requires a tool name")
|
||||||
}
|
}
|
||||||
|
|
@ -1044,15 +1048,15 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
||||||
a.SetToolsPreamble(agent.RenderTools(infos))
|
a.SetToolsPreamble(agent.RenderTools(infos))
|
||||||
}
|
}
|
||||||
return []byte(args[0] + "\n"), nil
|
return []byte(args[0] + "\n"), nil
|
||||||
},
|
}},
|
||||||
"tools_all": func(args []string) ([]byte, error) {
|
{"tools_all", "list all available tools", func(args []string) ([]byte, error) {
|
||||||
ts := a.ToolServer()
|
ts := a.ToolServer()
|
||||||
if ts == nil {
|
if ts == nil {
|
||||||
return nil, fmt.Errorf("no tool server")
|
return nil, fmt.Errorf("no tool server")
|
||||||
}
|
}
|
||||||
return ts.ListAllTools()
|
return ts.ListAllTools()
|
||||||
},
|
}},
|
||||||
"tool_unload": func(args []string) ([]byte, error) {
|
{"tool_unload", "unload a tool: tool_unload <name>", func(args []string) ([]byte, error) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return nil, fmt.Errorf("tool_unload requires a tool name")
|
return nil, fmt.Errorf("tool_unload requires a tool name")
|
||||||
}
|
}
|
||||||
|
|
@ -1067,8 +1071,8 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
||||||
a.SetToolsPreamble(agent.RenderTools(infos))
|
a.SetToolsPreamble(agent.RenderTools(infos))
|
||||||
}
|
}
|
||||||
return []byte(args[0] + "\n"), nil
|
return []byte(args[0] + "\n"), nil
|
||||||
},
|
}},
|
||||||
"backend": func(args []string) ([]byte, error) {
|
{"backend", "get/set backend: backend [name]", func(args []string) ([]byte, error) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
if be := a.Backend(); be != nil {
|
if be := a.Backend(); be != nil {
|
||||||
return []byte(be.Name() + "\n"), nil
|
return []byte(be.Name() + "\n"), nil
|
||||||
|
|
@ -1080,18 +1084,18 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return []byte(name + "\n"), nil
|
return []byte(name + "\n"), nil
|
||||||
},
|
}},
|
||||||
"name": func(args []string) ([]byte, error) {
|
{"name", "get/set agent name: name [value]", func(args []string) ([]byte, error) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return []byte(a.Name() + "\n"), nil
|
return []byte(a.Name() + "\n"), nil
|
||||||
}
|
}
|
||||||
a.SetName(strings.Join(args, " "))
|
a.SetName(strings.Join(args, " "))
|
||||||
return []byte(strings.Join(args, " ") + "\n"), nil
|
return []byte(strings.Join(args, " ") + "\n"), nil
|
||||||
},
|
}},
|
||||||
"cwd": func(args []string) ([]byte, error) {
|
{"cwd", "print the working directory", func(args []string) ([]byte, error) {
|
||||||
return []byte(a.Cwd() + "\n"), nil
|
return []byte(a.Cwd() + "\n"), nil
|
||||||
},
|
}},
|
||||||
"proc": func(args []string) ([]byte, error) {
|
{"proc", "manage background procs: proc [top|term|kill|out|dismiss <pid>]", func(args []string) ([]byte, error) {
|
||||||
ts := a.ToolServer()
|
ts := a.ToolServer()
|
||||||
if ts == nil {
|
if ts == nil {
|
||||||
return nil, fmt.Errorf("no tool server")
|
return nil, fmt.Errorf("no tool server")
|
||||||
|
|
@ -1160,11 +1164,11 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown proc subcommand: %s (use: top, term <pid>, kill <pid>, out <pid>, dismiss <pid>)", subcmd)
|
return nil, fmt.Errorf("unknown proc subcommand: %s (use: top, term <pid>, kill <pid>, out <pid>, dismiss <pid>)", subcmd)
|
||||||
}
|
}
|
||||||
},
|
}},
|
||||||
"systemprompt": func(_ []string) ([]byte, error) {
|
{"systemprompt", "print the rendered system prompt", func(_ []string) ([]byte, error) {
|
||||||
return []byte(a.SystemPrompt() + "\n"), nil
|
return []byte(a.SystemPrompt() + "\n"), nil
|
||||||
},
|
}},
|
||||||
"peeradd": func(args []string) ([]byte, error) {
|
{"peeradd", "link a peer agent: peeradd <name>", func(args []string) ([]byte, error) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return nil, fmt.Errorf("peeradd requires agent name")
|
return nil, fmt.Errorf("peeradd requires agent name")
|
||||||
}
|
}
|
||||||
|
|
@ -1176,8 +1180,8 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
||||||
target.AddPeer(a.Name())
|
target.AddPeer(a.Name())
|
||||||
go s.Save()
|
go s.Save()
|
||||||
return []byte("ok\n"), nil
|
return []byte("ok\n"), nil
|
||||||
},
|
}},
|
||||||
"peerdel": func(args []string) ([]byte, error) {
|
{"peerdel", "unlink a peer agent: peerdel <name>", func(args []string) ([]byte, error) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return nil, fmt.Errorf("peerdel requires agent name")
|
return nil, fmt.Errorf("peerdel requires agent name")
|
||||||
}
|
}
|
||||||
|
|
@ -1187,18 +1191,15 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
||||||
}
|
}
|
||||||
go s.Save()
|
go s.Save()
|
||||||
return []byte("ok\n"), nil
|
return []byte("ok\n"), nil
|
||||||
},
|
}},
|
||||||
"peers": func(_ []string) ([]byte, error) {
|
{"peers", "list linked peer agents", func(_ []string) ([]byte, error) {
|
||||||
peers := a.Peers()
|
peers := a.Peers()
|
||||||
if len(peers) == 0 {
|
if len(peers) == 0 {
|
||||||
return []byte("(no peers)\n"), nil
|
return []byte("(no peers)\n"), nil
|
||||||
}
|
}
|
||||||
return []byte(strings.Join(peers, "\n") + "\n"), nil
|
return []byte(strings.Join(peers, "\n") + "\n"), nil
|
||||||
},
|
}},
|
||||||
"stats": stats,
|
{"stats", "print usage and cost statistics", stats},
|
||||||
"help": func(_ []string) ([]byte, error) {
|
|
||||||
return []byte("stop detach compact clear inject agent model models tools tool_load tool_unload cwd proc name backend systemprompt peeradd peerdel peers stats help\n"), nil
|
|
||||||
},
|
|
||||||
}, data)
|
}, data)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,25 @@ func wireAgentEvents(sessID string, ag *agent.Agent) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// dispatch routes a command to the matching handler based on the first word.
|
// ctlCmd is one control verb: its name, a one-line description for the
|
||||||
func dispatch(handlers map[string]func([]string) ([]byte, error), data []byte) ([]byte, error) {
|
// self-describing help listing, and its handler.
|
||||||
|
type ctlCmd struct {
|
||||||
|
name string
|
||||||
|
desc string
|
||||||
|
fn func([]string) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatch routes a ctl write to a command by its first word. Reading the ctl
|
||||||
|
// file (empty write) or writing "help" returns the command listing. An unknown
|
||||||
|
// verb returns an error listing the valid verbs.
|
||||||
|
func dispatch(cmds []ctlCmd, data []byte) ([]byte, error) {
|
||||||
input := strings.TrimSpace(string(data))
|
input := strings.TrimSpace(string(data))
|
||||||
if input == "" {
|
if input == "" || input == "help" {
|
||||||
return nil, nil
|
var sb strings.Builder
|
||||||
|
for _, c := range cmds {
|
||||||
|
fmt.Fprintf(&sb, "%-16s %s\n", c.name, c.desc)
|
||||||
|
}
|
||||||
|
return []byte(sb.String()), nil
|
||||||
}
|
}
|
||||||
parts := strings.Fields(input)
|
parts := strings.Fields(input)
|
||||||
command := parts[0]
|
command := parts[0]
|
||||||
|
|
@ -35,11 +49,16 @@ func dispatch(handlers map[string]func([]string) ([]byte, error), data []byte) (
|
||||||
command = name
|
command = name
|
||||||
args = append([]string{value}, args...)
|
args = append([]string{value}, args...)
|
||||||
}
|
}
|
||||||
h, ok := handlers[command]
|
for _, c := range cmds {
|
||||||
if !ok {
|
if c.name == command {
|
||||||
return nil, fmt.Errorf("unknown ctl command: %s", command)
|
return c.fn(args)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return h(args)
|
names := make([]string, len(cmds))
|
||||||
|
for i, c := range cmds {
|
||||||
|
names[i] = c.name
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unknown ctl command %q; valid: %s", command, strings.Join(names, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// stripMarkers removes [[[...]]] marker lines, source fence delimiter lines,
|
// stripMarkers removes [[[...]]] marker lines, source fence delimiter lines,
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ CTX_SESSION="" # Session name if in session/agent context
|
||||||
CTX_AGENT="" # Agent name if in agent context
|
CTX_AGENT="" # Agent name if in agent context
|
||||||
|
|
||||||
# ── Known commands (to distinguish from context) ─────────────────
|
# ── Known commands (to distinguish from context) ─────────────────
|
||||||
COMMANDS="read write rdwr ls new ctl prompt log tui env generate help"
|
COMMANDS="read write rdwr ls new ctl prompt log tui env generate overview help"
|
||||||
|
|
||||||
is_command() {
|
is_command() {
|
||||||
local arg="$1"
|
local arg="$1"
|
||||||
|
|
@ -148,14 +148,14 @@ cmd_generate() {
|
||||||
|
|
||||||
cmd_ctl() {
|
cmd_ctl() {
|
||||||
if [[ $# -lt 1 ]]; then
|
if [[ $# -lt 1 ]]; then
|
||||||
echo "usage: o ctl <command> [args...]" >&2
|
if [[ -z "$PREFIX" ]]; then
|
||||||
if [[ -n "$CTX_AGENT" ]]; then
|
echo "usage: o <session> ctl <cmd> (or o <session>/<agent> ctl <cmd>)" >&2
|
||||||
echo " Agent: stop, compact, clear, inject, agent, model, cwd, name" >&2
|
exit 1
|
||||||
elif [[ -n "$CTX_SESSION" ]]; then
|
|
||||||
echo " Session: kill, pause, resume" >&2
|
|
||||||
else
|
|
||||||
echo " (requires session or agent context)" >&2
|
|
||||||
fi
|
fi
|
||||||
|
# The ctl file is self-describing: an empty request returns the
|
||||||
|
# valid commands with descriptions.
|
||||||
|
echo "usage: o ctl <command> [args...]" >&2
|
||||||
|
printf '' | ollie-9p rdwr "${PREFIX}/ctl" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -191,7 +191,7 @@ cmd_prompt() {
|
||||||
# Interactive REPL mode
|
# Interactive REPL mode
|
||||||
echo "[$CTX_SESSION/$CTX_AGENT]"
|
echo "[$CTX_SESSION/$CTX_AGENT]"
|
||||||
echo "Type . on a blank line to send, Ctrl+D to exit."
|
echo "Type . on a blank line to send, Ctrl+D to exit."
|
||||||
echo "/ prefix runs ctl: /stop, /compact, /model X, /inject X"
|
echo "/ prefix runs ctl: /stop, /compact, /model X, /i X (type /help for all)"
|
||||||
echo "!q exits the TUI."
|
echo "!q exits the TUI."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|
@ -330,7 +330,8 @@ cmd_tui() {
|
||||||
*) bg="default" ;;
|
*) bg="default" ;;
|
||||||
esac
|
esac
|
||||||
tmux set-option -t "=${tmux_session}" status-right-style "fg=default,bg=$bg" 2>/dev/null
|
tmux set-option -t "=${tmux_session}" status-right-style "fg=default,bg=$bg" 2>/dev/null
|
||||||
tmux set-option -t "=${tmux_session}" status-right "$state | %H:%M" 2>/dev/null
|
status=$(ollie-9p read "${PREFIX}/status" 2>/dev/null | head -n1)
|
||||||
|
tmux set-option -t "=${tmux_session}" status-right "${status:-$state} | %H:%M" 2>/dev/null
|
||||||
done
|
done
|
||||||
done &
|
done &
|
||||||
local bg_pid=$!
|
local bg_pid=$!
|
||||||
|
|
@ -346,6 +347,35 @@ cmd_env() {
|
||||||
[[ -n "$CTX_AGENT" ]] && echo "export OLLIE_AGENT=$CTX_AGENT"
|
[[ -n "$CTX_AGENT" ]] && echo "export OLLIE_AGENT=$CTX_AGENT"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# cmd_overview shows all running sessions and their agents with a human-readable
|
||||||
|
# status, so you can see what's live without knowing any names in advance.
|
||||||
|
cmd_overview() {
|
||||||
|
local idx
|
||||||
|
idx=$(ollie-9p read "session/idx" 2>/dev/null || true)
|
||||||
|
if [[ -z "$idx" ]]; then
|
||||||
|
echo "no active sessions"
|
||||||
|
echo "start one: o <session>/<agent> new [cwd]"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
# session/idx: id \t name \t paused \t connected \t remote \t cwd
|
||||||
|
while IFS=$'\t' read -r _id sname paused _conn remote cwd; do
|
||||||
|
[[ -z "$sname" ]] && continue
|
||||||
|
local tag=""
|
||||||
|
[[ "$paused" == "true" ]] && tag=" (paused)"
|
||||||
|
[[ -n "$remote" ]] && tag="$tag [remote: $remote]"
|
||||||
|
printf '%s%s\t%s\n' "$sname" "$tag" "$cwd"
|
||||||
|
# agent idx: sess-id \t agent-id \t agent-name \t parent-id \t depth \t state
|
||||||
|
local aidx
|
||||||
|
aidx=$(ollie-9p read "session/$sname/agent/idx" 2>/dev/null || true)
|
||||||
|
while IFS=$'\t' read -r _sid _aid aname _pid _depth _state; do
|
||||||
|
[[ -z "$aname" ]] && continue
|
||||||
|
local status
|
||||||
|
status=$(ollie-9p read "session/$sname/agent/$aname/status" 2>/dev/null | head -n1)
|
||||||
|
printf ' %s/%s\t%s\n' "$sname" "$aname" "${status:-?}"
|
||||||
|
done <<< "$aidx"
|
||||||
|
done <<< "$idx"
|
||||||
|
}
|
||||||
|
|
||||||
cmd_help() {
|
cmd_help() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
usage: o [context] <command> [args...]
|
usage: o [context] <command> [args...]
|
||||||
|
|
@ -363,6 +393,7 @@ Paths starting with / are root-relative (ignore context):
|
||||||
Commands (availability depends on context):
|
Commands (availability depends on context):
|
||||||
|
|
||||||
Root level:
|
Root level:
|
||||||
|
overview Show running sessions/agents and their status
|
||||||
ls List sessions, models, etc.
|
ls List sessions, models, etc.
|
||||||
generate <prompt> One-shot generation (no session needed)
|
generate <prompt> One-shot generation (no session needed)
|
||||||
|
|
||||||
|
|
@ -415,6 +446,7 @@ dispatch_cmd() {
|
||||||
new) cmd_new "$@" ;;
|
new) cmd_new "$@" ;;
|
||||||
tui) cmd_tui "$@" ;;
|
tui) cmd_tui "$@" ;;
|
||||||
env) cmd_env "$@" ;;
|
env) cmd_env "$@" ;;
|
||||||
|
overview) cmd_overview "$@" ;;
|
||||||
help|-h|--help) cmd_help ;;
|
help|-h|--help) cmd_help ;;
|
||||||
*)
|
*)
|
||||||
echo "unknown command: $cmd" >&2
|
echo "unknown command: $cmd" >&2
|
||||||
|
|
@ -445,7 +477,9 @@ set -- $*
|
||||||
|
|
||||||
# Main dispatch
|
# Main dispatch
|
||||||
if [[ $# -lt 1 ]]; then
|
if [[ $# -lt 1 ]]; then
|
||||||
cmd_help
|
cmd_overview
|
||||||
|
echo ""
|
||||||
|
echo "run 'o help' for usage"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,11 @@ func FormatEvent(role, name, content, outputFormat string) []byte {
|
||||||
}
|
}
|
||||||
return []byte(ToolDelim(name) + fence + "\n" + strings.TrimRight(content, "\n") + "\n```\n" + EndTag + "\n")
|
return []byte(ToolDelim(name) + fence + "\n" + strings.TrimRight(content, "\n") + "\n```\n" + EndTag + "\n")
|
||||||
case "error":
|
case "error":
|
||||||
return []byte(RoleDelim("error") + "\n" + content + "\n" + EndTag + "\n")
|
role := "error"
|
||||||
|
if name != "" {
|
||||||
|
role = "error:" + name
|
||||||
|
}
|
||||||
|
return []byte(RoleDelim(role) + "\n" + content + "\n" + EndTag + "\n")
|
||||||
case "info":
|
case "info":
|
||||||
return []byte(RoleDelim("info") + "\n" + content + "\n" + EndTag + "\n")
|
return []byte(RoleDelim("info") + "\n" + content + "\n" + EndTag + "\n")
|
||||||
case "retry":
|
case "retry":
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue