diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2026-01-19 15:15:18 +0100 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2026-01-20 21:12:57 +0000 |
| commit | 5b6bebdcb7da46d1471b3aeacb28b54ba905b3b2 (patch) | |
| tree | d60fdce83c9b47fb39327f0bdc40b734a7213985 /pkg/aflow/func_tool_test.go | |
| parent | 8088ac4199a6e947c38db669c11d4441a9d59581 (diff) | |
pkg/aflow: add BadCallError
The error allows tools to communicate that an error is not an infrastructure error
that must fail the whole workflow, but rather a bad tool invocation by an LLM
(e.g. asking for a non-existent file contents).
Previously in the codesearcher tool we used a separate Missing bool
to communicate that. With the error everything just becomes cleaner and nicer.
The errors also allows all other tools to communicate any errors to the LLM
when the normal results cannot be provided and don't make sense.
Diffstat (limited to 'pkg/aflow/func_tool_test.go')
| -rw-r--r-- | pkg/aflow/func_tool_test.go | 111 |
1 files changed, 111 insertions, 0 deletions
diff --git a/pkg/aflow/func_tool_test.go b/pkg/aflow/func_tool_test.go new file mode 100644 index 000000000..429566dbe --- /dev/null +++ b/pkg/aflow/func_tool_test.go @@ -0,0 +1,111 @@ +// Copyright 2026 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package aflow + +import ( + "context" + "errors" + "path/filepath" + "testing" + + "github.com/google/syzkaller/pkg/aflow/trajectory" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/genai" +) + +func TestToolErrors(t *testing.T) { + type flowOutputs struct { + Reply string + } + type toolArgs struct { + CallError bool `jsonschema:"call error"` + } + flows := make(map[string]*Flow) + err := register[struct{}, flowOutputs]("test", "description", flows, []*Flow{ + { + Root: &LLMAgent{ + Name: "smarty", + Model: "model", + Reply: "Reply", + Temperature: 0, + Instruction: "Do something!", + Prompt: "Prompt", + Tools: []Tool{ + NewFuncTool("faulty", func(ctx *Context, state struct{}, args toolArgs) (struct{}, error) { + if args.CallError { + return struct{}{}, BadCallError("you are wrong") + } + return struct{}{}, errors.New("hard error") + }, "tool 1 description"), + }, + }, + }, + }) + require.NoError(t, err) + replySeq := 0 + stub := &stubContext{ + // nolint:dupl + generateContent: func(model string, cfg *genai.GenerateContentConfig, req []*genai.Content) ( + *genai.GenerateContentResponse, error) { + replySeq++ + switch replySeq { + case 1: + return &genai.GenerateContentResponse{ + Candidates: []*genai.Candidate{{ + Content: &genai.Content{ + Role: string(genai.RoleModel), + Parts: []*genai.Part{ + { + FunctionCall: &genai.FunctionCall{ + ID: "id0", + Name: "faulty", + Args: map[string]any{ + "CallError": true, + }, + }, + }, + }}}}}, nil + case 2: + assert.Equal(t, req[2], &genai.Content{ + Role: string(genai.RoleUser), + Parts: []*genai.Part{ + { + FunctionResponse: &genai.FunctionResponse{ + ID: "id0", + Name: "faulty", + Response: map[string]any{ + "error": "you are wrong", + }, + }, + }}}) + return &genai.GenerateContentResponse{ + Candidates: []*genai.Candidate{{ + Content: &genai.Content{ + Role: string(genai.RoleModel), + Parts: []*genai.Part{ + { + FunctionCall: &genai.FunctionCall{ + ID: "id0", + Name: "faulty", + Args: map[string]any{ + "CallError": false, + }, + }, + }, + }}}}}, nil + default: + t.Fatal("unexpected LLM calls") + return nil, nil + } + }, + } + ctx := context.WithValue(context.Background(), stubContextKey, stub) + workdir := t.TempDir() + cache, err := newTestCache(t, filepath.Join(workdir, "cache"), 0, stub.timeNow) + require.NoError(t, err) + onEvent := func(span *trajectory.Span) error { return nil } + _, err = flows["test"].Execute(ctx, "", workdir, nil, cache, onEvent) + require.Equal(t, err.Error(), "tool faulty failed: error: hard error\nargs: map[CallError:false]") +} |
