feat(tools): add agentic_map and llm_map operators with FlatBuffers persistence

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
This commit is contained in:
ZanzyTHEbar 2026-02-21 18:59:19 +00:00
parent b03e42bceb
commit 00802f78be
14 changed files with 3253 additions and 0 deletions

334
pkg/tools/agentic_map.go Normal file
View file

@ -0,0 +1,334 @@
package tools
import (
"context"
"fmt"
"strconv"
"strings"
memsqlc "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc"
jsonv2 "github.com/go-json-experiment/json"
)
// AgenticMapTool maps tasks over items using subagent execution with retries.
type AgenticMapTool struct {
manager *SubagentManager
originChannel string
originChatID string
runtime *MapRuntime
}
func NewAgenticMapTool(manager *SubagentManager) *AgenticMapTool {
return &AgenticMapTool{
manager: manager,
originChannel: "cli",
originChatID: "direct",
}
}
func (t *AgenticMapTool) SetRuntime(runtime *MapRuntime) {
t.runtime = runtime
}
func (t *AgenticMapTool) Name() string {
return "agentic_map"
}
func (t *AgenticMapTool) Description() string {
return "Run subagent processing over each item with retries. Supports worker-backed runs and generated run reads."
}
func (t *AgenticMapTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"items": map[string]interface{}{
"type": "array",
"description": "Items to process via subagent execution.",
},
"input_jsonl": map[string]interface{}{
"type": "string",
"description": "Boundary-only JSONL input. Exactly one of items or input_jsonl is required.",
},
"task_template": map[string]interface{}{
"type": "string",
"description": "Task template. Supports {{item_json}} and {{index}} placeholders.",
},
"max_retries": map[string]interface{}{
"type": "integer",
"description": "Retries per item after the first attempt (default 1).",
},
"delegated_scope": map[string]interface{}{
"type": "string",
"description": "Delegated scope metadata passed to nested subagent calls.",
},
"kept_work": map[string]interface{}{
"type": "string",
"description": "Kept work metadata passed to nested subagent calls.",
},
"execution_mode": map[string]interface{}{
"type": "string",
"description": "inline or worker. Defaults to worker for JSONL/large batches, inline otherwise.",
},
"session_key": map[string]interface{}{
"type": "string",
"description": "Optional map run session key for persistence (default: default).",
},
"idempotency_key": map[string]interface{}{
"type": "string",
"description": "Optional key to deduplicate repeated run creation.",
},
},
"required": []string{"task_template"},
}
}
func (t *AgenticMapTool) SetContext(channel, chatID string) {
t.originChannel = channel
t.originChatID = chatID
}
func (t *AgenticMapTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
if t.manager == nil && t.runtime == nil {
return ErrorResult("agentic_map manager is not configured").WithError(fmt.Errorf("agentic_map manager is nil"))
}
items, usedJSONL, err := parseMapBoundaryItems(args)
if err != nil {
return ErrorResult(err.Error()).WithError(err)
}
taskTemplate, ok := args["task_template"].(string)
if !ok || strings.TrimSpace(taskTemplate) == "" {
return ErrorResult("task_template is required").WithError(fmt.Errorf("task_template is required"))
}
if !strings.Contains(taskTemplate, "{{item_json}}") || !strings.Contains(taskTemplate, "{{index}}") {
return ErrorResult("task_template must include both {{item_json}} and {{index}} placeholders").
WithError(fmt.Errorf("task_template missing required placeholders"))
}
maxRetries, err := parseMapMaxRetries(args, 1)
if err != nil {
return ErrorResult(err.Error()).WithError(err)
}
delegatedScope, err := parseOptionalStringArg(args, "delegated_scope")
if err != nil {
return ErrorResult(err.Error()).WithError(err)
}
keptWork, err := parseOptionalStringArg(args, "kept_work")
if err != nil {
return ErrorResult(err.Error()).WithError(err)
}
mode, err := resolveMapExecutionMode(args, len(items), usedJSONL)
if err != nil {
return ErrorResult(err.Error()).WithError(err)
}
sessionKey, err := parseOptionalStringArg(args, "session_key")
if err != nil {
return ErrorResult(err.Error()).WithError(err)
}
idempotencyKey, err := parseOptionalStringArg(args, "idempotency_key")
if err != nil {
return ErrorResult(err.Error()).WithError(err)
}
if sessionKey == "" {
sessionKey = "default"
}
if t.runtime != nil {
run, reused, err := t.runtime.EnqueueRun(ctx, sessionKey, MapRunSpec{
OperatorKind: MapOperatorAgentic,
TaskTemplate: taskTemplate,
MaxRetries: uint16(maxRetries),
DelegatedScope: delegatedScope,
KeptWork: keptWork,
OriginChannel: t.originChannel,
OriginChatID: t.originChatID,
}, items, idempotencyKey)
if err != nil {
return ErrorResult(fmt.Sprintf("failed to enqueue agentic_map run: %v", err)).WithError(err)
}
if mode == "worker" {
payload := map[string]interface{}{
"run_id": run.ID.String(),
"status": run.Status,
"accepted_count": len(items),
"queued_count": run.QueuedItems,
"execution_mode": "worker",
"idempotent_reuse": reused,
}
data, err := jsonv2.Marshal(payload)
if err != nil {
return ErrorResult("failed to serialize agentic_map enqueue result").WithError(err)
}
return &ToolResult{
ForLLM: string(data),
ForUser: string(data),
Silent: false,
IsError: false,
Async: false,
}
}
runFinal, err := t.runtime.ProcessRunToTerminal(ctx, run.ID, len(items)*(maxRetries+2)+32)
if err != nil {
return ErrorResult(fmt.Sprintf("failed to complete inline agentic_map run %s: %v", run.ID.String(), err)).WithError(err)
}
itemRows, err := t.runtime.ReadRunItems(ctx, run.ID, 0, len(items))
if err != nil {
return ErrorResult(fmt.Sprintf("failed to read inline agentic_map results: %v", err)).WithError(err)
}
return buildAgenticMapInlineResult(runFinal, itemRows, maxRetries)
}
// Legacy synchronous path when runtime persistence is not configured.
if mode == "worker" {
return ErrorResult("worker mode requires map runtime persistence").WithError(fmt.Errorf("map runtime is nil"))
}
if t.manager == nil {
return ErrorResult("agentic_map manager is not configured").WithError(fmt.Errorf("agentic_map manager is nil"))
}
subTool := NewSubagentTool(t.manager)
subTool.SetContext(t.originChannel, t.originChatID)
type itemResult struct {
Index int `json:"index"`
Success bool `json:"success"`
Attempts int `json:"attempts"`
Output string `json:"output,omitempty"`
Error string `json:"error,omitempty"`
}
results := make([]itemResult, 0, len(items))
successCount := 0
for i, item := range items {
if err := ctx.Err(); err != nil {
return ErrorResult("agentic_map cancelled before completion").WithError(err)
}
task := strings.ReplaceAll(taskTemplate, "{{item_json}}", item.ItemJSON)
task = strings.ReplaceAll(task, "{{index}}", strconv.Itoa(i))
attempts := 0
var lastErr string
var output string
okItem := false
for attempts <= maxRetries {
if err := ctx.Err(); err != nil {
lastErr = err.Error()
break
}
attempts++
callArgs := map[string]interface{}{
"task": task,
"label": fmt.Sprintf("agentic-map-%d", i),
}
if delegatedScope != "" {
callArgs["delegated_scope"] = delegatedScope
}
if keptWork != "" {
callArgs["kept_work"] = keptWork
}
r := subTool.Execute(ctx, callArgs)
if r != nil && !r.IsError {
okItem = true
output = r.ForUser
break
}
if r != nil {
lastErr = r.ForLLM
} else {
lastErr = "unknown subagent error"
}
}
if okItem {
successCount++
}
results = append(results, itemResult{
Index: i,
Success: okItem,
Attempts: attempts,
Output: output,
Error: lastErr,
})
}
payload := map[string]interface{}{
"count": len(results),
"summary": map[string]interface{}{
"success_count": successCount,
"failure_count": len(results) - successCount,
"max_retries": maxRetries,
},
"results": results,
}
data, err := jsonv2.Marshal(payload)
if err != nil {
return ErrorResult("failed to serialize agentic_map results").WithError(err)
}
return &ToolResult{
ForLLM: string(data),
ForUser: string(data),
Silent: false,
IsError: false,
Async: false,
}
}
func buildAgenticMapInlineResult(run memsqlc.MapRun, items []MapItemProjection, maxRetries int) *ToolResult {
type itemResult struct {
Index int64 `json:"index"`
Success bool `json:"success"`
Attempts int64 `json:"attempts"`
Output string `json:"output,omitempty"`
Error string `json:"error,omitempty"`
}
results := make([]itemResult, 0, len(items))
successCount := 0
for _, item := range items {
outText := ""
switch out := item.Output.(type) {
case map[string]interface{}:
if forUser, ok := out["for_user"].(string); ok {
outText = forUser
}
case string:
outText = out
}
ok := item.Status == mapItemStatusSucceeded
if ok {
successCount++
}
results = append(results, itemResult{
Index: item.Index,
Success: ok,
Attempts: item.Attempts,
Output: outText,
Error: item.Error,
})
}
payload := map[string]interface{}{
"run_id": run.ID.String(),
"status": run.Status,
"count": len(results),
"summary": map[string]interface{}{
"success_count": successCount,
"failure_count": len(results) - successCount,
"max_retries": maxRetries,
},
"results": results,
}
data, err := jsonv2.Marshal(payload)
if err != nil {
return ErrorResult("failed to serialize agentic_map inline results").WithError(err)
}
return &ToolResult{
ForLLM: string(data),
ForUser: string(data),
Silent: false,
IsError: false,
Async: false,
}
}

View file

@ -0,0 +1,113 @@
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")
}

327
pkg/tools/llm_map.go Normal file
View file

