pkg/tools/agentic_map.go + agentic_map_test.go
- agentic_map: batch operator that dispatches each item to a subagent
worker; supports configurable concurrency, retries, and timeout per item
- Items are processed in parallel up to MaxWorkers; failed items are
retried up to MaxRetries with exponential backoff
- Results are collected into a structured MapResult with per-item status
pkg/tools/llm_map.go + llm_map_test.go
- llm_map: batch operator that applies a prompt template to each item
using a direct LLM call (no subagent overhead)
- Supports Jinja-style {{item}} template substitution and structured
JSON output extraction from LLM responses
pkg/tools/map_runtime.go + map_runtime_integration_test.go
- MapRuntime: manages the lifecycle of a map operator run (create, update,
complete, fail); persists run state via memory delegate
- Tracks per-item status transitions and aggregates run-level metrics
pkg/tools/map_run_tools.go
- map_run_status: returns the current status and progress of a running
map operation; enables the agent to monitor long-running batch jobs
- map_run_cancel: cancels an in-progress map run
pkg/tools/map_boundary.go
- MapBoundary: defines the input/output contract for map operators;
validates item schemas and enforces output format requirements
pkg/tools/map_payloads.fbs
- FlatBuffers schema for MapRunSpec, MapItemInput, MapItemOutput;
enables zero-copy serialization of map operator payloads
pkg/tools/mapopsfb/MapRunSpec.go
pkg/tools/mapopsfb/MapItemInput.go
pkg/tools/mapopsfb/MapItemOutput.go
- Generated FlatBuffers Go bindings for map operator payload types
pkg/tools/map_flatbuffer_codec.go + map_flatbuffer_codec_test.go
- MapFlatbufferCodec: bidirectional codec between Go map payload structs
and FlatBuffers wire format; used by MapRuntime for persistence
113 lines
3.6 KiB
Go
113 lines
3.6 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
|
|
jsonv2 "github.com/go-json-experiment/json"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/ZanzyTHEbar/dragonscale/pkg/bus"
|
|
)
|
|
|
|
func TestAgenticMapTool_Execute_Success(t *testing.T) {
|
|
provider := &MockLanguageModel{}
|
|
manager := NewSubagentManager(provider, "test-model", "/tmp/test", bus.NewMessageBus())
|
|
manager.SetRunLoop(func(_ context.Context, _ ToolLoopConfig, _, userPrompt, _, _ string) (*ToolLoopResult, error) {
|
|
return &ToolLoopResult{Content: "processed: " + userPrompt, Iterations: 1}, nil
|
|
})
|
|
|
|
tool := NewAgenticMapTool(manager)
|
|
result := tool.Execute(context.Background(), map[string]interface{}{
|
|
"items": []interface{}{
|
|
map[string]interface{}{"name": "a"},
|
|
map[string]interface{}{"name": "b"},
|
|
},
|
|
"task_template": "Handle item {{index}} => {{item_json}}",
|
|
"max_retries": float64(1),
|
|
})
|
|
|
|
require.NotNil(t, result)
|
|
require.False(t, result.IsError, result.ForLLM)
|
|
|
|
var payload struct {
|
|
Count int `json:"count"`
|
|
Summary struct {
|
|
SuccessCount int `json:"success_count"`
|
|
FailureCount int `json:"failure_count"`
|
|
} `json:"summary"`
|
|
}
|
|
require.NoError(t, jsonv2.Unmarshal([]byte(result.ForLLM), &payload))
|
|
assert.Equal(t, 2, payload.Count)
|
|
assert.Equal(t, 2, payload.Summary.SuccessCount)
|
|
assert.Equal(t, 0, payload.Summary.FailureCount)
|
|
}
|
|
|
|
func TestAgenticMapTool_Execute_RetriesFailedItems(t *testing.T) {
|
|
provider := &MockLanguageModel{}
|
|
manager := NewSubagentManager(provider, "test-model", "/tmp/test", bus.NewMessageBus())
|
|
callCount := 0
|
|
manager.SetRunLoop(func(_ context.Context, _ ToolLoopConfig, _, userPrompt, _, _ string) (*ToolLoopResult, error) {
|
|
callCount++
|
|
if callCount == 1 {
|
|
return nil, fmt.Errorf("transient failure for %s", userPrompt)
|
|
}
|
|
return &ToolLoopResult{Content: "processed on retry", Iterations: 1}, nil
|
|
})
|
|
|
|
tool := NewAgenticMapTool(manager)
|
|
result := tool.Execute(context.Background(), map[string]interface{}{
|
|
"items": []interface{}{map[string]interface{}{"name": "retry-me"}},
|
|
"task_template": "Retry item {{index}} => {{item_json}}",
|
|
"max_retries": float64(2),
|
|
})
|
|
|
|
require.NotNil(t, result)
|
|
require.False(t, result.IsError, result.ForLLM)
|
|
|
|
var payload struct {
|
|
Results []struct {
|
|
Success bool `json:"success"`
|
|
Attempts int `json:"attempts"`
|
|
} `json:"results"`
|
|
}
|
|
require.NoError(t, jsonv2.Unmarshal([]byte(result.ForLLM), &payload))
|
|
require.Len(t, payload.Results, 1)
|
|
assert.True(t, payload.Results[0].Success)
|
|
assert.Equal(t, 2, payload.Results[0].Attempts)
|
|
}
|
|
|
|
func TestAgenticMapTool_Execute_RequiresPlaceholders(t *testing.T) {
|
|
provider := &MockLanguageModel{}
|
|
manager := NewSubagentManager(provider, "test-model", "/tmp/test", bus.NewMessageBus())
|
|
tool := NewAgenticMapTool(manager)
|
|
|
|
result := tool.Execute(context.Background(), map[string]interface{}{
|
|
"items": []interface{}{map[string]interface{}{"name": "x"}},
|
|
"task_template": "Handle item without placeholders",
|
|
})
|
|
|
|
require.NotNil(t, result)
|
|
require.True(t, result.IsError)
|
|
assert.Contains(t, result.ForLLM, "placeholders")
|
|
}
|
|
|
|
func TestAgenticMapTool_Execute_ContextCancelled(t *testing.T) {
|
|
provider := &MockLanguageModel{}
|
|
manager := NewSubagentManager(provider, "test-model", "/tmp/test", bus.NewMessageBus())
|
|
tool := NewAgenticMapTool(manager)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
result := tool.Execute(ctx, map[string]interface{}{
|
|
"items": []interface{}{map[string]interface{}{"name": "x"}},
|
|
"task_template": "Handle {{index}} => {{item_json}}",
|
|
})
|
|
|
|
require.NotNil(t, result)
|
|
require.True(t, result.IsError)
|
|
assert.Contains(t, result.ForLLM, "cancelled")
|
|
}
|