feat(work): add persistent solution search orchestration policy #208
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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.`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user