@ -0,0 +1,327 @@
package tools
import (
"context"
"fmt"
"strings"
jsonv2 "github.com/go-json-experiment/json"
fantasy "charm.land/fantasy"
memsqlc "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc"
)
// LLMMapTool performs schema-validated per-item processing with model calls
// and no side effects (toolless, deterministic structure).
type LLMMapTool struct {
model fantasy.LanguageModel
modelID string
runtime *MapRuntime
}
func NewLLMMapTool(model fantasy.LanguageModel, modelID string) *LLMMapTool {
return &LLMMapTool{
model: model,
modelID: modelID,
}
}
func (t *LLMMapTool) SetRuntime(runtime *MapRuntime) {
t.runtime = runtime
}
func (t *LLMMapTool) Name() string {
return "llm_map"
}
func (t *LLMMapTool) Description() string {
return "Apply an instruction to each item with schema-validated JSON output. Supports worker-backed runs with run handles for large batches."
}
func (t *LLMMapTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"items": map[string]interface{}{
"type": "array",
"description": "Array of items to process.",
},
"input_jsonl": map[string]interface{}{
"type": "string",
"description": "Boundary-only JSONL input. Exactly one of items or input_jsonl is required.",
},
"instruction": map[string]interface{}{
"type": "string",
"description": "Instruction applied to each item. Output must be JSON object only.",
},
"output_schema": map[string]interface{}{
"type": "object",
"description": "Optional lightweight JSON schema (properties + required) used to validate each output object.",
},
"execution_mode": map[string]interface{}{
"type": "string",
"description": "inline or worker. Defaults to worker for JSONL/large batches, inline otherwise.",
},
"max_retries": map[string]interface{}{
"type": "integer",
"description": "Retries per item after first attempt when using worker execution (default 1, max 10).",
},
"session_key": map[string]interface{}{
"type": "string",
"description": "Optional map run session key for persistence (default: default).",
},
"idempotency_key": map[string]interface{}{
"type": "string",
"description": "Optional key to deduplicate repeated run creation.",
},
},
"required": []string{"instruction"},
}
}
func (t *LLMMapTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
if t.model == nil && t.runtime == nil {
return ErrorResult("llm_map model is not configured").WithError(fmt.Errorf("llm_map model is nil"))
}
items, usedJSONL, err := parseMapBoundaryItems(args)
if err != nil {
return ErrorResult(err.Error()).WithError(err)
}
instruction, ok := args["instruction"].(string)
if !ok || strings.TrimSpace(instruction) == "" {
return ErrorResult("instruction is required").WithError(fmt.Errorf("instruction is required"))
}
var schema map[string]interface{}
if rawSchema, ok := args["output_schema"].(map[string]interface{}); ok {
schema = rawSchema
}
schemaJSON := ""
if schema != nil {
b, err := canonicalJSONString(schema)
if err != nil {
return ErrorResult("output_schema is not JSON-serializable").WithError(err)
}
schemaJSON = b
}
maxRetries, err := parseMapMaxRetries(args, 1)
if err != nil {
return ErrorResult(err.Error()).WithError(err)
}
mode, err := resolveMapExecutionMode(args, len(items), usedJSONL)
if err != nil {
return ErrorResult(err.Error()).WithError(err)
}
sessionKey, err := parseOptionalStringArg(args, "session_key")
if err != nil {
return ErrorResult(err.Error()).WithError(err)
}
idempotencyKey, err := parseOptionalStringArg(args, "idempotency_key")
if err != nil {
return ErrorResult(err.Error()).WithError(err)
}
if sessionKey == "" {
sessionKey = "default"
}
if t.runtime != nil {
run, reused, err := t.runtime.EnqueueRun(ctx, sessionKey, MapRunSpec{
OperatorKind: MapOperatorLLM,
Instruction: strings.TrimSpace(instruction),
OutputSchemaJSON: schemaJSON,
MaxRetries: uint16(maxRetries),
}, items, idempotencyKey)
if err != nil {
return ErrorResult(fmt.Sprintf("failed to enqueue llm_map run: %v", err)).WithError(err)
}
if mode == "worker" {
payload := map[string]interface{}{
"run_id": run.ID.String(),
"status": run.Status,
"accepted_count": len(items),
"queued_count": run.QueuedItems,
"execution_mode": "worker",
"idempotent_reuse": reused,
}
data, err := jsonv2.Marshal(payload)
if err != nil {
return ErrorResult("failed to serialize llm_map enqueue result").WithError(err)
}
return &ToolResult{
ForLLM: string(data),
ForUser: string(data),
Silent: false,
IsError: false,
Async: false,
}
}
// Inline mode still goes through the worker lifecycle for a single runtime path.
runFinal, err := t.runtime.ProcessRunToTerminal(ctx, run.ID, len(items)*(maxRetries+2)+32)
if err != nil {
return ErrorResult(fmt.Sprintf("failed to complete inline llm_map run %s: %v", run.ID.String(), err)).WithError(err)
}
itemRows, err := t.runtime.ReadRunItems(ctx, run.ID, 0, len(items))
if err != nil {
return ErrorResult(fmt.Sprintf("failed to read inline llm_map results: %v", err)).WithError(err)
}
return buildLLMMapInlineResult(runFinal, itemRows)
}
// Legacy synchronous path when runtime persistence is not configured.
if mode == "worker" {
return ErrorResult("worker mode requires map runtime persistence").WithError(fmt.Errorf("map runtime is nil"))
}
results := make([]map[string]interface{}, 0, len(items))
for _, item := range items {
prompt := fmt.Sprintf(
"Instruction:\n%s\n\nItem JSON:\n%s\n\nReturn exactly one JSON object and nothing else.",
instruction,
item.ItemJSON,
)
maxOut := int64(512)
call := fantasy.Call{
Prompt: fantasy.Prompt{
fantasy.NewSystemMessage("You are a pure mapper. Return only strict JSON object output."),
fantasy.NewUserMessage(prompt),
},
MaxOutputTokens: &maxOut,
}
resp, err := t.model.Generate(ctx, call)
if err != nil {
return ErrorResult(fmt.Sprintf("llm_map failed at index %d", item.Index)).WithError(err)
}
var out map[string]interface{}
if err := jsonv2.Unmarshal([]byte(resp.Content.Text()), &out); err != nil {
return ErrorResult(fmt.Sprintf("llm_map output at index %d is not valid JSON object", item.Index)).WithError(err)
}
if schema != nil {
if err := validateLLMMapOutputSchema(out, schema); err != nil {
return ErrorResult(fmt.Sprintf("llm_map schema validation failed at index %d: %v", item.Index, err)).WithError(err)
}
}
results = append(results, out)
}
payload := map[string]interface{}{
"count": len(results),
"results": results,
}
data, err := jsonv2.Marshal(payload)
if err != nil {
return ErrorResult("failed to serialize llm_map results").WithError(err)
}
return &ToolResult{
ForLLM: string(data),
ForUser: string(data),
Silent: false,
IsError: false,
Async: false,
}
}
func buildLLMMapInlineResult(run memsqlc.MapRun, items []MapItemProjection) *ToolResult {
results := make([]map[string]interface{}, 0, len(items))
failures := make([]map[string]interface{}, 0)
for _, item := range items {
if item.Status == mapItemStatusSucceeded {
outMap, ok := item.Output.(map[string]interface{})
if !ok {
outMap = map[string]interface{}{
"value": item.Output,
}
}
results = append(results, outMap)
continue
}
failures = append(failures, map[string]interface{}{
"index": item.Index,
"status": item.Status,
"attempts": item.Attempts,
"error": item.Error,
})
}
payload := map[string]interface{}{
"run_id": run.ID.String(),
"status": run.Status,
"count": len(results),
"summary": map[string]interface{}{
"success_count": len(results),
"failure_count": len(failures),
},
"results": results,
"failures": failures,
}
data, err := jsonv2.Marshal(payload)
if err != nil {
return ErrorResult("failed to serialize llm_map inline results").WithError(err)
}
return &ToolResult{
ForLLM: string(data),
ForUser: string(data),
Silent: false,
IsError: false,
Async: false,
}
}
func validateLLMMapOutputSchema(out map[string]interface{}, schema map[string]interface{}) error {
if requiredRaw, ok := schema["required"].([]interface{}); ok {
for _, r := range requiredRaw {
key, _ := r.(string)
if key == "" {
continue
}
if _, exists := out[key]; !exists {
return fmt.Errorf("missing required field %q", key)
}
}
}
props, _ := schema["properties"].(map[string]interface{})
for key, defRaw := range props {
value, exists := out[key]
if !exists {
continue
}
def, _ := defRaw.(map[string]interface{})
wantType, _ := def["type"].(string)
if wantType == "" {
continue
}
if !matchesJSONType(value, wantType) {
return fmt.Errorf("field %q expected type %q", key, wantType)
}
}
return nil
}
func matchesJSONType(value interface{}, want string) bool {
switch want {
case "string":
_, ok := value.(string)
return ok
case "number":
_, ok := value.(float64)
return ok
case "integer":
f, ok := value.(float64)
return ok && f == float64(int64(f))
case "boolean":
_, ok := value.(bool)
return ok
case "object":
_, ok := value.(map[string]interface{})
return ok
case "array":
_, ok := value.([]interface{})
return ok
case "null":
return value == nil
default:
return true
}
}

111
pkg/tools/llm_map_test.go Normal file
View file

@ -0,0 +1,111 @@
package tools
import (
"context"
"fmt"
"testing"
jsonv2 "github.com/go-json-experiment/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
fantasy "charm.land/fantasy"
)
type llmMapMockModel struct {
responses []string
next int
}
func (m *llmMapMockModel) Generate(_ context.Context, _ fantasy.Call) (*fantasy.Response, error) {
if len(m.responses) == 0 {
return nil, fmt.Errorf("no responses configured")
}
resp := m.responses[m.next%len(m.responses)]
m.next++
return &fantasy.Response{
Content: fantasy.ResponseContent{fantasy.TextContent{Text: resp}},
FinishReason: fantasy.FinishReasonStop,
}, nil
}
func (m *llmMapMockModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) {
resp, err := m.Generate(ctx, call)
if err != nil {
return nil, err
}
return func(yield func(fantasy.StreamPart) bool) {
if !yield(fantasy.StreamPart{Type: fantasy.StreamPartTypeTextDelta, Delta: resp.Content.Text()}) {
return
}
yield(fantasy.StreamPart{Type: fantasy.StreamPartTypeFinish, FinishReason: fantasy.FinishReasonStop})
}, nil
}
func (m *llmMapMockModel) GenerateObject(_ context.Context, _ fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
return nil, fmt.Errorf("not implemented")
}
func (m *llmMapMockModel) StreamObject(_ context.Context, _ fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
return nil, fmt.Errorf("not implemented")
}
func (m *llmMapMockModel) Provider() string { return "mock" }
func (m *llmMapMockModel) Model() string { return "mock-llm-map" }
func TestLLMMapTool_Execute_Success(t *testing.T) {
model := &llmMapMockModel{
responses: []string{
`{"label":"alpha","priority":1}`,
`{"label":"beta","priority":2}`,
},
}
tool := NewLLMMapTool(model, "mock-llm-map")
result := tool.Execute(context.Background(), map[string]interface{}{
"instruction": "Convert item to {label, priority}",
"items": []interface{}{
map[string]interface{}{"name": "A"},
map[string]interface{}{"name": "B"},
},
"output_schema": map[string]interface{}{
"required": []interface{}{"label", "priority"},
"properties": map[string]interface{}{
"label": map[string]interface{}{"type": "string"},
"priority": map[string]interface{}{"type": "integer"},
},
},
})
require.NotNil(t, result)
require.False(t, result.IsError, result.ForLLM)
var payload struct {
Count int `json:"count"`
Results []map[string]interface{} `json:"results"`
}
require.NoError(t, jsonv2.Unmarshal([]byte(result.ForLLM), &payload))
assert.Equal(t, 2, payload.Count)
require.Len(t, payload.Results, 2)
assert.Equal(t, "alpha", payload.Results[0]["label"])
}
func TestLLMMapTool_Execute_SchemaValidationFailure(t *testing.T) {
model := &llmMapMockModel{
responses: []string{`{"only":"value"}`},
}
tool := NewLLMMapTool(model, "mock-llm-map")
result := tool.Execute(context.Background(), map[string]interface{}{
"instruction": "Return normalized object",
"items": []interface{}{map[string]interface{}{"name": "x"}},
"output_schema": map[string]interface{}{
"required": []interface{}{"label"},
"properties": map[string]interface{}{
"label": map[string]interface{}{"type": "string"},
},
},
})
require.NotNil(t, result)
require.True(t, result.IsError)
assert.Contains(t, result.ForLLM, "schema validation failed")
}

204
pkg/tools/map_boundary.go Normal file
View file

