Expose the step directory and a tool-call cap
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:
@@ -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()
|
||||
|
||||
|
||||
@@ -62,6 +62,33 @@ func TestHostToolAndUsageEvent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostToolSeesStepWorkDir(t *testing.T) {
|
||||
var seen string
|
||||
b := New(Options{
|
||||
Model: "fake",
|
||||
Stream: script(llm.Message{Role: llm.RoleAssistant, StopReason: llm.StopToolCalls, ToolCalls: []llm.ToolCall{{ID: "c1", Name: "peek", Arguments: `{}`}}}),
|
||||
HostTools: []tools.Tool{peekTool{got: &seen}},
|
||||
})
|
||||
dir := t.TempDir()
|
||||
_, err := b.Execute(context.Background(), exec.Request{WorkDir: dir, Prompt: "x", Targets: []string{"output.md"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if seen != dir {
|
||||
t.Fatalf("workdir = %q, want %s", seen, dir)
|
||||
}
|
||||
}
|
||||
|
||||
type peekTool struct{ got *string }
|
||||
|
||||
func (p peekTool) Name() string { return "peek" }
|
||||
func (p peekTool) Description() string { return "peek" }
|
||||
func (p peekTool) Parameters() json.RawMessage { return json.RawMessage(`{"type":"object"}`) }
|
||||
func (p peekTool) Execute(ctx context.Context, args json.RawMessage) (tools.Result, error) {
|
||||
*p.got = WorkDir(ctx)
|
||||
return tools.Result{Content: "ok"}, nil
|
||||
}
|
||||
|
||||
func TestHostToolNameConflictFailsClosed(t *testing.T) {
|
||||
b := New(Options{
|
||||
Model: "fake",
|
||||
|
||||
Reference in New Issue
Block a user