146 lines
4 KiB
Go
146 lines
4 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"nub/internal/llm"
|
|
"nub/internal/permission"
|
|
"nub/internal/tool"
|
|
)
|
|
|
|
type toolResult struct {
|
|
id string
|
|
result tool.Result
|
|
}
|
|
|
|
// runTools startet alle Tool-Calls eines Turns parallel, an einen von ctx
|
|
// abgeleiteten Kontext gebunden (Ablauf pro Turn, Schritt 3). Jeder Call
|
|
// bekommt garantiert ein Ergebnis — auch bei Timeout oder Abbruch — weil das
|
|
// Protokoll für jeden tool_use zwingend einen tool_result verlangt.
|
|
//
|
|
// Permission-Prüfung (E-11) läuft davor, sequentiell für den ganzen Batch:
|
|
// so muss die UI nie mehrere gleichzeitige Rückfragen anzeigen. Erst danach
|
|
// starten die tatsächlich erlaubten Calls parallel wie bisher.
|
|
func (l *Loop) runTools(ctx context.Context, calls []llm.Block, out chan<- tool.UIEvent) []toolResult {
|
|
results := make([]toolResult, len(calls))
|
|
runnable := make([]int, 0, len(calls))
|
|
|
|
for i, call := range calls {
|
|
if res, blocked := l.checkPermission(ctx, call); blocked {
|
|
results[i] = res
|
|
out <- tool.ToolCallOutput{ID: call.ID, Name: call.Name, Result: res.result}
|
|
continue
|
|
}
|
|
runnable = append(runnable, i)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
for _, i := range runnable {
|
|
call := calls[i]
|
|
wg.Add(1)
|
|
go func(i int, call llm.Block) {
|
|
defer wg.Done()
|
|
|
|
toolCtx, cancel := context.WithTimeout(ctx, l.maxToolTime())
|
|
defer cancel()
|
|
|
|
env := l.Env
|
|
env.Emit = func(ev tool.UIEvent) {
|
|
if st, ok := ev.(tool.ToolStream); ok {
|
|
st.ID = call.ID
|
|
out <- st
|
|
return
|
|
}
|
|
out <- ev
|
|
}
|
|
env.Ask = l.AskUser
|
|
|
|
res, err := l.Tools.Run(toolCtx, call.Name, call.Input, env)
|
|
if err != nil {
|
|
res = tool.Result{ForModel: err.Error(), IsError: true}
|
|
}
|
|
if toolCtx.Err() == context.DeadlineExceeded && !res.IsError {
|
|
res = tool.Result{ForModel: fmt.Sprintf("tool %s timed out after %s", call.Name, l.maxToolTime()), IsError: true}
|
|
}
|
|
|
|
results[i] = toolResult{id: call.ID, result: res}
|
|
out <- tool.ToolCallOutput{ID: call.ID, Name: call.Name, Result: res}
|
|
}(i, call)
|
|
}
|
|
|
|
wg.Wait()
|
|
return results
|
|
}
|
|
|
|
// checkPermission entscheidet, ob ein Tool-Call überhaupt starten darf.
|
|
// blocked=true bedeutet: res ist bereits das finale (Fehler-)Ergebnis, der
|
|
// Call wird nicht ausgeführt.
|
|
func (l *Loop) checkPermission(ctx context.Context, call llm.Block) (res toolResult, blocked bool) {
|
|
switch l.Permissions.Check(call.Name, call.Input) {
|
|
case permission.ModeDeny:
|
|
return toolResult{id: call.ID, result: tool.Result{
|
|
ForModel: fmt.Sprintf("permission denied: %s is not allowed by policy", call.Name),
|
|
IsError: true,
|
|
}}, true
|
|
|
|
case permission.ModeAsk:
|
|
if l.RequestPermission == nil {
|
|
return toolResult{id: call.ID, result: tool.Result{
|
|
ForModel: fmt.Sprintf("tool %s requires confirmation ('ask'), which this mode does not support", call.Name),
|
|
IsError: true,
|
|
}}, true
|
|
}
|
|
if !l.RequestPermission(ctx, call.Name, call.Input) {
|
|
return toolResult{id: call.ID, result: tool.Result{
|
|
ForModel: fmt.Sprintf("permission denied by user for %s", call.Name),
|
|
IsError: true,
|
|
}}, true
|
|
}
|
|
}
|
|
return toolResult{}, false
|
|
}
|
|
|
|
func resultsMessage(results []toolResult) llm.Message {
|
|
msg := llm.Message{Role: llm.RoleUser}
|
|
for _, r := range results {
|
|
msg.Content = append(msg.Content, llm.Block{
|
|
Kind: llm.KindToolResult,
|
|
ToolUseID: r.id,
|
|
Result: []llm.Block{{Kind: llm.KindText, Text: r.result.ForModel}},
|
|
IsError: r.result.IsError,
|
|
})
|
|
}
|
|
return msg
|
|
}
|
|
|
|
func cancelledResults(calls []llm.Block, reason string) []toolResult {
|
|
out := make([]toolResult, len(calls))
|
|
for i, c := range calls {
|
|
out[i] = toolResult{id: c.ID, result: tool.Result{ForModel: reason, IsError: true}}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func extractToolUse(msg llm.Message) []llm.Block {
|
|
var out []llm.Block
|
|
for _, b := range msg.Content {
|
|
if b.Kind == llm.KindToolUse {
|
|
out = append(out, b)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func turnSignature(calls []llm.Block) string {
|
|
var b []byte
|
|
for _, c := range calls {
|
|
b = append(b, c.Name...)
|
|
b = append(b, 0)
|
|
b = append(b, c.Input...)
|
|
b = append(b, 0x1f)
|
|
}
|
|
return string(b)
|
|
}
|