Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09d53adfd1 | ||
|
|
89694047b1 | ||
|
|
23e0525c08 | ||
|
|
45874f2186 |
@@ -61,6 +61,32 @@ func main() {
|
||||
}
|
||||
writeJSON(w, map[string]any{"graph": graph, "scheduled_nodes": ready})
|
||||
})
|
||||
mux.HandleFunc("POST /v1/work-graphs/orchestration/next", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request work.SolutionSearchInput
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10)).Decode(&request); err != nil {
|
||||
http.Error(w, "invalid orchestration input", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
decision, err := application.DecideSolutionSearch(request)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid orchestration input", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
writeJSON(w, decision)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/work-graphs/orchestration/wave", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request work.SolutionSearchWaveRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&request); err != nil {
|
||||
http.Error(w, "invalid orchestration wave request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
result, err := application.OrchestrateSolutionSearch(r.Context(), repository, request)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid orchestration wave request", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
writeJSON(w, result)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/work-graphs/complete", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request application.GraphCompleteRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&request); err != nil {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package work
|
||||
|
||||
type OrchestrationState string
|
||||
type SearchOptionKind string
|
||||
|
||||
const (
|
||||
OrchestrationSearching OrchestrationState = "SEARCHING"
|
||||
OrchestrationExecuting OrchestrationState = "EXECUTING"
|
||||
OrchestrationVerifying OrchestrationState = "VERIFYING"
|
||||
OrchestrationWaiting OrchestrationState = "WAITING"
|
||||
OrchestrationCompleted OrchestrationState = "COMPLETED"
|
||||
OrchestrationCancelled OrchestrationState = "CANCELLED"
|
||||
|
||||
SearchOptionAgents SearchOptionKind = "AGENT_TEAM"
|
||||
SearchOptionCapability SearchOptionKind = "CAPABILITY_DISCOVERY"
|
||||
SearchOptionResearch SearchOptionKind = "TRUTH_RESEARCH"
|
||||
SearchOptionReflection SearchOptionKind = "REFLECTION_MEMORY"
|
||||
SearchOptionHuman SearchOptionKind = "HUMAN_ATTENTION"
|
||||
)
|
||||
|
||||
// SolutionSearchInput is the durable orchestration snapshot. The scheduler
|
||||
// stores the graph/goal separately; this value only decides the next bounded
|
||||
// search wave and never grants authority by itself.
|
||||
type SolutionSearchInput struct {
|
||||
GoalID GoalID
|
||||
GoalState GoalState
|
||||
SearchRound int
|
||||
MaxRound int
|
||||
CancelRequested bool
|
||||
ActiveOptionCount int
|
||||
VerificationReady bool
|
||||
Unresolved bool
|
||||
}
|
||||
|
||||
type SolutionSearchDecision struct {
|
||||
GoalID GoalID
|
||||
State OrchestrationState
|
||||
ContinueSearching bool
|
||||
NextSearchRound int
|
||||
Options []SearchOptionKind
|
||||
Reason string
|
||||
}
|
||||
|
||||
type SolutionSearchWaveRequest struct {
|
||||
Input SolutionSearchInput `json:"input"`
|
||||
GraphID string `json:"graph_id"`
|
||||
IdempotencyPrefix string `json:"idempotency_prefix"`
|
||||
Dispatches []SolutionSearchDispatch `json:"dispatches"`
|
||||
Budget GraphBudget `json:"budget"`
|
||||
ParentGraphID string `json:"parent_graph_id,omitempty"`
|
||||
ParentGraphRevision int64 `json:"parent_graph_revision,omitempty"`
|
||||
TriggerEvidenceRef string `json:"trigger_evidence_ref,omitempty"`
|
||||
NowUnixMillis int64 `json:"now_unix_millis"`
|
||||
}
|
||||
|
||||
type SolutionSearchDispatch struct {
|
||||
Option SearchOptionKind `json:"option"`
|
||||
Intent AgentDispatchIntent `json:"intent"`
|
||||
}
|
||||
|
||||
type SolutionSearchWaveResult struct {
|
||||
Decision SolutionSearchDecision `json:"decision"`
|
||||
Graph WorkGraph `json:"graph"`
|
||||
ScheduledNodeIDs []GraphNodeID `json:"scheduled_node_ids,omitempty"`
|
||||
GraphMaterialized bool `json:"graph_materialized"`
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000230-development-gate-controller.json",
|
||||
"task_id": "DEV-000230",
|
||||
"task_file": "dev/tasks/DEV-000232-durable-solution-wave-dispatch.json",
|
||||
"task_id": "DEV-000232",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000231",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "45874f218646d7879dfcbc814782fe56bf0aa06a",
|
||||
"risk_tier": "E",
|
||||
"intent": "Provide a durable, parallel-first solution-search decision policy for Work Graph goals so unresolved work continues through independent agent, capability, research and reflection avenues instead of dead-ending after one bounded wave.",
|
||||
"requirement_ids": ["solution orchestration", "parallel agent team", "durable retry waves", "independent verification"],
|
||||
"allowed_change_scope": {"modules": ["work-graph", "work-domain"], "paths": ["contracts/work/orchestration.go", "modules/work/domain/orchestration.go", "modules/work/domain/orchestration_test.go", "modules/work/application/orchestration.go", "cmd/work-graph-runtime/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000231-solution-orchestrator.json", "docs/PROJECT_STATUS.md"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "LLM-local/**", "modules/work/secrets/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["Work Goal state", "durable Work Graph scheduling", "independent verification result"], "may_change": ["typed solution-search decision contract", "Work Graph orchestration decision endpoint"]},
|
||||
"semantic_owner": "Work solution orchestration policy",
|
||||
"canonical_state": ["existing Work Goal and Work Graph records; caller persists the returned wave decision"],
|
||||
"state_transitions": ["SEARCHING -> EXECUTING -> VERIFYING -> COMPLETED", "SEARCHING -> WAITING -> SEARCHING", "any unresolved state -> next durable search wave", "any active state -> CANCELLED"],
|
||||
"side_effects_and_privileges": ["pure policy decision and unauthenticated internal runtime route only; no provider call, secret access, code mutation, promotion or deployment"],
|
||||
"persistence_and_migrations": ["none; this stage returns a typed decision for the existing durable scheduler"],
|
||||
"failure_retry_restart": ["bounded wave exhaustion becomes WAITING with a new wave number and human-attention avenue; it is never represented as failure; cancellation and independently verified completion are terminal"],
|
||||
"assumptions": ["the existing Work Graph dispatcher or next integration stage will persist the decision and fan out options", "model quality and provider routing remain separate capability decisions"],
|
||||
"acceptance_scenarios": ["first unresolved goal selects all independent search avenues", "active wave remains EXECUTING", "exhausted wave continues with a higher durable round and human attention", "only cancellation or independently verified completion stops search", "invalid input is rejected", "the decision endpoint does not mutate source, secrets or production"],
|
||||
"required_tests": ["go test ./modules/work/domain ./modules/work/application ./cmd/work-graph-runtime/...", "go vet ./modules/work/domain ./modules/work/application ./cmd/work-graph-runtime/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production health smoke"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000232",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "89694047b1e6c7729b00c20a52acb5a2487e6dd6",
|
||||
"risk_tier": "E",
|
||||
"intent": "Connect the solution-search policy to the existing durable Work Graph scheduler so each unresolved wave is materialized as parallel dispatch intents and queued nodes.",
|
||||
"requirement_ids": ["durable solution waves", "parallel fan-out", "agent/reflex orchestration", "restartable scheduling"],
|
||||
"allowed_change_scope": {"modules": ["work-graph", "work-domain"], "paths": ["contracts/work/orchestration.go", "modules/work/domain/orchestration.go", "modules/work/domain/orchestration_test.go", "modules/work/application/orchestration.go", "cmd/work-graph-runtime/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000232-durable-solution-wave-dispatch.json", "docs/PROJECT_STATUS.md"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "LLM-local/**", "modules/work/secrets/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["typed solution-search decision", "existing Work Graph and dispatch queue ports"], "may_change": ["solution-search wave materialization request/result", "Work Graph orchestration wave route"]},
|
||||
"semantic_owner": "Work solution orchestration and Work Graph",
|
||||
"canonical_state": ["existing work.graphs, graph_nodes, dispatch_intents and dispatch_queue stores"],
|
||||
"state_transitions": ["policy SEARCHING/WAITING -> graph ADMITTED/RUNNING", "each option -> one queued dispatch node", "human attention remains decision metadata"],
|
||||
"side_effects_and_privileges": ["durable Work-owned graph and queue writes only; no provider call, secret access, external mutation, promotion or deployment"],
|
||||
"persistence_and_migrations": ["none; existing graph and queue tables are reused"],
|
||||
"failure_retry_restart": ["existing queue idempotency and graph revision persistence govern restart; invalid wave shape is rejected; no unresolved goal is marked failed by wave exhaustion"],
|
||||
"assumptions": ["dispatch intents are supplied by the upstream authority-aware planner", "provider/model selection and execution remain worker responsibilities"],
|
||||
"acceptance_scenarios": ["four independent solution avenues materialize as four graph nodes", "all ready nodes are queued up to graph MaxParallel", "human attention is not silently executed as an agent", "terminal policy decisions do not create a graph", "invalid or duplicate options are rejected"],
|
||||
"required_tests": ["go test ./modules/work/domain ./modules/work/application ./cmd/work-graph-runtime/...", "go vet ./modules/work/domain ./modules/work/application ./cmd/work-graph-runtime/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production health smoke"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
|
||||
}
|
||||
@@ -24,8 +24,8 @@
|
||||
|
||||
- `COMPLETION_MATRIX = docs/evidence/audit-completion-matrix-2026-08-23-v3.json`; all seven objective areas are mapped to current repository/production proof, with external bridge, GPU promotion/quality and deep-acceptance gaps explicit.
|
||||
|
||||
- `ACTIVE_TASK = dev/tasks/DEV-000230-development-gate-controller.json revision 1`; authenticated Core now exposes Work-owned durable development gate creation and optimistic-concurrency progression. Code execution, protected-main access, automatic promotion and production mutation remain outside this controller.
|
||||
- `ACTIVE_TASK = dev/tasks/DEV-000232-durable-solution-wave-dispatch.json revision 1`; the solution-search decision now materializes the next unresolved wave into the existing durable Work Graph and dispatch queue, preserving independent agent/capability/research/reflection directions. Provider selection, execution and result fan-in verification remain worker responsibilities.
|
||||
- `LINEAGE_DECISION = docs/GOVERNANCE_LINEAGE_DECISION_2026-08-22.md`; historical packets remain provenance and are not current authority.
|
||||
- `REGISTRY_STATUS = YAML and JSON semantic mirrors agree`
|
||||
- `PRODUCTION_DOMAIN = docker.cin.su`; shared docker-test is not used for this project.
|
||||
- `NEXT_WORK = qualify an independently operated Agent Execution bridge when available and complete deferred deep browser acceptance with a current signed-in session; retain local fallback and general-first task evaluation when either capability is unavailable.`
|
||||
- `NEXT_WORK = connect completed-wave callbacks to automatic replan/fan-in verification, then add bounded recovery of stale waves; retain local fallback, truth research and governed development gates.`
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
"git.cin.su/m/Assistant/modules/work/domain"
|
||||
"git.cin.su/m/Assistant/modules/work/ports"
|
||||
"time"
|
||||
)
|
||||
|
||||
func DecideSolutionSearch(input work.SolutionSearchInput) (work.SolutionSearchDecision, error) {
|
||||
return domain.DecideSolutionSearch(input)
|
||||
}
|
||||
|
||||
func OrchestrateSolutionSearch(ctx context.Context, store ports.GraphStore, request work.SolutionSearchWaveRequest) (work.SolutionSearchWaveResult, error) {
|
||||
decision, err := domain.DecideSolutionSearch(request.Input)
|
||||
if err != nil {
|
||||
return work.SolutionSearchWaveResult{}, err
|
||||
}
|
||||
result := work.SolutionSearchWaveResult{Decision: decision}
|
||||
if !decision.ContinueSearching || decision.State == work.OrchestrationExecuting {
|
||||
return result, nil
|
||||
}
|
||||
graph, intents, err := domain.BuildSolutionSearchWave(decision, request)
|
||||
if err != nil {
|
||||
return work.SolutionSearchWaveResult{}, err
|
||||
}
|
||||
if request.NowUnixMillis < 1 {
|
||||
request.NowUnixMillis = time.Now().UnixMilli()
|
||||
}
|
||||
graph, ready, err := AdmitAndSchedule(ctx, store, GraphScheduleRequest{Graph: graph, Intents: intents, Now: request.NowUnixMillis})
|
||||
if err != nil {
|
||||
return work.SolutionSearchWaveResult{}, err
|
||||
}
|
||||
result.Graph = graph
|
||||
result.ScheduledNodeIDs = ready
|
||||
result.GraphMaterialized = true
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
var ErrInvalidSolutionSearch = errors.New("invalid solution search input")
|
||||
|
||||
// DecideSolutionSearch keeps an unresolved goal alive across bounded waves.
|
||||
// A wave has limits, but an unresolved goal is never converted into FAILED
|
||||
// merely because one wave is exhausted. The caller persists the decision and
|
||||
// schedules the next wave through the durable Work Graph.
|
||||
func DecideSolutionSearch(input work.SolutionSearchInput) (work.SolutionSearchDecision, error) {
|
||||
if input.GoalID == "" || input.SearchRound < 0 || input.MaxRound < 1 || input.ActiveOptionCount < 0 {
|
||||
return work.SolutionSearchDecision{}, ErrInvalidSolutionSearch
|
||||
}
|
||||
decision := work.SolutionSearchDecision{GoalID: input.GoalID, NextSearchRound: input.SearchRound}
|
||||
if input.CancelRequested || input.GoalState == work.GoalCancelled {
|
||||
decision.State = work.OrchestrationCancelled
|
||||
decision.Reason = "goal cancellation is durable and stops further search waves"
|
||||
return decision, nil
|
||||
}
|
||||
if input.GoalState == work.GoalCompleted && input.VerificationReady {
|
||||
decision.State = work.OrchestrationCompleted
|
||||
decision.Reason = "goal has an independently verified accepted result"
|
||||
return decision, nil
|
||||
}
|
||||
if input.ActiveOptionCount > 0 {
|
||||
decision.State = work.OrchestrationExecuting
|
||||
decision.ContinueSearching = true
|
||||
decision.Options = []work.SearchOptionKind{work.SearchOptionAgents}
|
||||
decision.Reason = "current solution wave is still running"
|
||||
return decision, nil
|
||||
}
|
||||
decision.State = work.OrchestrationSearching
|
||||
decision.ContinueSearching = true
|
||||
decision.NextSearchRound++
|
||||
decision.Options = []work.SearchOptionKind{work.SearchOptionAgents, work.SearchOptionCapability, work.SearchOptionResearch, work.SearchOptionReflection}
|
||||
if input.Unresolved {
|
||||
decision.Reason = "previous wave did not establish a verified solution; start all independent search avenues"
|
||||
} else {
|
||||
decision.Reason = "no verified result exists; start the first parallel solution wave"
|
||||
}
|
||||
if input.SearchRound >= input.MaxRound {
|
||||
decision.State = work.OrchestrationWaiting
|
||||
decision.Options = append(decision.Options, work.SearchOptionHuman)
|
||||
decision.Reason = "bounded wave budget exhausted; continue with a new durable wave and human attention, never claim failure"
|
||||
}
|
||||
return decision, nil
|
||||
}
|
||||
|
||||
var ErrInvalidSolutionSearchWave = errors.New("invalid solution search wave")
|
||||
|
||||
// BuildSolutionSearchWave turns a policy decision into a durable Work Graph
|
||||
// fan-out. Human attention remains in the decision but is not an agent dispatch.
|
||||
func BuildSolutionSearchWave(decision work.SolutionSearchDecision, request work.SolutionSearchWaveRequest) (work.WorkGraph, []work.AgentDispatchIntent, error) {
|
||||
if !decision.ContinueSearching || (decision.State != work.OrchestrationSearching && decision.State != work.OrchestrationWaiting) || request.GraphID == "" || request.IdempotencyPrefix == "" || request.Budget.MaxParallel < 1 || request.Budget.MaxAttempts < 1 {
|
||||
return work.WorkGraph{}, nil, ErrInvalidSolutionSearchWave
|
||||
}
|
||||
seen := make(map[work.SearchOptionKind]struct{}, len(request.Dispatches))
|
||||
nodes := make([]work.GraphNode, 0, len(request.Dispatches))
|
||||
intents := make([]work.AgentDispatchIntent, 0, len(request.Dispatches))
|
||||
for index, dispatch := range request.Dispatches {
|
||||
if dispatch.Option == work.SearchOptionHuman || dispatch.Option == "" || dispatch.Intent.DispatchID == "" || dispatch.Intent.IdempotencyKey == "" {
|
||||
return work.WorkGraph{}, nil, ErrInvalidSolutionSearchWave
|
||||
}
|
||||
if _, exists := seen[dispatch.Option]; exists || !containsSearchOption(decision.Options, dispatch.Option) {
|
||||
return work.WorkGraph{}, nil, ErrInvalidSolutionSearchWave
|
||||
}
|
||||
seen[dispatch.Option] = struct{}{}
|
||||
intent := dispatch.Intent
|
||||
intent.SpecialistPurpose = string(dispatch.Option)
|
||||
intents = append(intents, intent)
|
||||
nodes = append(nodes, work.GraphNode{ID: work.GraphNodeID(fmt.Sprintf("%s-node-%d", request.GraphID, index+1)), DispatchID: intent.DispatchID, IdempotencyKey: fmt.Sprintf("%s:%s:%d", request.IdempotencyPrefix, dispatch.Option, decision.NextSearchRound), State: work.GraphNodeBlocked, Purpose: work.GraphNodePurposeGeneric})
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
return work.WorkGraph{}, nil, ErrInvalidSolutionSearchWave
|
||||
}
|
||||
return work.WorkGraph{ID: request.GraphID, Revision: 1, State: work.GraphDraft, Nodes: nodes, Budget: request.Budget, ParentGraphID: request.ParentGraphID, ParentGraphRevision: request.ParentGraphRevision, TriggerEvidenceRef: request.TriggerEvidenceRef}, intents, nil
|
||||
}
|
||||
|
||||
func containsSearchOption(options []work.SearchOptionKind, target work.SearchOptionKind) bool {
|
||||
for _, option := range options {
|
||||
if option == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSolutionSearchDoesNotDeadEndAfterBudgetedWave(t *testing.T) {
|
||||
decision, err := DecideSolutionSearch(work.SolutionSearchInput{GoalID: "goal-1", GoalState: work.GoalInProgress, SearchRound: 3, MaxRound: 3, Unresolved: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !decision.ContinueSearching || decision.State != work.OrchestrationWaiting || decision.NextSearchRound != 4 {
|
||||
t.Fatalf("unexpected exhausted-wave decision: %#v", decision)
|
||||
}
|
||||
if len(decision.Options) != 5 || decision.Options[4] != work.SearchOptionHuman {
|
||||
t.Fatalf("all search avenues were not retained: %#v", decision.Options)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSolutionSearchStopsOnlyForCancelOrVerifiedCompletion(t *testing.T) {
|
||||
completed, err := DecideSolutionSearch(work.SolutionSearchInput{GoalID: "goal-1", GoalState: work.GoalCompleted, VerificationReady: true, MaxRound: 1})
|
||||
if err != nil || completed.ContinueSearching || completed.State != work.OrchestrationCompleted {
|
||||
t.Fatalf("verified completion was not terminal: %#v %v", completed, err)
|
||||
}
|
||||
cancelled, err := DecideSolutionSearch(work.SolutionSearchInput{GoalID: "goal-1", GoalState: work.GoalInProgress, CancelRequested: true, MaxRound: 1})
|
||||
if err != nil || cancelled.ContinueSearching || cancelled.State != work.OrchestrationCancelled {
|
||||
t.Fatalf("cancel was not terminal: %#v %v", cancelled, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSolutionSearchWaveMaterializesParallelAgentDirections(t *testing.T) {
|
||||
decision, err := DecideSolutionSearch(work.SolutionSearchInput{GoalID: "goal-1", GoalState: work.GoalInProgress, MaxRound: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
options := []work.SearchOptionKind{work.SearchOptionAgents, work.SearchOptionCapability, work.SearchOptionResearch, work.SearchOptionReflection}
|
||||
dispatches := make([]work.SolutionSearchDispatch, 0, len(options))
|
||||
for i, option := range options {
|
||||
dispatches = append(dispatches, work.SolutionSearchDispatch{Option: option, Intent: work.AgentDispatchIntent{DispatchID: workDispatchID(i), IdempotencyKey: workDispatchID(i) + ":idem"}})
|
||||
}
|
||||
graph, intents, err := BuildSolutionSearchWave(decision, work.SolutionSearchWaveRequest{GraphID: "graph-1", IdempotencyPrefix: "goal-1", Dispatches: dispatches, Budget: work.GraphBudget{MaxParallel: 4, MaxAttempts: 2}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(graph.Nodes) != 4 || len(intents) != 4 || graph.State != work.GraphDraft {
|
||||
t.Fatalf("unexpected materialized wave: %#v %#v", graph, intents)
|
||||
}
|
||||
for i, node := range graph.Nodes {
|
||||
if node.State != work.GraphNodeBlocked || intents[i].SpecialistPurpose != string(options[i]) {
|
||||
t.Fatalf("wave direction not preserved: node=%#v intent=%#v", node, intents[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func workDispatchID(index int) string {
|
||||
return fmt.Sprintf("dispatch-%d", index)
|
||||
}
|
||||
Reference in New Issue
Block a user