feat(agentic): type fleet compute budgets

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 20:50:26 +00:00
parent 02aea97b7d
commit 8712c7c921
3 changed files with 181 additions and 4 deletions

View file

@ -11,6 +11,16 @@ import (
core "dappco.re/go/core"
)
// node := agentic.FleetNode{AgentID: "charon", Platform: "linux", Status: "online"}
type ComputeBudget struct {
MaxDailyHours float64 `json:"max_daily_hours,omitempty"`
MaxWeeklyCostUSD float64 `json:"max_weekly_cost_usd,omitempty"`
QuietStart string `json:"quiet_start,omitempty"`
QuietEnd string `json:"quiet_end,omitempty"`
PreferModels []string `json:"prefer_models,omitempty"`
AvoidModels []string `json:"avoid_models,omitempty"`
}
// node := agentic.FleetNode{AgentID: "charon", Platform: "linux", Status: "online"}
type FleetNode struct {
ID int `json:"id"`
@ -20,7 +30,7 @@ type FleetNode struct {
Models []string `json:"models,omitempty"`
Capabilities []string `json:"capabilities,omitempty"`
Status string `json:"status"`
ComputeBudget map[string]any `json:"compute_budget,omitempty"`
ComputeBudget *ComputeBudget `json:"compute_budget,omitempty"`
CurrentTaskID int `json:"current_task_id,omitempty"`
LastHeartbeatAt string `json:"last_heartbeat_at,omitempty"`
RegisteredAt string `json:"registered_at,omitempty"`
@ -710,13 +720,77 @@ func parseFleetNode(values map[string]any) FleetNode {
Models: listValue(values["models"]),
Capabilities: listValue(values["capabilities"]),
Status: stringValue(values["status"]),
ComputeBudget: anyMapValue(values["compute_budget"]),
ComputeBudget: computeBudgetFromValue(values["compute_budget"]),
CurrentTaskID: intValue(values["current_task_id"]),
LastHeartbeatAt: stringValue(values["last_heartbeat_at"]),
RegisteredAt: stringValue(values["registered_at"]),
}
}
func computeBudgetFromValue(value any) *ComputeBudget {
switch typed := value.(type) {
case *ComputeBudget:
if typed == nil || computeBudgetIsZero(*typed) {
return nil
}
return typed
case ComputeBudget:
if computeBudgetIsZero(typed) {
return nil
}
return &typed
case map[string]any:
return computeBudgetFromMap(typed)
case map[string]string:
values := make(map[string]any, len(typed))
for key, item := range typed {
values[key] = item
}
return computeBudgetFromMap(values)
case string:
trimmed := core.Trim(typed)
if trimmed == "" {
return nil
}
if core.HasPrefix(trimmed, "{") {
var values map[string]any
if result := core.JSONUnmarshalString(trimmed, &values); result.OK {
return computeBudgetFromMap(values)
}
}
}
return nil
}
func computeBudgetFromMap(values map[string]any) *ComputeBudget {
if len(values) == 0 {
return nil
}
budget := &ComputeBudget{
MaxDailyHours: floatValue(values["max_daily_hours"]),
MaxWeeklyCostUSD: floatValue(values["max_weekly_cost_usd"]),
QuietStart: stringValue(values["quiet_start"]),
QuietEnd: stringValue(values["quiet_end"]),
PreferModels: listValue(values["prefer_models"]),
AvoidModels: listValue(values["avoid_models"]),
}
if computeBudgetIsZero(*budget) {
return nil
}
return budget
}
func computeBudgetIsZero(budget ComputeBudget) bool {
return budget.MaxDailyHours == 0 &&
budget.MaxWeeklyCostUSD == 0 &&
core.Trim(budget.QuietStart) == "" &&
core.Trim(budget.QuietEnd) == "" &&
len(budget.PreferModels) == 0 &&
len(budget.AvoidModels) == 0
}
func parseFleetTask(values map[string]any) FleetTask {
return FleetTask{
ID: intValue(values["id"]),
@ -1144,3 +1218,28 @@ func intValue(value any) int {
}
return 0
}
func floatValue(value any) float64 {
switch typed := value.(type) {
case float64:
return typed
case float32:
return float64(typed)
case int:
return float64(typed)
case int64:
return float64(typed)
case string:
trimmed := core.Trim(typed)
if trimmed == "" {
return 0
}
var parsed float64
if result := core.JSONUnmarshalString(core.Concat("{\"n\":", trimmed, "}"), &struct {
N *float64 `json:"n"`
}{N: &parsed}); result.OK {
return parsed
}
}
return 0
}

View file

@ -71,6 +71,50 @@ func TestPlatform_HandleFleetRegister_Good(t *testing.T) {
assert.Equal(t, []string{"go", "review"}, node.Capabilities)
}
func TestPlatform_HandleFleetHeartbeat_Good_ComputeBudget(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/v1/fleet/heartbeat", r.URL.Path)
require.Equal(t, "Bearer secret-token", r.Header.Get("Authorization"))
bodyResult := core.ReadAll(r.Body)
require.True(t, bodyResult.OK)
var payload map[string]any
parseResult := core.JSONUnmarshalString(bodyResult.Value.(string), &payload)
require.True(t, parseResult.OK)
budget, ok := payload["compute_budget"].(map[string]any)
require.True(t, ok)
assert.Equal(t, 2.5, budget["max_daily_hours"])
assert.Equal(t, 12.5, budget["max_weekly_cost_usd"])
assert.Equal(t, "22:00", budget["quiet_start"])
assert.Equal(t, "06:00", budget["quiet_end"])
assert.Equal(t, []any{"codex", "gpt-5.4"}, budget["prefer_models"])
assert.Equal(t, []any{"gpt-4.1"}, budget["avoid_models"])
_, _ = w.Write([]byte(`{"data":{"id":1,"agent_id":"charon","platform":"linux","status":"online","compute_budget":{"max_daily_hours":2.5,"max_weekly_cost_usd":12.5,"quiet_start":"22:00","quiet_end":"06:00","prefer_models":["codex","gpt-5.4"],"avoid_models":["gpt-4.1"]}}}`))
}))
defer server.Close()
subsystem := testPrepWithPlatformServer(t, server, "secret-token")
result := subsystem.handleFleetHeartbeat(context.Background(), core.NewOptions(
core.Option{Key: "agent_id", Value: "charon"},
core.Option{Key: "status", Value: "online"},
core.Option{Key: "compute_budget", Value: `{"max_daily_hours":2.5,"max_weekly_cost_usd":12.5,"quiet_start":"22:00","quiet_end":"06:00","prefer_models":["codex","gpt-5.4"],"avoid_models":["gpt-4.1"]}`},
))
require.True(t, result.OK)
node, ok := result.Value.(FleetNode)
require.True(t, ok)
require.NotNil(t, node.ComputeBudget)
assert.Equal(t, 2.5, node.ComputeBudget.MaxDailyHours)
assert.Equal(t, 12.5, node.ComputeBudget.MaxWeeklyCostUSD)
assert.Equal(t, "22:00", node.ComputeBudget.QuietStart)
assert.Equal(t, "06:00", node.ComputeBudget.QuietEnd)
assert.Equal(t, []string{"codex", "gpt-5.4"}, node.ComputeBudget.PreferModels)
assert.Equal(t, []string{"gpt-4.1"}, node.ComputeBudget.AvoidModels)
}
func TestPlatform_HandleFleetRegister_Bad(t *testing.T) {
subsystem := testPrepWithPlatformServer(t, nil, "")
@ -291,7 +335,7 @@ func TestPlatform_HandleCreditsHistory_Good(t *testing.T) {
func TestPlatform_HandleFleetNodes_Good_NestedEnvelope(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"data":{"nodes":[{"id":1,"workspace_id":7,"agent_id":"charon","platform":"linux","models":["codex"],"status":"online"}],"total":1}}`))
_, _ = w.Write([]byte(`{"data":{"nodes":[{"id":1,"workspace_id":7,"agent_id":"charon","platform":"linux","models":["codex"],"status":"online","compute_budget":{"max_daily_hours":3,"quiet_start":"22:00","prefer_models":["codex"]}}],"total":1}}`))
}))
defer server.Close()
@ -304,6 +348,10 @@ func TestPlatform_HandleFleetNodes_Good_NestedEnvelope(t *testing.T) {
require.Len(t, output.Nodes, 1)
assert.Equal(t, 1, output.Total)
assert.Equal(t, 7, output.Nodes[0].WorkspaceID)
require.NotNil(t, output.Nodes[0].ComputeBudget)
assert.Equal(t, 3.0, output.Nodes[0].ComputeBudget.MaxDailyHours)
assert.Equal(t, "22:00", output.Nodes[0].ComputeBudget.QuietStart)
assert.Equal(t, []string{"codex"}, output.Nodes[0].ComputeBudget.PreferModels)
}
func TestPlatform_HandleCreditsHistory_Good_NestedEnvelope(t *testing.T) {

View file

@ -222,7 +222,7 @@ func (s *PrepSubsystem) fleetHeartbeatTool(ctx context.Context, _ *mcp.CallToolR
options := platformOptions(
core.Option{Key: "agent_id", Value: input.AgentID},
core.Option{Key: "status", Value: input.Status},
core.Option{Key: "compute_budget", Value: input.ComputeBudget},
core.Option{Key: "compute_budget", Value: computeBudgetMapValue(input.ComputeBudget)},
)
result := s.handleFleetHeartbeat(ctx, options)
if !result.OK {
@ -235,6 +235,36 @@ func (s *PrepSubsystem) fleetHeartbeatTool(ctx context.Context, _ *mcp.CallToolR
return nil, output, nil
}
func computeBudgetMapValue(budget *ComputeBudget) map[string]any {
if budget == nil || computeBudgetIsZero(*budget) {
return nil
}
values := map[string]any{}
if budget.MaxDailyHours != 0 {
values["max_daily_hours"] = budget.MaxDailyHours
}
if budget.MaxWeeklyCostUSD != 0 {
values["max_weekly_cost_usd"] = budget.MaxWeeklyCostUSD
}
if trimmed := core.Trim(budget.QuietStart); trimmed != "" {
values["quiet_start"] = trimmed
}
if trimmed := core.Trim(budget.QuietEnd); trimmed != "" {
values["quiet_end"] = trimmed
}
if len(budget.PreferModels) > 0 {
values["prefer_models"] = cleanStrings(budget.PreferModels)
}
if len(budget.AvoidModels) > 0 {
values["avoid_models"] = cleanStrings(budget.AvoidModels)
}
if len(values) == 0 {
return nil
}
return values
}
func (s *PrepSubsystem) fleetDeregisterTool(ctx context.Context, _ *mcp.CallToolRequest, input FleetDeregisterInput) (*mcp.CallToolResult, map[string]any, error) {
result := s.handleFleetDeregister(ctx, platformOptions(core.Option{Key: "agent_id", Value: input.AgentID}))
if !result.OK {