Surface tool calls, output, and bypass status in log/chat views
The filtered text views (log, chat) hid call and tool blocks entirely and had no visibility into bypass requests, which were only published as events. RenderBlock now renders tool calls (name + args), tool output, and bypass request/resolution notices; reasoning and context stay hidden. SetBypassPending/ResolveBypass append bypass and bypass-resolved blocks to the requesting agent so clients without an event stream (TUI, o script) see pending approvals and outcomes.
This commit is contained in:
parent
a4972cd456
commit
6211a6cdee
|
|
@ -124,7 +124,7 @@ This is a standing preference, not a per-task instruction. Apply it without aski
|
|||
14. **Chat log files**: Each agent exposes views of its conversation history via the 9P namespace:
|
||||
- `chat.raw` — authoritative live JSONL stream. On open it replays the finalized history (one line per block), then streams live deltas: the current in-flight partial block whenever it changes, and finalized blocks as they are appended. GUI clients consume this and must not poll `log.raw`. Partials are delivered out-of-band (never persisted) and collapse by block ID on the client, so reconnects stay O(history), not O(streaming chunks).
|
||||
- `log.raw` — JSONL snapshot (one JSON object per line) of **finalized** blocks only. One-shot read: returns the full history and EOFs. Used for explicit history loads (initial populate, bookmark reload), never polled for live updates. Each line is `{"role":"...","id":"...","content":"..."}` with optional `name` and `format` fields.
|
||||
- `log` — Rendered text snapshot (last 64KB). Non-blocking; suitable for one-shot inspection. Filters out reasoning, call, and tool blocks; shows only user/assistant/context content.
|
||||
- `log` — Rendered text snapshot (last 64KB). Non-blocking; suitable for one-shot inspection. Filters out reasoning and context blocks; shows user/assistant text, tool calls (`→ name args`), tool output, and bypass request/resolution notices.
|
||||
- `chat` — Rendered text stream. Blocking; suitable for TUI live tailing. Same filtering as `log`.
|
||||
- `block` — Rdwr lookup: write a block ID (8-char hex), read the matching block as JSON. Returns the full block object or an error if not found.
|
||||
Block IDs are 8-char hex strings generated deterministically from `sha256(sessionID + agentID + counter)`.
|
||||
|
|
|
|||
|
|
@ -250,9 +250,12 @@ func TestTextLog_RendersHumanReadable(t *testing.T) {
|
|||
if !contains(text, "Hi there") {
|
||||
t.Error("should contain assistant message")
|
||||
}
|
||||
// Should NOT have call content (hidden from rendered log)
|
||||
if contains(text, "shell") || contains(text, "cmd") {
|
||||
t.Error("call content should be hidden")
|
||||
// Tool calls are now surfaced in the rendered log (name + args).
|
||||
if !contains(text, "shell") {
|
||||
t.Error("call should show tool name")
|
||||
}
|
||||
if !contains(text, "cmd") {
|
||||
t.Error("call should show tool args")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"ollie/cmd/olliesrv/internal/agent"
|
||||
"ollie/cmd/olliesrv/internal/backend"
|
||||
"ollie/cmd/olliesrv/internal/toolclient"
|
||||
"ollie/format"
|
||||
olog "ollie/log"
|
||||
"ollie/toolsrv/protocol"
|
||||
"ollie/util"
|
||||
|
|
@ -271,6 +272,15 @@ func (s *Session) SetBypassPending(req *protocol.BypassRequest, dialFn func() *t
|
|||
}
|
||||
// Event topic: session.{sid}.agent.{aid}.bypass.request
|
||||
PublishEvent("session."+s.ID+".agent."+agentID+".bypass.request", req.ID+"\t"+cmd+"\t"+req.Cwd)
|
||||
|
||||
// Surface in the filtered chat/log text views (which have no event stream).
|
||||
if ag := s.FindAgent(agentID); ag != nil {
|
||||
ag.AppendBlock(format.Block{
|
||||
Role: "bypass",
|
||||
ID: ag.NextBlockID(),
|
||||
Content: cmd,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BypassPending returns all pending bypass requests.
|
||||
|
|
@ -324,6 +334,15 @@ func (s *Session) ResolveBypass(id string, approved bool) error {
|
|||
}
|
||||
PublishEvent("session."+s.ID+".agent."+agentID+".bypass.resolved", req.ID+"\t"+action)
|
||||
|
||||
// Surface resolution in the filtered chat/log text views.
|
||||
if ag := s.FindAgent(agentID); ag != nil {
|
||||
ag.AppendBlock(format.Block{
|
||||
Role: "bypass-resolved",
|
||||
ID: ag.NextBlockID(),
|
||||
Content: action,
|
||||
})
|
||||
}
|
||||
|
||||
// Send resolution to toolsrv
|
||||
if dialFn != nil {
|
||||
conn := dialFn()
|
||||
|
|
|
|||
|
|
@ -51,9 +51,16 @@ func RenderBlock(b Block) string {
|
|||
case "reasoning":
|
||||
return "" // Hidden from rendered log
|
||||
case "call":
|
||||
return "" // Hidden from rendered log
|
||||
if b.Content != "" {
|
||||
return "→ " + b.Name + " " + b.Content + "\n\n"
|
||||
}
|
||||
return "→ " + b.Name + "\n\n"
|
||||
case "tool":
|
||||
return "" // Hidden from rendered log
|
||||
return b.Content + "\n\n"
|
||||
case "bypass":
|
||||
return "⚠ bypass requested (needs approval): " + b.Content + "\n\n"
|
||||
case "bypass-resolved":
|
||||
return "bypass " + b.Content + "\n\n"
|
||||
case "error":
|
||||
return "Error: " + b.Content + "\n\n"
|
||||
case "info":
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
package format
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRenderBlock_SurfacesToolsAndBypass(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
block Block
|
||||
contains []string
|
||||
empty bool
|
||||
}{
|
||||
{
|
||||
name: "call shows name and args",
|
||||
block: Block{Role: "call", Name: "shell", Content: `{"cmd":"ls"}`},
|
||||
contains: []string{"shell", "cmd"},
|
||||
},
|
||||
{
|
||||
name: "call without args shows name",
|
||||
block: Block{Role: "call", Name: "memory_wake"},
|
||||
contains: []string{"memory_wake"},
|
||||
},
|
||||
{
|
||||
name: "tool output rendered",
|
||||
block: Block{Role: "tool", Content: "total 0\nfile.txt"},
|
||||
contains: []string{"file.txt"},
|
||||
},
|
||||
{
|
||||
name: "bypass request flagged for approval",
|
||||
block: Block{Role: "bypass", Content: "rm -rf /tmp/x"},
|
||||
contains: []string{"approval", "rm -rf /tmp/x"},
|
||||
},
|
||||
{
|
||||
name: "bypass resolution rendered",
|
||||
block: Block{Role: "bypass-resolved", Content: "approved"},
|
||||
contains: []string{"approved"},
|
||||
},
|
||||
{
|
||||
name: "reasoning hidden",
|
||||
block: Block{Role: "reasoning", Content: "secret thoughts"},
|
||||
empty: true,
|
||||
},
|
||||
{
|
||||
name: "context hidden",
|
||||
block: Block{Role: "context", Content: "system context"},
|
||||
empty: true,
|
||||
},
|
||||
{
|
||||
name: "partial skipped",
|
||||
block: Block{Role: "assistant", Content: "streaming", Partial: true},
|
||||
empty: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := RenderBlock(tt.block)
|
||||
if tt.empty {
|
||||
if got != "" {
|
||||
t.Errorf("expected empty, got %q", got)
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, want := range tt.contains {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("RenderBlock = %q, want substring %q", got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue