Expose the step directory and a tool-call cap
ci / vet-and-test (pull_request) Failing after 2s
ci / docker-checkout-hostgateway (pull_request) Successful in 2s

Why: Host tools need the kernel workspace to register files the step just wrote, and product budgets must stop a step that keeps calling tools.
Evidence: go test ./adapters/native/ ./runtime/ -count=1 passed, including the workdir visibility test.
Impact: Host tools can read WorkDir from context. MaxToolCalls limits one kernel step when the request budget leaves it unset.
Refs: deliverable-quality-system B1
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-01 09:06:22 +00:00
co-authored by Cursor
parent 951c141f95
commit cfc840c11d
2 changed files with 50 additions and 2 deletions
+23 -2
View File
@@ -52,6 +52,8 @@ type Options struct {
Subagent subagent.Factory
// HostTools 是宿主产品工具(登记工件、卡片、知识库)。与内核工具同名时失败,不覆盖。
HostTools []tools.Tool
// MaxToolCalls 是单步工具调用上限。0 表示不限制。请求自带预算时以请求为准。
MaxToolCalls int
// Log 可选。nil 时丢弃日志。
Log *logsys.Logger
// ExecutionID 写入日志,不参与执行控制。
@@ -77,6 +79,19 @@ type Backend struct {
lastFailure map[string]string
}
type workDirKey struct{}
// WithWorkDir 把本步工作目录放进上下文,供宿主工具读取内核刚写出的文件。
func WithWorkDir(ctx context.Context, dir string) context.Context {
return context.WithValue(ctx, workDirKey{}, dir)
}
// WorkDir 返回 WithWorkDir 写入的目录。没有时返回空串。
func WorkDir(ctx context.Context) string {
dir, _ := ctx.Value(workDirKey{}).(string)
return dir
}
// New 创建 native 后端。
func New(opts Options) *Backend {
return &Backend{opts: opts, lastFailure: map[string]string{}}
@@ -205,6 +220,9 @@ func (b *Backend) Execute(ctx context.Context, req exec.Request) (*exec.Result,
timeout = b.opts.Timeout
}
var runBudget *runtime.Budget
if b.opts.MaxToolCalls > 0 {
runBudget = &runtime.Budget{MaxToolCalls: b.opts.MaxToolCalls}
}
if req.Budget != nil {
runBudget = &runtime.Budget{
MaxPromptTokens: req.Budget.MaxPromptTokens,
@@ -212,6 +230,9 @@ func (b *Backend) Execute(ctx context.Context, req exec.Request) (*exec.Result,
MaxToolCalls: req.Budget.MaxToolCalls,
Deadline: req.Budget.Deadline,
}
if runBudget.MaxToolCalls == 0 {
runBudget.MaxToolCalls = b.opts.MaxToolCalls
}
if !req.Budget.Deadline.IsZero() {
// Deadline 与 Timeout 先到者生效:把更短者作为 ctx 超时。
if until := time.Until(req.Budget.Deadline); timeout <= 0 || until < timeout {
@@ -219,10 +240,10 @@ func (b *Backend) Execute(ctx context.Context, req exec.Request) (*exec.Result,
}
}
}
runCtx := ctx
runCtx := WithWorkDir(ctx, workDir)
cancel := func() {}
if timeout > 0 {
runCtx, cancel = context.WithTimeout(ctx, timeout)
runCtx, cancel = context.WithTimeout(runCtx, timeout)
}
defer cancel()