Improve the transcript: tool grouping, report auto-ack, context meter, spinner, Enter to send
This commit is contained in:
@@ -170,12 +170,14 @@ func (m *Manager) validateAsk(ctx context.Context, l *live, args askArgs) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) fileReport(l *live, r report.Report) {
|
||||
func (m *Manager) fileReport(l *live, r report.Report) string {
|
||||
id := store.NewID()
|
||||
err := m.store.CreateInboxItem(context.Background(), m.newItem(l, id, id, api.Report, api.Fyi, false, store.ToPayload(r)))
|
||||
if err != nil {
|
||||
slog.Error("file report", "session", l.id, "err", err)
|
||||
return ""
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func (m *Manager) fileError(l *live, msg string) {
|
||||
|
||||
@@ -59,6 +59,10 @@ type live struct {
|
||||
stopping bool
|
||||
interrupted bool
|
||||
rateLimited map[string]any
|
||||
|
||||
contextModel string
|
||||
contextUsed int
|
||||
contextWindow int
|
||||
}
|
||||
|
||||
func New(st *store.Store, limits Limits) *Manager {
|
||||
@@ -455,11 +459,12 @@ func (m *Manager) ensureLive(ctx context.Context, sess api.AgentSession, task ap
|
||||
|
||||
l = &live{id: sess.Id, vendor: sess.Vendor, taskID: task.Id, projectID: task.ProjectId}
|
||||
l.gate = &report.Gate{OnReport: func(r report.Report) {
|
||||
err := m.emit(context.Background(), l, agentActor, "agent.report", store.ToPayload(r))
|
||||
payload := store.ToPayload(r)
|
||||
payload["inbox_item_id"] = m.fileReport(l, r)
|
||||
err := m.emit(context.Background(), l, agentActor, "agent.report", payload)
|
||||
if err != nil {
|
||||
slog.Error("record report", "session", l.id, "err", err)
|
||||
}
|
||||
m.fileReport(l, r)
|
||||
}}
|
||||
|
||||
opts := claude.Options{
|
||||
@@ -549,7 +554,15 @@ func (m *Manager) handle(ctx context.Context, l *live, ev claude.Event) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
usage, err := ev.ContextUsage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if usage.Tokens == 0 {
|
||||
return nil
|
||||
}
|
||||
l.contextModel = usage.Model
|
||||
return m.updateContext(ctx, l, usage.Tokens, l.contextWindow)
|
||||
|
||||
case "user":
|
||||
blocks, err := ev.Blocks()
|
||||
@@ -585,6 +598,14 @@ func (m *Manager) handle(ctx context.Context, l *live, ev claude.Event) error {
|
||||
return err
|
||||
}
|
||||
|
||||
window := res.ModelUsage[l.contextModel].ContextWindow
|
||||
if window > 0 {
|
||||
err = m.updateContext(ctx, l, l.contextUsed, window)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
interrupted := l.interrupted
|
||||
rateLimited := l.rateLimited
|
||||
@@ -644,6 +665,15 @@ func (m *Manager) handle(ctx context.Context, l *live, ev claude.Event) error {
|
||||
return m.emit(ctx, l, agentActor, "agent.vendor", raw)
|
||||
}
|
||||
|
||||
func (m *Manager) updateContext(ctx context.Context, l *live, used, window int) error {
|
||||
if used == l.contextUsed && window == l.contextWindow {
|
||||
return nil
|
||||
}
|
||||
l.contextUsed = used
|
||||
l.contextWindow = window
|
||||
return m.emit(ctx, l, agentActor, "session.context", map[string]any{"model": l.contextModel, "used": used, "window": window})
|
||||
}
|
||||
|
||||
func (m *Manager) handleAssistantBlock(ctx context.Context, l *live, b claude.Block) error {
|
||||
switch b.Type {
|
||||
case "text":
|
||||
|
||||
@@ -156,12 +156,22 @@ func TestTurnAndFollowUp(t *testing.T) {
|
||||
}
|
||||
|
||||
events := e.waitIdle()
|
||||
for _, kind := range []string{"session.created", "user.message", "agent.tool_call", "agent.tool_result", "agent.report", "turn.ended"} {
|
||||
for _, kind := range []string{"session.created", "user.message", "agent.tool_call", "agent.tool_result", "agent.report", "turn.ended", "session.context"} {
|
||||
if !has(events, kind) {
|
||||
t.Fatalf("missing %s in %v", kind, kinds(events))
|
||||
}
|
||||
}
|
||||
|
||||
window := 0
|
||||
for _, ev := range events {
|
||||
if ev.Kind == "session.context" {
|
||||
window = ev.Payload["window"].(int)
|
||||
}
|
||||
}
|
||||
if window <= 0 {
|
||||
t.Fatalf("no context window reported after the turn ended")
|
||||
}
|
||||
|
||||
log := gitOut(t, e.task.WorktreePath, "log", "-1", "--format=%an <%ae>")
|
||||
if log != gitName+" <"+gitEmail+">" {
|
||||
t.Fatalf("agent committed as %q", log)
|
||||
|
||||
@@ -58,12 +58,45 @@ type Block struct {
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Subtype string `json:"subtype"`
|
||||
IsError bool `json:"is_error"`
|
||||
Result string `json:"result"`
|
||||
DurationMS int `json:"duration_ms"`
|
||||
NumTurns int `json:"num_turns"`
|
||||
TotalCostUSD float64 `json:"total_cost_usd"`
|
||||
Subtype string `json:"subtype"`
|
||||
IsError bool `json:"is_error"`
|
||||
Result string `json:"result"`
|
||||
DurationMS int `json:"duration_ms"`
|
||||
NumTurns int `json:"num_turns"`
|
||||
TotalCostUSD float64 `json:"total_cost_usd"`
|
||||
ModelUsage map[string]ModelUsage `json:"modelUsage"`
|
||||
}
|
||||
|
||||
type ModelUsage struct {
|
||||
ContextWindow int `json:"contextWindow"`
|
||||
}
|
||||
|
||||
type ContextUsage struct {
|
||||
Model string
|
||||
Tokens int
|
||||
}
|
||||
|
||||
func (e Event) ContextUsage() (ContextUsage, error) {
|
||||
msg := struct {
|
||||
Message struct {
|
||||
Model string `json:"model"`
|
||||
Usage struct {
|
||||
InputTokens int `json:"input_tokens"`
|
||||
CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
|
||||
CacheReadInputTokens int `json:"cache_read_input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
} `json:"usage"`
|
||||
} `json:"message"`
|
||||
}{}
|
||||
err := json.Unmarshal(e.Raw, &msg)
|
||||
if err != nil {
|
||||
return ContextUsage{}, err
|
||||
}
|
||||
u := msg.Message.Usage
|
||||
return ContextUsage{
|
||||
Model: msg.Message.Model,
|
||||
Tokens: u.InputTokens + u.CacheCreationInputTokens + u.CacheReadInputTokens + u.OutputTokens,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e Event) Blocks() ([]Block, error) {
|
||||
|
||||
Reference in New Issue
Block a user