Phase 5: SQLite + Redis AgentRegistry (mirrors AllowanceStore pattern) Phase 6: Enforce or remove dead HourlyRateLimit/CostCeiling fields Phase 7: Priority-ordered dispatch with retry backoff and dead-letter Phase 8: EventEmitter interface for task lifecycle notifications Co-Authored-By: Virgil <virgil@lethean.io>
8.7 KiB
8.7 KiB
TODO.md -- go-agentic
Phase 1: Test Coverage
- Verify all 5 test files pass standalone after split (
go test ./...) - Add integration test for full task lifecycle: claim -> process -> complete
- Add edge-case tests for allowance exhaustion mid-task
- Fill coverage gaps: embed, service types, git operations, config YAML/env paths, error store mock
- Target 85%+ coverage achieved: 85.6% (from 70.1%) —
23aa635
Phase 2: Allowance Persistence
- MemoryStore is in-memory only -- state lost on restart
- Add Redis backend for
AllowanceStoreinterface (multi-process safe) —0be744e - Add SQLite backend for
AllowanceStoreinterface (single-node fallback) - Config already supports YAML -- wire backend selection into config loader
Phase 3: Multi-Agent Coordination — 646cc02
3.1 Agent Registry
- Create
registry.go—AgentInfostruct (ID, Name, Capabilities []string, Status enum, LastHeartbeat, CurrentLoad int, MaxLoad int),AgentStatusenum (Available/Busy/Offline) AgentRegistryinterface —Register(AgentInfo) error,Deregister(id string) error,Get(id string) (AgentInfo, error),List() []AgentInfo,Heartbeat(id string) error,Reap(ttl time.Duration) []string(returns IDs of reaped agents)MemoryRegistryimplementation —sync.RWMutexguarded map,Reap()marks agents offline if heartbeat older than TTL- Tests — registration, deregistration, heartbeat updates, reap stale agents, concurrent access
3.2 Task Router
- Create
router.go—TaskRouterinterface withRoute(task *Task, agents []AgentInfo) (string, error)returning agent ID DefaultRouterimplementation — capability matching (task.Labels ⊆ agent.Capabilities), then least-loaded agent (CurrentLoad / MaxLoad ratio), priority weighting (critical tasks skip load balancing)ErrNoEligibleAgentsentinel error when no agents match capabilities or all are at capacity- Tests — capability matching, load distribution, critical priority bypass, no eligible agent error, tie-breaking by agent ID (deterministic)
3.3 Dispatcher
- Create
dispatcher.go—Dispatcherstruct wrappingAgentRegistry,TaskRouter,AllowanceService, andClient Dispatch(ctx, task) (string, error)— Route → allowance check → claim via client → record usage. Returns assigned agent IDDispatchLoop(ctx, interval)— polls client.ListTasks(pending) → dispatches each. Respects context cancellation- Tests — full dispatch flow with mock client, allowance rejection path, no-agent-available path, loop cancellation
Phase 4: CLI Backing Functions — ef81db7
Phase 4 provides the data-fetching and formatting functions that core agent CLI commands will call. The CLI commands themselves live in core/cli.
4.1 Status Summary
- Create
status.go—StatusSummarystruct (Agents []AgentInfo, PendingTasks int, InProgressTasks int, AllowanceRemaining map[string]int64) GetStatus(ctx, registry, client, allowanceSvc) (*StatusSummary, error)— aggregates registry.List(), client.ListTasks counts, allowance remaining per agentFormatStatus(summary) string— tabular text output for CLI rendering- Tests — with mock registry + nil client, full summary with mock client
4.2 Task Submission
SubmitTask(ctx, client, title, description, labels, priority) (*Task, error)— creates a new task via client (requires new Client.CreateTask method)- Add
Client.CreateTask(ctx, task) (*Task, error)— POST /api/tasks - Tests — creation with all fields, validation (empty title), httptest mock
4.3 Log Streaming
- Create
logs.go—StreamLogs(ctx, client, taskID, writer) error— polls task updates and writes progress to io.Writer - Tests — mock client with progress updates, context cancellation
Phase 5: Persistent Agent Registry
The AgentRegistry interface only has MemoryRegistry — a restart drops all agent registrations. This mirrors the AllowanceStore pattern: memory → SQLite → Redis.
5.1 SQLite Registry
- Create
registry_sqlite.go—SQLiteRegistryimplementingAgentRegistryinterface - Schema:
agentstable (id TEXT PK, name TEXT, capabilities TEXT JSON, status INT, last_heartbeat DATETIME, current_load INT, max_load INT, registered_at DATETIME) - Use
modernc.org/sqlite(already a transitive dep via go-store) with WAL mode Register→ UPSERT,Deregister→ DELETE,Get→ SELECT,List→ SELECT all,Heartbeat→ UPDATE last_heartbeat,Reap(ttl)→ UPDATE status=Offline WHERE last_heartbeat < now-ttl RETURNING id- Tests — full parity with
registry_test.gousing:memory:SQLite, concurrent access under-race
5.2 Redis Registry
- Create
registry_redis.go—RedisRegistryimplementingAgentRegistrywith TTL-based reaping - Key pattern:
{prefix}:agent:{id}→ JSON AgentInfo, with TTL = heartbeat interval * 3 Heartbeat→ re-SET with TTL refresh (natural expiry = auto-reap)List→ SCAN{prefix}:agent:*,Reap→ explicit scan for expired (backup to natural TTL)- Tests — skip-if-no-Redis pattern, unique prefix per test
5.3 Config Factory
- Add
RegistryConfigtoconfig.go—RegistryBackend string(memory/sqlite/redis),RegistryPath string,RegistryRedisAddr string NewAgentRegistryFromConfig(cfg) (AgentRegistry, error)— factory mirroringNewAllowanceStoreFromConfig- Tests — all backends, unknown backend error
Phase 6: Dead Code Cleanup + Rate Enforcement
HourlyRateLimit and CostCeiling on ModelQuota are stored but never enforced in AllowanceService.Check. Either implement or remove.
6.1 Enforce or Remove Dead Fields
- Audit
HourlyRateLimitandCostCeilinginallowance_service.go— these fields exist inModelQuotabutCheck()never evaluates them - If keeping: add hourly sliding window check in
AllowanceService.Checkbefore daily limit. AddCostCeilingas cumulative spend cap. AddHourlyUsagetracking toAllowanceStoreinterface (new method:GetHourlyUsage(agentID, model string) (int64, error)) - If removing: delete fields from
ModelQuota, update tests, document decision in FINDINGS.md - Tests — hourly rate exceeded, cost ceiling exceeded, both combined with existing daily limits
6.2 Fix DefaultBaseURL
DefaultBaseURLinconfig.gopoints toapi.core-agentic.devwhich doesn't exist. Change to empty string (require explicit config) orhttp://localhost:8080for local dev- Test — verify default config doesn't silently fail
Phase 7: Priority-Ordered Dispatch + Retry
DispatchLoop dispatches tasks in arbitrary order with no retry backoff.
7.1 Priority Sorting
- Sort pending tasks in
DispatchLoopbyPriority(Critical > High > Normal > Low) before dispatching - Tie-break by
CreatedAt(oldest first within same priority) - Tests — 5 tasks with mixed priorities dispatched in correct order
7.2 Retry Backoff
- Add
MaxRetriesandRetryCountfields toTasktype - Exponential backoff in
DispatchLoop— skip tasks whereRetryCount > 0andLastAttempt + backoff(RetryCount) > now - Dead-letter — tasks exceeding
MaxRetries(default 3) get statusTaskFailedwith reason "max retries exceeded" - Tests — retry delay respected, dead-letter after max retries, backoff calculation
Phase 8: Event Hooks
Production orchestration needs event notifications for task lifecycle transitions.
8.1 EventEmitter Interface
- Create
events.go—Eventstruct (Type string, TaskID string, AgentID string, Timestamp time.Time, Payload any) EventEmitterinterface —Emit(ctx context.Context, event Event) errorChannelEmitter— in-processchan Eventfor local subscribers (buffered, non-blocking)MultiEmitter— fans out to multiple emitters- Tests — emit and receive, buffer overflow drops, multi-emitter fan-out
8.2 Dispatcher Integration
- Wire
EventEmitterintoDispatcher— emit on: task_dispatched, task_claimed, dispatch_failed (no agent), dispatch_failed (quota) - Wire into
AllowanceService— emit on: quota_warning (80%), quota_exceeded, usage_recorded - Tests — verify events emitted at correct lifecycle points
Workflow
- Virgil in core/go writes tasks here after research
- This repo's dedicated session picks up tasks in phase order
- Mark
[x]when done, note commit hash