@ -0,0 +1,204 @@
package tools
import (
"bufio"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
jsonv2 "github.com/go-json-experiment/json"
)
const (
defaultMapInlineCutoff = 12
defaultMapReadLimit = 100
maxMapReadLimit = 500
maxMapItems = 5000
)
type mapBoundaryItem struct {
Index int
ItemJSON string
InputHash string
}
func parseMapBoundaryItems(args map[string]interface{}) ([]mapBoundaryItem, bool, error) {
itemsRaw, hasItems := args["items"]
inputJSONLRaw, hasJSONL := args["input_jsonl"]
if hasItems == hasJSONL {
return nil, false, fmt.Errorf("provide exactly one of items or input_jsonl")
}
items := make([]mapBoundaryItem, 0)
if hasItems {
array, ok := itemsRaw.([]interface{})
if !ok {
return nil, false, fmt.Errorf("items must be an array")
}
if len(array) == 0 {
return nil, false, fmt.Errorf("items array is empty")
}
if len(array) > maxMapItems {
return nil, false, fmt.Errorf("items array exceeds max size %d", maxMapItems)
}
for idx, item := range array {
itemJSON, err := canonicalJSONString(item)
if err != nil {
return nil, false, fmt.Errorf("serialize item at index %d: %w", idx, err)
}
items = append(items, mapBoundaryItem{
Index: idx,
ItemJSON: itemJSON,
InputHash: hashString(itemJSON),
})
}
return items, false, nil
}
inputJSONL, ok := inputJSONLRaw.(string)
if !ok || strings.TrimSpace(inputJSONL) == "" {
return nil, true, fmt.Errorf("input_jsonl must be a non-empty string")
}
scanner := bufio.NewScanner(strings.NewReader(inputJSONL))
// Allow large records while still preventing unbounded memory growth.
const maxJSONLLineBytes = 4 * 1024 * 1024
scanner.Buffer(make([]byte, 64*1024), maxJSONLLineBytes)
lineNo := 0
for scanner.Scan() {
lineNo++
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
var item interface{}
if err := jsonv2.Unmarshal([]byte(line), &item); err != nil {
return nil, true, fmt.Errorf("invalid JSONL at line %d: %w", lineNo, err)
}
itemJSON, err := canonicalJSONString(item)
if err != nil {
return nil, true, fmt.Errorf("canonicalize JSONL item at line %d: %w", lineNo, err)
}
items = append(items, mapBoundaryItem{
Index: len(items),
ItemJSON: itemJSON,
InputHash: hashString(itemJSON),
})
if len(items) > maxMapItems {
return nil, true, fmt.Errorf("input_jsonl exceeds max item count %d", maxMapItems)
}
}
if err := scanner.Err(); err != nil {
return nil, true, fmt.Errorf("read input_jsonl: %w", err)
}
if len(items) == 0 {
return nil, true, fmt.Errorf("input_jsonl contains no records")
}
return items, true, nil
}
func resolveMapExecutionMode(args map[string]interface{}, itemCount int, usedJSONL bool) (string, error) {
if raw, ok := args["execution_mode"]; ok {
mode, ok := raw.(string)
if !ok {
return "", fmt.Errorf("execution_mode must be a string")
}
switch strings.ToLower(strings.TrimSpace(mode)) {
case "inline":
return "inline", nil
case "worker":
return "worker", nil
default:
return "", fmt.Errorf("execution_mode must be one of inline|worker")
}
}
if usedJSONL || itemCount > defaultMapInlineCutoff {
return "worker", nil
}
return "inline", nil
}
func parseMapReadFormat(args map[string]interface{}) (string, error) {
raw, ok := args["format"]
if !ok {
return "json", nil
}
format, ok := raw.(string)
if !ok {
return "", fmt.Errorf("format must be a string")
}
switch strings.ToLower(strings.TrimSpace(format)) {
case "json", "":
return "json", nil
case "jsonl":
return "jsonl", nil
default:
return "", fmt.Errorf("format must be json or jsonl")
}
}
func parseMapLimit(args map[string]interface{}, key string, defaultValue, maxValue int) (int, error) {
value, ok := args[key]
if !ok {
return defaultValue, nil
}
switch v := value.(type) {
case float64:
if v < 0 {
return 0, fmt.Errorf("%s must be >= 0", key)
}
out := int(v)
if out > maxValue {
out = maxValue
}
return out, nil
case int:
if v < 0 {
return 0, fmt.Errorf("%s must be >= 0", key)
}
if v > maxValue {
return maxValue, nil
}
return v, nil
default:
return 0, fmt.Errorf("%s must be numeric", key)
}
}
func parseMapMaxRetries(args map[string]interface{}, defaultValue int) (int, error) {
raw, ok := args["max_retries"]
if !ok {
return defaultValue, nil
}
value, err := parseMapLimit(map[string]interface{}{"value": raw}, "value", defaultValue, 10)
if err != nil {
return 0, fmt.Errorf("max_retries %w", err)
}
return value, nil
}
func parseOptionalStringArg(args map[string]interface{}, key string) (string, error) {
raw, ok := args[key]
if !ok {
return "", nil
}
value, ok := raw.(string)
if !ok {
return "", fmt.Errorf("%s must be a string", key)
}
return strings.TrimSpace(value), nil
}
func canonicalJSONString(v interface{}) (string, error) {
b, err := jsonv2.Marshal(v)
if err != nil {
return "", err
}
return string(b), nil
}
func hashString(value string) string {
sum := sha256.Sum256([]byte(value))
return hex.EncodeToString(sum[:])
}

View file

@ -0,0 +1,230 @@
package tools
import (
"fmt"
"github.com/ZanzyTHEbar/dragonscale/pkg/tools/mapopsfb"
flatbuffers "github.com/google/flatbuffers/go"
)
type MapOperatorKind string
const (
MapOperatorLLM MapOperatorKind = "llm_map"
MapOperatorAgentic MapOperatorKind = "agentic_map"
mapPayloadVersion uint16 = 1
)
type MapRunSpec struct {
Version uint16
OperatorKind MapOperatorKind
Instruction string
TaskTemplate string
OutputSchemaJSON string
MaxRetries uint16
DelegatedScope string
KeptWork string
OriginChannel string
OriginChatID string
}
type MapItemInputRecord struct {
Version uint16
ItemIndex uint32
ItemJSON string
InputHash string
}
type MapItemOutputRecord struct {
Version uint16
ItemIndex uint32
Success bool
Attempts uint16
OutputJSON string
ErrorText string
OutputHash string
}
func EncodeMapRunSpecFlatBuffer(spec MapRunSpec) ([]byte, error) {
if spec.OperatorKind == "" {
return nil, fmt.Errorf("operator kind is required")
}
version := spec.Version
if version == 0 {
version = 1
}
maxRetries := spec.MaxRetries
if maxRetries == 0 {
maxRetries = 1
}
b := flatbuffers.NewBuilder(256)
operatorKind := b.CreateString(string(spec.OperatorKind))
instruction := createOptionalFBString(b, spec.Instruction)
taskTemplate := createOptionalFBString(b, spec.TaskTemplate)
outputSchemaJSON := createOptionalFBString(b, spec.OutputSchemaJSON)
delegatedScope := createOptionalFBString(b, spec.DelegatedScope)
keptWork := createOptionalFBString(b, spec.KeptWork)
originChannel := createOptionalFBString(b, spec.OriginChannel)
originChatID := createOptionalFBString(b, spec.OriginChatID)
mapopsfb.MapRunSpecStart(b)
mapopsfb.MapRunSpecAddVersion(b, version)
mapopsfb.MapRunSpecAddOperatorKind(b, operatorKind)
if instruction != 0 {
mapopsfb.MapRunSpecAddInstruction(b, instruction)
}
if taskTemplate != 0 {
mapopsfb.MapRunSpecAddTaskTemplate(b, taskTemplate)
}
if outputSchemaJSON != 0 {
mapopsfb.MapRunSpecAddOutputSchemaJson(b, outputSchemaJSON)
}
mapopsfb.MapRunSpecAddMaxRetries(b, maxRetries)
if delegatedScope != 0 {
mapopsfb.MapRunSpecAddDelegatedScope(b, delegatedScope)
}
if keptWork != 0 {
mapopsfb.MapRunSpecAddKeptWork(b, keptWork)
}
if originChannel != 0 {
mapopsfb.MapRunSpecAddOriginChannel(b, originChannel)
}
if originChatID != 0 {
mapopsfb.MapRunSpecAddOriginChatId(b, originChatID)
}
obj := mapopsfb.MapRunSpecEnd(b)
b.Finish(obj)
return b.FinishedBytes(), nil
}
func DecodeMapRunSpecFlatBuffer(buf []byte) (MapRunSpec, error) {
if len(buf) < flatbuffers.SizeUint32 {
return MapRunSpec{}, fmt.Errorf("flatbuffer payload too short")
}
specFB := mapopsfb.GetRootAsMapRunSpec(buf, 0)
version := specFB.Version()
if version != mapPayloadVersion {
return MapRunSpec{}, fmt.Errorf("unsupported map run spec version %d", version)
}
spec := MapRunSpec{
Version: version,
OperatorKind: MapOperatorKind(string(specFB.OperatorKind())),
Instruction: string(specFB.Instruction()),
TaskTemplate: string(specFB.TaskTemplate()),
OutputSchemaJSON: string(specFB.OutputSchemaJson()),
MaxRetries: specFB.MaxRetries(),
DelegatedScope: string(specFB.DelegatedScope()),
KeptWork: string(specFB.KeptWork()),
OriginChannel: string(specFB.OriginChannel()),
OriginChatID: string(specFB.OriginChatId()),
}
if spec.OperatorKind == "" {
return MapRunSpec{}, fmt.Errorf("invalid map run spec: operator kind is empty")
}
return spec, nil
}
func EncodeMapItemInputFlatBuffer(rec MapItemInputRecord) ([]byte, error) {
if rec.ItemJSON == "" {
return nil, fmt.Errorf("item JSON is required")
}
version := rec.Version
if version == 0 {
version = 1
}
b := flatbuffers.NewBuilder(128)
itemJSON := b.CreateString(rec.ItemJSON)
inputHash := createOptionalFBString(b, rec.InputHash)
mapopsfb.MapItemInputStart(b)
mapopsfb.MapItemInputAddVersion(b, version)
mapopsfb.MapItemInputAddItemIndex(b, rec.ItemIndex)
mapopsfb.MapItemInputAddItemJson(b, itemJSON)
if inputHash != 0 {
mapopsfb.MapItemInputAddInputHash(b, inputHash)
}
obj := mapopsfb.MapItemInputEnd(b)
b.Finish(obj)
return b.FinishedBytes(), nil
}
func DecodeMapItemInputFlatBuffer(buf []byte) (MapItemInputRecord, error) {
if len(buf) < flatbuffers.SizeUint32 {
return MapItemInputRecord{}, fmt.Errorf("flatbuffer payload too short")
}
recFB := mapopsfb.GetRootAsMapItemInput(buf, 0)
version := recFB.Version()
if version != mapPayloadVersion {
return MapItemInputRecord{}, fmt.Errorf("unsupported map item input version %d", version)
}
rec := MapItemInputRecord{
Version: version,
ItemIndex: recFB.ItemIndex(),
ItemJSON: string(recFB.ItemJson()),
InputHash: string(recFB.InputHash()),
}
if rec.ItemJSON == "" {
return MapItemInputRecord{}, fmt.Errorf("invalid map item input: item JSON is empty")
}
return rec, nil
}
func EncodeMapItemOutputFlatBuffer(rec MapItemOutputRecord) ([]byte, error) {
version := rec.Version
if version == 0 {
version = 1
}
b := flatbuffers.NewBuilder(128)
outputJSON := createOptionalFBString(b, rec.OutputJSON)
errorText := createOptionalFBString(b, rec.ErrorText)
outputHash := createOptionalFBString(b, rec.OutputHash)
mapopsfb.MapItemOutputStart(b)
mapopsfb.MapItemOutputAddVersion(b, version)
mapopsfb.MapItemOutputAddItemIndex(b, rec.ItemIndex)
mapopsfb.MapItemOutputAddSuccess(b, rec.Success)
mapopsfb.MapItemOutputAddAttempts(b, rec.Attempts)
if outputJSON != 0 {
mapopsfb.MapItemOutputAddOutputJson(b, outputJSON)
}
if errorText != 0 {
mapopsfb.MapItemOutputAddErrorText(b, errorText)
}
if outputHash != 0 {
mapopsfb.MapItemOutputAddOutputHash(b, outputHash)
}
obj := mapopsfb.MapItemOutputEnd(b)
b.Finish(obj)
return b.FinishedBytes(), nil
}
func DecodeMapItemOutputFlatBuffer(buf []byte) (MapItemOutputRecord, error) {
if len(buf) < flatbuffers.SizeUint32 {
return MapItemOutputRecord{}, fmt.Errorf("flatbuffer payload too short")
}
recFB := mapopsfb.GetRootAsMapItemOutput(buf, 0)
version := recFB.Version()
if version != mapPayloadVersion {
return MapItemOutputRecord{}, fmt.Errorf("unsupported map item output version %d", version)
}
return MapItemOutputRecord{
Version: version,
ItemIndex: recFB.ItemIndex(),
Success: recFB.Success(),
Attempts: recFB.Attempts(),
OutputJSON: string(recFB.OutputJson()),
ErrorText: string(recFB.ErrorText()),
OutputHash: string(recFB.OutputHash()),
}, nil
}
func createOptionalFBString(b *flatbuffers.Builder, value string) flatbuffers.UOffsetT {
if value == "" {
return 0
}
return b.CreateString(value)
}

View file

@ -0,0 +1,83 @@
package tools
import (
"testing"
)
func TestMapRunSpecFlatBuffer_RoundTrip(t *testing.T) {
original := MapRunSpec{
Version: 1,
OperatorKind: MapOperatorLLM,
Instruction: "extract label and priority",
TaskTemplate: "",
OutputSchemaJSON: `{"type":"object"}`,
MaxRetries: 3,
DelegatedScope: "",
KeptWork: "",
OriginChannel: "cli",
OriginChatID: "direct",
}
data, err := EncodeMapRunSpecFlatBuffer(original)
if err != nil {
t.Fatalf("encode run spec: %v", err)
}
decoded, err := DecodeMapRunSpecFlatBuffer(data)
if err != nil {
t.Fatalf("decode run spec: %v", err)
}
if decoded.OperatorKind != original.OperatorKind {
t.Fatalf("operator kind mismatch: got %q want %q", decoded.OperatorKind, original.OperatorKind)
}
if decoded.Instruction != original.Instruction {
t.Fatalf("instruction mismatch: got %q want %q", decoded.Instruction, original.Instruction)
}
if decoded.OutputSchemaJSON != original.OutputSchemaJSON {
t.Fatalf("schema mismatch: got %q want %q", decoded.OutputSchemaJSON, original.OutputSchemaJSON)
}
if decoded.MaxRetries != original.MaxRetries {
t.Fatalf("max retries mismatch: got %d want %d", decoded.MaxRetries, original.MaxRetries)
}
}
func TestMapItemFlatBuffer_RoundTrip(t *testing.T) {
input := MapItemInputRecord{
Version: 1,
ItemIndex: 7,
ItemJSON: `{"name":"alpha","priority":1}`,
InputHash: "abc123",
}
inBuf, err := EncodeMapItemInputFlatBuffer(input)
if err != nil {
t.Fatalf("encode input: %v", err)
}
inDecoded, err := DecodeMapItemInputFlatBuffer(inBuf)
if err != nil {
t.Fatalf("decode input: %v", err)
}
if inDecoded.ItemIndex != input.ItemIndex || inDecoded.ItemJSON != input.ItemJSON || inDecoded.InputHash != input.InputHash {
t.Fatalf("input mismatch: got %+v want %+v", inDecoded, input)
}
output := MapItemOutputRecord{
Version: 1,
ItemIndex: 7,
Success: true,
Attempts: 2,
OutputJSON: `{"label":"alpha"}`,
ErrorText: "",
OutputHash: "def456",
}
outBuf, err := EncodeMapItemOutputFlatBuffer(output)
if err != nil {
t.Fatalf("encode output: %v", err)
}
outDecoded, err := DecodeMapItemOutputFlatBuffer(outBuf)
if err != nil {
t.Fatalf("decode output: %v", err)
}
if outDecoded.ItemIndex != output.ItemIndex || outDecoded.Success != output.Success || outDecoded.Attempts != output.Attempts || outDecoded.OutputJSON != output.OutputJSON {
t.Fatalf("output mismatch: got %+v want %+v", outDecoded, output)
}
}

View file

@ -0,0 +1,33 @@
// FlatBuffers contract for map operator persistence payloads.
// Boundary JSON/JSONL is converted to these binary records before DB persistence.
namespace mapopsfb;
table MapRunSpec {
version: uint16 = 1;
operator_kind: string (required); // llm_map | agentic_map
instruction: string;
task_template: string;
output_schema_json: string;
max_retries: uint16 = 1;
delegated_scope: string;
kept_work: string;
origin_channel: string;
origin_chat_id: string;
}
table MapItemInput {
version: uint16 = 1;
item_index: uint32;
item_json: string (required); // canonical boundary JSON object
input_hash: string;
}
table MapItemOutput {
version: uint16 = 1;
item_index: uint32;
success: bool = false;
attempts: uint16 = 0;
output_json: string;
error_text: string;
output_hash: string;
}

216
pkg/tools/map_run_tools.go Normal file
View file

@ -0,0 +1,216 @@
package tools
import (
"context"
"fmt"
"strings"
"github.com/ZanzyTHEbar/dragonscale/pkg/ids"
jsonv2 "github.com/go-json-experiment/json"
)
type MapRunStatusTool struct {
runtime *MapRuntime
}
func NewMapRunStatusTool(runtime *MapRuntime) *MapRunStatusTool {
return &MapRunStatusTool{runtime: runtime}
}
func (t *MapRunStatusTool) Name() string {
return "map_run_status"
}
func (t *MapRunStatusTool) Description() string {
return "Return current status/progress for an llm_map or agentic_map run."
}
func (t *MapRunStatusTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"run_id": map[string]interface{}{
"type": "string",
"description": "Map run ID returned by llm_map or agentic_map.",
},
"process_steps": map[string]interface{}{
"type": "integer",
"description": "Optional number of worker steps to execute before reading status (default 8).",
},
},
"required": []string{"run_id"},
}
}
func (t *MapRunStatusTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
if t.runtime == nil {
return ErrorResult("map runtime is not configured").WithError(fmt.Errorf("map runtime is nil"))
}
runIDRaw, ok := args["run_id"].(string)
if !ok || strings.TrimSpace(runIDRaw) == "" {
return ErrorResult("run_id is required").WithError(fmt.Errorf("run_id is required"))
}
runID, err := ids.Parse(strings.TrimSpace(runIDRaw))
if err != nil {
return ErrorResult("run_id must be a valid UUID").WithError(err)
}
steps, err := parseMapLimit(args, "process_steps", 8, 500)
if err != nil {
return ErrorResult(err.Error()).WithError(err)
}
if steps > 0 {
if _, err := t.runtime.ProcessPending(ctx, steps); err != nil {
return ErrorResult(fmt.Sprintf("failed to process map jobs: %v", err)).WithError(err)
}
}
run, err := t.runtime.GetRun(ctx, runID)
if err != nil {
return ErrorResult(fmt.Sprintf("failed to load map run: %v", err)).WithError(err)
}
payload := toMapRunProjection(run)
data, err := jsonv2.Marshal(payload)
if err != nil {
return ErrorResult("failed to serialize map run status").WithError(err)
}
return &ToolResult{
ForLLM: string(data),
ForUser: string(data),
Silent: false,
IsError: false,
Async: false,
}
}
type MapRunReadTool struct {
runtime *MapRuntime
}
func NewMapRunReadTool(runtime *MapRuntime) *MapRunReadTool {
return &MapRunReadTool{runtime: runtime}
}
func (t *MapRunReadTool) Name() string {
return "map_run_read"
}
func (t *MapRunReadTool) Description() string {
return "Read paged item results for a map run; output as JSON or generated JSONL."
}
func (t *MapRunReadTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"run_id": map[string]interface{}{
"type": "string",
"description": "Map run ID returned by llm_map or agentic_map.",
},
"offset": map[string]interface{}{
"type": "integer",
"description": "Pagination offset (default 0).",
},
"limit": map[string]interface{}{
"type": "integer",
"description": "Pagination limit (default 100, max 500).",
},
"format": map[string]interface{}{
"type": "string",
"description": "Projection format: json (default) or jsonl.",
},
"process_steps": map[string]interface{}{
"type": "integer",
"description": "Optional worker steps to process before reading (default 8).",
},
},
"required": []string{"run_id"},
}
}
func (t *MapRunReadTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
if t.runtime == nil {
return ErrorResult("map runtime is not configured").WithError(fmt.Errorf("map runtime is nil"))
}
runIDRaw, ok := args["run_id"].(string)
if !ok || strings.TrimSpace(runIDRaw) == "" {
return ErrorResult("run_id is required").WithError(fmt.Errorf("run_id is required"))
}
runID, err := ids.Parse(strings.TrimSpace(runIDRaw))
if err != nil {
return ErrorResult("run_id must be a valid UUID").WithError(err)
}
offset, err := parseMapLimit(args, "offset", 0, 1_000_000)
if err != nil {
return ErrorResult(err.Error()).WithError(err)
}
limit, err := parseMapLimit(args, "limit", defaultMapReadLimit, maxMapReadLimit)
if err != nil {
return ErrorResult(err.Error()).WithError(err)
}
format, err := parseMapReadFormat(args)
if err != nil {
return ErrorResult(err.Error()).WithError(err)
}
steps, err := parseMapLimit(args, "process_steps", 8, 500)
if err != nil {
return ErrorResult(err.Error()).WithError(err)
}
if steps > 0 {
if _, err := t.runtime.ProcessPending(ctx, steps); err != nil {
return ErrorResult(fmt.Sprintf("failed to process map jobs: %v", err)).WithError(err)
}
}
run, err := t.runtime.GetRun(ctx, runID)
if err != nil {
return ErrorResult(fmt.Sprintf("failed to load map run: %v", err)).WithError(err)
}
items, err := t.runtime.ReadRunItems(ctx, runID, offset, limit)
if err != nil {
return ErrorResult(fmt.Sprintf("failed to load map run items: %v", err)).WithError(err)
}
payload := map[string]interface{}{
"run": toMapRunProjection(run),
"offset": offset,
"limit": limit,
"format": format,
}
if format == "jsonl" {
jsonlText, err := encodeMapItemsJSONL(items)
if err != nil {
return ErrorResult("failed to encode JSONL projection").WithError(err)
}
payload["items_jsonl"] = jsonlText
payload["item_count"] = len(items)
} else {
payload["items"] = items
}
data, err := jsonv2.Marshal(payload)
if err != nil {
return ErrorResult("failed to serialize map run page").WithError(err)
}
return &ToolResult{
ForLLM: string(data),
ForUser: string(data),
Silent: false,
IsError: false,
Async: false,
}
}
func encodeMapItemsJSONL(items []MapItemProjection) (string, error) {
if len(items) == 0 {
return "", nil
}
lines := make([]string, 0, len(items))
for _, item := range items {
b, err := jsonv2.Marshal(item)
if err != nil {
return "", err
}
lines = append(lines, string(b))
}
return strings.Join(lines, "\n"), nil
}

833
pkg/tools/map_runtime.go Normal file
View file

