@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
|
@@ -2,23 +2,14 @@ import { genId } from '@/lib/id'
|
|
|
2
2
|
import { loadTasks, saveTasks } from '@/lib/server/tasks/task-repository'
|
|
3
3
|
import { logActivity } from '@/lib/server/activity/activity-log'
|
|
4
4
|
import type { BoardTask } from '@/types/task'
|
|
5
|
-
|
|
6
|
-
interface SwarmDockTask {
|
|
7
|
-
id: string
|
|
8
|
-
requesterId: string
|
|
9
|
-
title: string
|
|
10
|
-
description: string
|
|
11
|
-
skillRequirements: string[]
|
|
12
|
-
budgetMax: string
|
|
13
|
-
deadline: string | null
|
|
14
|
-
}
|
|
5
|
+
import type { Task } from '@swarmdock/shared'
|
|
15
6
|
|
|
16
7
|
/**
|
|
17
8
|
* Create a SwarmClaw BoardTask from a SwarmDock task assignment.
|
|
18
9
|
* Uses `externalSource` to link back to the SwarmDock task (same pattern as GitHub issue import).
|
|
19
10
|
*/
|
|
20
11
|
export async function createBoardTaskFromAssignment(
|
|
21
|
-
task:
|
|
12
|
+
task: Task,
|
|
22
13
|
agentId: string,
|
|
23
14
|
connectorId: string,
|
|
24
15
|
apiUrl: string,
|
|
@@ -100,6 +91,14 @@ export async function updateBoardTaskFromEvent(
|
|
|
100
91
|
boardTask.status = 'failed'
|
|
101
92
|
boardTask.checkoutRunId = null
|
|
102
93
|
break
|
|
94
|
+
case 'task.review':
|
|
95
|
+
// Work submitted, awaiting requester review on SwarmDock
|
|
96
|
+
if (boardTask.externalSource) boardTask.externalSource.state = 'review'
|
|
97
|
+
break
|
|
98
|
+
case 'task.disputed':
|
|
99
|
+
// Task disputed on SwarmDock
|
|
100
|
+
if (boardTask.externalSource) boardTask.externalSource.state = 'disputed'
|
|
101
|
+
break
|
|
103
102
|
}
|
|
104
103
|
|
|
105
104
|
boardTask.updatedAt = now
|
|
@@ -5,29 +5,10 @@ import type { Connector, InboundMessage } from '@/types/connector'
|
|
|
5
5
|
import type { PlatformConnector, ConnectorInstance } from '@/lib/server/connectors/types'
|
|
6
6
|
import { createBoardTaskFromAssignment, updateBoardTaskFromEvent, findBoardTaskBySwarmdockId } from './swarmdock-tasks'
|
|
7
7
|
import { shouldAutoBid, submitAutoBid } from './swarmdock-bidding'
|
|
8
|
-
import type { TaskSubmitInput } from '@swarmdock/shared'
|
|
8
|
+
import type { Task, SSEEvent, TaskSubmitInput } from '@swarmdock/shared'
|
|
9
9
|
|
|
10
10
|
const TAG = 'swarmdock'
|
|
11
11
|
|
|
12
|
-
// SDK types inlined until @swarmdock/sdk is built and linked
|
|
13
|
-
interface SwarmDockTask {
|
|
14
|
-
id: string
|
|
15
|
-
requesterId: string
|
|
16
|
-
assigneeId: string | null
|
|
17
|
-
title: string
|
|
18
|
-
description: string
|
|
19
|
-
skillRequirements: string[]
|
|
20
|
-
budgetMax: string
|
|
21
|
-
status: string
|
|
22
|
-
deadline: string | null
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface SwarmDockSSEEvent {
|
|
26
|
-
type: string
|
|
27
|
-
data: Record<string, unknown>
|
|
28
|
-
timestamp: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
12
|
interface SwarmDockConfig {
|
|
32
13
|
apiUrl: string
|
|
33
14
|
walletAddress: string
|
|
@@ -35,6 +16,7 @@ interface SwarmDockConfig {
|
|
|
35
16
|
skills: string
|
|
36
17
|
autoDiscover: boolean
|
|
37
18
|
maxBudget: string
|
|
19
|
+
paymentPrivateKey?: string
|
|
38
20
|
}
|
|
39
21
|
|
|
40
22
|
function parseConfig(connector: Connector): SwarmDockConfig {
|
|
@@ -46,10 +28,11 @@ function parseConfig(connector: Connector): SwarmDockConfig {
|
|
|
46
28
|
skills: c.skills || '',
|
|
47
29
|
autoDiscover: c.autoDiscover === 'true',
|
|
48
30
|
maxBudget: c.maxBudget || '0',
|
|
31
|
+
paymentPrivateKey: c.paymentPrivateKey || undefined,
|
|
49
32
|
}
|
|
50
33
|
}
|
|
51
34
|
|
|
52
|
-
function buildTaskPrompt(task:
|
|
35
|
+
function buildTaskPrompt(task: Task): string {
|
|
53
36
|
const lines: string[] = [
|
|
54
37
|
`# SwarmDock Task: ${task.title}`,
|
|
55
38
|
'',
|
|
@@ -63,6 +46,17 @@ function buildTaskPrompt(task: SwarmDockTask): string {
|
|
|
63
46
|
return lines.join('\n')
|
|
64
47
|
}
|
|
65
48
|
|
|
49
|
+
export function generateExamplePrompts(skillId: string): string[] {
|
|
50
|
+
const name = skillId.replace(/-/g, ' ')
|
|
51
|
+
return [
|
|
52
|
+
`Perform a ${name} task`,
|
|
53
|
+
`Help me with ${name}`,
|
|
54
|
+
`I need ${name} work done`,
|
|
55
|
+
`Complete a ${name} assignment`,
|
|
56
|
+
`Handle a ${name} request`,
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
|
|
66
60
|
function formatUsdc(microUnits: string): string {
|
|
67
61
|
const cents = BigInt(microUnits)
|
|
68
62
|
const dollars = Number(cents) / 1_000_000
|
|
@@ -73,11 +67,13 @@ export async function submitSwarmdockTaskResult(
|
|
|
73
67
|
client: { tasks: { submit: (taskId: string, input: TaskSubmitInput) => Promise<unknown> } },
|
|
74
68
|
swarmdockTaskId: string,
|
|
75
69
|
text: string,
|
|
70
|
+
notes?: string,
|
|
76
71
|
): Promise<void> {
|
|
77
72
|
const payload: TaskSubmitInput = {
|
|
78
73
|
artifacts: [{ type: 'text/markdown', content: text }],
|
|
79
74
|
files: [],
|
|
80
75
|
}
|
|
76
|
+
if (notes) payload.notes = notes
|
|
81
77
|
await client.tasks.submit(swarmdockTaskId, payload)
|
|
82
78
|
}
|
|
83
79
|
|
|
@@ -94,11 +90,15 @@ const swarmdock: PlatformConnector = {
|
|
|
94
90
|
if (!privateKey) throw new Error('SwarmDock connector requires an Ed25519 private key credential')
|
|
95
91
|
if (!config.walletAddress) throw new Error('SwarmDock connector requires a Base L2 wallet address in config')
|
|
96
92
|
|
|
97
|
-
// Dynamic import of the SDK
|
|
93
|
+
// Dynamic import of the SDK
|
|
98
94
|
let SwarmDockClient: typeof import('@swarmdock/sdk').SwarmDockClient
|
|
95
|
+
let ConflictError: typeof import('@swarmdock/sdk').ConflictError
|
|
96
|
+
let AuthenticationError: typeof import('@swarmdock/sdk').AuthenticationError
|
|
99
97
|
try {
|
|
100
98
|
const sdk = await import('@swarmdock/sdk')
|
|
101
99
|
SwarmDockClient = sdk.SwarmDockClient
|
|
100
|
+
ConflictError = sdk.ConflictError
|
|
101
|
+
AuthenticationError = sdk.AuthenticationError
|
|
102
102
|
} catch {
|
|
103
103
|
throw new Error('SwarmDock SDK (@swarmdock/sdk) is not installed. Run: npm install @swarmdock/sdk')
|
|
104
104
|
}
|
|
@@ -106,6 +106,9 @@ const swarmdock: PlatformConnector = {
|
|
|
106
106
|
const client = new SwarmDockClient({
|
|
107
107
|
baseUrl: config.apiUrl,
|
|
108
108
|
privateKey,
|
|
109
|
+
...(config.paymentPrivateKey?.startsWith('0x')
|
|
110
|
+
? { paymentPrivateKey: config.paymentPrivateKey as `0x${string}` }
|
|
111
|
+
: {}),
|
|
109
112
|
})
|
|
110
113
|
|
|
111
114
|
// Register agent on SwarmDock (Ed25519 challenge-response)
|
|
@@ -119,36 +122,49 @@ const swarmdock: PlatformConnector = {
|
|
|
119
122
|
description: `${skillId} capability`,
|
|
120
123
|
category: skillId,
|
|
121
124
|
basePrice: '1000000', // $1.00 default
|
|
125
|
+
inputModes: ['text'],
|
|
126
|
+
outputModes: ['text'],
|
|
127
|
+
examplePrompts: generateExamplePrompts(skillId),
|
|
122
128
|
}))
|
|
123
129
|
|
|
124
130
|
log.info(TAG, `Registering agent "${connector.name}" on SwarmDock at ${config.apiUrl}`)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
131
|
+
try {
|
|
132
|
+
const registration = await client.register({
|
|
133
|
+
displayName: connector.name,
|
|
134
|
+
description: config.agentDescription,
|
|
135
|
+
framework: 'swarmclaw',
|
|
136
|
+
walletAddress: config.walletAddress,
|
|
137
|
+
skills: skillList,
|
|
138
|
+
})
|
|
139
|
+
log.info(TAG, `Registered as ${registration.agent.did} (trust level ${registration.agent.trustLevel})`)
|
|
140
|
+
|
|
141
|
+
logActivity({
|
|
142
|
+
entityType: 'connector',
|
|
143
|
+
entityId: connectorId,
|
|
144
|
+
action: 'swarmdock-registered',
|
|
145
|
+
actor: 'system',
|
|
146
|
+
summary: `Agent "${connector.name}" registered on SwarmDock as ${registration.agent.did}`,
|
|
147
|
+
})
|
|
148
|
+
} catch (err) {
|
|
149
|
+
if (err instanceof ConflictError) {
|
|
150
|
+
log.info(TAG, `Agent already registered, authenticating`)
|
|
151
|
+
await client.authenticate()
|
|
152
|
+
} else {
|
|
153
|
+
throw err
|
|
154
|
+
}
|
|
155
|
+
}
|
|
141
156
|
|
|
142
157
|
// Set up SSE event stream
|
|
143
158
|
let alive = true
|
|
144
159
|
|
|
145
|
-
const handleSSEEvent = async (event:
|
|
160
|
+
const handleSSEEvent = async (event: SSEEvent) => {
|
|
146
161
|
if (!alive) return
|
|
147
162
|
try {
|
|
148
163
|
switch (event.type) {
|
|
149
|
-
case 'task.created':
|
|
164
|
+
case 'task.created':
|
|
165
|
+
case 'task.invited': {
|
|
150
166
|
if (!config.autoDiscover) break
|
|
151
|
-
const task = event.data as unknown as
|
|
167
|
+
const task = event.data as unknown as Task
|
|
152
168
|
if (shouldAutoBid(task, config)) {
|
|
153
169
|
await submitAutoBid(client, task.id, config)
|
|
154
170
|
logActivity({
|
|
@@ -163,7 +179,7 @@ const swarmdock: PlatformConnector = {
|
|
|
163
179
|
}
|
|
164
180
|
|
|
165
181
|
case 'task.assigned': {
|
|
166
|
-
const task = event.data as unknown as
|
|
182
|
+
const task = event.data as unknown as Task
|
|
167
183
|
if (!task.assigneeId) break
|
|
168
184
|
|
|
169
185
|
// Signal work started on SwarmDock
|
|
@@ -195,6 +211,27 @@ const swarmdock: PlatformConnector = {
|
|
|
195
211
|
break
|
|
196
212
|
}
|
|
197
213
|
|
|
214
|
+
case 'task.review': {
|
|
215
|
+
const taskId = (event.data as Record<string, string>).taskId
|
|
216
|
+
if (taskId) await updateBoardTaskFromEvent(taskId, 'task.review')
|
|
217
|
+
break
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
case 'task.disputed': {
|
|
221
|
+
const taskId = (event.data as Record<string, string>).taskId
|
|
222
|
+
if (taskId) {
|
|
223
|
+
await updateBoardTaskFromEvent(taskId, 'task.disputed')
|
|
224
|
+
logActivity({
|
|
225
|
+
entityType: 'connector',
|
|
226
|
+
entityId: connectorId,
|
|
227
|
+
action: 'incident',
|
|
228
|
+
actor: 'system',
|
|
229
|
+
summary: `SwarmDock task ${taskId} disputed`,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
break
|
|
233
|
+
}
|
|
234
|
+
|
|
198
235
|
case 'payment.released': {
|
|
199
236
|
const data = event.data as Record<string, string>
|
|
200
237
|
logActivity({
|
|
@@ -206,6 +243,32 @@ const swarmdock: PlatformConnector = {
|
|
|
206
243
|
})
|
|
207
244
|
break
|
|
208
245
|
}
|
|
246
|
+
|
|
247
|
+
case 'escrow.releasing':
|
|
248
|
+
case 'escrow.refunding': {
|
|
249
|
+
const data = event.data as Record<string, string>
|
|
250
|
+
logActivity({
|
|
251
|
+
entityType: 'connector',
|
|
252
|
+
entityId: connectorId,
|
|
253
|
+
action: 'swarmdock-escrow',
|
|
254
|
+
actor: 'system',
|
|
255
|
+
summary: `Escrow ${event.type.split('.')[1]} for task ${data.taskId}`,
|
|
256
|
+
})
|
|
257
|
+
break
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
case 'escrow.release_failed':
|
|
261
|
+
case 'escrow.refund_failed': {
|
|
262
|
+
const data = event.data as Record<string, string>
|
|
263
|
+
logActivity({
|
|
264
|
+
entityType: 'connector',
|
|
265
|
+
entityId: connectorId,
|
|
266
|
+
action: 'incident',
|
|
267
|
+
actor: 'system',
|
|
268
|
+
summary: `Escrow ${event.type.replace('escrow.', '')} for task ${data.taskId}`,
|
|
269
|
+
})
|
|
270
|
+
break
|
|
271
|
+
}
|
|
209
272
|
}
|
|
210
273
|
} catch (err) {
|
|
211
274
|
log.error(TAG, `Error handling SSE event ${event.type}: ${err instanceof Error ? err.message : String(err)}`)
|
|
@@ -220,7 +283,12 @@ const swarmdock: PlatformConnector = {
|
|
|
220
283
|
await client.heartbeat()
|
|
221
284
|
log.debug(TAG, 'SwarmDock token refreshed')
|
|
222
285
|
} catch (err) {
|
|
223
|
-
|
|
286
|
+
if (err instanceof AuthenticationError) {
|
|
287
|
+
log.warn(TAG, 'SwarmDock token expired, re-authenticating')
|
|
288
|
+
try { await client.authenticate() } catch {}
|
|
289
|
+
} else {
|
|
290
|
+
log.error(TAG, `SwarmDock heartbeat failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
291
|
+
}
|
|
224
292
|
}
|
|
225
293
|
}, 23 * 60 * 60 * 1000)
|
|
226
294
|
|
|
@@ -12,6 +12,7 @@ import { getSession } from '@/lib/server/sessions/session-repository'
|
|
|
12
12
|
import { loadSessionWorkingState } from '@/lib/server/working-state/service'
|
|
13
13
|
import { ensureRunContext } from '@/lib/server/run-context'
|
|
14
14
|
import { cleanText, cleanMultiline } from '@/lib/server/text-normalization'
|
|
15
|
+
import { resolveEffectiveGoal, getGoalChain, formatGoalChainForBrief } from '@/lib/server/goals/goal-service'
|
|
15
16
|
|
|
16
17
|
const MAX_PLAN_ITEMS = 8
|
|
17
18
|
const MAX_FACTS = 8
|
|
@@ -224,8 +225,25 @@ export function buildExecutionBriefContextBlock(
|
|
|
224
225
|
|| brief.evidenceRefs.length > 0,
|
|
225
226
|
)
|
|
226
227
|
if (!hasContent && brief.status === 'idle') return ''
|
|
228
|
+
// Resolve goal chain for the session's agent/task/project context
|
|
229
|
+
let goalBlock = ''
|
|
230
|
+
if (brief.sessionId) {
|
|
231
|
+
const session = getSession(brief.sessionId)
|
|
232
|
+
if (session) {
|
|
233
|
+
const goal = resolveEffectiveGoal({
|
|
234
|
+
agentId: session.agentId || null,
|
|
235
|
+
projectId: session.projectId || null,
|
|
236
|
+
})
|
|
237
|
+
if (goal) {
|
|
238
|
+
const chain = getGoalChain(goal.id)
|
|
239
|
+
goalBlock = formatGoalChainForBrief(chain)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
227
244
|
const sections = [
|
|
228
245
|
options?.title || '## Execution Brief',
|
|
246
|
+
goalBlock,
|
|
229
247
|
brief.parentContext ? `Parent context:\n${brief.parentContext}` : '',
|
|
230
248
|
brief.objective ? `Objective: ${brief.objective}` : '',
|
|
231
249
|
brief.summary ? `Summary: ${brief.summary}` : '',
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Goal } from '@/types'
|
|
2
|
+
import { loadGoals, loadGoal, upsertGoal, deleteGoalItem } from '@/lib/server/storage'
|
|
3
|
+
import { perf } from '@/lib/server/runtime/perf'
|
|
4
|
+
|
|
5
|
+
export function listGoals(): Record<string, Goal> {
|
|
6
|
+
return perf.measureSync('repository', 'goals.list', () => loadGoals()) as Record<string, Goal>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getGoal(id: string): Goal | null {
|
|
10
|
+
return perf.measureSync('repository', 'goals.get', () => loadGoal(id)) as Goal | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function saveGoal(id: string, goal: Goal): void {
|
|
14
|
+
perf.measureSync('repository', 'goals.upsert', () => upsertGoal(id, goal), { id })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function removeGoal(id: string): void {
|
|
18
|
+
perf.measureSync('repository', 'goals.delete', () => deleteGoalItem(id), { id })
|
|
19
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { Goal, GoalLevel } from '@/types'
|
|
2
|
+
import { genId } from '@/lib/id'
|
|
3
|
+
import { listGoals, getGoal, saveGoal, removeGoal } from './goal-repository'
|
|
4
|
+
import { logActivity } from '@/lib/server/activity/activity-log'
|
|
5
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
6
|
+
|
|
7
|
+
export function getAllGoals(): Goal[] {
|
|
8
|
+
return Object.values(listGoals())
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getGoalById(id: string): Goal | null {
|
|
12
|
+
return getGoal(id)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createGoal(input: {
|
|
16
|
+
title: string
|
|
17
|
+
description?: string
|
|
18
|
+
level: GoalLevel
|
|
19
|
+
parentGoalId?: string | null
|
|
20
|
+
projectId?: string | null
|
|
21
|
+
agentId?: string | null
|
|
22
|
+
taskId?: string | null
|
|
23
|
+
objective: string
|
|
24
|
+
constraints?: string[]
|
|
25
|
+
successMetric?: string | null
|
|
26
|
+
budgetUsd?: number | null
|
|
27
|
+
deadlineAt?: number | null
|
|
28
|
+
}): Goal {
|
|
29
|
+
const id = genId()
|
|
30
|
+
const now = Date.now()
|
|
31
|
+
const goal: Goal = {
|
|
32
|
+
id,
|
|
33
|
+
title: input.title,
|
|
34
|
+
description: input.description,
|
|
35
|
+
level: input.level,
|
|
36
|
+
parentGoalId: input.parentGoalId ?? null,
|
|
37
|
+
projectId: input.projectId ?? null,
|
|
38
|
+
agentId: input.agentId ?? null,
|
|
39
|
+
taskId: input.taskId ?? null,
|
|
40
|
+
objective: input.objective,
|
|
41
|
+
constraints: input.constraints ?? [],
|
|
42
|
+
successMetric: input.successMetric ?? null,
|
|
43
|
+
budgetUsd: input.budgetUsd ?? null,
|
|
44
|
+
deadlineAt: input.deadlineAt ?? null,
|
|
45
|
+
status: 'active',
|
|
46
|
+
createdAt: now,
|
|
47
|
+
updatedAt: now,
|
|
48
|
+
}
|
|
49
|
+
saveGoal(id, goal)
|
|
50
|
+
logActivity({ entityType: 'task', entityId: id, action: 'created', actor: 'user', summary: `Goal created: "${goal.title}" (${goal.level})` })
|
|
51
|
+
notify('goals')
|
|
52
|
+
return goal
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function updateGoal(id: string, updates: Partial<Omit<Goal, 'id' | 'createdAt'>>): Goal | null {
|
|
56
|
+
const existing = getGoal(id)
|
|
57
|
+
if (!existing) return null
|
|
58
|
+
const updated: Goal = {
|
|
59
|
+
...existing,
|
|
60
|
+
...updates,
|
|
61
|
+
id,
|
|
62
|
+
createdAt: existing.createdAt,
|
|
63
|
+
updatedAt: Date.now(),
|
|
64
|
+
}
|
|
65
|
+
saveGoal(id, updated)
|
|
66
|
+
logActivity({ entityType: 'task', entityId: id, action: 'updated', actor: 'user', summary: `Goal updated: "${updated.title}"` })
|
|
67
|
+
notify('goals')
|
|
68
|
+
return updated
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function deleteGoal(id: string): boolean {
|
|
72
|
+
const existing = getGoal(id)
|
|
73
|
+
if (!existing) return false
|
|
74
|
+
removeGoal(id)
|
|
75
|
+
logActivity({ entityType: 'task', entityId: id, action: 'deleted', actor: 'user', summary: `Goal deleted: "${existing.title}"` })
|
|
76
|
+
notify('goals')
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Walk the goal hierarchy to build a "why chain" from a specific goal up to the organization root.
|
|
82
|
+
* Returns goals in order from most specific to most general.
|
|
83
|
+
*/
|
|
84
|
+
export function getGoalChain(goalId: string): Goal[] {
|
|
85
|
+
const chain: Goal[] = []
|
|
86
|
+
const visited = new Set<string>()
|
|
87
|
+
let currentId: string | null | undefined = goalId
|
|
88
|
+
while (currentId && !visited.has(currentId)) {
|
|
89
|
+
visited.add(currentId)
|
|
90
|
+
const goal = getGoal(currentId)
|
|
91
|
+
if (!goal) break
|
|
92
|
+
chain.push(goal)
|
|
93
|
+
currentId = goal.parentGoalId
|
|
94
|
+
}
|
|
95
|
+
return chain
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolve the effective goal for a given context by walking the hierarchy:
|
|
100
|
+
* task goal → agent goal → project goal → organization default.
|
|
101
|
+
*/
|
|
102
|
+
export function resolveEffectiveGoal(context: {
|
|
103
|
+
taskId?: string | null
|
|
104
|
+
agentId?: string | null
|
|
105
|
+
projectId?: string | null
|
|
106
|
+
}): Goal | null {
|
|
107
|
+
const goals = getAllGoals().filter((g) => g.status === 'active')
|
|
108
|
+
|
|
109
|
+
// 1. Task-level goal
|
|
110
|
+
if (context.taskId) {
|
|
111
|
+
const taskGoal = goals.find((g) => g.taskId === context.taskId)
|
|
112
|
+
if (taskGoal) return taskGoal
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 2. Agent-level goal
|
|
116
|
+
if (context.agentId) {
|
|
117
|
+
const agentGoal = goals.find((g) => g.level === 'agent' && g.agentId === context.agentId)
|
|
118
|
+
if (agentGoal) return agentGoal
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 3. Project-level goal
|
|
122
|
+
if (context.projectId) {
|
|
123
|
+
const projectGoal = goals.find((g) => g.level === 'project' && g.projectId === context.projectId)
|
|
124
|
+
if (projectGoal) return projectGoal
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 4. Organization default
|
|
128
|
+
const orgGoal = goals.find((g) => g.level === 'organization')
|
|
129
|
+
return orgGoal ?? null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Format a goal chain as a concise text block for injection into agent execution briefs.
|
|
134
|
+
*/
|
|
135
|
+
export function formatGoalChainForBrief(chain: Goal[]): string {
|
|
136
|
+
if (chain.length === 0) return ''
|
|
137
|
+
const lines = chain.map((g, i) => {
|
|
138
|
+
const indent = ' '.repeat(i)
|
|
139
|
+
const label = g.level.charAt(0).toUpperCase() + g.level.slice(1)
|
|
140
|
+
return `${indent}${label}: ${g.title} — ${g.objective}`
|
|
141
|
+
})
|
|
142
|
+
return `Goal alignment:\n${lines.join('\n')}`
|
|
143
|
+
}
|
|
@@ -507,6 +507,11 @@ function normalizeStoredRecordInner(
|
|
|
507
507
|
agent.delegationEnabled = false
|
|
508
508
|
agent.heartbeatEnabled = false
|
|
509
509
|
}
|
|
510
|
+
// Persisted spend rollup defaults
|
|
511
|
+
if (typeof agent.spentMonthlyCents !== 'number') agent.spentMonthlyCents = 0
|
|
512
|
+
if (typeof agent.spentDailyCents !== 'number') agent.spentDailyCents = 0
|
|
513
|
+
if (typeof agent.spentHourlyCents !== 'number') agent.spentHourlyCents = 0
|
|
514
|
+
if (typeof agent.lastSpendRollupAt !== 'number') agent.lastSpendRollupAt = 0
|
|
510
515
|
// Org chart normalization
|
|
511
516
|
if (agent.orgChart && typeof agent.orgChart === 'object' && !Array.isArray(agent.orgChart)) {
|
|
512
517
|
const oc = agent.orgChart as Record<string, unknown>
|
|
@@ -157,6 +157,7 @@ const COLLECTIONS = [
|
|
|
157
157
|
'daemon_status',
|
|
158
158
|
'wallets',
|
|
159
159
|
'wallet_transactions',
|
|
160
|
+
'goals',
|
|
160
161
|
] as const
|
|
161
162
|
|
|
162
163
|
export type StorageCollection = (typeof COLLECTIONS)[number]
|
|
@@ -168,6 +169,10 @@ for (const table of COLLECTIONS) {
|
|
|
168
169
|
// Index for efficient protocol_run_events queries by runId
|
|
169
170
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_protocol_run_events_runid ON protocol_run_events (json_extract(data, '$.runId'))`)
|
|
170
171
|
|
|
172
|
+
// Indexes for efficient activity log queries
|
|
173
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_activity_timestamp ON activity (json_extract(data, '$.timestamp'))`)
|
|
174
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_activity_entity ON activity (json_extract(data, '$.entityType'), json_extract(data, '$.entityId'))`)
|
|
175
|
+
|
|
171
176
|
// Singleton tables (single row)
|
|
172
177
|
db.exec(`CREATE TABLE IF NOT EXISTS settings (id INTEGER PRIMARY KEY CHECK (id = 1), data TEXT NOT NULL)`)
|
|
173
178
|
db.exec(`CREATE TABLE IF NOT EXISTS queue (id INTEGER PRIMARY KEY CHECK (id = 1), data TEXT NOT NULL)`)
|
|
@@ -1465,6 +1470,51 @@ export function logActivity(entry: {
|
|
|
1465
1470
|
notify('activity')
|
|
1466
1471
|
}
|
|
1467
1472
|
|
|
1473
|
+
/** Paginated activity query using SQL WHERE + LIMIT/OFFSET instead of loading the full collection. */
|
|
1474
|
+
export function queryActivity(filters: {
|
|
1475
|
+
entityType?: string
|
|
1476
|
+
entityId?: string
|
|
1477
|
+
actor?: string
|
|
1478
|
+
action?: string
|
|
1479
|
+
since?: number
|
|
1480
|
+
limit?: number
|
|
1481
|
+
offset?: number
|
|
1482
|
+
}): unknown[] {
|
|
1483
|
+
const conditions: string[] = []
|
|
1484
|
+
const params: unknown[] = []
|
|
1485
|
+
|
|
1486
|
+
if (filters.entityType) {
|
|
1487
|
+
conditions.push(`json_extract(data, '$.entityType') = ?`)
|
|
1488
|
+
params.push(filters.entityType)
|
|
1489
|
+
}
|
|
1490
|
+
if (filters.entityId) {
|
|
1491
|
+
conditions.push(`json_extract(data, '$.entityId') = ?`)
|
|
1492
|
+
params.push(filters.entityId)
|
|
1493
|
+
}
|
|
1494
|
+
if (filters.actor) {
|
|
1495
|
+
conditions.push(`json_extract(data, '$.actor') = ?`)
|
|
1496
|
+
params.push(filters.actor)
|
|
1497
|
+
}
|
|
1498
|
+
if (filters.action) {
|
|
1499
|
+
conditions.push(`json_extract(data, '$.action') = ?`)
|
|
1500
|
+
params.push(filters.action)
|
|
1501
|
+
}
|
|
1502
|
+
if (typeof filters.since === 'number' && Number.isFinite(filters.since)) {
|
|
1503
|
+
conditions.push(`json_extract(data, '$.timestamp') >= ?`)
|
|
1504
|
+
params.push(filters.since)
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
|
1508
|
+
const limit = Math.min(200, Math.max(1, filters.limit ?? 50))
|
|
1509
|
+
const offset = Math.max(0, filters.offset ?? 0)
|
|
1510
|
+
|
|
1511
|
+
const sql = `SELECT data FROM activity ${where} ORDER BY json_extract(data, '$.timestamp') DESC LIMIT ? OFFSET ?`
|
|
1512
|
+
params.push(limit, offset)
|
|
1513
|
+
|
|
1514
|
+
const rows = db.prepare(sql).all(...params) as Array<{ data: string }>
|
|
1515
|
+
return rows.map((r) => JSON.parse(r.data))
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1468
1518
|
// --- Webhook Retry Queue ---
|
|
1469
1519
|
const webhookRetryQueueStore = createCollectionStore('webhook_retry_queue')
|
|
1470
1520
|
export const loadWebhookRetryQueue = webhookRetryQueueStore.load
|
|
@@ -1586,6 +1636,13 @@ export const loadWalletTransaction = walletTransactionsStore.loadItem
|
|
|
1586
1636
|
export const upsertWalletTransaction = walletTransactionsStore.upsert
|
|
1587
1637
|
export const deleteWalletTransaction = walletTransactionsStore.deleteItem
|
|
1588
1638
|
|
|
1639
|
+
// --- Goals ---
|
|
1640
|
+
const goalsStore = createCollectionStore('goals')
|
|
1641
|
+
export const loadGoals = goalsStore.load
|
|
1642
|
+
export const loadGoal = goalsStore.loadItem
|
|
1643
|
+
export const upsertGoal = goalsStore.upsert
|
|
1644
|
+
export const deleteGoalItem = goalsStore.deleteItem
|
|
1645
|
+
|
|
1589
1646
|
export function getSessionMessages(sessionId: string): Message[] {
|
|
1590
1647
|
const session = loadSession(sessionId)
|
|
1591
1648
|
return Array.isArray(session?.messages) ? session.messages : []
|