Add permission ask loop, lifecycle hook emissions, and Policy type (N1 WP2/WP3) #17
@@ -13,10 +13,12 @@ const (
|
||||
PermissionRequest Event = "PermissionRequest"
|
||||
PostToolUse Event = "PostToolUse"
|
||||
PostToolUseFailure Event = "PostToolUseFailure"
|
||||
PostToolBatch Event = "PostToolBatch"
|
||||
Stop Event = "Stop"
|
||||
SubagentStart Event = "SubagentStart"
|
||||
SubagentStop Event = "SubagentStop"
|
||||
PreCompact Event = "PreCompact"
|
||||
SessionEnd Event = "SessionEnd"
|
||||
)
|
||||
|
||||
type Decision string
|
||||
|
||||
+38
-7
@@ -110,6 +110,17 @@ type Config struct {
|
||||
KeepToolResults int
|
||||
// Hooks 是进程内钩子。空总线不改变停止、提醒和拒绝次数。
|
||||
Hooks *hooks.Bus
|
||||
// PermissionAsk 在钩子对一次工具调用表态 ask 时向宿主请求裁决。
|
||||
// nil 时保持旧行为:ask 与 deny 一样按拒绝处理。
|
||||
// 返回 Allow 时工具照常执行;宿主需要「本会话始终允许」时自行记录,
|
||||
// 内核不替宿主保存放行状态。
|
||||
PermissionAsk func(ctx context.Context, call llm.ToolCall) AskVerdict
|
||||
}
|
||||
|
||||
// AskVerdict 是宿主对一次 ask 决策的裁决。
|
||||
type AskVerdict struct {
|
||||
Allow bool
|
||||
Reason string // 拒绝时写进配对 tool 结果的理由;放行时忽略
|
||||
}
|
||||
|
||||
// StopInfo 是 Stop 钩子看到的本 run 进度。
|
||||
@@ -192,6 +203,8 @@ func Run(ctx context.Context, cfg Config, user llm.Message) (Result, error) {
|
||||
}
|
||||
messages = append(messages, user)
|
||||
emit(cfg, Event{Type: "agent_start"})
|
||||
cfg.Hooks.Emit(hooks.Call{Event: hooks.SessionStart})
|
||||
cfg.Hooks.Emit(hooks.Call{Event: hooks.UserPromptSubmit, LastText: user.Content})
|
||||
|
||||
var last llm.Message
|
||||
var usage llm.Usage
|
||||
@@ -200,6 +213,9 @@ func Run(ctx context.Context, cfg Config, user llm.Message) (Result, error) {
|
||||
lengthContinues := 0
|
||||
lengthNudge := false
|
||||
var repeats repeatState
|
||||
defer func() {
|
||||
cfg.Hooks.Emit(hooks.Call{Event: hooks.SessionEnd, LastText: last.Content, ToolCalls: toolCalls})
|
||||
}()
|
||||
for turn := 0; turn < cfg.MaxTurns; turn++ {
|
||||
if ctx.Err() != nil {
|
||||
cfg.logger().Error("runtime turn aborted", ctx.Err())
|
||||
@@ -266,6 +282,7 @@ func Run(ctx context.Context, cfg Config, user llm.Message) (Result, error) {
|
||||
messages = append(messages, m)
|
||||
}
|
||||
}
|
||||
cfg.Hooks.Emit(hooks.Call{Event: hooks.PostToolBatch, Turn: turn, ToolCalls: len(last.ToolCalls)})
|
||||
emit(cfg, Event{Type: "turn_end", Message: last})
|
||||
continue
|
||||
}
|
||||
@@ -525,19 +542,26 @@ func stopDecision(cfg Config, last llm.Message, continuations, toolCalls int) (S
|
||||
return StopDecision{}, false
|
||||
}
|
||||
|
||||
func gateTool(cfg Config, call llm.ToolCall) (llm.ToolCall, llm.Message, bool) {
|
||||
func gateTool(ctx context.Context, cfg Config, call llm.ToolCall) (llm.ToolCall, llm.Message, bool) {
|
||||
if cfg.Hooks == nil {
|
||||
return call, llm.Message{}, false
|
||||
}
|
||||
out := cfg.Hooks.Emit(hooks.Call{Event: hooks.PreToolUse, Tool: call.Name, Args: call.Arguments})
|
||||
switch out.Decision {
|
||||
case hooks.Deny, hooks.Block, hooks.Ask:
|
||||
case hooks.Ask:
|
||||
// ask 先问宿主;没有裁决端口(或宿主拒绝)时保持旧行为:按拒绝处理。
|
||||
cfg.Hooks.Emit(hooks.Call{Event: hooks.PermissionRequest, Tool: call.Name, Args: call.Arguments})
|
||||
why := out.Reason
|
||||
if why == "" {
|
||||
why = "工具调用被拒绝"
|
||||
if cfg.PermissionAsk != nil {
|
||||
if verdict := cfg.PermissionAsk(ctx, call); verdict.Allow {
|
||||
return call, llm.Message{}, false
|
||||
} else if verdict.Reason != "" {
|
||||
why = verdict.Reason
|
||||
}
|
||||
}
|
||||
msg := llm.Message{Role: llm.RoleTool, Name: call.Name, ToolCallID: call.ID, Content: why, IsError: true}
|
||||
return call, msg, true
|
||||
return call, rejectToolMessage(call, why), true
|
||||
case hooks.Deny, hooks.Block:
|
||||
return call, rejectToolMessage(call, out.Reason), true
|
||||
case hooks.Modify:
|
||||
if out.Args != "" {
|
||||
call.Arguments = out.Args
|
||||
@@ -546,6 +570,13 @@ func gateTool(cfg Config, call llm.ToolCall) (llm.ToolCall, llm.Message, bool) {
|
||||
return call, llm.Message{}, false
|
||||
}
|
||||
|
||||
func rejectToolMessage(call llm.ToolCall, why string) llm.Message {
|
||||
if why == "" {
|
||||
why = "工具调用被拒绝"
|
||||
}
|
||||
return llm.Message{Role: llm.RoleTool, Name: call.Name, ToolCallID: call.ID, Content: why, IsError: true}
|
||||
}
|
||||
|
||||
func noteToolHook(cfg Config, call llm.ToolCall, msg llm.Message) {
|
||||
if cfg.Hooks == nil {
|
||||
return
|
||||
@@ -571,7 +602,7 @@ func runOne(ctx context.Context, cfg Config, call llm.ToolCall) llm.Message {
|
||||
return msg
|
||||
}
|
||||
emit(cfg, Event{Type: "tool_execution_start", Tool: call.Name, Detail: call.Arguments})
|
||||
if gated, msg, blocked := gateTool(cfg, call); blocked {
|
||||
if gated, msg, blocked := gateTool(ctx, cfg, call); blocked {
|
||||
emit(cfg, Event{Type: "tool_execution_end", Tool: call.Name, Text: msg.Content})
|
||||
noteToolHook(cfg, call, msg)
|
||||
if cfg.AfterTool != nil {
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"agent-native/runtime/hooks"
|
||||
"agent-native/runtime/llm"
|
||||
"agent-native/runtime/tools"
|
||||
)
|
||||
|
||||
type eventRecorder struct {
|
||||
seen []hooks.Event
|
||||
}
|
||||
|
||||
func (r *eventRecorder) handler(c hooks.Call) hooks.Outcome {
|
||||
r.seen = append(r.seen, c.Event)
|
||||
return hooks.Outcome{}
|
||||
}
|
||||
|
||||
func (r *eventRecorder) has(ev hooks.Event) bool {
|
||||
for _, e := range r.seen {
|
||||
if e == ev {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TestHookLifecycleOrder 断言一次带工具调用的 run 的事件顺序为
|
||||
// SessionStart → UserPromptSubmit → PreToolUse → PostToolUse →
|
||||
// PostToolBatch → Stop → SessionEnd。
|
||||
func TestHookLifecycleOrder(t *testing.T) {
|
||||
rec := &eventRecorder{}
|
||||
bus := hooks.New()
|
||||
bus.On(rec.handler)
|
||||
hits := 0
|
||||
reg := tools.NewRegistry(t.TempDir())
|
||||
reg.Register(hitTool{hits: &hits})
|
||||
_, err := Run(context.Background(), Config{
|
||||
Model: "fake",
|
||||
Tools: reg,
|
||||
Hooks: bus,
|
||||
Stream: script(
|
||||
llm.Message{Role: llm.RoleAssistant, StopReason: llm.StopToolCalls, ToolCalls: []llm.ToolCall{{
|
||||
ID: "c1", Name: "echo", Arguments: `{}`,
|
||||
}}},
|
||||
llm.Message{Role: llm.RoleAssistant, Content: "done", StopReason: llm.StopEnd},
|
||||
),
|
||||
}, llm.Message{Role: llm.RoleUser, Content: "go"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []hooks.Event{hooks.SessionStart, hooks.UserPromptSubmit, hooks.PreToolUse,
|
||||
hooks.PostToolUse, hooks.PostToolBatch, hooks.Stop, hooks.SessionEnd}
|
||||
if len(rec.seen) != len(want) {
|
||||
t.Fatalf("events = %v, want %v", rec.seen, want)
|
||||
}
|
||||
for i := range want {
|
||||
if rec.seen[i] != want[i] {
|
||||
t.Fatalf("events = %v, want %v", rec.seen, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHookUserPromptSubmitCarriesText 断言 UserPromptSubmit 事件带用户输入正文。
|
||||
func TestHookUserPromptSubmitCarriesText(t *testing.T) {
|
||||
var got string
|
||||
bus := hooks.New()
|
||||
bus.On(func(c hooks.Call) hooks.Outcome {
|
||||
if c.Event == hooks.UserPromptSubmit {
|
||||
got = c.LastText
|
||||
}
|
||||
return hooks.Outcome{}
|
||||
})
|
||||
_, err := Run(context.Background(), Config{
|
||||
Model: "fake",
|
||||
Hooks: bus,
|
||||
Stream: script(llm.Message{Role: llm.RoleAssistant, Content: "ok", StopReason: llm.StopEnd}),
|
||||
}, llm.Message{Role: llm.RoleUser, Content: "ping"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "ping" {
|
||||
t.Fatalf("UserPromptSubmit text = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAskPortAllowProceeds:ask 决策经宿主裁决放行后工具照常执行。
|
||||
func TestAskPortAllowProceeds(t *testing.T) {
|
||||
hits := 0
|
||||
reg := tools.NewRegistry(t.TempDir())
|
||||
reg.Register(hitTool{hits: &hits})
|
||||
bus := hooks.New()
|
||||
bus.On(func(c hooks.Call) hooks.Outcome {
|
||||
if c.Event == hooks.PreToolUse {
|
||||
return hooks.Outcome{Decision: hooks.Ask, Reason: "需要确认"}
|
||||
}
|
||||
return hooks.Outcome{}
|
||||
})
|
||||
sawRequest := false
|
||||
bus.On(func(c hooks.Call) hooks.Outcome {
|
||||
if c.Event == hooks.PermissionRequest {
|
||||
sawRequest = true
|
||||
}
|
||||
return hooks.Outcome{}
|
||||
})
|
||||
res, err := Run(context.Background(), Config{
|
||||
Model: "fake",
|
||||
Tools: reg,
|
||||
Hooks: bus,
|
||||
PermissionAsk: func(ctx context.Context, call llm.ToolCall) AskVerdict {
|
||||
return AskVerdict{Allow: true}
|
||||
},
|
||||
Stream: script(
|
||||
llm.Message{Role: llm.RoleAssistant, StopReason: llm.StopToolCalls, ToolCalls: []llm.ToolCall{{
|
||||
ID: "c1", Name: "echo", Arguments: `{}`,
|
||||
}}},
|
||||
llm.Message{Role: llm.RoleAssistant, Content: "done", StopReason: llm.StopEnd},
|
||||
),
|
||||
}, llm.Message{Role: llm.RoleUser, Content: "go"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hits != 1 {
|
||||
t.Fatalf("tool executed %d times, want 1", hits)
|
||||
}
|
||||
if !sawRequest {
|
||||
t.Fatal("PermissionRequest event missing")
|
||||
}
|
||||
if !strings.Contains(transcriptText(res.Messages), "ran") {
|
||||
t.Fatalf("tool result missing: %s", transcriptText(res.Messages))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAskPortDenyPairsResult:宿主拒绝时写配对错误结果,理由取宿主理由。
|
||||
func TestAskPortDenyPairsResult(t *testing.T) {
|
||||
hits := 0
|
||||
reg := tools.NewRegistry(t.TempDir())
|
||||
reg.Register(hitTool{hits: &hits})
|
||||
bus := hooks.New()
|
||||
bus.On(func(c hooks.Call) hooks.Outcome {
|
||||
if c.Event == hooks.PreToolUse {
|
||||
return hooks.Outcome{Decision: hooks.Ask, Reason: "需要确认"}
|
||||
}
|
||||
return hooks.Outcome{}
|
||||
})
|
||||
res, err := Run(context.Background(), Config{
|
||||
Model: "fake",
|
||||
Tools: reg,
|
||||
Hooks: bus,
|
||||
PermissionAsk: func(ctx context.Context, call llm.ToolCall) AskVerdict {
|
||||
return AskVerdict{Allow: false, Reason: "用户拒绝"}
|
||||
},
|
||||
Stream: script(
|
||||
llm.Message{Role: llm.RoleAssistant, StopReason: llm.StopToolCalls, ToolCalls: []llm.ToolCall{{
|
||||
ID: "c1", Name: "echo", Arguments: `{}`,
|
||||
}}},
|
||||
llm.Message{Role: llm.RoleAssistant, Content: "done", StopReason: llm.StopEnd},
|
||||
),
|
||||
}, llm.Message{Role: llm.RoleUser, Content: "go"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hits != 0 {
|
||||
t.Fatalf("tool executed %d times, want 0", hits)
|
||||
}
|
||||
paired := false
|
||||
for _, m := range res.Messages {
|
||||
if m.Role == llm.RoleTool && m.ToolCallID == "c1" && m.IsError && m.Content == "用户拒绝" {
|
||||
paired = true
|
||||
}
|
||||
}
|
||||
if !paired {
|
||||
t.Fatalf("missing paired deny result: %s", transcriptText(res.Messages))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAskWithoutPortKeepsLegacyReject:没有裁决端口时 ask 仍按拒绝处理(旧行为)。
|
||||
func TestAskWithoutPortKeepsLegacyReject(t *testing.T) {
|
||||
hits := 0
|
||||
reg := tools.NewRegistry(t.TempDir())
|
||||
reg.Register(hitTool{hits: &hits})
|
||||
bus := hooks.New()
|
||||
bus.On(func(c hooks.Call) hooks.Outcome {
|
||||
if c.Event == hooks.PreToolUse {
|
||||
return hooks.Outcome{Decision: hooks.Ask, Reason: "需要确认"}
|
||||
}
|
||||
return hooks.Outcome{}
|
||||
})
|
||||
res, err := Run(context.Background(), Config{
|
||||
Model: "fake",
|
||||
Tools: reg,
|
||||
Hooks: bus,
|
||||
Stream: script(
|
||||
llm.Message{Role: llm.RoleAssistant, StopReason: llm.StopToolCalls, ToolCalls: []llm.ToolCall{{
|
||||
ID: "c1", Name: "echo", Arguments: `{}`,
|
||||
}}},
|
||||
llm.Message{Role: llm.RoleAssistant, Content: "done", StopReason: llm.StopEnd},
|
||||
),
|
||||
}, llm.Message{Role: llm.RoleUser, Content: "go"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hits != 0 {
|
||||
t.Fatalf("tool executed %d times, want 0", hits)
|
||||
}
|
||||
paired := false
|
||||
for _, m := range res.Messages {
|
||||
if m.Role == llm.RoleTool && m.ToolCallID == "c1" && m.IsError && m.Content == "需要确认" {
|
||||
paired = true
|
||||
}
|
||||
}
|
||||
if !paired {
|
||||
t.Fatalf("missing paired ask-reject result: %s", transcriptText(res.Messages))
|
||||
}
|
||||
}
|
||||
|
||||
// TestStopContinuationFlagVisible:续跑后 Stop 事件的 Continuations 计数对下游可见。
|
||||
func TestStopContinuationFlagVisible(t *testing.T) {
|
||||
var counts []int
|
||||
bus := hooks.New()
|
||||
bus.On(func(c hooks.Call) hooks.Outcome {
|
||||
if c.Event == hooks.Stop {
|
||||
counts = append(counts, c.Continuations)
|
||||
return hooks.Outcome{Decision: hooks.Block, Reason: "再试"}
|
||||
}
|
||||
return hooks.Outcome{}
|
||||
})
|
||||
_, err := Run(context.Background(), Config{
|
||||
Model: "fake",
|
||||
MaxTurns: 8,
|
||||
Hooks: bus,
|
||||
Stream: script(
|
||||
llm.Message{Role: llm.RoleAssistant, Content: "a", StopReason: llm.StopEnd},
|
||||
llm.Message{Role: llm.RoleAssistant, Content: "a", StopReason: llm.StopEnd},
|
||||
llm.Message{Role: llm.RoleAssistant, Content: "a", StopReason: llm.StopEnd},
|
||||
llm.Message{Role: llm.RoleAssistant, Content: "a", StopReason: llm.StopEnd},
|
||||
),
|
||||
}, llm.Message{Role: llm.RoleUser, Content: "go"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(counts) != 4 {
|
||||
t.Fatalf("stop events = %v", counts)
|
||||
}
|
||||
for i, c := range counts {
|
||||
if c != i {
|
||||
t.Fatalf("stop continuations = %v, want 0,1,2,3", counts)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
// 默认模式不额外表态,调用方继续用原有画像策略。
|
||||
package policy
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Mode string
|
||||
|
||||
@@ -92,6 +95,57 @@ func Decide(req Request, rules []Rule) Effect {
|
||||
return EffectNone
|
||||
}
|
||||
|
||||
// SessionAllows 记录并查询「本会话始终允许」的决定。键的口径由实现方定义;
|
||||
// 宿主可以用自己的模式匹配(如参数通配)实现同一接口。
|
||||
type SessionAllows interface {
|
||||
Remember(name, args string)
|
||||
Allowed(name, args string) bool
|
||||
}
|
||||
|
||||
// MemoryAllows 是进程内的会话放行存储,按「工具名 + 原样参数串」精确匹配。
|
||||
type MemoryAllows struct {
|
||||
mu sync.Mutex
|
||||
allowed map[string]bool
|
||||
}
|
||||
|
||||
func NewMemoryAllows() *MemoryAllows {
|
||||
return &MemoryAllows{allowed: make(map[string]bool)}
|
||||
}
|
||||
|
||||
func (m *MemoryAllows) Remember(name, args string) {
|
||||
m.mu.Lock()
|
||||
m.allowed[name+"\n"+args] = true
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *MemoryAllows) Allowed(name, args string) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.allowed[name+"\n"+args]
|
||||
}
|
||||
|
||||
// Policy 是一次会话的权限配置:模式、规则与会话放行存在同一个对象上,
|
||||
// Decide/Visible 语义与同名函数完全一致。
|
||||
type Policy struct {
|
||||
Mode Mode
|
||||
Rules []Rule
|
||||
SessionAllows SessionAllows // 可为 nil:等价于 SessionAllow 恒为 false
|
||||
}
|
||||
|
||||
// Decide 按 Policy 的模式、规则与会话放行判定一次工具调用。
|
||||
func (p Policy) Decide(name, args string, traits Traits) Effect {
|
||||
var allowed bool
|
||||
if p.SessionAllows != nil {
|
||||
allowed = p.SessionAllows.Allowed(name, args)
|
||||
}
|
||||
return Decide(Request{Mode: p.Mode, Name: name, Args: args, Traits: traits, SessionAllow: allowed}, p.Rules)
|
||||
}
|
||||
|
||||
// Visible 报告该模式下这个工具能否出现在清单里。
|
||||
func (p Policy) Visible(name string) bool {
|
||||
return Visible(string(p.Mode), name, mergeTraits(name, Traits{}).ReadOnly)
|
||||
}
|
||||
|
||||
// Visible 报告计划模式下这个工具能否出现在清单里。其他模式一律可见。
|
||||
func Visible(mode string, name string, readOnly bool) bool {
|
||||
if Normalize(mode) != ModePlan {
|
||||
|
||||
@@ -39,3 +39,52 @@ func TestAutoEditAllowsWorkspaceWrites(t *testing.T) {
|
||||
t.Fatalf("auto edit %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPolicyDecideMatchesFunctionForm:Policy 结构体形态与函数形态判定一致。
|
||||
func TestPolicyDecideMatchesFunctionForm(t *testing.T) {
|
||||
rules := []Rule{{Effect: EffectAllow, Tool: "write_file"}, {Effect: EffectDeny, Tool: "write_*", ArgGlob: "*secret*"}}
|
||||
cases := []struct {
|
||||
p Policy
|
||||
name string
|
||||
args string
|
||||
want Effect
|
||||
}{
|
||||
{Policy{Mode: ModePlan}, "write_file", `{"path":"a.md"}`, EffectDeny},
|
||||
{Policy{Mode: ModeDefault, Rules: rules}, "write_file", `{"path":"a.md"}`, EffectAllow},
|
||||
{Policy{Mode: ModeDefault, Rules: rules}, "write_file", `{"path":"secret.txt"}`, EffectDeny},
|
||||
{Policy{Mode: ModeDefault}, "run_shell", `{"command":"rm -rf /"}`, EffectAsk},
|
||||
}
|
||||
for i, c := range cases {
|
||||
if got := c.p.Decide(c.name, c.args, Traits{}); got != c.want {
|
||||
t.Fatalf("case %d: effect %q, want %q", i, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPolicySessionAllowsStore:会话放行经存储短路 ask,且跨调用存活。
|
||||
func TestPolicySessionAllowsStore(t *testing.T) {
|
||||
store := NewMemoryAllows()
|
||||
p := Policy{Mode: ModeDefault, Rules: []Rule{{Effect: EffectAsk, Tool: "run_shell"}}, SessionAllows: store}
|
||||
if got := p.Decide("run_shell", `{"command":"ls"}`, Traits{}); got != EffectAsk {
|
||||
t.Fatalf("before remember: %q, want ask", got)
|
||||
}
|
||||
store.Remember("run_shell", `{"command":"ls"}`)
|
||||
if got := p.Decide("run_shell", `{"command":"ls"}`, Traits{}); got != EffectAllow {
|
||||
t.Fatalf("after remember: %q, want allow", got)
|
||||
}
|
||||
if got := p.Decide("run_shell", `{"command":"rm -rf /"}`, Traits{}); got != EffectAsk {
|
||||
t.Fatalf("other args: %q, want ask(精确匹配不放行)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPolicyNilStoreAndVisible:nil 存储等价于 SessionAllow 恒 false;Visible 方法可用。
|
||||
func TestPolicyNilStoreAndVisible(t *testing.T) {
|
||||
p := Policy{Mode: ModePlan}
|
||||
if got := p.Decide("write_file", `{}`, Traits{}); got != EffectDeny {
|
||||
t.Fatalf("plan deny: %q", got)
|
||||
}
|
||||
if !p.Visible("update_plan") || p.Visible("write_file") {
|
||||
t.Fatal("policy visible mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"agent-native/runtime/hooks"
|
||||
"agent-native/runtime/llm"
|
||||
"agent-native/runtime/session"
|
||||
)
|
||||
@@ -167,6 +168,7 @@ func (a *AgentSession) Compact(keep int, summarize func([]llm.Message) (string,
|
||||
if a.cfg.Session == nil {
|
||||
return false, nil
|
||||
}
|
||||
a.cfg.Hooks.Emit(hooks.Call{Event: hooks.PreCompact})
|
||||
emit(a.cfg, Event{Type: "compaction_start"})
|
||||
done, err := a.cfg.Session.Compact(keep, summarize)
|
||||
text := "noop"
|
||||
|
||||
Reference in New Issue
Block a user