@swarmclawai/swarmclaw 0.8.0 → 0.8.2
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 +8 -7
- package/package.json +2 -2
- package/src/app/api/notifications/route.ts +11 -12
- package/src/app/page.tsx +9 -0
- package/src/components/chat/chat-list.tsx +10 -9
- package/src/components/home/home-view.tsx +13 -2
- package/src/components/layout/app-layout.tsx +1 -0
- package/src/components/shared/command-palette.tsx +4 -1
- package/src/components/shared/notification-center.tsx +7 -1
- package/src/components/shared/search-dialog.tsx +10 -2
- package/src/lib/local-observability.test.ts +73 -0
- package/src/lib/local-observability.ts +47 -0
- package/src/lib/notification-utils.test.ts +72 -0
- package/src/lib/notification-utils.ts +68 -0
- package/src/lib/providers/openclaw.test.ts +21 -1
- package/src/lib/providers/openclaw.ts +22 -0
- package/src/lib/runtime-loop.ts +1 -1
- package/src/lib/server/agent-thread-session.test.ts +41 -0
- package/src/lib/server/agent-thread-session.ts +1 -0
- package/src/lib/server/chat-execution-advanced.test.ts +7 -0
- package/src/lib/server/chat-execution-eval-history.test.ts +111 -0
- package/src/lib/server/chat-execution.ts +22 -5
- package/src/lib/server/create-notification.test.ts +94 -0
- package/src/lib/server/create-notification.ts +31 -25
- package/src/lib/server/daemon-state.test.ts +50 -0
- package/src/lib/server/daemon-state.ts +121 -38
- package/src/lib/server/eval/agent-regression-advanced.test.ts +11 -0
- package/src/lib/server/eval/agent-regression.test.ts +13 -1
- package/src/lib/server/eval/agent-regression.ts +221 -1
- package/src/lib/server/memory-policy.test.ts +32 -0
- package/src/lib/server/memory-policy.ts +25 -0
- package/src/lib/server/plugins-advanced.test.ts +7 -0
- package/src/lib/server/runtime-settings.test.ts +2 -2
- package/src/lib/server/session-tools/crud.test.ts +136 -0
- package/src/lib/server/session-tools/crud.ts +44 -2
- package/src/lib/server/session-tools/delegate-fallback.test.ts +36 -0
- package/src/lib/server/session-tools/delegate.ts +30 -0
- package/src/lib/server/session-tools/discovery-approvals.test.ts +40 -0
- package/src/lib/server/session-tools/discovery.ts +7 -6
- package/src/lib/server/session-tools/memory.ts +156 -6
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +12 -0
- package/src/lib/server/session-tools/subagent.ts +4 -4
- package/src/lib/server/storage.ts +14 -1
- package/src/lib/server/stream-agent-chat.test.ts +78 -1
- package/src/lib/server/stream-agent-chat.ts +225 -22
- package/src/lib/server/tool-aliases.ts +1 -1
- package/src/lib/server/tool-capability-policy.ts +1 -1
- package/src/stores/use-app-store.ts +26 -1
- package/src/types/index.ts +4 -0
|
@@ -8,7 +8,7 @@ import Database from 'better-sqlite3'
|
|
|
8
8
|
import { DATA_DIR, IS_BUILD_BOOTSTRAP, WORKSPACE_DIR } from './data-dir'
|
|
9
9
|
import { normalizeHeartbeatSettingFields } from '@/lib/heartbeat-defaults'
|
|
10
10
|
import { normalizeRuntimeSettingFields } from '@/lib/runtime-loop'
|
|
11
|
-
import type { ExternalAgentRuntime, GatewayProfile, Message } from '@/types'
|
|
11
|
+
import type { AppNotification, ExternalAgentRuntime, GatewayProfile, Message } from '@/types'
|
|
12
12
|
export const UPLOAD_DIR = path.join(DATA_DIR, 'uploads')
|
|
13
13
|
|
|
14
14
|
// --- LRU Cache ---
|
|
@@ -1175,6 +1175,19 @@ export function deleteNotification(id: string) {
|
|
|
1175
1175
|
deleteCollectionItem('notifications', id)
|
|
1176
1176
|
}
|
|
1177
1177
|
|
|
1178
|
+
export function findNotificationByDedupKey(dedupKey: string): AppNotification | null {
|
|
1179
|
+
const raw = getCollectionRawCache('notifications')
|
|
1180
|
+
for (const json of raw.values()) {
|
|
1181
|
+
try {
|
|
1182
|
+
const notification = JSON.parse(json) as AppNotification
|
|
1183
|
+
if (notification.dedupKey === dedupKey) return notification
|
|
1184
|
+
} catch {
|
|
1185
|
+
// ignore malformed
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return null
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1178
1191
|
export function hasUnreadNotificationWithKey(dedupKey: string): boolean {
|
|
1179
1192
|
const raw = getCollectionRawCache('notifications')
|
|
1180
1193
|
for (const json of raw.values()) {
|
|
@@ -7,10 +7,13 @@ import {
|
|
|
7
7
|
buildExternalWalletExecutionBlock,
|
|
8
8
|
buildToolDisciplineLines,
|
|
9
9
|
getExplicitRequiredToolNames,
|
|
10
|
+
isNarrowDirectMemoryWriteTurn,
|
|
10
11
|
isWalletSimulationResult,
|
|
11
12
|
looksLikeOpenEndedDeliverableTask,
|
|
12
13
|
resolveContinuationAssistantText,
|
|
13
14
|
resolveFinalStreamResponseText,
|
|
15
|
+
shouldAllowToolForDirectMemoryWrite,
|
|
16
|
+
shouldAllowToolForCurrentThreadRecall,
|
|
14
17
|
shouldTerminateOnSuccessfulMemoryMutation,
|
|
15
18
|
shouldForceDeliverableFollowthrough,
|
|
16
19
|
shouldForceExternalExecutionFollowthrough,
|
|
@@ -37,6 +40,8 @@ describe('buildToolDisciplineLines', () => {
|
|
|
37
40
|
const lines = buildToolDisciplineLines(['files'])
|
|
38
41
|
|
|
39
42
|
assert.ok(lines.some((line) => line.includes('{"action":"read","filePath":"path/to/file.md"}')))
|
|
43
|
+
assert.ok(lines.some((line) => line.includes('exactly N bullet points')))
|
|
44
|
+
assert.ok(lines.some((line) => line.includes('Lower-priority logistics belong in FYI')))
|
|
40
45
|
})
|
|
41
46
|
|
|
42
47
|
it('adds schedule reuse and stop guidance when schedule tools are enabled', () => {
|
|
@@ -63,6 +68,7 @@ describe('buildToolDisciplineLines', () => {
|
|
|
63
68
|
assert.ok(lines.some((line) => line.includes('{"action":"send","to":"user@example.com","subject":"...","body":"..."}')))
|
|
64
69
|
assert.ok(lines.some((line) => line.includes('do not guess or keep re-submitting blank forms')))
|
|
65
70
|
assert.ok(lines.some((line) => line.includes('store it with `manage_secrets`') && line.includes('do not echo the raw value')))
|
|
71
|
+
assert.ok(lines.some((line) => line.includes('Use `manage_secrets` only for sensitive credentials or tokens')))
|
|
66
72
|
})
|
|
67
73
|
|
|
68
74
|
it('adds bounded execution guidance for wallet-connected external-service tasks', () => {
|
|
@@ -71,6 +77,7 @@ describe('buildToolDisciplineLines', () => {
|
|
|
71
77
|
assert.ok(lines.some((line) => line.includes('inspect the available wallet first with `wallet_tool`')))
|
|
72
78
|
assert.ok(lines.some((line) => line.includes('use a bounded loop') && line.includes('Do not keep browsing once the blocker is clear')))
|
|
73
79
|
assert.ok(lines.some((line) => line.includes('do not shop across venues indefinitely')))
|
|
80
|
+
assert.ok(lines.some((line) => line.includes('If a direct tool for the job is already enabled in this session, call that tool immediately')))
|
|
74
81
|
})
|
|
75
82
|
|
|
76
83
|
it('tells agents to stay local when coding tools are already available', () => {
|
|
@@ -126,6 +133,57 @@ describe('buildToolDisciplineLines', () => {
|
|
|
126
133
|
assert.ok(!streamAgentChatSource.includes('langchainMessages.push(new AIMessage({ content: fullText }))'))
|
|
127
134
|
})
|
|
128
135
|
|
|
136
|
+
it('adds a dedicated current-thread recall block and removes long-term memory tools for those turns', () => {
|
|
137
|
+
assert.ok(streamAgentChatSource.includes('## Current Thread Recall'))
|
|
138
|
+
assert.ok(streamAgentChatSource.includes('## Immediate Memory Routes'))
|
|
139
|
+
assert.ok(streamAgentChatSource.includes('## Direct Memory Write'))
|
|
140
|
+
assert.ok(streamAgentChatSource.includes('call `memory_store` or `memory_update` immediately before any planning, delegation, task creation, or agent management'))
|
|
141
|
+
assert.ok(streamAgentChatSource.includes('Do not inspect skills, browse the workspace, request capabilities, manage tasks, manage agents, or delegate before the direct memory write is complete.'))
|
|
142
|
+
assert.ok(streamAgentChatSource.includes('Do NOT call memory tools, web search, or session-history tools'))
|
|
143
|
+
assert.ok(streamAgentChatSource.includes('const currentThreadRecallRequest = !directMemoryWriteOnlyTurn && isCurrentThreadRecallRequest(message)'))
|
|
144
|
+
assert.ok(streamAgentChatSource.includes('const directMemoryWriteOnlyTurn = isNarrowDirectMemoryWriteTurn(message)'))
|
|
145
|
+
assert.ok(streamAgentChatSource.includes('shouldAllowToolForDirectMemoryWrite(toolName)'))
|
|
146
|
+
assert.ok(streamAgentChatSource.includes('shouldAllowToolForCurrentThreadRecall(toolName)'))
|
|
147
|
+
assert.ok(streamAgentChatSource.includes('Preserve hard structural constraints from the original request'))
|
|
148
|
+
assert.ok(streamAgentChatSource.includes('## Exact Structural Constraints'))
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('blocks memory, session-history, web, and context tools during same-thread recall turns', () => {
|
|
152
|
+
assert.equal(shouldAllowToolForCurrentThreadRecall('memory_tool'), false)
|
|
153
|
+
assert.equal(shouldAllowToolForCurrentThreadRecall('memory_search'), false)
|
|
154
|
+
assert.equal(shouldAllowToolForCurrentThreadRecall('memory_get'), false)
|
|
155
|
+
assert.equal(shouldAllowToolForCurrentThreadRecall('memory_store'), false)
|
|
156
|
+
assert.equal(shouldAllowToolForCurrentThreadRecall('memory_update'), false)
|
|
157
|
+
assert.equal(shouldAllowToolForCurrentThreadRecall('search_history_tool'), false)
|
|
158
|
+
assert.equal(shouldAllowToolForCurrentThreadRecall('sessions_tool'), false)
|
|
159
|
+
assert.equal(shouldAllowToolForCurrentThreadRecall('web_search'), false)
|
|
160
|
+
assert.equal(shouldAllowToolForCurrentThreadRecall('context_status'), false)
|
|
161
|
+
assert.equal(shouldAllowToolForCurrentThreadRecall('files'), true)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('only allows direct memory write tools during pure remember/store turns', () => {
|
|
165
|
+
assert.equal(shouldAllowToolForDirectMemoryWrite('memory_store'), true)
|
|
166
|
+
assert.equal(shouldAllowToolForDirectMemoryWrite('memory_update'), true)
|
|
167
|
+
assert.equal(shouldAllowToolForDirectMemoryWrite('memory_tool'), false)
|
|
168
|
+
assert.equal(shouldAllowToolForDirectMemoryWrite('manage_capabilities'), false)
|
|
169
|
+
assert.equal(shouldAllowToolForDirectMemoryWrite('files'), false)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('treats long remember-and-confirm turns as narrow direct memory writes', () => {
|
|
173
|
+
assert.equal(
|
|
174
|
+
isNarrowDirectMemoryWriteTurn('Remember that my favorite programming language is Rust and I prefer functional programming patterns. Then confirm what you just stored.'),
|
|
175
|
+
true,
|
|
176
|
+
)
|
|
177
|
+
assert.equal(
|
|
178
|
+
isNarrowDirectMemoryWriteTurn('Remember these facts for future conversations: My favorite programming language is Rust. My deploy target is Fly.io. My team size is 7 people. The project is codenamed "Neptune".'),
|
|
179
|
+
true,
|
|
180
|
+
)
|
|
181
|
+
assert.equal(
|
|
182
|
+
isNarrowDirectMemoryWriteTurn('Remember that my favorite programming language is Rust, then write a file summarizing it and send it to me.'),
|
|
183
|
+
false,
|
|
184
|
+
)
|
|
185
|
+
})
|
|
186
|
+
|
|
129
187
|
it('canonicalizes required tool names when checking completion', () => {
|
|
130
188
|
// The requiredToolsPending filter must canonicalize tool names so that
|
|
131
189
|
// alias names (e.g. ask_human) match canonical names from LangGraph events.
|
|
@@ -256,7 +314,7 @@ describe('resolveContinuationAssistantText', () => {
|
|
|
256
314
|
})
|
|
257
315
|
|
|
258
316
|
it('rolls back partial iteration text before transient retries restart the turn', () => {
|
|
259
|
-
assert.ok(streamAgentChatSource.includes('const iterationStartState
|
|
317
|
+
assert.ok(streamAgentChatSource.includes('const iterationStartState:'))
|
|
260
318
|
assert.ok(streamAgentChatSource.includes('fullText = iterationStartState.fullText'))
|
|
261
319
|
assert.ok(streamAgentChatSource.includes('lastSegment = iterationStartState.lastSegment'))
|
|
262
320
|
assert.ok(streamAgentChatSource.includes('lastSettledSegment = iterationStartState.lastSettledSegment'))
|
|
@@ -276,6 +334,25 @@ describe('shouldTerminateOnSuccessfulMemoryMutation', () => {
|
|
|
276
334
|
)
|
|
277
335
|
})
|
|
278
336
|
|
|
337
|
+
it('treats successful narrow memory write tools as terminal', () => {
|
|
338
|
+
assert.equal(
|
|
339
|
+
shouldTerminateOnSuccessfulMemoryMutation({
|
|
340
|
+
toolName: 'memory_store',
|
|
341
|
+
toolInput: { title: 'Project Kodiak details', value: 'freeze date April 18, 2026' },
|
|
342
|
+
toolOutput: 'Stored memory "Project Kodiak details" (id: abc123). No further memory lookup is needed unless the user asked you to verify.',
|
|
343
|
+
}),
|
|
344
|
+
true,
|
|
345
|
+
)
|
|
346
|
+
assert.equal(
|
|
347
|
+
shouldTerminateOnSuccessfulMemoryMutation({
|
|
348
|
+
toolName: 'memory_update',
|
|
349
|
+
toolInput: { id: 'abc123', value: 'freeze date April 21, 2026' },
|
|
350
|
+
toolOutput: 'Updated memory "Project Kodiak details" (id: abc123). No further memory lookup is needed unless the user asked you to verify.',
|
|
351
|
+
}),
|
|
352
|
+
true,
|
|
353
|
+
)
|
|
354
|
+
})
|
|
355
|
+
|
|
279
356
|
it('parses JSON tool input and accepts canonical update results', () => {
|
|
280
357
|
assert.equal(
|
|
281
358
|
shouldTerminateOnSuccessfulMemoryMutation({
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from './tool-planning'
|
|
27
27
|
import { ToolLoopTracker } from './tool-loop-detection'
|
|
28
28
|
import type { LoopDetectionResult } from './tool-loop-detection'
|
|
29
|
+
import { isCurrentThreadRecallRequest, isDirectMemoryWriteRequest } from './memory-policy'
|
|
29
30
|
|
|
30
31
|
/** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
|
|
31
32
|
interface StreamAgentChatOpts {
|
|
@@ -125,6 +126,16 @@ export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
|
|
|
125
126
|
|
|
126
127
|
if (uniqueTools.includes('manage_secrets')) {
|
|
127
128
|
lines.push('When a workflow reveals a password, app password, API key, recovery token, or other secret, store it with `manage_secrets` and do not echo the raw value in assistant text. Refer to the secret by name, service, or secret id instead.')
|
|
129
|
+
lines.push('Use `manage_secrets` only for sensitive credentials or tokens. Do not use it for normal memory, user preferences, durable facts, or project notes.')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (uniqueTools.includes('manage_capabilities')) {
|
|
133
|
+
lines.push('Use `manage_capabilities` only when a needed tool is actually unavailable. If a direct tool for the job is already enabled in this session, call that tool immediately instead of requesting access or re-running discovery.')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (uniqueTools.includes('files') || uniqueTools.includes('edit_file')) {
|
|
137
|
+
lines.push('When the user specifies exact counts or exact section titles for file content, treat those as hard constraints. If a file must have exactly N bullet points, keep the total bullet count at N and put extra required detail into short prose under titled sections unless the user explicitly asked for more bullets.')
|
|
138
|
+
lines.push('When summarizing or restructuring a source document into named sections, make sure each top-level source section is represented somewhere in the output. Lower-priority logistics belong in FYI rather than being dropped.')
|
|
128
139
|
}
|
|
129
140
|
|
|
130
141
|
if (uniqueTools.includes('delegate') && (uniqueTools.includes('shell') || uniqueTools.includes('files') || uniqueTools.includes('edit_file'))) {
|
|
@@ -271,7 +282,12 @@ export function shouldTerminateOnSuccessfulMemoryMutation(params: {
|
|
|
271
282
|
}): boolean {
|
|
272
283
|
const canonicalToolName = canonicalizePluginId(params.toolName) || params.toolName
|
|
273
284
|
if (canonicalToolName !== 'memory') return false
|
|
274
|
-
const
|
|
285
|
+
const exactToolName = String(params.toolName || '').trim().toLowerCase()
|
|
286
|
+
const action = exactToolName === 'memory_store'
|
|
287
|
+
? 'store'
|
|
288
|
+
: exactToolName === 'memory_update'
|
|
289
|
+
? 'update'
|
|
290
|
+
: resolveToolAction(params.toolInput)
|
|
275
291
|
if (action !== 'store' && action !== 'update') return false
|
|
276
292
|
const output = extractSuggestions(params.toolOutput || '').clean.trim()
|
|
277
293
|
if (!output || /^error[:\s]/i.test(output)) return false
|
|
@@ -386,9 +402,20 @@ export function shouldForceDeliverableFollowthrough(params: {
|
|
|
386
402
|
// If the user asked for file output but no file-write tool was used, force continuation
|
|
387
403
|
const userNormalized = params.userMessage.toLowerCase()
|
|
388
404
|
if (/\b(save|write|output)\b[^.!?\n]{0,60}\b(to|as)\b[^.!?\n]{0,40}(\/|~\/|\.[a-z]{2,5}\b)/.test(userNormalized)) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
405
|
+
// Check if a file-writing tool was actually used (not just file-reading).
|
|
406
|
+
// The `files` tool with action: 'read' or 'list' doesn't count as writing.
|
|
407
|
+
const usedFileWriteTools = params.toolEvents.some((e) => {
|
|
408
|
+
if (!e.name) return false
|
|
409
|
+
if (['write_file', 'edit_file'].includes(e.name)) return true
|
|
410
|
+
if (e.name === 'shell' || e.name === 'execute_command') return true
|
|
411
|
+
if (e.name === 'files') {
|
|
412
|
+
// Only count as a write if the tool input specifies action: "write"
|
|
413
|
+
const input = e.input || ''
|
|
414
|
+
return /"action"\s*:\s*"write"/i.test(input)
|
|
415
|
+
}
|
|
416
|
+
return false
|
|
417
|
+
})
|
|
418
|
+
if (!usedFileWriteTools) return true
|
|
392
419
|
}
|
|
393
420
|
if (looksLikeIncompleteDeliverableResponse(trimmed)) return true
|
|
394
421
|
return trimmed.length < 120 && params.toolEvents.length >= 3
|
|
@@ -496,19 +523,51 @@ function buildDeliverableFollowthroughPrompt(params: {
|
|
|
496
523
|
fullText: string
|
|
497
524
|
toolEvents: MessageToolEvent[]
|
|
498
525
|
}): string {
|
|
499
|
-
|
|
526
|
+
const lines = [
|
|
500
527
|
'You are in the middle of a multi-step deliverable and stopped after only a partial batch of work.',
|
|
501
528
|
'Continue from the existing workspace and artifacts. Do not restart from scratch and do not ask the user to restate the request.',
|
|
502
529
|
'Do not stop after one partial batch. Finish every requested deliverable that is still outstanding before concluding.',
|
|
503
530
|
'If a requested artifact cannot be produced, say exactly which artifact is missing, what blocked it, and what you already completed.',
|
|
504
531
|
'Use the existing files, screenshots, and generated outputs first. Inspect them if needed, then complete the remaining work.',
|
|
532
|
+
'Preserve hard structural constraints from the original request: exact counts stay exact, required titled sections stay present, and source coverage gaps should be filled instead of skipped.',
|
|
505
533
|
'End with a concise grouped completion summary that lists exact file paths, upload URLs, localhost URLs/ports, and screenshots you produced.',
|
|
534
|
+
]
|
|
535
|
+
|
|
536
|
+
// If the user explicitly asked for file output, remind the model to use file tools
|
|
537
|
+
const userNormalized = params.userMessage.toLowerCase()
|
|
538
|
+
const fileOutputMatch = userNormalized.match(/\b(?:save|write|output|export)\b[^.!?\n]{0,80}\b(?:to|as|at|in)\b[^.!?\n]{0,60}(\/[^\s,'"]+|~\/[^\s,'"]+|\.\/[^\s,'"]+)/i)
|
|
539
|
+
if (fileOutputMatch) {
|
|
540
|
+
const fileToolNames = ['write_file', 'edit_file', 'files', 'shell', 'execute_command']
|
|
541
|
+
const usedFileTools = params.toolEvents.some((e) => e.name && fileToolNames.includes(e.name))
|
|
542
|
+
if (!usedFileTools) {
|
|
543
|
+
lines.push(
|
|
544
|
+
'',
|
|
545
|
+
`CRITICAL: The user asked you to save output to a file path (${fileOutputMatch[1] || 'see objective'}). You have NOT used any file-writing tool yet.`,
|
|
546
|
+
'You MUST use the `files` or `write_file` tool to write the content to the requested path. Do not just include the content in your text response — actually write the file.',
|
|
547
|
+
)
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
lines.push(
|
|
506
552
|
'',
|
|
507
553
|
`Objective:\n${params.userMessage}`,
|
|
508
554
|
'',
|
|
509
555
|
`Current partial response:\n${params.fullText || '(none)'}`,
|
|
510
556
|
'',
|
|
511
557
|
`Recent tool evidence:\n${renderToolEvidence(params.toolEvents) || '(none)'}`,
|
|
558
|
+
)
|
|
559
|
+
return lines.join('\n')
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function buildExactStructureBlock(userMessage: string): string {
|
|
563
|
+
const exactBulletMatch = userMessage.match(/\bexactly\s+(\d+)\s+bullet points?\b/i)
|
|
564
|
+
if (!exactBulletMatch) return ''
|
|
565
|
+
const bulletCount = exactBulletMatch[1]
|
|
566
|
+
return [
|
|
567
|
+
'## Exact Structural Constraints',
|
|
568
|
+
`The user required exactly ${bulletCount} bullet points.`,
|
|
569
|
+
'Treat that as a hard file-wide constraint unless the user explicitly says later sections get their own separate bullets.',
|
|
570
|
+
'If the file also needs titled sections such as Owners or Risks, use short prose under those headings instead of adding more bullet lines.',
|
|
512
571
|
].join('\n')
|
|
513
572
|
}
|
|
514
573
|
|
|
@@ -529,6 +588,7 @@ const GOAL_DECOMPOSITION_BLOCK = [
|
|
|
529
588
|
'When you receive a broad, open-ended goal:',
|
|
530
589
|
'1. Break it into 3-7 concrete, sequentially-executable subtasks before taking action.',
|
|
531
590
|
'2. If manage_tasks is available, use it only for durable tracking: multi-turn work, delegation, explicit backlog requests, or work you expect to resume later. Do not create a task for every micro-step.',
|
|
591
|
+
'Single-step instructions are not broad goals. For direct actions like storing a memory, answering a recall question, editing one file, or sending one message, execute the relevant tool immediately instead of creating tasks or delegating.',
|
|
532
592
|
'3. Present the plan as a short checklist or numbered list in plain language. If durable tracking is unnecessary, keep it inline instead of creating tasks.',
|
|
533
593
|
'4. Execute the first substantive subtask immediately — do not stop after planning.',
|
|
534
594
|
'5. Update only the durable tasks you actually created; otherwise just continue executing and report progress plainly.',
|
|
@@ -541,12 +601,15 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
541
601
|
heartbeatIntervalSec: number
|
|
542
602
|
platformAssignScope?: 'self' | 'all'
|
|
543
603
|
userMessage?: string
|
|
604
|
+
history?: Message[]
|
|
544
605
|
responseStyle?: 'concise' | 'normal' | 'detailed' | null
|
|
545
606
|
responseMaxChars?: number | null
|
|
546
607
|
}) {
|
|
547
608
|
const hasTooling = opts.enabledPlugins.length > 0
|
|
548
609
|
const pluginLines = buildPluginCapabilityLines(opts.enabledPlugins, { platformAssignScope: opts.platformAssignScope })
|
|
549
610
|
const toolDisciplineLines = buildToolDisciplineLines(opts.enabledPlugins)
|
|
611
|
+
const hasMemoryTools = opts.enabledPlugins.some((toolId) => (canonicalizePluginId(toolId) || toolId) === 'memory')
|
|
612
|
+
const directMemoryWriteOnlyTurn = Boolean(opts.userMessage && isNarrowDirectMemoryWriteTurn(opts.userMessage))
|
|
550
613
|
|
|
551
614
|
const parts: string[] = []
|
|
552
615
|
|
|
@@ -556,7 +619,7 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
556
619
|
hasTooling
|
|
557
620
|
? 'I take initiative — plan briefly, execute tools, evaluate, iterate until done. Never stop at advice when action is implied.'
|
|
558
621
|
: 'No tools enabled. Be explicit about what tool access is needed.',
|
|
559
|
-
'IMPORTANT: If information was already mentioned in THIS conversation, answer from context — do NOT call
|
|
622
|
+
'IMPORTANT: If information was already mentioned in THIS conversation, answer from context — do NOT call memory tools or web search to look it up again. Only use memory tools to recall info from PREVIOUS conversations not in the current thread.',
|
|
560
623
|
'If a skill applies to the task, follow its recommended approach first. Skill-specific commands are faster and more reliable than generic web search. Minimize tool calls — combine steps where possible.',
|
|
561
624
|
'If a task explicitly names an enabled tool, use that tool before declaring success. A prose request is not a substitute for `ask_human`, and browser work is not a substitute for `email` delivery.',
|
|
562
625
|
'When `ask_human` is enabled, collect required human input through the tool instead of asking for it only in plain assistant text.',
|
|
@@ -567,6 +630,18 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
567
630
|
: 'Loop: BOUNDED — execute multiple steps but finish within recursion budget.',
|
|
568
631
|
)
|
|
569
632
|
|
|
633
|
+
if (hasMemoryTools) {
|
|
634
|
+
parts.push(
|
|
635
|
+
'## Immediate Memory Routes',
|
|
636
|
+
'If the user asks you to remember, store, or correct a durable fact, call `memory_store` or `memory_update` immediately before any planning, delegation, task creation, or agent management.',
|
|
637
|
+
'If the user asks about prior work, decisions, dates, people, preferences, or todos from earlier conversations, start with `memory_search`. Use `memory_get` only when you need one targeted follow-up read.',
|
|
638
|
+
'Do not use `manage_tasks`, `manage_agents`, or `delegate` as a substitute for a direct memory write or recall step.',
|
|
639
|
+
)
|
|
640
|
+
}
|
|
641
|
+
if (hasMemoryTools && directMemoryWriteOnlyTurn) {
|
|
642
|
+
parts.push(buildDirectMemoryWriteBlock())
|
|
643
|
+
}
|
|
644
|
+
|
|
570
645
|
// Plugin-specific operating guidance (collected dynamically from plugins)
|
|
571
646
|
const guidanceLines = getPluginManager().collectOperatingGuidance(opts.enabledPlugins)
|
|
572
647
|
if (guidanceLines.length) parts.push(...guidanceLines)
|
|
@@ -597,10 +672,104 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
597
672
|
if (opts.userMessage && looksLikeOpenEndedDeliverableTask(opts.userMessage) && opts.enabledPlugins.some((toolId) => toolId === 'files' || toolId === 'edit_file')) {
|
|
598
673
|
parts.push(OPEN_ENDED_REVISION_BLOCK)
|
|
599
674
|
}
|
|
675
|
+
if (opts.userMessage) {
|
|
676
|
+
const exactStructureBlock = buildExactStructureBlock(opts.userMessage)
|
|
677
|
+
if (exactStructureBlock) parts.push(exactStructureBlock)
|
|
678
|
+
}
|
|
679
|
+
if (opts.userMessage && isCurrentThreadRecallRequest(opts.userMessage)) {
|
|
680
|
+
parts.push(buildCurrentThreadRecallBlock(opts.history || []))
|
|
681
|
+
}
|
|
600
682
|
|
|
601
683
|
return parts.filter(Boolean).join('\n')
|
|
602
684
|
}
|
|
603
685
|
|
|
686
|
+
function compactThreadRecallText(text: string, maxChars = 180): string {
|
|
687
|
+
const compact = extractSuggestions(text || '').clean.replace(/\s+/g, ' ').trim()
|
|
688
|
+
if (!compact) return ''
|
|
689
|
+
return compact.length > maxChars ? `${compact.slice(0, maxChars - 3)}...` : compact
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function buildCurrentThreadRecallBlock(history: Message[]): string {
|
|
693
|
+
const recentUserFacts = history
|
|
694
|
+
.filter((entry) => entry.role === 'user' && typeof entry.text === 'string' && entry.text.trim())
|
|
695
|
+
.slice(-3)
|
|
696
|
+
const relevant = history
|
|
697
|
+
.filter((entry) => (entry.role === 'user' || entry.role === 'assistant') && typeof entry.text === 'string' && entry.text.trim())
|
|
698
|
+
.slice(-6)
|
|
699
|
+
const lines = [
|
|
700
|
+
'## Current Thread Recall',
|
|
701
|
+
'The user is asking about information from this same conversation.',
|
|
702
|
+
'Treat the current chat history as the authoritative source for this request.',
|
|
703
|
+
'Do NOT call memory tools, web search, or session-history tools unless the user explicitly asks you to verify outside the current thread.',
|
|
704
|
+
'Answer directly from the existing conversation with the exact values already stated.',
|
|
705
|
+
'Prefer the user\'s own earlier words and facts over assistant summaries, persona defaults, soul/config values, or generic background context.',
|
|
706
|
+
'If the answer is present in the recent thread context below, do not say the information is missing, unknown, or from a first exchange.',
|
|
707
|
+
]
|
|
708
|
+
if (recentUserFacts.length > 0) {
|
|
709
|
+
lines.push('Recent user-provided facts to trust first:')
|
|
710
|
+
for (const message of recentUserFacts) {
|
|
711
|
+
const snippet = compactThreadRecallText(message.text || '')
|
|
712
|
+
if (!snippet) continue
|
|
713
|
+
lines.push(`- user: ${snippet}`)
|
|
714
|
+
}
|
|
715
|
+
lines.push('These user messages override tool traces, failed tool attempts, persona defaults, and generic background context.')
|
|
716
|
+
}
|
|
717
|
+
if (relevant.length > 0) {
|
|
718
|
+
lines.push('Recent thread context:')
|
|
719
|
+
for (const message of relevant) {
|
|
720
|
+
const snippet = compactThreadRecallText(message.text || '')
|
|
721
|
+
if (!snippet) continue
|
|
722
|
+
lines.push(`- ${message.role}: ${snippet}`)
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return lines.join('\n')
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function buildDirectMemoryWriteBlock(): string {
|
|
729
|
+
return [
|
|
730
|
+
'## Direct Memory Write',
|
|
731
|
+
'This turn is a direct request to remember, store, or correct a durable fact.',
|
|
732
|
+
'Call `memory_store` or `memory_update` immediately, then confirm the stored value succinctly.',
|
|
733
|
+
'If the user bundled several related facts into one remember request, store them together in one canonical memory write unless they explicitly asked for separate entries.',
|
|
734
|
+
'Do not inspect skills, browse the workspace, request capabilities, manage tasks, manage agents, or delegate before the direct memory write is complete.',
|
|
735
|
+
].join('\n')
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE = /\b(?:then|and then|after that)?\s*(?:confirm|recap|repeat|summarize|tell me|say)\b[\s\S]{0,120}\b(?:stored|saved|updated|remembered|wrote|write)\b/i
|
|
739
|
+
const DIRECT_MEMORY_WRITE_EXTRA_ACTION_RE = /\b(?:then|and then|after that|also)\b[\s\S]{0,160}\b(?:write|create|send|email|message|delegate|research|search|browse|open|edit|build|schedule|plan|review|analy[sz]e)\b/i
|
|
740
|
+
|
|
741
|
+
export function isNarrowDirectMemoryWriteTurn(message: string): boolean {
|
|
742
|
+
const trimmed = String(message || '').trim()
|
|
743
|
+
if (!trimmed || !isDirectMemoryWriteRequest(trimmed)) return false
|
|
744
|
+
if (looksLikeOpenEndedDeliverableTask(trimmed)) return false
|
|
745
|
+
if (DIRECT_MEMORY_WRITE_EXTRA_ACTION_RE.test(trimmed) && !DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE.test(trimmed)) {
|
|
746
|
+
return false
|
|
747
|
+
}
|
|
748
|
+
return !isBroadGoal(trimmed) || DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE.test(trimmed) || !/[?]$/.test(trimmed)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const CURRENT_THREAD_RECALL_BLOCKED_TOOL_IDS = new Set([
|
|
752
|
+
'memory',
|
|
753
|
+
'manage_sessions',
|
|
754
|
+
'web',
|
|
755
|
+
'context_mgmt',
|
|
756
|
+
])
|
|
757
|
+
|
|
758
|
+
export function shouldAllowToolForCurrentThreadRecall(toolName: string): boolean {
|
|
759
|
+
const canonicalToolName = canonicalizePluginId(toolName) || toolName.trim().toLowerCase()
|
|
760
|
+
return !CURRENT_THREAD_RECALL_BLOCKED_TOOL_IDS.has(canonicalToolName)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const DIRECT_MEMORY_WRITE_ALLOWED_TOOL_IDS = new Set([
|
|
764
|
+
'memory_store',
|
|
765
|
+
'memory_update',
|
|
766
|
+
])
|
|
767
|
+
|
|
768
|
+
export function shouldAllowToolForDirectMemoryWrite(toolName: string): boolean {
|
|
769
|
+
const rawToolName = toolName.trim().toLowerCase()
|
|
770
|
+
return DIRECT_MEMORY_WRITE_ALLOWED_TOOL_IDS.has(rawToolName)
|
|
771
|
+
}
|
|
772
|
+
|
|
604
773
|
export interface StreamAgentChatResult {
|
|
605
774
|
/** All text accumulated across every LLM turn (for SSE / web UI history). */
|
|
606
775
|
fullText: string
|
|
@@ -704,6 +873,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
704
873
|
|
|
705
874
|
const stateModifierParts: string[] = []
|
|
706
875
|
const hasProvidedSystemPrompt = typeof systemPrompt === 'string' && systemPrompt.trim().length > 0
|
|
876
|
+
const directMemoryWriteOnlyTurn = isNarrowDirectMemoryWriteTurn(message)
|
|
877
|
+
const currentThreadRecallRequest = !directMemoryWriteOnlyTurn && isCurrentThreadRecallRequest(message)
|
|
707
878
|
|
|
708
879
|
if (hasProvidedSystemPrompt) {
|
|
709
880
|
stateModifierParts.push(systemPrompt!.trim())
|
|
@@ -897,6 +1068,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
897
1068
|
heartbeatIntervalSec,
|
|
898
1069
|
platformAssignScope: agentPlatformAssignScope,
|
|
899
1070
|
userMessage: message,
|
|
1071
|
+
history,
|
|
900
1072
|
responseStyle: agentResponseStyle,
|
|
901
1073
|
responseMaxChars: agentResponseMaxChars,
|
|
902
1074
|
}),
|
|
@@ -916,7 +1088,22 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
916
1088
|
projectDescription: activeProjectContext.project?.description || null,
|
|
917
1089
|
memoryScopeMode: agentMemoryScopeMode,
|
|
918
1090
|
})
|
|
919
|
-
const
|
|
1091
|
+
const toolsForTurn = currentThreadRecallRequest
|
|
1092
|
+
? tools.filter((tool) => {
|
|
1093
|
+
const toolName = typeof (tool as { name?: unknown }).name === 'string'
|
|
1094
|
+
? String((tool as { name?: unknown }).name)
|
|
1095
|
+
: ''
|
|
1096
|
+
return shouldAllowToolForCurrentThreadRecall(toolName)
|
|
1097
|
+
})
|
|
1098
|
+
: directMemoryWriteOnlyTurn
|
|
1099
|
+
? tools.filter((tool) => {
|
|
1100
|
+
const toolName = typeof (tool as { name?: unknown }).name === 'string'
|
|
1101
|
+
? String((tool as { name?: unknown }).name)
|
|
1102
|
+
: ''
|
|
1103
|
+
return shouldAllowToolForDirectMemoryWrite(toolName)
|
|
1104
|
+
})
|
|
1105
|
+
: tools
|
|
1106
|
+
const agent = createReactAgent({ llm, tools: toolsForTurn, stateModifier })
|
|
920
1107
|
const recursionLimit = getAgentLoopRecursionLimit(runtime)
|
|
921
1108
|
|
|
922
1109
|
// Build message history for context
|
|
@@ -1112,7 +1299,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1112
1299
|
const MAX_REQUIRED_TOOL_CONTINUES = 2
|
|
1113
1300
|
const MAX_EXECUTION_FOLLOWTHROUGHS = 1
|
|
1114
1301
|
const MAX_DELIVERABLE_FOLLOWTHROUGHS = 2
|
|
1115
|
-
const MAX_TOOL_SUMMARY_RETRIES =
|
|
1302
|
+
const MAX_TOOL_SUMMARY_RETRIES = 2
|
|
1116
1303
|
let autoContinueCount = 0
|
|
1117
1304
|
let transientRetryCount = 0
|
|
1118
1305
|
let requiredToolContinueCount = 0
|
|
@@ -1496,10 +1683,18 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1496
1683
|
|
|
1497
1684
|
if (reachedExecutionBoundary) break
|
|
1498
1685
|
|
|
1499
|
-
// Tool loop detection: critical severity stops
|
|
1686
|
+
// Tool loop detection: critical severity stops further tool calls.
|
|
1687
|
+
// However, if tools already produced results but the model has no/trivial text,
|
|
1688
|
+
// we attempt a tool_summary continuation instead of just erroring out.
|
|
1500
1689
|
if (loopDetectionTriggered) {
|
|
1501
|
-
|
|
1502
|
-
|
|
1690
|
+
const loopTextIsTrivial = !fullText.trim() || (fullText.trim().length < 150 && streamedToolEvents.length >= 2)
|
|
1691
|
+
if (loopTextIsTrivial && streamedToolEvents.length > 0 && toolSummaryRetryCount < MAX_TOOL_SUMMARY_RETRIES) {
|
|
1692
|
+
// Override: let the tool_summary check below handle it instead of breaking
|
|
1693
|
+
loopDetectionTriggered = null
|
|
1694
|
+
} else {
|
|
1695
|
+
write(`data: ${JSON.stringify({ t: 'err', text: loopDetectionTriggered.message })}\n\n`)
|
|
1696
|
+
break
|
|
1697
|
+
}
|
|
1503
1698
|
}
|
|
1504
1699
|
|
|
1505
1700
|
if (
|
|
@@ -1590,25 +1785,28 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1590
1785
|
})}\n\n`)
|
|
1591
1786
|
}
|
|
1592
1787
|
|
|
1593
|
-
// Generic fallback: tools were called but the model produced no text
|
|
1594
|
-
//
|
|
1595
|
-
//
|
|
1788
|
+
// Generic fallback: tools were called but the model produced no substantive text.
|
|
1789
|
+
// Triggers when: (a) text is empty, or (b) text is trivially short (< 150 chars)
|
|
1790
|
+
// and multiple tools ran — the agent likely emitted a "I'll do X" preamble but
|
|
1791
|
+
// never synthesized the tool outputs into a real response.
|
|
1792
|
+
const textIsTrivial = !fullText.trim() || (fullText.trim().length < 150 && streamedToolEvents.length >= 2)
|
|
1596
1793
|
if (
|
|
1597
1794
|
!shouldContinue
|
|
1598
1795
|
&& hasToolCalls
|
|
1599
|
-
&&
|
|
1796
|
+
&& textIsTrivial
|
|
1600
1797
|
&& streamedToolEvents.length > 0
|
|
1601
1798
|
&& toolSummaryRetryCount < MAX_TOOL_SUMMARY_RETRIES
|
|
1602
1799
|
) {
|
|
1603
1800
|
shouldContinue = 'tool_summary'
|
|
1604
1801
|
toolSummaryRetryCount++
|
|
1605
|
-
logExecution(session.id, 'decision', `Tools called but
|
|
1802
|
+
logExecution(session.id, 'decision', `Tools called but response text is trivial (${fullText.trim().length} chars) — forcing summary continuation`, {
|
|
1606
1803
|
agentId: session.agentId,
|
|
1607
|
-
detail: { toolEventCount: streamedToolEvents.length, toolSummaryRetryCount },
|
|
1804
|
+
detail: { toolEventCount: streamedToolEvents.length, toolSummaryRetryCount, textLength: fullText.trim().length },
|
|
1608
1805
|
})
|
|
1806
|
+
const summaryReason = !fullText.trim() ? 'empty_response_after_tools' : 'trivial_preamble_after_tools'
|
|
1609
1807
|
write(`data: ${JSON.stringify({
|
|
1610
1808
|
t: 'status',
|
|
1611
|
-
text: JSON.stringify({ toolSummary: toolSummaryRetryCount, reason:
|
|
1809
|
+
text: JSON.stringify({ toolSummary: toolSummaryRetryCount, reason: summaryReason }),
|
|
1612
1810
|
})}\n\n`)
|
|
1613
1811
|
}
|
|
1614
1812
|
|
|
@@ -1669,7 +1867,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1669
1867
|
}))
|
|
1670
1868
|
lastSegment = ''
|
|
1671
1869
|
} else if (shouldContinue === 'tool_summary') {
|
|
1672
|
-
// Model called tools but produced no text — prompt it to
|
|
1870
|
+
// Model called tools but produced no/trivial text — prompt it to synthesize results.
|
|
1673
1871
|
if (continuationAssistantText) {
|
|
1674
1872
|
langchainMessages.push(new AIMessage({ content: continuationAssistantText }))
|
|
1675
1873
|
}
|
|
@@ -1677,13 +1875,18 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1677
1875
|
.filter((e) => e.output)
|
|
1678
1876
|
.map((e) => `[${e.name}]: ${(e.output || '').slice(0, 500)}`)
|
|
1679
1877
|
.slice(0, 6)
|
|
1878
|
+
const preambleNote = fullText.trim()
|
|
1879
|
+
? `You started with "${fullText.trim().slice(0, 100)}..." but did not follow through with actual results.`
|
|
1880
|
+
: 'Your tool calls completed but you did not provide a response.'
|
|
1680
1881
|
langchainMessages.push(new HumanMessage({
|
|
1681
1882
|
content: [
|
|
1682
|
-
|
|
1883
|
+
preambleNote,
|
|
1683
1884
|
'Here are the tool results:',
|
|
1684
1885
|
...toolSummaryLines,
|
|
1685
1886
|
'',
|
|
1686
|
-
|
|
1887
|
+
`Original request: ${message.slice(0, 500)}`,
|
|
1888
|
+
'',
|
|
1889
|
+
'Now answer the original request using these tool results. Be concise and direct. Present the findings clearly.',
|
|
1687
1890
|
].join('\n'),
|
|
1688
1891
|
}))
|
|
1689
1892
|
lastSegment = ''
|
|
@@ -1769,7 +1972,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
1769
1972
|
const totalTokens = totalInputTokens + totalOutputTokens
|
|
1770
1973
|
if (totalTokens > 0) {
|
|
1771
1974
|
const cost = estimateCost(session.model, totalInputTokens, totalOutputTokens)
|
|
1772
|
-
const pluginDefinitionCosts = buildPluginDefinitionCosts(
|
|
1975
|
+
const pluginDefinitionCosts = buildPluginDefinitionCosts(toolsForTurn, toolToPluginMap)
|
|
1773
1976
|
const usageRecord: UsageRecord = {
|
|
1774
1977
|
sessionId: session.id,
|
|
1775
1978
|
messageIndex: history.length,
|
|
@@ -20,7 +20,7 @@ const PLUGIN_ALIAS_GROUPS: string[][] = [
|
|
|
20
20
|
['manage_sessions', 'session_info', 'sessions_tool', 'whoami_tool', 'search_history_tool'],
|
|
21
21
|
['schedule_wake', 'schedule'],
|
|
22
22
|
['http_request', 'http'],
|
|
23
|
-
['memory', 'memory_tool'],
|
|
23
|
+
['memory', 'memory_tool', 'memory_search', 'memory_get', 'memory_store', 'memory_update'],
|
|
24
24
|
['sandbox', 'sandbox_exec', 'sandbox_list_runtimes'],
|
|
25
25
|
['wallet', 'wallet_tool'],
|
|
26
26
|
['monitor', 'monitor_tool'],
|
|
@@ -55,7 +55,7 @@ const TOOL_DESCRIPTORS: Record<string, ToolDescriptor> = {
|
|
|
55
55
|
codex_cli: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_codex_cli'] },
|
|
56
56
|
opencode_cli: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_opencode_cli'] },
|
|
57
57
|
gemini_cli: { categories: ['delegation', 'execution'], concreteTools: ['delegate_to_gemini_cli'] },
|
|
58
|
-
memory: { categories: ['memory'], concreteTools: ['memory', 'memory_tool', 'context_status', 'context_summarize'] },
|
|
58
|
+
memory: { categories: ['memory'], concreteTools: ['memory', 'memory_tool', 'memory_search', 'memory_get', 'memory_store', 'memory_update', 'context_status', 'context_summarize'] },
|
|
59
59
|
sandbox: { categories: ['execution', 'filesystem'], concreteTools: ['sandbox', 'sandbox_exec', 'sandbox_list_runtimes'] },
|
|
60
60
|
git: { categories: ['execution', 'filesystem'], concreteTools: ['git'] },
|
|
61
61
|
http_request: { categories: ['network'], concreteTools: ['http_request'] },
|
|
@@ -6,6 +6,7 @@ import { fetchChats, fetchDirs, fetchProviders, fetchCredentials } from '../lib/
|
|
|
6
6
|
import { fetchAgents } from '../lib/agents'
|
|
7
7
|
import { fetchSchedules } from '../lib/schedules'
|
|
8
8
|
import { fetchTasks } from '../lib/tasks'
|
|
9
|
+
import { findLatestObservablePlatformSession, isLocalhostBrowser } from '../lib/local-observability'
|
|
9
10
|
import { api } from '../lib/api-client'
|
|
10
11
|
import { safeStorageGet, safeStorageGetJson, safeStorageRemove, safeStorageSet } from '../lib/safe-storage'
|
|
11
12
|
|
|
@@ -247,7 +248,11 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|
|
247
248
|
loadSessions: async () => {
|
|
248
249
|
try {
|
|
249
250
|
const sessions = await fetchChats()
|
|
250
|
-
|
|
251
|
+
const currentSessionId = get().currentSessionId
|
|
252
|
+
set({
|
|
253
|
+
sessions,
|
|
254
|
+
currentSessionId: currentSessionId && sessions[currentSessionId] ? currentSessionId : null,
|
|
255
|
+
})
|
|
251
256
|
} catch {
|
|
252
257
|
// ignore
|
|
253
258
|
}
|
|
@@ -348,6 +353,26 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|
|
348
353
|
}
|
|
349
354
|
set({ currentAgentId: id })
|
|
350
355
|
safeStorageSet('sc_agent', id)
|
|
356
|
+
if (isLocalhostBrowser()) {
|
|
357
|
+
let livePlatformSession = findLatestObservablePlatformSession(get().sessions, id)
|
|
358
|
+
if (!livePlatformSession) {
|
|
359
|
+
try {
|
|
360
|
+
const refreshedSessions = await fetchChats()
|
|
361
|
+
const currentSessionId = get().currentSessionId
|
|
362
|
+
set({
|
|
363
|
+
sessions: refreshedSessions,
|
|
364
|
+
currentSessionId: currentSessionId && refreshedSessions[currentSessionId] ? currentSessionId : null,
|
|
365
|
+
})
|
|
366
|
+
livePlatformSession = findLatestObservablePlatformSession(refreshedSessions, id)
|
|
367
|
+
} catch {
|
|
368
|
+
// ignore and fall back to the normal thread path below
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (livePlatformSession?.id) {
|
|
372
|
+
set({ currentSessionId: livePlatformSession.id })
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
}
|
|
351
376
|
try {
|
|
352
377
|
const user = get().currentUser || 'default'
|
|
353
378
|
const session = await api<Session>('POST', `/agents/${id}/thread`, { user })
|