@swarmclawai/swarmclaw 1.2.9 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/package.json +2 -2
- package/src/app/api/activity/route.ts +9 -23
- package/src/app/api/agents/route.ts +17 -1
- package/src/app/api/approvals/route.ts +13 -5
- package/src/app/api/credentials/[id]/route.ts +2 -0
- package/src/app/api/credentials/route.ts +4 -1
- package/src/app/api/goals/[id]/route.ts +28 -0
- package/src/app/api/goals/route.ts +33 -0
- package/src/app/api/protocols/templates/[id]/route.ts +2 -1
- package/src/app/api/protocols/templates/route.ts +2 -1
- package/src/app/api/settings/route.ts +13 -0
- package/src/app/home/page.tsx +3 -0
- package/src/cli/index.js +11 -0
- package/src/cli/spec.js +10 -0
- package/src/lib/server/activity/activity-log.ts +16 -1
- package/src/lib/server/agents/agent-service.ts +24 -11
- package/src/lib/server/approval-match.ts +14 -0
- package/src/lib/server/approvals/approval-hooks.ts +81 -0
- package/src/lib/server/approvals.ts +11 -0
- package/src/lib/server/connectors/swarmdock-bidding.ts +2 -9
- package/src/lib/server/connectors/swarmdock-payloads.test.ts +18 -1
- package/src/lib/server/connectors/swarmdock-tasks.ts +10 -11
- package/src/lib/server/connectors/swarmdock.ts +111 -43
- package/src/lib/server/execution-brief.ts +18 -0
- package/src/lib/server/goals/goal-repository.ts +19 -0
- package/src/lib/server/goals/goal-service.ts +143 -0
- package/src/lib/server/storage-normalization.ts +5 -0
- package/src/lib/server/storage.ts +57 -0
- package/src/lib/server/usage/cost-rollup.ts +124 -0
- package/src/lib/server/usage/usage-repository.ts +6 -0
- package/src/lib/validation/schemas.ts +3 -30
- package/src/lib/validation/server-schemas.ts +35 -0
- package/src/types/agent.ts +10 -0
- package/src/types/app-settings.ts +6 -0
- package/src/types/approval.ts +3 -0
- package/src/types/goal.ts +30 -0
- package/src/types/index.ts +1 -0
- package/src/types/misc.ts +2 -2
- package/src/types/task.ts +2 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Agent } from '@/types'
|
|
2
|
+
import { patchAgent } from '@/lib/server/agents/agent-repository'
|
|
3
|
+
import { logActivity } from '@/lib/server/activity/activity-log'
|
|
4
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
import { log } from '@/lib/server/logger'
|
|
6
|
+
|
|
7
|
+
const TAG = 'cost-rollup'
|
|
8
|
+
|
|
9
|
+
const ONE_HOUR_MS = 60 * 60 * 1000
|
|
10
|
+
|
|
11
|
+
function toWindowBoundaries(now: number) {
|
|
12
|
+
const d = new Date(now)
|
|
13
|
+
const dayStart = new Date(d)
|
|
14
|
+
dayStart.setUTCHours(0, 0, 0, 0)
|
|
15
|
+
const monthStart = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1))
|
|
16
|
+
return {
|
|
17
|
+
hourStartTs: now - ONE_HOUR_MS,
|
|
18
|
+
dayStartTs: dayStart.getTime(),
|
|
19
|
+
monthStartTs: monthStart.getTime(),
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Push-based cost rollup: atomically increments the agent's persisted spend
|
|
25
|
+
* fields and checks budgets. Called immediately after each usage record is appended.
|
|
26
|
+
*/
|
|
27
|
+
export function rollupCostToAgent(agentId: string, costUsd: number): void {
|
|
28
|
+
if (!agentId || !Number.isFinite(costUsd) || costUsd <= 0) return
|
|
29
|
+
|
|
30
|
+
const now = Date.now()
|
|
31
|
+
const costCents = Math.round(costUsd * 100)
|
|
32
|
+
const { hourStartTs, dayStartTs, monthStartTs } = toWindowBoundaries(now)
|
|
33
|
+
|
|
34
|
+
const updated = patchAgent(agentId, (current) => {
|
|
35
|
+
if (!current) return null
|
|
36
|
+
const lastRollup = current.lastSpendRollupAt ?? 0
|
|
37
|
+
const lastBounds = toWindowBoundaries(lastRollup)
|
|
38
|
+
|
|
39
|
+
// Reset windows that have rolled over since last rollup
|
|
40
|
+
let hourly = current.spentHourlyCents ?? 0
|
|
41
|
+
let daily = current.spentDailyCents ?? 0
|
|
42
|
+
let monthly = current.spentMonthlyCents ?? 0
|
|
43
|
+
|
|
44
|
+
if (lastRollup < hourStartTs) hourly = 0
|
|
45
|
+
if (lastBounds.dayStartTs < dayStartTs) daily = 0
|
|
46
|
+
if (lastBounds.monthStartTs < monthStartTs) monthly = 0
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
...current,
|
|
50
|
+
spentHourlyCents: hourly + costCents,
|
|
51
|
+
spentDailyCents: daily + costCents,
|
|
52
|
+
spentMonthlyCents: monthly + costCents,
|
|
53
|
+
lastSpendRollupAt: now,
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
if (!updated) return
|
|
58
|
+
|
|
59
|
+
// Check budgets and enforce
|
|
60
|
+
checkAndEnforceBudget(updated)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Checks agent's persisted spend against configured budgets and logs
|
|
65
|
+
* activity entries when thresholds are hit.
|
|
66
|
+
*/
|
|
67
|
+
function checkAndEnforceBudget(agent: Agent): void {
|
|
68
|
+
const WARNING_RATIO = 0.8
|
|
69
|
+
|
|
70
|
+
const windows = [
|
|
71
|
+
{ key: 'hourly' as const, budget: agent.hourlyBudget, spent: (agent.spentHourlyCents ?? 0) / 100 },
|
|
72
|
+
{ key: 'daily' as const, budget: agent.dailyBudget, spent: (agent.spentDailyCents ?? 0) / 100 },
|
|
73
|
+
{ key: 'monthly' as const, budget: agent.monthlyBudget, spent: (agent.spentMonthlyCents ?? 0) / 100 },
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
for (const { key, budget, spent } of windows) {
|
|
77
|
+
if (!budget || !Number.isFinite(budget) || budget <= 0) continue
|
|
78
|
+
const ratio = spent / budget
|
|
79
|
+
|
|
80
|
+
if (ratio >= 1) {
|
|
81
|
+
log.warn(TAG, `Agent "${agent.name}" exceeded ${key} budget: $${spent.toFixed(4)} / $${budget.toFixed(2)}`)
|
|
82
|
+
logActivity({
|
|
83
|
+
entityType: 'budget',
|
|
84
|
+
entityId: agent.id,
|
|
85
|
+
action: 'budget_exceeded',
|
|
86
|
+
actor: 'system',
|
|
87
|
+
summary: `Agent "${agent.name}" exceeded ${key} budget: $${spent.toFixed(4)} / $${budget.toFixed(2)}`,
|
|
88
|
+
detail: { window: key, spent, budget, ratio },
|
|
89
|
+
})
|
|
90
|
+
notify('agents')
|
|
91
|
+
} else if (ratio >= WARNING_RATIO) {
|
|
92
|
+
logActivity({
|
|
93
|
+
entityType: 'budget',
|
|
94
|
+
entityId: agent.id,
|
|
95
|
+
action: 'budget_warning',
|
|
96
|
+
actor: 'system',
|
|
97
|
+
summary: `Agent "${agent.name}" nearing ${key} budget: $${spent.toFixed(4)} / $${budget.toFixed(2)} (${Math.round(ratio * 100)}%)`,
|
|
98
|
+
detail: { window: key, spent, budget, ratio },
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reset all agents' daily spend counters. Call at UTC midnight.
|
|
106
|
+
*/
|
|
107
|
+
export function resetDailySpends(agents: Record<string, Agent>): void {
|
|
108
|
+
for (const agent of Object.values(agents)) {
|
|
109
|
+
if ((agent.spentDailyCents ?? 0) > 0) {
|
|
110
|
+
patchAgent(agent.id, (current) => current ? { ...current, spentDailyCents: 0 } : null)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Reset all agents' monthly spend counters. Call at UTC month boundary.
|
|
117
|
+
*/
|
|
118
|
+
export function resetMonthlySpends(agents: Record<string, Agent>): void {
|
|
119
|
+
for (const agent of Object.values(agents)) {
|
|
120
|
+
if ((agent.spentMonthlyCents ?? 0) > 0) {
|
|
121
|
+
patchAgent(agent.id, (current) => current ? { ...current, spentMonthlyCents: 0 } : null)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
saveUsage as saveStoredUsage,
|
|
9
9
|
} from '@/lib/server/storage'
|
|
10
10
|
import { perf } from '@/lib/server/runtime/perf'
|
|
11
|
+
import { rollupCostToAgent } from './cost-rollup'
|
|
11
12
|
|
|
12
13
|
export function loadUsage(): Record<string, UsageRecord[]> {
|
|
13
14
|
return perf.measureSync('repository', 'usage.list', () => loadStoredUsage())
|
|
@@ -19,6 +20,11 @@ export function saveUsage(data: Record<string, UsageRecord[]>): void {
|
|
|
19
20
|
|
|
20
21
|
export function appendUsage(sessionId: string, record: unknown): void {
|
|
21
22
|
perf.measureSync('repository', 'usage.append', () => appendStoredUsage(sessionId, record), { sessionId })
|
|
23
|
+
// Push-based cost rollup: update persisted spend fields and check budgets
|
|
24
|
+
const rec = record as Partial<UsageRecord>
|
|
25
|
+
if (rec.agentId && typeof rec.estimatedCost === 'number' && rec.estimatedCost > 0) {
|
|
26
|
+
rollupCostToAgent(rec.agentId, rec.estimatedCost)
|
|
27
|
+
}
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
export function getUsageSpendSince(minTimestamp: number): number {
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import type { ProtocolStepDefinition } from '@/types'
|
|
3
|
-
import { validateStepDag, validateStepRefs } from '@/lib/server/protocols/step-dag-validation'
|
|
4
2
|
|
|
5
3
|
const OllamaModeSchema = z.enum(['local', 'cloud']).nullable().optional().default(null)
|
|
6
4
|
|
|
@@ -424,7 +422,9 @@ export const ProtocolRunCreateSchema = z.object({
|
|
|
424
422
|
entryStepId: z.string().nullable().optional().default(null),
|
|
425
423
|
})
|
|
426
424
|
|
|
427
|
-
|
|
425
|
+
/** Base protocol template schema without server-side DAG validation.
|
|
426
|
+
* Use ProtocolTemplateUpsertSchema from '@/lib/validation/server-schemas' for full server-side validation. */
|
|
427
|
+
export const ProtocolTemplateUpsertBaseSchema = z.object({
|
|
428
428
|
name: z.string().min(1, 'A template name is required'),
|
|
429
429
|
description: z.string().min(1, 'A template description is required'),
|
|
430
430
|
singleAgentAllowed: z.boolean().optional().default(true),
|
|
@@ -433,33 +433,6 @@ export const ProtocolTemplateUpsertSchema = z.object({
|
|
|
433
433
|
defaultPhases: z.array(ProtocolPhaseDefinitionSchema).optional().default([]),
|
|
434
434
|
steps: z.array(ProtocolStepDefinitionSchema).optional().default([]),
|
|
435
435
|
entryStepId: z.string().nullable().optional().default(null),
|
|
436
|
-
}).superRefine((value, ctx) => {
|
|
437
|
-
if (value.defaultPhases.length === 0 && value.steps.length === 0) {
|
|
438
|
-
ctx.addIssue({
|
|
439
|
-
code: z.ZodIssueCode.custom,
|
|
440
|
-
path: ['steps'],
|
|
441
|
-
message: 'Provide at least one phase or one step.',
|
|
442
|
-
})
|
|
443
|
-
}
|
|
444
|
-
if (value.steps.length > 0) {
|
|
445
|
-
const steps = value.steps as ProtocolStepDefinition[]
|
|
446
|
-
const dagResult = validateStepDag(steps)
|
|
447
|
-
if (!dagResult.valid) {
|
|
448
|
-
ctx.addIssue({
|
|
449
|
-
code: z.ZodIssueCode.custom,
|
|
450
|
-
path: ['steps'],
|
|
451
|
-
message: `Cycle detected in step dependencies: ${dagResult.cycle?.join(' → ')}`,
|
|
452
|
-
})
|
|
453
|
-
}
|
|
454
|
-
const invalidRefs = validateStepRefs(steps)
|
|
455
|
-
for (const ref of invalidRefs) {
|
|
456
|
-
ctx.addIssue({
|
|
457
|
-
code: z.ZodIssueCode.custom,
|
|
458
|
-
path: ['steps'],
|
|
459
|
-
message: `Step references unknown step ID: "${ref}"`,
|
|
460
|
-
})
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
436
|
})
|
|
464
437
|
|
|
465
438
|
export const ProtocolRunActionSchema = z.object({
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { ProtocolStepDefinition } from '@/types'
|
|
3
|
+
import { validateStepDag, validateStepRefs } from '@/lib/server/protocols/step-dag-validation'
|
|
4
|
+
import { ProtocolTemplateUpsertBaseSchema } from './schemas'
|
|
5
|
+
|
|
6
|
+
/** Full protocol template upsert schema with server-side DAG validation.
|
|
7
|
+
* For client-side use (without server deps), use ProtocolTemplateUpsertBaseSchema from './schemas'. */
|
|
8
|
+
export const ProtocolTemplateUpsertSchema = ProtocolTemplateUpsertBaseSchema.superRefine((value, ctx) => {
|
|
9
|
+
if (value.defaultPhases.length === 0 && value.steps.length === 0) {
|
|
10
|
+
ctx.addIssue({
|
|
11
|
+
code: z.ZodIssueCode.custom,
|
|
12
|
+
path: ['steps'],
|
|
13
|
+
message: 'Provide at least one phase or one step.',
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
if (value.steps.length > 0) {
|
|
17
|
+
const steps = value.steps as ProtocolStepDefinition[]
|
|
18
|
+
const dagResult = validateStepDag(steps)
|
|
19
|
+
if (!dagResult.valid) {
|
|
20
|
+
ctx.addIssue({
|
|
21
|
+
code: z.ZodIssueCode.custom,
|
|
22
|
+
path: ['steps'],
|
|
23
|
+
message: `Cycle detected in step dependencies: ${dagResult.cycle?.join(' → ')}`,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
const invalidRefs = validateStepRefs(steps)
|
|
27
|
+
for (const ref of invalidRefs) {
|
|
28
|
+
ctx.addIssue({
|
|
29
|
+
code: z.ZodIssueCode.custom,
|
|
30
|
+
path: ['steps'],
|
|
31
|
+
message: `Step references unknown step ID: "${ref}"`,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
})
|
package/src/types/agent.ts
CHANGED
|
@@ -88,6 +88,8 @@ export interface Agent {
|
|
|
88
88
|
monthlyBudget?: number | null
|
|
89
89
|
dailyBudget?: number | null
|
|
90
90
|
hourlyBudget?: number | null
|
|
91
|
+
/** Reference to a Goal in the goal hierarchy. */
|
|
92
|
+
goalId?: string | null
|
|
91
93
|
autoRecovery?: boolean
|
|
92
94
|
proactiveMemory?: boolean
|
|
93
95
|
/** Auto-refresh a reviewed skill draft from meaningful chat turns for this agent. */
|
|
@@ -152,6 +154,14 @@ export interface Agent {
|
|
|
152
154
|
dailySpend?: number
|
|
153
155
|
/** Runtime-enriched: trailing 1-hour spend. Populated by GET /api/agents when hourlyBudget is set. */
|
|
154
156
|
hourlySpend?: number
|
|
157
|
+
/** Persisted: accumulated spend in current monthly window (USD). Updated on each usage event. */
|
|
158
|
+
spentMonthlyCents?: number
|
|
159
|
+
/** Persisted: accumulated spend in current daily window (USD). Updated on each usage event. */
|
|
160
|
+
spentDailyCents?: number
|
|
161
|
+
/** Persisted: accumulated spend in current hourly window (USD). Updated on each usage event. */
|
|
162
|
+
spentHourlyCents?: number
|
|
163
|
+
/** Timestamp of last spend rollup; used to detect window resets. */
|
|
164
|
+
lastSpendRollupAt?: number
|
|
155
165
|
maxFollowupChain?: number
|
|
156
166
|
|
|
157
167
|
// Orchestrator Mode
|
|
@@ -148,6 +148,12 @@ export interface AppSettings {
|
|
|
148
148
|
toolLoopCircuitBreaker?: number
|
|
149
149
|
// Per-extension settings (keyed by extensionId)
|
|
150
150
|
extensionSettings?: Record<string, Record<string, unknown>>
|
|
151
|
+
// Approval policies — opt-in governance gates for sensitive operations
|
|
152
|
+
approvalPolicies?: {
|
|
153
|
+
requireApprovalForAgentCreate?: boolean
|
|
154
|
+
requireApprovalForBudgetChange?: boolean
|
|
155
|
+
requireApprovalForDelegationEnable?: boolean
|
|
156
|
+
}
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
export interface EstopState {
|
package/src/types/approval.ts
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type GoalLevel = 'organization' | 'team' | 'project' | 'agent' | 'task'
|
|
2
|
+
|
|
3
|
+
export type GoalStatus = 'active' | 'achieved' | 'abandoned'
|
|
4
|
+
|
|
5
|
+
export interface Goal {
|
|
6
|
+
id: string
|
|
7
|
+
title: string
|
|
8
|
+
description?: string
|
|
9
|
+
level: GoalLevel
|
|
10
|
+
parentGoalId?: string | null
|
|
11
|
+
/** Link to a project (for project-level goals). */
|
|
12
|
+
projectId?: string | null
|
|
13
|
+
/** Link to an agent (for agent-level goals). */
|
|
14
|
+
agentId?: string | null
|
|
15
|
+
/** Link to a task (for task-level goals). */
|
|
16
|
+
taskId?: string | null
|
|
17
|
+
/** The concrete objective this goal achieves. */
|
|
18
|
+
objective: string
|
|
19
|
+
/** Constraints or guardrails on how the goal should be pursued. */
|
|
20
|
+
constraints?: string[]
|
|
21
|
+
/** How success is measured. */
|
|
22
|
+
successMetric?: string | null
|
|
23
|
+
/** Optional budget cap for this goal (USD). */
|
|
24
|
+
budgetUsd?: number | null
|
|
25
|
+
/** Optional deadline. */
|
|
26
|
+
deadlineAt?: number | null
|
|
27
|
+
status: GoalStatus
|
|
28
|
+
createdAt: number
|
|
29
|
+
updatedAt: number
|
|
30
|
+
}
|
package/src/types/index.ts
CHANGED
package/src/types/misc.ts
CHANGED
|
@@ -126,9 +126,9 @@ export interface Chatroom {
|
|
|
126
126
|
|
|
127
127
|
export interface ActivityEntry {
|
|
128
128
|
id: string
|
|
129
|
-
entityType: 'agent' | 'task' | 'connector' | 'session' | 'webhook' | 'schedule' | 'delegation' | 'swarm' | 'chatroom' | 'coordination'
|
|
129
|
+
entityType: 'agent' | 'task' | 'connector' | 'session' | 'webhook' | 'schedule' | 'delegation' | 'swarm' | 'chatroom' | 'coordination' | 'approval' | 'settings' | 'budget' | 'credential'
|
|
130
130
|
entityId: string
|
|
131
|
-
action: 'created' | 'updated' | 'deleted' | 'started' | 'stopped' | 'queued' | 'completed' | 'failed' | 'archived' | 'restored' | 'approved' | 'rejected' | 'delegated' | 'queried' | 'spawned' | 'timeout' | 'cancelled' | 'incident' | 'running' | 'claimed'
|
|
131
|
+
action: 'created' | 'updated' | 'deleted' | 'started' | 'stopped' | 'queued' | 'completed' | 'failed' | 'archived' | 'restored' | 'approved' | 'rejected' | 'delegated' | 'queried' | 'spawned' | 'timeout' | 'cancelled' | 'incident' | 'running' | 'claimed' | 'configured' | 'budget_exceeded' | 'budget_warning'
|
|
132
132
|
actor: 'user' | 'agent' | 'system' | 'daemon'
|
|
133
133
|
actorId?: string
|
|
134
134
|
summary: string
|
package/src/types/task.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface BoardTask {
|
|
|
35
35
|
rootTaskId?: string | null
|
|
36
36
|
projectId?: string
|
|
37
37
|
goalContract?: GoalContract | null
|
|
38
|
+
/** Reference to a Goal in the goal hierarchy. Takes precedence over goalContract when set. */
|
|
39
|
+
goalId?: string | null
|
|
38
40
|
cwd?: string | null
|
|
39
41
|
file?: string | null
|
|
40
42
|
sessionId?: string | null
|