Record tool intent before execution and add file checkpoints
ci / vet-and-test (pull_request) Successful in 46s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s

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:
2026-10-03 17:27:04 +00:00
co-authored by Cursor
parent ba90a7d9ae
commit fb2b3b796c
17 changed files with 1138 additions and 21 deletions
+98 -6
View File
@@ -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 {