feat(phase2-E): multi-provider routing via secutools delegation
Adds optional delegation of agent-queue tasks to the SecuAAS secutools AI platform (GPU / Gemini / Claude API) instead of dispatching to a local Claude Code tmux session. Per-task opt-in via YAML frontmatter fields preferred_ai, allow_delegation, complexity_hint — absence keeps the Phase 1 behaviour exactly (zero breaking change). Go side: - internal/secutools: HTTP client with exponential-backoff retries (SubmitJob/GetJob/WaitForResult), DecideProvider map adapter for CLI use, table tests. - internal/router: struct-typed Decide() with strict precedence (needs_claude_code > preferred_ai=claude-code > allow_delegation=false > preferred_ai > fail-safe local on unknown). - internal/delegation: Manager submits jobs, writes .md.delegated markers for on-restart recovery, runs a periodic reaper that moves completed jobs into done/ with provider/cost footer and failed jobs into failed/. - internal/dispatcher: WithDelegation() opt-in, routeTask hook before findFreeSession, skips .md.delegated in assignNextTask. - internal/api: /api/delegated/status (active jobs + counters), /watchdog/status extended with delegation counters. - cmd/ccl-delegate: small CLI exposing submit/get/result/decide so the bash dispatcher can call the same contract without duplicating logic. - cmd/claude-failover: delegation wired opt-in via SECUTOOLS_API_KEY. Tests: - 29+ new unit tests across router, secutools, delegation, dispatcher, api packages. go test -race -count=1 clean. - tests/phase2-E-integration.sh: bash end-to-end against a Python stdlib mock HTTP server, exercising the dev-management scripts. Forward-compat with watchdog (Phase 1 B1 already ignores state=delegated_to_secutools) so delegated tasks aren't flagged stale.
This commit is contained in:
parent
47ab86eef9
commit
3e20085204
18 changed files with 2819 additions and 22 deletions
72
internal/secutools/routing_test.go
Normal file
72
internal/secutools/routing_test.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package secutools
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestDecideProvider_TableDriven exercises the full decision matrix for
|
||||
// the map-input adapter used by the bash-side ccl-delegate CLI. The
|
||||
// richer Task-struct variant lives in internal/router.
|
||||
func TestDecideProvider_TableDriven(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
fm map[string]any
|
||||
want string
|
||||
}{
|
||||
{"nil map falls back to local", nil, "local"},
|
||||
{"empty map → local (allow_delegation default false)", map[string]any{}, "local"},
|
||||
{"needs_claude_code wins", map[string]any{
|
||||
"needs_claude_code": true,
|
||||
"allow_delegation": true,
|
||||
"preferred_ai": "gpu",
|
||||
}, "local"},
|
||||
{"explicit claude-code stays local", map[string]any{
|
||||
"preferred_ai": "claude-code",
|
||||
"allow_delegation": true,
|
||||
}, "local"},
|
||||
{"allow_delegation=false blocks even with preferred_ai=gpu", map[string]any{
|
||||
"preferred_ai": "gpu",
|
||||
"allow_delegation": false,
|
||||
}, "local"},
|
||||
{"gpu", map[string]any{
|
||||
"preferred_ai": "gpu",
|
||||
"allow_delegation": true,
|
||||
}, "gpu"},
|
||||
{"GPU (case-insensitive)", map[string]any{
|
||||
"preferred_ai": "GPU",
|
||||
"allow_delegation": true,
|
||||
}, "gpu"},
|
||||
{"gemini", map[string]any{
|
||||
"preferred_ai": "gemini",
|
||||
"allow_delegation": true,
|
||||
}, "gemini"},
|
||||
{"claude-api", map[string]any{
|
||||
"preferred_ai": "claude-api",
|
||||
"allow_delegation": true,
|
||||
}, "claude-api"},
|
||||
{"auto", map[string]any{
|
||||
"preferred_ai": "auto",
|
||||
"allow_delegation": true,
|
||||
}, "auto"},
|
||||
{"empty preferred_ai + allow_delegation → auto", map[string]any{
|
||||
"allow_delegation": true,
|
||||
}, "auto"},
|
||||
{"unknown provider → fail-safe local", map[string]any{
|
||||
"preferred_ai": "claude-3-mystery",
|
||||
"allow_delegation": true,
|
||||
}, "local"},
|
||||
{"allow_delegation as string 'true'", map[string]any{
|
||||
"preferred_ai": "gpu",
|
||||
"allow_delegation": "true",
|
||||
}, "gpu"},
|
||||
{"allow_delegation as int 1", map[string]any{
|
||||
"preferred_ai": "gemini",
|
||||
"allow_delegation": 1,
|
||||
}, "gemini"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
if got := DecideProvider(c.fm); got != c.want {
|
||||
t.Errorf("DecideProvider(%v) = %q, want %q", c.fm, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue