@swarmclawai/swarmclaw 1.3.6 → 1.4.0
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 +32 -1
- package/package.json +9 -3
- package/src/.env.local +4 -0
- package/src/app/api/.well-known/agent-card/route.ts +46 -0
- package/src/app/api/a2a/route.ts +56 -0
- package/src/app/api/a2a/tasks/[taskId]/status/route.ts +49 -0
- package/src/app/api/chats/[id]/deploy/route.ts +2 -2
- package/src/app/api/openclaw/sync/route.ts +1 -1
- package/src/app/api/swarmfeed/channels/route.ts +14 -0
- package/src/app/api/swarmfeed/posts/route.ts +60 -0
- package/src/app/api/swarmfeed/route.ts +37 -0
- package/src/app/protocols/builder/[templateId]/page.tsx +93 -0
- package/src/app/protocols/page.tsx +16 -7
- package/src/app/swarmfeed/page.tsx +7 -0
- package/src/cli/index.js +19 -0
- package/src/cli/spec.js +8 -0
- package/src/components/agents/agent-avatar.tsx +2 -5
- package/src/components/agents/agent-sheet.tsx +10 -0
- package/src/components/auth/access-key-gate.tsx +25 -0
- package/src/components/layout/sidebar-rail.tsx +52 -0
- package/src/components/protocols/builder/edge-editor.tsx +43 -0
- package/src/components/protocols/builder/edge-types/branch-edge.tsx +33 -0
- package/src/components/protocols/builder/edge-types/default-edge.tsx +18 -0
- package/src/components/protocols/builder/edge-types/index.ts +3 -0
- package/src/components/protocols/builder/edge-types/loop-edge.tsx +19 -0
- package/src/components/protocols/builder/node-inspector.tsx +227 -0
- package/src/components/protocols/builder/node-palette.tsx +97 -0
- package/src/components/protocols/builder/node-types/branch-node.tsx +34 -0
- package/src/components/protocols/builder/node-types/complete-node.tsx +17 -0
- package/src/components/protocols/builder/node-types/for-each-node.tsx +21 -0
- package/src/components/protocols/builder/node-types/index.ts +9 -0
- package/src/components/protocols/builder/node-types/join-node.tsx +18 -0
- package/src/components/protocols/builder/node-types/loop-node.tsx +22 -0
- package/src/components/protocols/builder/node-types/parallel-node.tsx +31 -0
- package/src/components/protocols/builder/node-types/phase-node.tsx +52 -0
- package/src/components/protocols/builder/node-types/subflow-node.tsx +23 -0
- package/src/components/protocols/builder/node-types/swarm-node.tsx +26 -0
- package/src/components/protocols/builder/protocol-builder-canvas.tsx +184 -0
- package/src/components/protocols/builder/run-overlay.tsx +29 -0
- package/src/components/protocols/builder/template-gallery.tsx +53 -0
- package/src/components/protocols/builder/validation-panel.tsx +57 -0
- package/src/components/skills/skills-workspace.tsx +1 -9
- package/src/features/protocols/builder/hooks/index.ts +2 -0
- package/src/features/protocols/builder/hooks/use-canvas-validation.ts +14 -0
- package/src/features/protocols/builder/hooks/use-run-overlay.ts +39 -0
- package/src/features/protocols/builder/hooks/use-template-sync.ts +45 -0
- package/src/features/protocols/builder/protocol-builder-store.ts +233 -0
- package/src/features/protocols/builder/utils/node-position-layout.ts +41 -0
- package/src/features/protocols/builder/utils/nodes-to-template.test.ts +179 -0
- package/src/features/protocols/builder/utils/nodes-to-template.ts +49 -0
- package/src/features/protocols/builder/utils/template-to-nodes.test.ts +314 -0
- package/src/features/protocols/builder/utils/template-to-nodes.ts +169 -0
- package/src/features/protocols/builder/validators/dag-validator.test.ts +150 -0
- package/src/features/protocols/builder/validators/dag-validator.ts +119 -0
- package/src/features/swarmfeed/agent-social-settings.tsx +277 -0
- package/src/features/swarmfeed/compose-post.tsx +139 -0
- package/src/features/swarmfeed/feed-page.tsx +136 -0
- package/src/features/swarmfeed/post-card.tsx +114 -0
- package/src/features/swarmfeed/queries.ts +28 -0
- package/src/lib/a2a/agent-card.ts +61 -0
- package/src/lib/a2a/auth.ts +54 -0
- package/src/lib/a2a/client.ts +133 -0
- package/src/lib/a2a/discovery.ts +116 -0
- package/src/lib/a2a/handlers.ts +176 -0
- package/src/lib/a2a/json-rpc-router.ts +38 -0
- package/src/lib/a2a/types.ts +95 -0
- package/src/lib/app/navigation.ts +1 -0
- package/src/lib/app/view-constants.ts +9 -1
- package/src/lib/providers/anthropic.ts +111 -107
- package/src/lib/providers/openai.ts +146 -142
- package/src/lib/server/agents/main-agent-loop.test.ts +94 -0
- package/src/lib/server/agents/main-agent-loop.ts +377 -41
- package/src/lib/server/chat-execution/chat-execution.ts +12 -7
- package/src/lib/server/extensions.ts +11 -0
- package/src/lib/server/openclaw/sync.ts +4 -4
- package/src/lib/server/protocols/protocol-a2a-delegate.ts +135 -0
- package/src/lib/server/protocols/protocol-normalization.ts +1 -0
- package/src/lib/server/protocols/protocol-step-helpers.test.ts +1 -1
- package/src/lib/server/protocols/protocol-step-helpers.ts +1 -0
- package/src/lib/server/protocols/protocol-step-processors.ts +2 -0
- package/src/lib/server/protocols/protocol-types.ts +1 -0
- package/src/lib/server/session-tools/delegate.ts +151 -77
- package/src/lib/server/storage-auth.ts +10 -2
- package/src/lib/server/storage-normalization.ts +11 -0
- package/src/lib/server/storage.ts +100 -0
- package/src/lib/server/working-state/service.test.ts +2 -3
- package/src/lib/server/working-state/service.ts +37 -6
- package/src/lib/swarmfeed-client.ts +157 -0
- package/src/lib/validation/schemas.ts +1 -1
- package/src/stores/slices/data-slice.ts +3 -0
- package/src/stores/use-approval-store.ts +4 -1
- package/src/types/agent.ts +31 -1
- package/src/types/index.ts +1 -0
- package/src/types/protocol.ts +19 -0
- package/src/types/session.ts +1 -1
- package/src/types/swarmfeed.ts +30 -0
|
@@ -23,7 +23,7 @@ async function fileToContentParts(filePath: string): Promise<Array<Record<string
|
|
|
23
23
|
}
|
|
24
24
|
if (filePath.endsWith('.pdf')) {
|
|
25
25
|
try {
|
|
26
|
-
// @ts-
|
|
26
|
+
// @ts-expect-error — pdf-parse types
|
|
27
27
|
const pdfParse = (await import(/* webpackIgnore: true */ 'pdf-parse')).default
|
|
28
28
|
const buf = await fs.promises.readFile(filePath)
|
|
29
29
|
const result = await pdfParse(buf)
|
|
@@ -45,153 +45,157 @@ async function fileToContentParts(filePath: string): Promise<Array<Record<string
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey, systemPrompt, write, active, loadHistory, onUsage, signal }: StreamChatOptions): Promise<string> {
|
|
48
|
-
return new Promise(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
// Try with stream_options first; if the provider rejects with 400, retry without it
|
|
72
|
-
let res: Response | undefined
|
|
73
|
-
let usageEnabled = true
|
|
74
|
-
for (const includeStreamOptions of [true, false]) {
|
|
75
|
-
const payloadObj: Record<string, unknown> = {
|
|
76
|
-
model,
|
|
77
|
-
messages,
|
|
78
|
-
stream: true,
|
|
79
|
-
}
|
|
80
|
-
if (includeStreamOptions) {
|
|
81
|
-
payloadObj.stream_options = { include_usage: true }
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
;(async () => {
|
|
50
|
+
try {
|
|
51
|
+
const messages = await buildMessages(session, message, imagePath, systemPrompt, loadHistory, imageUrl)
|
|
52
|
+
const model = session.model || 'gpt-4o'
|
|
53
|
+
|
|
54
|
+
let fullResponse = ''
|
|
55
|
+
|
|
56
|
+
// Support custom base URLs for custom providers
|
|
57
|
+
const baseUrl = session.apiEndpoint || PROVIDER_DEFAULTS.openai
|
|
58
|
+
const url = `${baseUrl.replace(/\/+$/, '')}/chat/completions`
|
|
59
|
+
|
|
60
|
+
// OpenClaw endpoints behind Hostinger's proxy use express.json() middleware
|
|
61
|
+
// which consumes the request body before http-proxy-middleware can forward it.
|
|
62
|
+
// Sending as text/plain bypasses the body parser while the gateway still parses JSON.
|
|
63
|
+
const contentType = session.contentType || 'application/json'
|
|
64
|
+
|
|
65
|
+
const abortController = new AbortController()
|
|
66
|
+
if (signal) {
|
|
67
|
+
if (signal.aborted) abortController.abort()
|
|
68
|
+
else signal.addEventListener('abort', () => abortController.abort(), { once: true })
|
|
82
69
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
res = await fetch(url, {
|
|
86
|
-
method: 'POST',
|
|
87
|
-
headers: {
|
|
88
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
89
|
-
'Content-Type': contentType,
|
|
90
|
-
},
|
|
91
|
-
body: payload,
|
|
92
|
-
signal: abortController.signal,
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
if (res.status === 400 && includeStreamOptions) {
|
|
96
|
-
// Provider likely rejected stream_options — retry without it
|
|
97
|
-
usageEnabled = false
|
|
98
|
-
continue
|
|
99
|
-
}
|
|
100
|
-
usageEnabled = includeStreamOptions
|
|
101
|
-
break
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (!res) {
|
|
105
|
-
active.delete(session.id)
|
|
106
|
-
reject(new Error('No response from provider'))
|
|
107
|
-
return
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Detect HTML responses (e.g. landing page returned instead of API)
|
|
111
|
-
const resContentType = res.headers.get('content-type') || ''
|
|
112
|
-
if (resContentType.includes('text/html')) {
|
|
113
|
-
const msg = 'Received HTML instead of API response. The endpoint may be misconfigured or returning a landing page.'
|
|
114
|
-
log.error(TAG, `[${session.id}] received HTML instead of API response from ${baseUrl} (provider: ${session.provider})`)
|
|
115
|
-
writeSSE(write, 'err', msg)
|
|
116
|
-
active.delete(session.id)
|
|
117
|
-
reject(new Error(msg))
|
|
118
|
-
return
|
|
119
|
-
}
|
|
70
|
+
active.set(session.id, { kill: () => abortController.abort() })
|
|
120
71
|
|
|
121
|
-
if (!res.ok) {
|
|
122
|
-
const errBody = await res.text().catch(() => '')
|
|
123
|
-
log.error(TAG, `[${session.id}] openai error ${res.status}:`, errBody.slice(0, 200))
|
|
124
|
-
let errMsg = `API error (${res.status})`
|
|
125
72
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (!res.body) {
|
|
138
|
-
const msg = `No response body from ${baseUrl}`
|
|
139
|
-
log.error(TAG, `[${session.id}] ${msg}`)
|
|
140
|
-
active.delete(session.id)
|
|
141
|
-
reject(new Error(msg))
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const reader = res.body.getReader()
|
|
146
|
-
const decoder = new TextDecoder()
|
|
147
|
-
let buf = ''
|
|
148
|
-
|
|
149
|
-
while (true) {
|
|
150
|
-
const { done, value } = await reader.read()
|
|
151
|
-
if (done) break
|
|
152
|
-
if (abortController.signal.aborted) break
|
|
153
|
-
|
|
154
|
-
buf += decoder.decode(value, { stream: true })
|
|
155
|
-
const lines = buf.split('\n')
|
|
156
|
-
buf = lines.pop()!
|
|
157
|
-
|
|
158
|
-
for (const line of lines) {
|
|
159
|
-
if (!line.startsWith('data: ')) continue
|
|
160
|
-
const data = line.slice(6).trim()
|
|
161
|
-
if (data === '[DONE]') continue
|
|
162
|
-
try {
|
|
163
|
-
const parsed = JSON.parse(data)
|
|
164
|
-
const delta = parsed.choices?.[0]?.delta?.content
|
|
165
|
-
if (delta) {
|
|
166
|
-
fullResponse += delta
|
|
167
|
-
writeSSE(write, 'd', delta)
|
|
73
|
+
// Try with stream_options first; if the provider rejects with 400, retry without it
|
|
74
|
+
let res: Response | undefined
|
|
75
|
+
let usageEnabled = true
|
|
76
|
+
for (const includeStreamOptions of [true, false]) {
|
|
77
|
+
const payloadObj: Record<string, unknown> = {
|
|
78
|
+
model,
|
|
79
|
+
messages,
|
|
80
|
+
stream: true,
|
|
81
|
+
}
|
|
82
|
+
if (includeStreamOptions) {
|
|
83
|
+
payloadObj.stream_options = { include_usage: true }
|
|
168
84
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
85
|
+
const payload = JSON.stringify(payloadObj)
|
|
86
|
+
|
|
87
|
+
res = await fetch(url, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: {
|
|
90
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
91
|
+
'Content-Type': contentType,
|
|
92
|
+
},
|
|
93
|
+
body: payload,
|
|
94
|
+
signal: abortController.signal,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
if (res.status === 400 && includeStreamOptions) {
|
|
98
|
+
// Provider likely rejected stream_options — retry without it
|
|
99
|
+
usageEnabled = false
|
|
100
|
+
continue
|
|
175
101
|
}
|
|
176
|
-
|
|
102
|
+
usageEnabled = includeStreamOptions
|
|
103
|
+
break
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!res) {
|
|
107
|
+
active.delete(session.id)
|
|
108
|
+
reject(new Error('No response from provider'))
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Detect HTML responses (e.g. landing page returned instead of API)
|
|
113
|
+
const resContentType = res.headers.get('content-type') || ''
|
|
114
|
+
if (resContentType.includes('text/html')) {
|
|
115
|
+
const msg = 'Received HTML instead of API response. The endpoint may be misconfigured or returning a landing page.'
|
|
116
|
+
log.error(TAG, `[${session.id}] received HTML instead of API response from ${baseUrl} (provider: ${session.provider})`)
|
|
117
|
+
writeSSE(write, 'err', msg)
|
|
118
|
+
active.delete(session.id)
|
|
119
|
+
reject(new Error(msg))
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
const errBody = await res.text().catch(() => '')
|
|
125
|
+
log.error(TAG, `[${session.id}] openai error ${res.status}:`, errBody.slice(0, 200))
|
|
126
|
+
let errMsg = `API error (${res.status})`
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(errBody)
|
|
129
|
+
if (parsed.error?.message) errMsg = parsed.error.message
|
|
130
|
+
else if (parsed.message) errMsg = parsed.message
|
|
131
|
+
else if (parsed.detail) errMsg = parsed.detail
|
|
132
|
+
} catch {}
|
|
133
|
+
writeSSE(write, 'err', errMsg)
|
|
134
|
+
active.delete(session.id)
|
|
135
|
+
reject(new Error(`OpenAI error ${res.status}: ${errMsg}`))
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!res.body) {
|
|
140
|
+
const msg = `No response body from ${baseUrl}`
|
|
141
|
+
log.error(TAG, `[${session.id}] ${msg}`)
|
|
142
|
+
active.delete(session.id)
|
|
143
|
+
reject(new Error(msg))
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const reader = res.body.getReader()
|
|
148
|
+
const decoder = new TextDecoder()
|
|
149
|
+
let buf = ''
|
|
150
|
+
|
|
151
|
+
while (true) {
|
|
152
|
+
const { done, value } = await reader.read()
|
|
153
|
+
if (done) break
|
|
154
|
+
if (abortController.signal.aborted) break
|
|
155
|
+
|
|
156
|
+
buf += decoder.decode(value, { stream: true })
|
|
157
|
+
const lines = buf.split('\n')
|
|
158
|
+
buf = lines.pop()!
|
|
159
|
+
|
|
160
|
+
for (const line of lines) {
|
|
161
|
+
if (!line.startsWith('data: ')) continue
|
|
162
|
+
const data = line.slice(6).trim()
|
|
163
|
+
if (data === '[DONE]') continue
|
|
164
|
+
try {
|
|
165
|
+
const parsed = JSON.parse(data)
|
|
166
|
+
const delta = parsed.choices?.[0]?.delta?.content
|
|
167
|
+
if (delta) {
|
|
168
|
+
fullResponse += delta
|
|
169
|
+
writeSSE(write, 'd', delta)
|
|
170
|
+
}
|
|
171
|
+
// Extract usage from the final chunk (stream_options: include_usage)
|
|
172
|
+
if (usageEnabled && parsed.usage && onUsage) {
|
|
173
|
+
onUsage({
|
|
174
|
+
inputTokens: parsed.usage.prompt_tokens || 0,
|
|
175
|
+
outputTokens: parsed.usage.completion_tokens || 0,
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
} catch {}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!fullResponse) {
|
|
183
|
+
log.error(TAG, `[${session.id}] openai stream ended with no content (provider: ${session.provider}, endpoint: ${baseUrl})`)
|
|
184
|
+
}
|
|
185
|
+
} catch (err: unknown) {
|
|
186
|
+
const errObj = err as { name?: string; message?: string }
|
|
187
|
+
if (errObj.name !== 'AbortError') {
|
|
188
|
+
log.error(TAG, `[${session.id}] openai request error:`, errObj.message)
|
|
189
|
+
writeSSE(write, 'err', `Connection failed: ${errObj.message}`)
|
|
190
|
+
}
|
|
191
|
+
active.delete(session.id)
|
|
192
|
+
reject(err)
|
|
193
|
+
return
|
|
177
194
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
} catch (err: unknown) {
|
|
184
|
-
const errObj = err as { name?: string; message?: string }
|
|
185
|
-
if (errObj.name !== 'AbortError') {
|
|
186
|
-
log.error(TAG, `[${session.id}] openai request error:`, errObj.message)
|
|
187
|
-
writeSSE(write, 'err', `Connection failed: ${errObj.message}`)
|
|
188
|
-
}
|
|
189
|
-
active.delete(session.id)
|
|
190
|
-
reject(err)
|
|
191
|
-
return
|
|
192
|
-
}
|
|
193
|
-
active.delete(session.id)
|
|
194
|
-
resolve(fullResponse)
|
|
195
|
+
active.delete(session.id)
|
|
196
|
+
resolve(fullResponse)
|
|
197
|
+
} catch (err) { reject(err) }
|
|
198
|
+
})()
|
|
195
199
|
})
|
|
196
200
|
}
|
|
197
201
|
|
|
@@ -346,6 +346,100 @@ describe('main-agent-loop', () => {
|
|
|
346
346
|
assert.equal(output.followupMessage, null)
|
|
347
347
|
})
|
|
348
348
|
|
|
349
|
+
it('accepts structured autonomy ticks for durable objectives without falling back to legacy tags', () => {
|
|
350
|
+
const output = runWithTempDataDir(`
|
|
351
|
+
const storageMod = await import('@/lib/server/storage')
|
|
352
|
+
const mainLoopMod = await import('@/lib/server/agents/main-agent-loop')
|
|
353
|
+
const storage = storageMod.default || storageMod
|
|
354
|
+
const mainLoop = mainLoopMod.default || mainLoopMod
|
|
355
|
+
|
|
356
|
+
storage.saveAgents({
|
|
357
|
+
'agent-a': {
|
|
358
|
+
id: 'agent-a',
|
|
359
|
+
name: 'Agent A',
|
|
360
|
+
provider: 'openai',
|
|
361
|
+
model: 'gpt-test',
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
storage.saveSessions({
|
|
366
|
+
main: {
|
|
367
|
+
id: 'main',
|
|
368
|
+
name: 'Main Agent Thread',
|
|
369
|
+
shortcutForAgentId: 'agent-a',
|
|
370
|
+
cwd: process.cwd(),
|
|
371
|
+
user: 'tester',
|
|
372
|
+
provider: 'openai',
|
|
373
|
+
model: 'gpt-test',
|
|
374
|
+
claudeSessionId: null,
|
|
375
|
+
messages: [],
|
|
376
|
+
createdAt: 1,
|
|
377
|
+
lastActiveAt: 1,
|
|
378
|
+
sessionType: 'human',
|
|
379
|
+
agentId: 'agent-a',
|
|
380
|
+
heartbeatEnabled: true,
|
|
381
|
+
missionId: 'mission-1',
|
|
382
|
+
},
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
storage.saveMissions({
|
|
386
|
+
'mission-1': {
|
|
387
|
+
id: 'mission-1',
|
|
388
|
+
source: 'heartbeat',
|
|
389
|
+
objective: 'Ship the autonomy hardening release',
|
|
390
|
+
status: 'active',
|
|
391
|
+
phase: 'executing',
|
|
392
|
+
sessionId: 'main',
|
|
393
|
+
agentId: 'agent-a',
|
|
394
|
+
taskIds: [],
|
|
395
|
+
currentStep: 'Verify the release checklist',
|
|
396
|
+
plannerSummary: 'Use the durable controller state.',
|
|
397
|
+
verifierSummary: null,
|
|
398
|
+
blockerSummary: null,
|
|
399
|
+
waitState: null,
|
|
400
|
+
createdAt: 1,
|
|
401
|
+
updatedAt: Date.now(),
|
|
402
|
+
},
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
const prompt = mainLoop.buildMainLoopHeartbeatPrompt(storage.loadSessions().main, 'fallback heartbeat')
|
|
406
|
+
mainLoop.handleMainLoopRunResult({
|
|
407
|
+
sessionId: 'main',
|
|
408
|
+
message: 'Heartbeat tick',
|
|
409
|
+
internal: true,
|
|
410
|
+
source: 'heartbeat',
|
|
411
|
+
resultText: [
|
|
412
|
+
'Validated the release checklist.',
|
|
413
|
+
'[AUTONOMY_TICK]{"status":"progress","summary":"Release checklist validated.","next_action":"publish release artifacts","plan_steps":["verify artifacts","publish release artifacts"],"current_step":"verify artifacts","completed_steps":["Verify the release checklist"],"review":{"note":"Ready for artifact publication.","confidence":0.88,"needs_replan":false}}',
|
|
414
|
+
].join('\\n'),
|
|
415
|
+
})
|
|
416
|
+
const state = mainLoop.getMainLoopStateForSession('main')
|
|
417
|
+
|
|
418
|
+
console.log(JSON.stringify({
|
|
419
|
+
promptIncludesAutonomyTick: prompt.includes('[AUTONOMY_TICK]'),
|
|
420
|
+
promptIncludesLegacyPlanTags: prompt.includes('[MAIN_LOOP_PLAN]'),
|
|
421
|
+
stateStatus: state?.status || null,
|
|
422
|
+
stateSummary: state?.summary || null,
|
|
423
|
+
stateNextAction: state?.nextAction || null,
|
|
424
|
+
statePlanSteps: state?.planSteps || [],
|
|
425
|
+
stateCurrentPlanStep: state?.currentPlanStep || null,
|
|
426
|
+
stateCompletedPlanSteps: state?.completedPlanSteps || [],
|
|
427
|
+
stateReviewNote: state?.reviewNote || null,
|
|
428
|
+
}))
|
|
429
|
+
`)
|
|
430
|
+
|
|
431
|
+
assert.equal(output.promptIncludesAutonomyTick, true)
|
|
432
|
+
assert.equal(output.promptIncludesLegacyPlanTags, false)
|
|
433
|
+
assert.equal(output.stateStatus, 'progress')
|
|
434
|
+
assert.match(String(output.stateSummary), /validated/i)
|
|
435
|
+
assert.equal(output.stateNextAction, 'publish release artifacts')
|
|
436
|
+
assert.ok((output.statePlanSteps as string[]).includes('verify artifacts'))
|
|
437
|
+
assert.ok((output.statePlanSteps as string[]).includes('publish release artifacts'))
|
|
438
|
+
assert.equal(output.stateCurrentPlanStep, 'verify artifacts')
|
|
439
|
+
assert.ok((output.stateCompletedPlanSteps as string[]).includes('Verify the release checklist'))
|
|
440
|
+
assert.match(String(output.stateReviewNote), /artifact publication/i)
|
|
441
|
+
})
|
|
442
|
+
|
|
349
443
|
it('does not let internal heartbeat prompts rewrite the stored goal contract', () => {
|
|
350
444
|
const output = runWithTempDataDir(`
|
|
351
445
|
const storageMod = await import('@/lib/server/storage')
|