feat(work): add persistent solution search orchestration policy #208

Merged
m merged 1 commits from codex/dev-000231-solution-orchestrator into main 2026-08-23 13:48:59 +00:00
8 changed files with 173 additions and 4 deletions
+13
View File
@@ -61,6 +61,19 @@ 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/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 {
+42
View File
@@ -0,0 +1,42 @@
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
}
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000230-development-gate-controller.json",
"task_id": "DEV-000230",
"task_file": "dev/tasks/DEV-000231-solution-orchestrator.json",
"task_id": "DEV-000231",
"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"]
}
+2 -2
View File
@@ -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-000231-solution-orchestrator.json revision 1`; Work Graph now exposes a typed parallel-first solution-search decision: active waves execute, unresolved goals open agent/capability/research/reflection avenues, and bounded exhaustion advances to a durable next wave with human attention instead of failure. Automatic dispatcher fan-out and persistence of the returned decision remain the next integration step.
- `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 this decision policy to the durable Work Graph dispatcher/worker so every unresolved goal persists its next wave, fans out independent options and joins verified evidence; retain local fallback, truth research and governed development gates.`
+10
View File
@@ -0,0 +1,10 @@
package application
import (
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/modules/work/domain"
)
func DecideSolutionSearch(input work.SolutionSearchInput) (work.SolutionSearchDecision, error) {
return domain.DecideSolutionSearch(input)
}
+51
View File
@@ -0,0 +1,51 @@
package domain
import (
"errors"
"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
}
+30
View File
@@ -0,0 +1,30 @@
package domain
import (
"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)
}
}