Record tool intent before execution and add file checkpoints
Why: a crash after a tool was about to run left the call unpaired, and workspace writes could not be rewound. Evidence: GOWORK=off go test ./runtime/ ./runtime/llm/ ./runtime/checkpoint/ ./runtime/telemetry/ ./runtime/tools/ ./runtime/session/ ./adapters/native/ Impact: pending calls rerun only when both the stored and current replay declarations are safe; otherwise the result is an error. Empty fallback lists and a nil checkpoint log keep the previous behavior. Refs: feature/durable-rewind Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+98
-6
@@ -47,11 +47,14 @@ type Event struct {
|
||||
|
||||
// Config drives one agent session.
|
||||
type Config struct {
|
||||
Model string
|
||||
SystemPrompt string
|
||||
Tools *tools.Registry
|
||||
Stream llm.StreamFunc
|
||||
Session *session.Store
|
||||
Model string
|
||||
SystemPrompt string
|
||||
Tools *tools.Registry
|
||||
Stream llm.StreamFunc
|
||||
Session *session.Store
|
||||
// Trace 开始一个 span,返回的函数结束它。nil 表示不采集。
|
||||
// 属性里不要放提示词正文。
|
||||
Trace func(name string, attrs map[string]string) func()
|
||||
MaxTurns int
|
||||
ToolExecution string
|
||||
GetSteering func() []llm.Message
|
||||
@@ -154,6 +157,8 @@ func Run(ctx context.Context, cfg Config, user llm.Message) (Result, error) {
|
||||
cfg.ToolExecution = ExecSequential
|
||||
}
|
||||
serializeHostCallbacks(&cfg)
|
||||
endRun := startSpan(cfg, "invoke_agent", map[string]string{"model": cfg.Model})
|
||||
defer endRun()
|
||||
var messages []llm.Message
|
||||
if cfg.Session != nil {
|
||||
loaded, err := cfg.Session.Load()
|
||||
@@ -162,6 +167,16 @@ func Run(ctx context.Context, cfg Config, user llm.Message) (Result, error) {
|
||||
}
|
||||
messages = loaded
|
||||
}
|
||||
if err := recoverPending(ctx, cfg); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if cfg.Session != nil {
|
||||
loaded, err := cfg.Session.Load()
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
messages = loaded
|
||||
}
|
||||
if len(messages) == 0 && len(cfg.History) > 0 {
|
||||
messages = append([]llm.Message{}, cfg.History...)
|
||||
}
|
||||
@@ -349,6 +364,8 @@ func systemText(cfg Config) string {
|
||||
}
|
||||
|
||||
func consume(ctx context.Context, cfg Config, messages []llm.Message) (llm.Event, error) {
|
||||
endSpan := startSpan(cfg, "chat", map[string]string{"model": cfg.Model})
|
||||
defer endSpan()
|
||||
var decls []llm.ToolDecl
|
||||
if cfg.Tools != nil {
|
||||
for _, d := range cfg.Tools.Decls() {
|
||||
@@ -576,6 +593,13 @@ func runOne(ctx context.Context, cfg Config, call llm.ToolCall) llm.Message {
|
||||
}
|
||||
}
|
||||
emit(cfg, Event{Type: "tool_execution_update", Tool: call.Name, Detail: call.Arguments})
|
||||
if err := commitToolIntent(cfg, call); err != nil {
|
||||
msg := llm.Message{Role: llm.RoleTool, Name: call.Name, ToolCallID: call.ID, Content: err.Error(), IsError: true}
|
||||
emit(cfg, Event{Type: "tool_execution_end", Tool: call.Name, Text: msg.Content})
|
||||
return msg
|
||||
}
|
||||
endSpan := startSpan(cfg, "execute_tool", map[string]string{"tool": call.Name})
|
||||
defer endSpan()
|
||||
res := tools.Result{Content: "no tools configured", IsError: true}
|
||||
known := cfg.Tools != nil && cfg.Tools.Has(call.Name)
|
||||
switch {
|
||||
@@ -609,10 +633,78 @@ func settle(cfg Config, last llm.Message, messages []llm.Message, usage llm.Usag
|
||||
}
|
||||
|
||||
func prepareRequest(cfg Config, messages []llm.Message, turn int, repeats *repeatState, lengthNudge *bool) []llm.Message {
|
||||
request := clearOldResults(cfg, messages)
|
||||
request := clearOldResults(cfg, persistedProjection(cfg, messages))
|
||||
return reminders.Attach(request, turnReminderTexts(cfg, turn, repeats, lengthNudge))
|
||||
}
|
||||
|
||||
// persistedProjection 在有会话存储时只采用已经落盘的消息。
|
||||
// steer 与 follow-up 必须先写入存储,下一轮请求才会看见它们。
|
||||
func persistedProjection(cfg Config, messages []llm.Message) []llm.Message {
|
||||
if cfg.Session == nil {
|
||||
return messages
|
||||
}
|
||||
loaded, err := cfg.Session.Load()
|
||||
if err != nil {
|
||||
return messages
|
||||
}
|
||||
return loaded
|
||||
}
|
||||
|
||||
const unknownSideEffect = "副作用未知,未重试"
|
||||
|
||||
func recoverPending(ctx context.Context, cfg Config) error {
|
||||
if cfg.Session == nil {
|
||||
return nil
|
||||
}
|
||||
pending, err := cfg.Session.PendingIntents()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, in := range pending {
|
||||
current := tools.ReplayNever
|
||||
if cfg.Tools != nil {
|
||||
current = cfg.Tools.Replay(in.Name)
|
||||
}
|
||||
msg := llm.Message{
|
||||
Role: llm.RoleTool, Name: in.Name, ToolCallID: in.CallID,
|
||||
Content: unknownSideEffect, IsError: true,
|
||||
}
|
||||
if in.Replay == tools.ReplaySafe && current == tools.ReplaySafe && cfg.Tools != nil {
|
||||
res := cfg.Tools.Execute(ctx, in.Name, json.RawMessage(in.Arguments))
|
||||
msg.Content = res.Content
|
||||
msg.IsError = res.IsError
|
||||
}
|
||||
if err := persist(cfg, msg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func commitToolIntent(cfg Config, call llm.ToolCall) error {
|
||||
if cfg.Session == nil {
|
||||
return nil
|
||||
}
|
||||
replay := tools.ReplayNever
|
||||
if cfg.Tools != nil {
|
||||
replay = cfg.Tools.Replay(call.Name)
|
||||
}
|
||||
return cfg.Session.AppendToolIntent(session.ToolIntent{
|
||||
CallID: call.ID, Name: call.Name, Arguments: call.Arguments, Replay: replay,
|
||||
})
|
||||
}
|
||||
|
||||
func startSpan(cfg Config, name string, attrs map[string]string) func() {
|
||||
if cfg.Trace == nil {
|
||||
return func() {}
|
||||
}
|
||||
end := cfg.Trace(name, attrs)
|
||||
if end == nil {
|
||||
return func() {}
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
func clearOldResults(cfg Config, messages []llm.Message) []llm.Message {
|
||||
window := cfg.ContextWindow
|
||||
if window <= 0 {
|
||||
|
||||
Reference in New Issue
Block a user