@ -0,0 +1,833 @@
package tools
import (
"context"
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
"sync"
"time"
fantasy "charm.land/fantasy"
"github.com/ZanzyTHEbar/dragonscale/pkg/ids"
"github.com/ZanzyTHEbar/dragonscale/pkg/logger"
memsqlc "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc"
"github.com/ZanzyTHEbar/dragonscale/pkg/worker"
jsonv2 "github.com/go-json-experiment/json"
)
const (
mapRunStatusQueued = "queued"
mapRunStatusRunning = "running"
mapRunStatusSucceeded = "succeeded"
mapRunStatusFailed = "failed"
mapRunStatusCancelled = "cancelled"
mapItemStatusQueued = "queued"
mapItemStatusRunning = "running"
mapItemStatusSucceeded = "succeeded"
mapItemStatusFailed = "failed"
mapJobKindLLMItem = "map_item_llm"
mapJobKindAgenticItem = "map_item_agentic"
)
type mapJobPayload struct {
RunID string `json:"run_id"`
ItemID string `json:"item_id"`
}
type MapRunProjection struct {
RunID string `json:"run_id"`
OperatorKind string `json:"operator_kind"`
Status string `json:"status"`
TotalItems int64 `json:"total_items"`
QueuedItems int64 `json:"queued_items"`
RunningItems int64 `json:"running_items"`
SucceededItems int64 `json:"succeeded_items"`
FailedItems int64 `json:"failed_items"`
LastError string `json:"last_error,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
CompletedAt string `json:"completed_at,omitempty"`
}
type MapItemProjection struct {
ItemID string `json:"item_id"`
Index int64 `json:"index"`
Status string `json:"status"`
Attempts int64 `json:"attempts"`
Input interface{} `json:"input,omitempty"`
Output interface{} `json:"output,omitempty"`
Error string `json:"error,omitempty"`
InputHash string `json:"input_hash,omitempty"`
OutputHash string `json:"output_hash,omitempty"`
}
type MapRuntime struct {
queries *memsqlc.Queries
agentID string
llmModel fantasy.LanguageModel
llmModelID string
subagentManager *SubagentManager
workerLockedBy string
processMu sync.Mutex
}
func NewMapRuntime(
queries *memsqlc.Queries,
agentID string,
llmModel fantasy.LanguageModel,
llmModelID string,
subagentManager *SubagentManager,
) *MapRuntime {
if queries == nil {
return nil
}
return &MapRuntime{
queries: queries,
agentID: agentID,
llmModel: llmModel,
llmModelID: llmModelID,
subagentManager: subagentManager,
workerLockedBy: "map-runtime",
}
}
func (rt *MapRuntime) EnqueueRun(
ctx context.Context,
sessionKey string,
spec MapRunSpec,
items []mapBoundaryItem,
idempotencyKey string,
) (memsqlc.MapRun, bool, error) {
if rt == nil || rt.queries == nil {
return memsqlc.MapRun{}, false, fmt.Errorf("map runtime is not configured")
}
if len(items) == 0 {
return memsqlc.MapRun{}, false, fmt.Errorf("at least one item is required")
}
if sessionKey == "" {
sessionKey = "default"
}
if spec.OperatorKind != MapOperatorLLM && spec.OperatorKind != MapOperatorAgentic {
return memsqlc.MapRun{}, false, fmt.Errorf("unsupported operator kind %q", spec.OperatorKind)
}
seenIndices := make(map[int]struct{}, len(items))
for _, item := range items {
if item.Index < 0 || uint64(item.Index) > uint64(^uint32(0)) {
return memsqlc.MapRun{}, false, fmt.Errorf("item index %d out of uint32 range", item.Index)
}
if _, exists := seenIndices[item.Index]; exists {
return memsqlc.MapRun{}, false, fmt.Errorf("duplicate item index %d", item.Index)
}
seenIndices[item.Index] = struct{}{}
}
specFB, err := EncodeMapRunSpecFlatBuffer(spec)
if err != nil {
return memsqlc.MapRun{}, false, fmt.Errorf("encode run spec: %w", err)
}
idemPtr := optionalStringPtr(strings.TrimSpace(idempotencyKey))
if idemPtr != nil {
existing, err := rt.queries.GetMapRunByIdempotencyKey(ctx, memsqlc.GetMapRunByIdempotencyKeyParams{
AgentID: rt.agentID,
SessionKey: sessionKey,
OperatorKind: string(spec.OperatorKind),
IdempotencyKey: idemPtr,
})
if err == nil {
return existing, true, nil
}
if !errors.Is(err, sql.ErrNoRows) {
return memsqlc.MapRun{}, false, fmt.Errorf("lookup idempotent run: %w", err)
}
}
runID := ids.New()
run, err := rt.queries.InsertMapRun(ctx, memsqlc.InsertMapRunParams{
ID: runID,
AgentID: rt.agentID,
SessionKey: sessionKey,
OperatorKind: string(spec.OperatorKind),
IdempotencyKey: idemPtr,
Status: mapRunStatusQueued,
TotalItems: int64(len(items)),
QueuedItems: int64(len(items)),
RunningItems: 0,
SucceededItems: 0,
FailedItems: 0,
SpecFb: specFB,
LastError: nil,
})
if err != nil {
if idemPtr != nil && isUniqueConstraintError(err) {
existing, lookupErr := rt.queries.GetMapRunByIdempotencyKey(ctx, memsqlc.GetMapRunByIdempotencyKeyParams{
AgentID: rt.agentID,
SessionKey: sessionKey,
OperatorKind: string(spec.OperatorKind),
IdempotencyKey: idemPtr,
})
if lookupErr == nil {
return existing, true, nil
}
}
return memsqlc.MapRun{}, false, fmt.Errorf("insert map run: %w", err)
}
jobKind := mapJobKindLLMItem
if spec.OperatorKind == MapOperatorAgentic {
jobKind = mapJobKindAgenticItem
}
maxAttempts := int64(spec.MaxRetries) + 1
if maxAttempts < 1 {
maxAttempts = 1
}
for _, item := range items {
inputFB, encErr := EncodeMapItemInputFlatBuffer(MapItemInputRecord{
ItemIndex: uint32(item.Index),
ItemJSON: item.ItemJSON,
InputHash: item.InputHash,
})
if encErr != nil {
msg := fmt.Sprintf("encode item %d: %v", item.Index, encErr)
_, _ = rt.failRun(ctx, run.ID, &msg)
return memsqlc.MapRun{}, false, errors.New(msg)
}
itemID := ids.New()
if _, err := rt.queries.InsertMapItem(ctx, memsqlc.InsertMapItemParams{
ID: itemID,
RunID: run.ID,
ItemIndex: int64(item.Index),
Status: mapItemStatusQueued,
Attempts: 0,
LastError: nil,
InputFb: inputFB,
OutputFb: nil,
InputHash: optionalStringPtr(item.InputHash),
OutputHash: nil,
}); err != nil {
msg := fmt.Sprintf("insert map item %d: %v", item.Index, err)
_, _ = rt.failRun(ctx, run.ID, &msg)
return memsqlc.MapRun{}, false, errors.New(msg)
}
dedupeKey := fmt.Sprintf("map:%s:%d", run.ID.String(), item.Index)
if _, err := rt.queries.EnqueueJob(ctx, memsqlc.EnqueueJobParams{
ID: ids.New(),
Kind: jobKind,
DedupeKey: &dedupeKey,
MaxAttempts: maxAttempts,
RunAt: time.Now().UTC(),
PayloadJson: []byte(`{}`),
}); err != nil {
msg := fmt.Sprintf("enqueue map item %d: %v", item.Index, err)
_, _ = rt.failRun(ctx, run.ID, &msg)
return memsqlc.MapRun{}, false, errors.New(msg)
}
}
logger.InfoCF("map_runtime", "map run enqueued", map[string]interface{}{
"run_id": run.ID.String(),
"operator_kind": run.OperatorKind,
"items": len(items),
"idempotency": idempotencyKey,
})
return run, false, nil
}
func (rt *MapRuntime) ProcessPending(ctx context.Context, maxSteps int) (int, error) {
if rt == nil || rt.queries == nil {
return 0, nil
}
if maxSteps <= 0 {
maxSteps = 1
}
opts := &worker.Options{
LockedBy: rt.workerLockedBy,
Handlers: map[string]worker.HandlerFunc{
mapJobKindLLMItem: rt.handleLLMMapJob,
mapJobKindAgenticItem: rt.handleAgenticMapJob,
},
Backoff: func(_ int) time.Duration {
return 10 * time.Millisecond
},
Now: func() time.Time {
return time.Now().UTC()
},
}
rt.processMu.Lock()
defer rt.processMu.Unlock()
processed := 0
for processed < maxSteps {
if _, err := rt.queries.FindNextRunnableJob(ctx); err != nil {
if errors.Is(err, sql.ErrNoRows) {
break
}
return processed, err
}
if err := worker.RunOnce(ctx, rt.queries, opts); err != nil {
return processed, err
}
processed++
}
return processed, nil
}
func (rt *MapRuntime) ProcessRunToTerminal(ctx context.Context, runID ids.UUID, maxSteps int) (memsqlc.MapRun, error) {
if maxSteps <= 0 {
maxSteps = 2000
}
for i := 0; i < maxSteps; i++ {
run, err := rt.queries.GetMapRunByID(ctx, memsqlc.GetMapRunByIDParams{ID: runID})
if err != nil {
return memsqlc.MapRun{}, err
}
if isTerminalMapRunStatus(run.Status) {
return run, nil
}
processed, err := rt.ProcessPending(ctx, 1)
if err != nil {
return memsqlc.MapRun{}, err
}
if processed == 0 {
select {
case <-ctx.Done():
return memsqlc.MapRun{}, ctx.Err()
case <-time.After(10 * time.Millisecond):
}
}
}
return memsqlc.MapRun{}, fmt.Errorf("map run %s did not reach terminal status within %d steps", runID.String(), maxSteps)
}
func (rt *MapRuntime) GetRun(ctx context.Context, runID ids.UUID) (memsqlc.MapRun, error) {
if rt == nil || rt.queries == nil {
return memsqlc.MapRun{}, fmt.Errorf("map runtime is not configured")
}
return rt.queries.GetMapRunByID(ctx, memsqlc.GetMapRunByIDParams{ID: runID})
}
func (rt *MapRuntime) ReadRunItems(ctx context.Context, runID ids.UUID, offset, limit int) ([]MapItemProjection, error) {
if rt == nil || rt.queries == nil {
return nil, fmt.Errorf("map runtime is not configured")
}
rows, err := rt.queries.ListMapItemsByRunPaged(ctx, memsqlc.ListMapItemsByRunPagedParams{
RunID: runID,
Lim: int64(limit),
Off: int64(offset),
})
if err != nil {
return nil, err
}
items := make([]MapItemProjection, 0, len(rows))
for _, row := range rows {
inputRec, inErr := DecodeMapItemInputFlatBuffer(row.InputFb)
if inErr != nil {
return nil, fmt.Errorf("decode item input %s: %w", row.ID.String(), inErr)
}
var inputAny interface{}
if err := jsonv2.Unmarshal([]byte(inputRec.ItemJSON), &inputAny); err != nil {
return nil, fmt.Errorf("decode item JSON %s: %w", row.ID.String(), err)
}
proj := MapItemProjection{
ItemID: row.ID.String(),
Index: row.ItemIndex,
Status: row.Status,
Attempts: row.Attempts,
Input: inputAny,
}
if row.InputHash != nil {
proj.InputHash = *row.InputHash
}
if row.LastError != nil {
proj.Error = *row.LastError
}
if len(row.OutputFb) > 0 {
outputRec, outErr := DecodeMapItemOutputFlatBuffer(row.OutputFb)
if outErr != nil {
return nil, fmt.Errorf("decode item output %s: %w", row.ID.String(), outErr)
}
if row.OutputHash != nil && outputRec.OutputHash != "" && *row.OutputHash != outputRec.OutputHash {
return nil, fmt.Errorf("output hash mismatch for item %s", row.ID.String())
}
if outputRec.ErrorText != "" && proj.Error == "" {
proj.Error = outputRec.ErrorText
}
if outputRec.OutputHash != "" {
proj.OutputHash = outputRec.OutputHash
} else if row.OutputHash != nil {
proj.OutputHash = *row.OutputHash
}
if strings.TrimSpace(outputRec.OutputJSON) != "" {
var outputAny interface{}
if err := jsonv2.Unmarshal([]byte(outputRec.OutputJSON), &outputAny); err == nil {
proj.Output = outputAny
} else {
// Keep malformed model output visible to callers for debugging.
proj.Output = outputRec.OutputJSON
}
}
}
items = append(items, proj)
}
return items, nil
}
func (rt *MapRuntime) handleLLMMapJob(ctx context.Context, q *memsqlc.Queries, job memsqlc.Job) error {
if rt.llmModel == nil {
return fmt.Errorf("llm_map runtime model is not configured")
}
item, run, spec, inputRec, err := rt.loadMapJobState(ctx, q, job)
if err != nil {
return err
}
if spec.OperatorKind != MapOperatorLLM {
errMsg := fmt.Sprintf("run %s operator kind mismatch: expected llm_map got %s", run.ID.String(), spec.OperatorKind)
_, _ = q.MarkMapItemFailed(ctx, memsqlc.MarkMapItemFailedParams{LastError: &errMsg, ID: item.ID})
if _, refreshErr := rt.refreshRunProgress(ctx, q, run.ID, &errMsg); refreshErr != nil {
logger.WarnCF("map_runtime", "failed to refresh run progress", map[string]interface{}{
"run_id": run.ID.String(),
"error": refreshErr.Error(),
})
}
return nil
}
outJSON, callErr := rt.executeLLMMapItem(ctx, spec, inputRec.ItemJSON)
if callErr != nil {
if job.Attempts >= job.MaxAttempts {
msg := callErr.Error()
_, _ = q.MarkMapItemFailed(ctx, memsqlc.MarkMapItemFailedParams{LastError: &msg, ID: item.ID})
if _, refreshErr := rt.refreshRunProgress(ctx, q, run.ID, &msg); refreshErr != nil {
logger.WarnCF("map_runtime", "failed to refresh run progress", map[string]interface{}{
"run_id": run.ID.String(),
"error": refreshErr.Error(),
})
}
return nil
}
return callErr
}
outHash := hashString(outJSON)
outputFB, err := EncodeMapItemOutputFlatBuffer(MapItemOutputRecord{
ItemIndex: uint32(item.ItemIndex),
Success: true,
Attempts: uint16(item.Attempts),
OutputJSON: outJSON,
OutputHash: outHash,
})
if err != nil {
msg := fmt.Sprintf("encode output for item %s: %v", item.ID.String(), err)
_, _ = q.MarkMapItemFailed(ctx, memsqlc.MarkMapItemFailedParams{LastError: &msg, ID: item.ID})
if _, refreshErr := rt.refreshRunProgress(ctx, q, run.ID, &msg); refreshErr != nil {
logger.WarnCF("map_runtime", "failed to refresh run progress", map[string]interface{}{
"run_id": run.ID.String(),
"error": refreshErr.Error(),
})
}
return nil
}
if _, err := q.MarkMapItemSucceeded(ctx, memsqlc.MarkMapItemSucceededParams{
OutputFb: outputFB,
OutputHash: &outHash,
ID: item.ID,
}); err != nil {
return err
}
if _, refreshErr := rt.refreshRunProgress(ctx, q, run.ID, nil); refreshErr != nil {
logger.WarnCF("map_runtime", "failed to refresh run progress", map[string]interface{}{
"run_id": run.ID.String(),
"error": refreshErr.Error(),
})
}
return nil
}
func (rt *MapRuntime) handleAgenticMapJob(ctx context.Context, q *memsqlc.Queries, job memsqlc.Job) error {
if rt.subagentManager == nil {
return fmt.Errorf("agentic_map runtime manager is not configured")
}
item, run, spec, inputRec, err := rt.loadMapJobState(ctx, q, job)
if err != nil {
return err
}
if spec.OperatorKind != MapOperatorAgentic {
errMsg := fmt.Sprintf("run %s operator kind mismatch: expected agentic_map got %s", run.ID.String(), spec.OperatorKind)
_, _ = q.MarkMapItemFailed(ctx, memsqlc.MarkMapItemFailedParams{LastError: &errMsg, ID: item.ID})
if _, refreshErr := rt.refreshRunProgress(ctx, q, run.ID, &errMsg); refreshErr != nil {
logger.WarnCF("map_runtime", "failed to refresh run progress", map[string]interface{}{
"run_id": run.ID.String(),
"error": refreshErr.Error(),
})
}
return nil
}
outJSON, callErr := rt.executeAgenticMapItem(ctx, spec, int(item.ItemIndex), inputRec.ItemJSON)
if callErr != nil {
if job.Attempts >= job.MaxAttempts {
msg := callErr.Error()
_, _ = q.MarkMapItemFailed(ctx, memsqlc.MarkMapItemFailedParams{LastError: &msg, ID: item.ID})
if _, refreshErr := rt.refreshRunProgress(ctx, q, run.ID, &msg); refreshErr != nil {
logger.WarnCF("map_runtime", "failed to refresh run progress", map[string]interface{}{
"run_id": run.ID.String(),
"error": refreshErr.Error(),
})
}
return nil
}
return callErr
}
outHash := hashString(outJSON)
outputFB, err := EncodeMapItemOutputFlatBuffer(MapItemOutputRecord{
ItemIndex: uint32(item.ItemIndex),
Success: true,
Attempts: uint16(item.Attempts),
OutputJSON: outJSON,
OutputHash: outHash,
})
if err != nil {
msg := fmt.Sprintf("encode output for item %s: %v", item.ID.String(), err)
_, _ = q.MarkMapItemFailed(ctx, memsqlc.MarkMapItemFailedParams{LastError: &msg, ID: item.ID})
if _, refreshErr := rt.refreshRunProgress(ctx, q, run.ID, &msg); refreshErr != nil {
logger.WarnCF("map_runtime", "failed to refresh run progress", map[string]interface{}{
"run_id": run.ID.String(),
"error": refreshErr.Error(),
})
}
return nil
}
if _, err := q.MarkMapItemSucceeded(ctx, memsqlc.MarkMapItemSucceededParams{
OutputFb: outputFB,
OutputHash: &outHash,
ID: item.ID,
}); err != nil {
return err
}
if _, refreshErr := rt.refreshRunProgress(ctx, q, run.ID, nil); refreshErr != nil {
logger.WarnCF("map_runtime", "failed to refresh run progress", map[string]interface{}{
"run_id": run.ID.String(),
"error": refreshErr.Error(),
})
}
return nil
}
func (rt *MapRuntime) loadMapJobState(
ctx context.Context,
q *memsqlc.Queries,
job memsqlc.Job,
) (memsqlc.MapItem, memsqlc.MapRun, MapRunSpec, MapItemInputRecord, error) {
itemID, dedupeErr := mapJobItemIDFromDedupeKey(ctx, q, job.DedupeKey)
if dedupeErr != nil {
payload, payloadErr := parseMapJobPayload(job.PayloadJson)
if payloadErr != nil {
return memsqlc.MapItem{}, memsqlc.MapRun{}, MapRunSpec{}, MapItemInputRecord{}, fmt.Errorf(
"invalid map job identity: dedupe_key_error=%v payload_error=%v",
dedupeErr,
payloadErr,
)
}
parsedID, parseErr := ids.Parse(payload.ItemID)
if parseErr != nil {
return memsqlc.MapItem{}, memsqlc.MapRun{}, MapRunSpec{}, MapItemInputRecord{}, fmt.Errorf("invalid item id in job payload: %w", parseErr)
}
itemID = parsedID
}
item, err := q.MarkMapItemRunning(ctx, memsqlc.MarkMapItemRunningParams{ID: itemID})
if err != nil {
return memsqlc.MapItem{}, memsqlc.MapRun{}, MapRunSpec{}, MapItemInputRecord{}, err
}
run, err := q.GetMapRunByID(ctx, memsqlc.GetMapRunByIDParams{ID: item.RunID})
if err != nil {
return memsqlc.MapItem{}, memsqlc.MapRun{}, MapRunSpec{}, MapItemInputRecord{}, err
}
spec, err := DecodeMapRunSpecFlatBuffer(run.SpecFb)
if err != nil {
return memsqlc.MapItem{}, memsqlc.MapRun{}, MapRunSpec{}, MapItemInputRecord{}, fmt.Errorf("decode run spec: %w", err)
}
inputRec, err := DecodeMapItemInputFlatBuffer(item.InputFb)
if err != nil {
return memsqlc.MapItem{}, memsqlc.MapRun{}, MapRunSpec{}, MapItemInputRecord{}, fmt.Errorf("decode item input: %w", err)
}
return item, run, spec, inputRec, nil
}
func (rt *MapRuntime) executeLLMMapItem(ctx context.Context, spec MapRunSpec, itemJSON string) (string, error) {
maxOut := int64(512)
prompt := fmt.Sprintf(
"Instruction:\n%s\n\nItem JSON:\n%s\n\nReturn exactly one JSON object and nothing else.",
spec.Instruction,
itemJSON,
)
call := fantasy.Call{
Prompt: fantasy.Prompt{
fantasy.NewSystemMessage("You are a pure mapper. Return only strict JSON object output."),
fantasy.NewUserMessage(prompt),
},
MaxOutputTokens: &maxOut,
}
resp, err := rt.llmModel.Generate(ctx, call)
if err != nil {
return "", err
}
outText := strings.TrimSpace(resp.Content.Text())
var out map[string]interface{}
if err := jsonv2.Unmarshal([]byte(outText), &out); err != nil {
return "", fmt.Errorf("llm_map output is not valid JSON object: %w", err)
}
if strings.TrimSpace(spec.OutputSchemaJSON) != "" {
var schema map[string]interface{}
if err := jsonv2.Unmarshal([]byte(spec.OutputSchemaJSON), &schema); err != nil {
return "", fmt.Errorf("invalid output schema in run spec: %w", err)
}
if err := validateLLMMapOutputSchema(out, schema); err != nil {
return "", fmt.Errorf("schema validation failed: %w", err)
}
}
outJSON, err := canonicalJSONString(out)
if err != nil {
return "", fmt.Errorf("serialize model output: %w", err)
}
return outJSON, nil
}
func (rt *MapRuntime) executeAgenticMapItem(ctx context.Context, spec MapRunSpec, index int, itemJSON string) (string, error) {
if strings.TrimSpace(spec.TaskTemplate) == "" {
return "", fmt.Errorf("task_template is required for agentic_map")
}
if !strings.Contains(spec.TaskTemplate, "{{item_json}}") || !strings.Contains(spec.TaskTemplate, "{{index}}") {
return "", fmt.Errorf("task_template must include both {{item_json}} and {{index}} placeholders")
}
task := strings.ReplaceAll(spec.TaskTemplate, "{{item_json}}", itemJSON)
task = strings.ReplaceAll(task, "{{index}}", fmt.Sprintf("%d", index))
subTool := NewSubagentTool(rt.subagentManager)
originChannel := spec.OriginChannel
if originChannel == "" {
originChannel = "cli"
}
originChatID := spec.OriginChatID
if originChatID == "" {
originChatID = "direct"
}
subTool.SetContext(originChannel, originChatID)
callArgs := map[string]interface{}{
"task": task,
"label": fmt.Sprintf("agentic-map-%d", index),
}
if strings.TrimSpace(spec.DelegatedScope) != "" {
callArgs["delegated_scope"] = spec.DelegatedScope
}
if strings.TrimSpace(spec.KeptWork) != "" {
callArgs["kept_work"] = spec.KeptWork
}
result := subTool.Execute(ctx, callArgs)
if result == nil {
return "", fmt.Errorf("subagent returned nil result")
}
if result.IsError {
if result.Err != nil {
return "", result.Err
}
return "", errors.New(result.ForLLM)
}
outputPayload := map[string]interface{}{
"for_user": result.ForUser,
"for_llm": result.ForLLM,
}
outJSON, err := canonicalJSONString(outputPayload)
if err != nil {
return "", fmt.Errorf("serialize agentic output: %w", err)
}
return outJSON, nil
}
func (rt *MapRuntime) refreshRunProgress(
ctx context.Context,
q *memsqlc.Queries,
runID ids.UUID,
lastErr *string,
) (memsqlc.MapRun, error) {
total, err := q.CountMapItemsByRun(ctx, memsqlc.CountMapItemsByRunParams{RunID: runID})
if err != nil {
return memsqlc.MapRun{}, err
}
statusCounts, err := q.CountMapItemsByRunAndStatus(ctx, memsqlc.CountMapItemsByRunAndStatusParams{RunID: runID})
if err != nil {
return memsqlc.MapRun{}, err
}
var queued, running, succeeded, failed int64
for _, row := range statusCounts {
switch row.Status {
case mapItemStatusQueued:
queued = row.Count
case mapItemStatusRunning:
running = row.Count
case mapItemStatusSucceeded:
succeeded = row.Count
case mapItemStatusFailed:
failed = row.Count
}
}
status := mapRunStatusQueued
if running > 0 {
status = mapRunStatusRunning
}
if queued > 0 && running == 0 {
status = mapRunStatusQueued
}
if queued == 0 && running == 0 {
switch {
case failed > 0:
status = mapRunStatusFailed
case succeeded == total:
status = mapRunStatusSucceeded
default:
status = mapRunStatusFailed
}
}
var completedAt *time.Time
if isTerminalMapRunStatus(status) {
now := time.Now().UTC()
completedAt = &now
}
return q.UpdateMapRunProgress(ctx, memsqlc.UpdateMapRunProgressParams{
Status: status,
QueuedItems: queued,
RunningItems: running,
SucceededItems: succeeded,
FailedItems: failed,
LastError: lastErr,
CompletedAt: completedAt,
ID: runID,
})
}
func (rt *MapRuntime) failRun(ctx context.Context, runID ids.UUID, lastErr *string) (memsqlc.MapRun, error) {
return rt.queries.UpdateMapRunProgress(ctx, memsqlc.UpdateMapRunProgressParams{
Status: mapRunStatusFailed,
QueuedItems: 0,
RunningItems: 0,
SucceededItems: 0,
FailedItems: 0,
LastError: lastErr,
CompletedAt: ptrTime(time.Now().UTC()),
ID: runID,
})
}
func parseMapJobPayload(raw []byte) (mapJobPayload, error) {
var payload mapJobPayload
if err := jsonv2.Unmarshal(raw, &payload); err != nil {
return mapJobPayload{}, err
}
if payload.RunID == "" || payload.ItemID == "" {
return mapJobPayload{}, fmt.Errorf("run_id and item_id are required")
}
return payload, nil
}
func mapJobItemIDFromDedupeKey(ctx context.Context, q *memsqlc.Queries, dedupeKey *string) (ids.UUID, error) {
runID, itemIndex, err := parseMapJobDedupeKey(dedupeKey)
if err != nil {
return ids.UUID{}, err
}
item, err := q.GetMapItemByRunAndIndex(ctx, memsqlc.GetMapItemByRunAndIndexParams{
RunID: runID,
ItemIndex: itemIndex,
})
if err != nil {
return ids.UUID{}, fmt.Errorf("resolve map item by dedupe key: %w", err)
}
return item.ID, nil
}
func parseMapJobDedupeKey(dedupeKey *string) (ids.UUID, int64, error) {
if dedupeKey == nil || strings.TrimSpace(*dedupeKey) == "" {
return ids.UUID{}, 0, fmt.Errorf("dedupe key is required")
}
parts := strings.Split(strings.TrimSpace(*dedupeKey), ":")
if len(parts) != 3 || parts[0] != "map" {
return ids.UUID{}, 0, fmt.Errorf("invalid dedupe key format: %q", strings.TrimSpace(*dedupeKey))
}
runID, err := ids.Parse(parts[1])
if err != nil {
return ids.UUID{}, 0, fmt.Errorf("invalid run id in dedupe key: %w", err)
}
itemIndex, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
return ids.UUID{}, 0, fmt.Errorf("invalid item index in dedupe key: %w", err)
}
if itemIndex < 0 {
return ids.UUID{}, 0, fmt.Errorf("item index in dedupe key must be non-negative")
}
return runID, itemIndex, nil
}
func toMapRunProjection(run memsqlc.MapRun) MapRunProjection {
p := MapRunProjection{
RunID: run.ID.String(),
OperatorKind: run.OperatorKind,
Status: run.Status,
TotalItems: run.TotalItems,
QueuedItems: run.QueuedItems,
RunningItems: run.RunningItems,
SucceededItems: run.SucceededItems,
FailedItems: run.FailedItems,
CreatedAt: run.CreatedAt.Format(time.RFC3339Nano),
UpdatedAt: run.UpdatedAt.Format(time.RFC3339Nano),
}
if run.LastError != nil {
p.LastError = *run.LastError
}
if run.CompletedAt != nil {
p.CompletedAt = run.CompletedAt.Format(time.RFC3339Nano)
}
return p
}
func isTerminalMapRunStatus(status string) bool {
switch status {
case mapRunStatusSucceeded, mapRunStatusFailed, mapRunStatusCancelled:
return true
default:
return false
}
}
func optionalStringPtr(v string) *string {
if strings.TrimSpace(v) == "" {
return nil
}
out := strings.TrimSpace(v)
return &out
}
func isUniqueConstraintError(err error) bool {
if err == nil {
return false
}
lower := strings.ToLower(err.Error())
return strings.Contains(lower, "unique constraint") ||
strings.Contains(lower, "constraint failed") ||
strings.Contains(lower, "duplicate key")
}
func ptrTime(v time.Time) *time.Time {
return &v
}

View file

@ -0,0 +1,383 @@
package tools
import (
"context"
"fmt"
"sync"
"testing"
fantasy "charm.land/fantasy"
jsonv2 "github.com/go-json-experiment/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/ZanzyTHEbar/dragonscale/pkg/ids"
"github.com/ZanzyTHEbar/dragonscale/pkg/memory/delegate"
memsqlc "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc"
)
type scriptedMapLLM struct {
mu sync.Mutex
responses []scriptedMapLLMResponse
index int
}
type scriptedMapLLMResponse struct {
Text string
Err error
}
func (m *scriptedMapLLM) Generate(_ context.Context, _ fantasy.Call) (*fantasy.Response, error) {
m.mu.Lock()
defer m.mu.Unlock()
if len(m.responses) == 0 {
return nil, fmt.Errorf("no scripted llm responses")
}
resp := m.responses[m.index%len(m.responses)]
m.index++
if resp.Err != nil {
return nil, resp.Err
}
return &fantasy.Response{
Content: fantasy.ResponseContent{fantasy.TextContent{Text: resp.Text}},
FinishReason: fantasy.FinishReasonStop,
}, nil
}
func (m *scriptedMapLLM) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) {
resp, err := m.Generate(ctx, call)
if err != nil {
return nil, err
}
return func(yield func(fantasy.StreamPart) bool) {
if !yield(fantasy.StreamPart{Type: fantasy.StreamPartTypeTextDelta, Delta: resp.Content.Text()}) {
return
}
yield(fantasy.StreamPart{Type: fantasy.StreamPartTypeFinish, FinishReason: fantasy.FinishReasonStop})
}, nil
}
func (m *scriptedMapLLM) GenerateObject(_ context.Context, _ fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
return nil, fmt.Errorf("not implemented")
}
func (m *scriptedMapLLM) StreamObject(_ context.Context, _ fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
return nil, fmt.Errorf("not implemented")
}
func (m *scriptedMapLLM) Provider() string { return "mock" }
func (m *scriptedMapLLM) Model() string { return "mock-map-llm" }
func newMapRuntimeForTest(t *testing.T, llm fantasy.LanguageModel, manager *SubagentManager) *MapRuntime {
t.Helper()
d, err := delegate.NewLibSQLInMemory()
require.NoError(t, err)
require.NoError(t, d.Init(context.Background()))
t.Cleanup(func() { _ = d.Close() })
return NewMapRuntime(d.Queries(), "dragonscale", llm, "mock-map-llm", manager)
}
func decodeResultMap(t *testing.T, result *ToolResult) map[string]interface{} {
t.Helper()
require.NotNil(t, result)
require.False(t, result.IsError, result.ForLLM)
var payload map[string]interface{}
require.NoError(t, jsonv2.Unmarshal([]byte(result.ForLLM), &payload))
return payload
}
func TestLLMMap_WorkerJSONL_StatusAndRead(t *testing.T) {
model := &scriptedMapLLM{
responses: []scriptedMapLLMResponse{
{Text: `{"label":"alpha","priority":1}`},
{Text: `{"label":"beta","priority":2}`},
},
}
runtime := newMapRuntimeForTest(t, model, nil)
mapTool := NewLLMMapTool(model, "mock-map-llm")
mapTool.SetRuntime(runtime)
statusTool := NewMapRunStatusTool(runtime)
readTool := NewMapRunReadTool(runtime)
enqueue := decodeResultMap(t, mapTool.Execute(context.Background(), map[string]interface{}{
"instruction": "normalize to {label, priority}",
"input_jsonl": "{\"name\":\"a\"}\n{\"name\":\"b\"}",
"execution_mode": "worker",
}))
runID, _ := enqueue["run_id"].(string)
require.NotEmpty(t, runID)
assert.Equal(t, "worker", enqueue["execution_mode"])
status := decodeResultMap(t, statusTool.Execute(context.Background(), map[string]interface{}{
"run_id": runID,
"process_steps": float64(20),
}))
assert.Equal(t, mapRunStatusSucceeded, status["status"])
assert.EqualValues(t, 2, status["succeeded_items"])
readJSON := decodeResultMap(t, readTool.Execute(context.Background(), map[string]interface{}{
"run_id": runID,
"limit": float64(10),
"format": "json",
}))
itemsAny, ok := readJSON["items"].([]interface{})
require.True(t, ok)
require.Len(t, itemsAny, 2)
readJSONL := decodeResultMap(t, readTool.Execute(context.Background(), map[string]interface{}{
"run_id": runID,
"format": "jsonl",
}))
jsonlText, _ := readJSONL["items_jsonl"].(string)
assert.Contains(t, jsonlText, `"status":"succeeded"`)
assert.Contains(t, jsonlText, "\n")
}
func TestLLMMap_IdempotencyReuse(t *testing.T) {
model := &scriptedMapLLM{
responses: []scriptedMapLLMResponse{
{Text: `{"label":"alpha"}`},
},
}
runtime := newMapRuntimeForTest(t, model, nil)
mapTool := NewLLMMapTool(model, "mock-map-llm")
mapTool.SetRuntime(runtime)
args := map[string]interface{}{
"instruction": "normalize",
"items": []interface{}{map[string]interface{}{"name": "a"}},
"execution_mode": "worker",
"idempotency_key": "run-1",
"session_key": "sess-a",
}
first := decodeResultMap(t, mapTool.Execute(context.Background(), args))
second := decodeResultMap(t, mapTool.Execute(context.Background(), args))
assert.Equal(t, first["run_id"], second["run_id"])
assert.Equal(t, true, second["idempotent_reuse"])
}
func TestLLMMap_IdempotencyReuse_Concurrent(t *testing.T) {
model := &scriptedMapLLM{
responses: []scriptedMapLLMResponse{
{Text: `{"label":"alpha"}`},
},
}
runtime := newMapRuntimeForTest(t, model, nil)
mapTool := NewLLMMapTool(model, "mock-map-llm")
mapTool.SetRuntime(runtime)
const workers = 12
start := make(chan struct{})
var wg sync.WaitGroup
runIDs := make(chan string, workers)
errs := make(chan error, workers)
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
<-start
result := mapTool.Execute(context.Background(), map[string]interface{}{
"instruction": "normalize",
"items": []interface{}{map[string]interface{}{"name": "a"}},
"execution_mode": "worker",
"idempotency_key": "run-1",
"session_key": "sess-a",
})
if result == nil || result.IsError {
if result != nil && result.Err != nil {
errs <- result.Err
return
}
errs <- fmt.Errorf("unexpected map tool error: %v", result)
return
}
var payload map[string]interface{}
if err := jsonv2.Unmarshal([]byte(result.ForLLM), &payload); err != nil {
errs <- fmt.Errorf("decode result: %w", err)
return
}
runID, _ := payload["run_id"].(string)
if runID == "" {
errs <- fmt.Errorf("missing run_id in payload")
return
}
runIDs <- runID
}()
}
close(start)
wg.Wait()
close(errs)
close(runIDs)
for err := range errs {
require.NoError(t, err)
}
unique := make(map[string]struct{})
for runID := range runIDs {
unique[runID] = struct{}{}
}
require.Len(t, unique, 1, "all concurrent requests should reuse the same run id")
}
func TestLLMMap_InlineRetriesThenSucceeds(t *testing.T) {
model := &scriptedMapLLM{
responses: []scriptedMapLLMResponse{
{Err: fmt.Errorf("transient model outage")},
{Text: `{"label":"ok"}`},
},
}
runtime := newMapRuntimeForTest(t, model, nil)
mapTool := NewLLMMapTool(model, "mock-map-llm")
mapTool.SetRuntime(runtime)
readTool := NewMapRunReadTool(runtime)
result := decodeResultMap(t, mapTool.Execute(context.Background(), map[string]interface{}{
"instruction": "normalize",
"items": []interface{}{map[string]interface{}{"name": "retry"}},
"execution_mode": "inline",
"max_retries": float64(2),
}))
assert.Equal(t, mapRunStatusSucceeded, result["status"])
runID, _ := result["run_id"].(string)
require.NotEmpty(t, runID)
read := decodeResultMap(t, readTool.Execute(context.Background(), map[string]interface{}{
"run_id": runID,
"format": "json",
}))
itemsAny, ok := read["items"].([]interface{})
require.True(t, ok)
require.Len(t, itemsAny, 1)
item0 := itemsAny[0].(map[string]interface{})
assert.EqualValues(t, 2, item0["attempts"])
assert.Equal(t, mapItemStatusSucceeded, item0["status"])
}
func TestLLMMap_InlineExhaustedRetriesFailsRun(t *testing.T) {
model := &scriptedMapLLM{
responses: []scriptedMapLLMResponse{
{Text: `not-json`},
{Text: `still-not-json`},
},
}
runtime := newMapRuntimeForTest(t, model, nil)
mapTool := NewLLMMapTool(model, "mock-map-llm")
mapTool.SetRuntime(runtime)
payload := decodeResultMap(t, mapTool.Execute(context.Background(), map[string]interface{}{
"instruction": "normalize",
"items": []interface{}{map[string]interface{}{"name": "bad"}},
"execution_mode": "inline",
"max_retries": float64(1),
}))
assert.Equal(t, mapRunStatusFailed, payload["status"])
summary, ok := payload["summary"].(map[string]interface{})
require.True(t, ok)
assert.EqualValues(t, 1, summary["failure_count"])
}
func TestMapRunRead_DetectsOutputHashMismatch(t *testing.T) {
model := &scriptedMapLLM{
responses: []scriptedMapLLMResponse{
{Text: `{"label":"alpha"}`},
},
}
runtime := newMapRuntimeForTest(t, model, nil)
mapTool := NewLLMMapTool(model, "mock-map-llm")
mapTool.SetRuntime(runtime)
readTool := NewMapRunReadTool(runtime)
result := decodeResultMap(t, mapTool.Execute(context.Background(), map[string]interface{}{
"instruction": "normalize",
"items": []interface{}{map[string]interface{}{"name": "a"}},
"execution_mode": "inline",
}))
runIDText, _ := result["run_id"].(string)
require.NotEmpty(t, runIDText)
runID, err := ids.Parse(runIDText)
require.NoError(t, err)
rows, err := runtime.queries.ListMapItemsByRunPaged(context.Background(), memsqlc.ListMapItemsByRunPagedParams{
RunID: runID,
Lim: 10,
Off: 0,
})
require.NoError(t, err)
require.Len(t, rows, 1)
_, err = runtime.queries.MarkMapItemSucceeded(context.Background(), memsqlc.MarkMapItemSucceededParams{
OutputFb: rows[0].OutputFb,
OutputHash: optionalStringPtr("forced-mismatch"),
ID: rows[0].ID,
})
require.NoError(t, err)
readResult := readTool.Execute(context.Background(), map[string]interface{}{
"run_id": runIDText,
"format": "json",
})
require.NotNil(t, readResult)
require.True(t, readResult.IsError)
assert.Contains(t, readResult.ForLLM, "output hash mismatch")
}
func TestAgenticMap_WorkerLifecycle(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
})
runtime := newMapRuntimeForTest(t, &scriptedMapLLM{responses: []scriptedMapLLMResponse{{Text: `{"unused":true}`}}}, manager)
tool := NewAgenticMapTool(manager)
tool.SetRuntime(runtime)
statusTool := NewMapRunStatusTool(runtime)
readTool := NewMapRunReadTool(runtime)
enqueue := decodeResultMap(t, tool.Execute(context.Background(), map[string]interface{}{
"items": []interface{}{map[string]interface{}{"name": "x"}},
"task_template": "Handle {{index}} => {{item_json}}",
"execution_mode": "worker",
}))
runID, _ := enqueue["run_id"].(string)
require.NotEmpty(t, runID)
status := decodeResultMap(t, statusTool.Execute(context.Background(), map[string]interface{}{
"run_id": runID,
"process_steps": float64(20),
}))
assert.Equal(t, mapRunStatusSucceeded, status["status"])
read := decodeResultMap(t, readTool.Execute(context.Background(), map[string]interface{}{
"run_id": runID,
"format": "json",
}))
itemsAny, ok := read["items"].([]interface{})
require.True(t, ok)
require.Len(t, itemsAny, 1)
item0 := itemsAny[0].(map[string]interface{})
assert.Equal(t, mapItemStatusSucceeded, item0["status"])
}
func TestLLMMap_InvalidJSONLIngestFails(t *testing.T) {
model := &scriptedMapLLM{
responses: []scriptedMapLLMResponse{{Text: `{"ok":true}`}},
}
runtime := newMapRuntimeForTest(t, model, nil)
mapTool := NewLLMMapTool(model, "mock-map-llm")
mapTool.SetRuntime(runtime)
result := mapTool.Execute(context.Background(), map[string]interface{}{
"instruction": "normalize",
"input_jsonl": "{\"name\":\"ok\"}\nnot-json",
})
require.NotNil(t, result)
require.True(t, result.IsError)
assert.Contains(t, result.ForLLM, "invalid JSONL")
}

View file

@ -0,0 +1,93 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package mapopsfb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type MapItemInput struct {
_tab flatbuffers.Table
}
func GetRootAsMapItemInput(buf []byte, offset flatbuffers.UOffsetT) *MapItemInput {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &MapItemInput{}
x.Init(buf, n+offset)
return x
}
func GetSizePrefixedRootAsMapItemInput(buf []byte, offset flatbuffers.UOffsetT) *MapItemInput {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &MapItemInput{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func (rcv *MapItemInput) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *MapItemInput) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *MapItemInput) Version() uint16 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.GetUint16(o + rcv._tab.Pos)
}
return 1
}
func (rcv *MapItemInput) MutateVersion(n uint16) bool {
return rcv._tab.MutateUint16Slot(4, n)
}
func (rcv *MapItemInput) ItemIndex() uint32 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.GetUint32(o + rcv._tab.Pos)
}
return 0
}
func (rcv *MapItemInput) MutateItemIndex(n uint32) bool {
return rcv._tab.MutateUint32Slot(6, n)
}
func (rcv *MapItemInput) ItemJson() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MapItemInput) InputHash() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(10))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func MapItemInputStart(builder *flatbuffers.Builder) {
builder.StartObject(4)
}
func MapItemInputAddVersion(builder *flatbuffers.Builder, version uint16) {
builder.PrependUint16Slot(0, version, 1)
}
func MapItemInputAddItemIndex(builder *flatbuffers.Builder, itemIndex uint32) {
builder.PrependUint32Slot(1, itemIndex, 0)
}
func MapItemInputAddItemJson(builder *flatbuffers.Builder, itemJson flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(itemJson), 0)
}
func MapItemInputAddInputHash(builder *flatbuffers.Builder, inputHash flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(3, flatbuffers.UOffsetT(inputHash), 0)
}
func MapItemInputEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}

View file

@ -0,0 +1,134 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package mapopsfb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type MapItemOutput struct {
_tab flatbuffers.Table
}
func GetRootAsMapItemOutput(buf []byte, offset flatbuffers.UOffsetT) *MapItemOutput {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &MapItemOutput{}
x.Init(buf, n+offset)
return x
}
func GetSizePrefixedRootAsMapItemOutput(buf []byte, offset flatbuffers.UOffsetT) *MapItemOutput {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &MapItemOutput{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func (rcv *MapItemOutput) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *MapItemOutput) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *MapItemOutput) Version() uint16 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.GetUint16(o + rcv._tab.Pos)
}
return 1
}
func (rcv *MapItemOutput) MutateVersion(n uint16) bool {
return rcv._tab.MutateUint16Slot(4, n)
}
func (rcv *MapItemOutput) ItemIndex() uint32 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.GetUint32(o + rcv._tab.Pos)
}
return 0
}
func (rcv *MapItemOutput) MutateItemIndex(n uint32) bool {
return rcv._tab.MutateUint32Slot(6, n)
}
func (rcv *MapItemOutput) Success() bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.GetBool(o + rcv._tab.Pos)
}
return false
}
func (rcv *MapItemOutput) MutateSuccess(n bool) bool {
return rcv._tab.MutateBoolSlot(8, n)
}
func (rcv *MapItemOutput) Attempts() uint16 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(10))
if o != 0 {
return rcv._tab.GetUint16(o + rcv._tab.Pos)
}
return 0
}
func (rcv *MapItemOutput) MutateAttempts(n uint16) bool {
return rcv._tab.MutateUint16Slot(10, n)
}
func (rcv *MapItemOutput) OutputJson() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(12))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MapItemOutput) ErrorText() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(14))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MapItemOutput) OutputHash() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(16))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func MapItemOutputStart(builder *flatbuffers.Builder) {
builder.StartObject(7)
}
func MapItemOutputAddVersion(builder *flatbuffers.Builder, version uint16) {
builder.PrependUint16Slot(0, version, 1)
}
func MapItemOutputAddItemIndex(builder *flatbuffers.Builder, itemIndex uint32) {
builder.PrependUint32Slot(1, itemIndex, 0)
}
func MapItemOutputAddSuccess(builder *flatbuffers.Builder, success bool) {
builder.PrependBoolSlot(2, success, false)
}
func MapItemOutputAddAttempts(builder *flatbuffers.Builder, attempts uint16) {
builder.PrependUint16Slot(3, attempts, 0)
}
func MapItemOutputAddOutputJson(builder *flatbuffers.Builder, outputJson flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(4, flatbuffers.UOffsetT(outputJson), 0)
}
func MapItemOutputAddErrorText(builder *flatbuffers.Builder, errorText flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(5, flatbuffers.UOffsetT(errorText), 0)
}
func MapItemOutputAddOutputHash(builder *flatbuffers.Builder, outputHash flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(6, flatbuffers.UOffsetT(outputHash), 0)
}
func MapItemOutputEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}

View file

@ -0,0 +1,159 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package mapopsfb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type MapRunSpec struct {
_tab flatbuffers.Table
}
func GetRootAsMapRunSpec(buf []byte, offset flatbuffers.UOffsetT) *MapRunSpec {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &MapRunSpec{}
x.Init(buf, n+offset)
return x
}
func GetSizePrefixedRootAsMapRunSpec(buf []byte, offset flatbuffers.UOffsetT) *MapRunSpec {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &MapRunSpec{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func (rcv *MapRunSpec) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *MapRunSpec) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *MapRunSpec) Version() uint16 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.GetUint16(o + rcv._tab.Pos)
}
return 1
}
func (rcv *MapRunSpec) MutateVersion(n uint16) bool {
return rcv._tab.MutateUint16Slot(4, n)
}
func (rcv *MapRunSpec) OperatorKind() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MapRunSpec) Instruction() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MapRunSpec) TaskTemplate() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(10))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MapRunSpec) OutputSchemaJson() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(12))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MapRunSpec) MaxRetries() uint16 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(14))
if o != 0 {
return rcv._tab.GetUint16(o + rcv._tab.Pos)
}
return 1
}
func (rcv *MapRunSpec) MutateMaxRetries(n uint16) bool {
return rcv._tab.MutateUint16Slot(14, n)
}
func (rcv *MapRunSpec) DelegatedScope() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(16))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MapRunSpec) KeptWork() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(18))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MapRunSpec) OriginChannel() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(20))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MapRunSpec) OriginChatId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(22))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func MapRunSpecStart(builder *flatbuffers.Builder) {
builder.StartObject(10)
}
func MapRunSpecAddVersion(builder *flatbuffers.Builder, version uint16) {
builder.PrependUint16Slot(0, version, 1)
}
func MapRunSpecAddOperatorKind(builder *flatbuffers.Builder, operatorKind flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(operatorKind), 0)
}
func MapRunSpecAddInstruction(builder *flatbuffers.Builder, instruction flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(instruction), 0)
}
func MapRunSpecAddTaskTemplate(builder *flatbuffers.Builder, taskTemplate flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(3, flatbuffers.UOffsetT(taskTemplate), 0)
}
func MapRunSpecAddOutputSchemaJson(builder *flatbuffers.Builder, outputSchemaJson flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(4, flatbuffers.UOffsetT(outputSchemaJson), 0)
}
func MapRunSpecAddMaxRetries(builder *flatbuffers.Builder, maxRetries uint16) {
builder.PrependUint16Slot(5, maxRetries, 1)
}
func MapRunSpecAddDelegatedScope(builder *flatbuffers.Builder, delegatedScope flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(6, flatbuffers.UOffsetT(delegatedScope), 0)
}
func MapRunSpecAddKeptWork(builder *flatbuffers.Builder, keptWork flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(7, flatbuffers.UOffsetT(keptWork), 0)
}
func MapRunSpecAddOriginChannel(builder *flatbuffers.Builder, originChannel flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(8, flatbuffers.UOffsetT(originChannel), 0)
}
func MapRunSpecAddOriginChatId(builder *flatbuffers.Builder, originChatId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(9, flatbuffers.UOffsetT(originChatId), 0)
}
func MapRunSpecEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}