@swarmclawai/swarmclaw 0.9.2 → 0.9.3
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/package.json +1 -1
- package/src/app/agents/page.tsx +2 -1
- package/src/app/globals.css +28 -0
- package/src/app/home/page.tsx +11 -0
- package/src/app/settings/page.tsx +12 -5
- package/src/components/connectors/connector-list.tsx +2 -5
- package/src/components/logs/log-list.tsx +2 -5
- package/src/components/providers/provider-list.tsx +2 -5
- package/src/components/runs/run-list.tsx +2 -6
- package/src/components/schedules/schedule-list.tsx +7 -1
- package/src/components/ui/full-screen-loader.tsx +0 -29
- package/src/components/ui/page-loader.tsx +69 -0
- package/src/lib/runtime/runtime-loop.ts +21 -1
- package/src/lib/server/chat-execution/chat-execution-utils.test.ts +56 -0
- package/src/lib/server/chat-execution/chat-execution-utils.ts +24 -0
- package/src/lib/server/chat-execution/chat-execution.ts +43 -4
- package/src/lib/server/chat-execution/chat-streaming-utils.ts +1 -38
- package/src/lib/server/chat-execution/stream-agent-chat.test.ts +2 -46
- package/src/lib/server/chat-execution/stream-agent-chat.ts +51 -86
- package/src/lib/server/chat-execution/stream-continuation.ts +1 -1
- package/src/lib/server/connectors/manager.ts +1 -1
- package/src/lib/server/memory/memory-policy.test.ts +5 -15
- package/src/lib/server/memory/memory-policy.ts +11 -41
- package/src/lib/server/runtime/heartbeat-service.test.ts +46 -0
- package/src/lib/server/runtime/heartbeat-service.ts +5 -1
- package/src/lib/server/runtime/runtime-settings.test.ts +4 -4
- package/src/lib/server/runtime/runtime-settings.ts +4 -0
- package/src/lib/server/runtime/session-run-manager.ts +2 -0
- package/src/lib/server/session-tools/delegate.ts +3 -3
- package/src/lib/server/session-tools/memory.ts +220 -48
- package/src/types/index.ts +4 -0
- package/src/views/settings/section-runtime-loop.tsx +38 -0
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import type { MessageToolEvent } from '@/types'
|
|
2
2
|
import { canonicalizePluginId } from '@/lib/server/tool-aliases'
|
|
3
3
|
import { extractSuggestions } from '@/lib/server/suggestions'
|
|
4
|
-
import { isDirectMemoryWriteRequest } from '@/lib/server/memory/memory-policy'
|
|
5
4
|
import {
|
|
6
|
-
isBroadGoal,
|
|
7
5
|
looksLikeExternalWalletTask,
|
|
8
|
-
looksLikeOpenEndedDeliverableTask,
|
|
9
6
|
} from '@/lib/server/chat-execution/stream-continuation'
|
|
10
7
|
|
|
11
|
-
const EXPLICIT_ARTIFACT_OUTPUT_RE = /\b(?:save|write|output|export)\b[^.!?\n]{0,80}\b(?:to|as|at|in)\b[^.!?\n]{0,60}(\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|~\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|\.\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|[a-z0-9._/-]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)\b)/i
|
|
8
|
+
const EXPLICIT_ARTIFACT_OUTPUT_RE = /\b(?:save|write|output|export|create|generate)\b[^.!?\n]{0,80}\b(?:to|as|at|in)\b[^.!?\n]{0,60}(\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|~\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|\.\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|[a-z0-9._/-]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)\b)/i
|
|
12
9
|
|
|
13
10
|
export function isLikelyToolErrorOutput(output: string): boolean {
|
|
14
11
|
const trimmed = String(output || '').trim()
|
|
@@ -168,37 +165,3 @@ export function compactThreadRecallText(text: string, maxChars = 180): string {
|
|
|
168
165
|
return compact.length > maxChars ? `${compact.slice(0, maxChars - 3)}...` : compact
|
|
169
166
|
}
|
|
170
167
|
|
|
171
|
-
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
|
|
172
|
-
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
|
|
173
|
-
|
|
174
|
-
export function isNarrowDirectMemoryWriteTurn(message: string): boolean {
|
|
175
|
-
const trimmed = String(message || '').trim()
|
|
176
|
-
if (!trimmed || !isDirectMemoryWriteRequest(trimmed)) return false
|
|
177
|
-
if (looksLikeOpenEndedDeliverableTask(trimmed)) return false
|
|
178
|
-
if (DIRECT_MEMORY_WRITE_EXTRA_ACTION_RE.test(trimmed) && !DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE.test(trimmed)) {
|
|
179
|
-
return false
|
|
180
|
-
}
|
|
181
|
-
return !isBroadGoal(trimmed) || DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE.test(trimmed) || !/[?]$/.test(trimmed)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const CURRENT_THREAD_RECALL_BLOCKED_TOOL_IDS = new Set([
|
|
185
|
-
'memory',
|
|
186
|
-
'manage_sessions',
|
|
187
|
-
'web',
|
|
188
|
-
'context_mgmt',
|
|
189
|
-
])
|
|
190
|
-
|
|
191
|
-
export function shouldAllowToolForCurrentThreadRecall(toolName: string): boolean {
|
|
192
|
-
const canonicalToolName = canonicalizePluginId(toolName) || toolName.trim().toLowerCase()
|
|
193
|
-
return !CURRENT_THREAD_RECALL_BLOCKED_TOOL_IDS.has(canonicalToolName)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const DIRECT_MEMORY_WRITE_ALLOWED_TOOL_IDS = new Set([
|
|
197
|
-
'memory_store',
|
|
198
|
-
'memory_update',
|
|
199
|
-
])
|
|
200
|
-
|
|
201
|
-
export function shouldAllowToolForDirectMemoryWrite(toolName: string): boolean {
|
|
202
|
-
const rawToolName = toolName.trim().toLowerCase()
|
|
203
|
-
return DIRECT_MEMORY_WRITE_ALLOWED_TOOL_IDS.has(rawToolName)
|
|
204
|
-
}
|
|
@@ -8,13 +8,10 @@ import {
|
|
|
8
8
|
buildExternalWalletExecutionBlock,
|
|
9
9
|
buildToolDisciplineLines,
|
|
10
10
|
getExplicitRequiredToolNames,
|
|
11
|
-
isNarrowDirectMemoryWriteTurn,
|
|
12
11
|
isWalletSimulationResult,
|
|
13
12
|
looksLikeOpenEndedDeliverableTask,
|
|
14
13
|
resolveContinuationAssistantText,
|
|
15
14
|
resolveFinalStreamResponseText,
|
|
16
|
-
shouldAllowToolForDirectMemoryWrite,
|
|
17
|
-
shouldAllowToolForCurrentThreadRecall,
|
|
18
15
|
shouldForceAttachmentFollowthrough,
|
|
19
16
|
shouldForceRecoverableToolErrorFollowthrough,
|
|
20
17
|
shouldTerminateOnSuccessfulMemoryMutation,
|
|
@@ -208,57 +205,16 @@ describe('buildToolDisciplineLines', () => {
|
|
|
208
205
|
assert.ok(streamAgentChatSource.includes('did not start the required workspace tool step'))
|
|
209
206
|
})
|
|
210
207
|
|
|
211
|
-
it('adds
|
|
208
|
+
it('adds current-thread recall guidance and immediate memory routes in the system prompt', () => {
|
|
212
209
|
assert.ok(streamAgentChatSource.includes('## Current Thread Recall'))
|
|
213
210
|
assert.ok(streamAgentChatSource.includes('## Immediate Memory Routes'))
|
|
214
|
-
assert.ok(streamAgentChatSource.includes('## Direct Memory Write'))
|
|
215
211
|
assert.ok(streamAgentChatSource.includes('call `memory_store` or `memory_update` immediately before any planning, delegation, task creation, or agent management'))
|
|
216
|
-
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.'))
|
|
217
212
|
assert.ok(streamAgentChatSource.includes('Do NOT call memory tools, web search, or session-history tools'))
|
|
218
|
-
assert.ok(streamAgentChatSource.includes('const currentThreadRecallRequest =
|
|
219
|
-
assert.ok(streamAgentChatSource.includes('const directMemoryWriteOnlyTurn = isNarrowDirectMemoryWriteTurn(message)'))
|
|
220
|
-
assert.ok(streamAgentChatSource.includes('shouldAllowToolForDirectMemoryWrite(toolName)'))
|
|
221
|
-
assert.ok(streamAgentChatSource.includes('shouldAllowToolForCurrentThreadRecall(toolName)'))
|
|
213
|
+
assert.ok(streamAgentChatSource.includes('const currentThreadRecallRequest = isCurrentThreadRecallRequest(message)'))
|
|
222
214
|
assert.ok(streamSources.includes('Preserve hard structural constraints from the original request'))
|
|
223
215
|
assert.ok(streamAgentChatSource.includes('## Exact Structural Constraints'))
|
|
224
216
|
})
|
|
225
217
|
|
|
226
|
-
it('blocks memory, session-history, web, and context tools during same-thread recall turns', () => {
|
|
227
|
-
assert.equal(shouldAllowToolForCurrentThreadRecall('memory_tool'), false)
|
|
228
|
-
assert.equal(shouldAllowToolForCurrentThreadRecall('memory_search'), false)
|
|
229
|
-
assert.equal(shouldAllowToolForCurrentThreadRecall('memory_get'), false)
|
|
230
|
-
assert.equal(shouldAllowToolForCurrentThreadRecall('memory_store'), false)
|
|
231
|
-
assert.equal(shouldAllowToolForCurrentThreadRecall('memory_update'), false)
|
|
232
|
-
assert.equal(shouldAllowToolForCurrentThreadRecall('search_history_tool'), false)
|
|
233
|
-
assert.equal(shouldAllowToolForCurrentThreadRecall('sessions_tool'), false)
|
|
234
|
-
assert.equal(shouldAllowToolForCurrentThreadRecall('web_search'), false)
|
|
235
|
-
assert.equal(shouldAllowToolForCurrentThreadRecall('context_status'), false)
|
|
236
|
-
assert.equal(shouldAllowToolForCurrentThreadRecall('files'), true)
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
it('only allows direct memory write tools during pure remember/store turns', () => {
|
|
240
|
-
assert.equal(shouldAllowToolForDirectMemoryWrite('memory_store'), true)
|
|
241
|
-
assert.equal(shouldAllowToolForDirectMemoryWrite('memory_update'), true)
|
|
242
|
-
assert.equal(shouldAllowToolForDirectMemoryWrite('memory_tool'), false)
|
|
243
|
-
assert.equal(shouldAllowToolForDirectMemoryWrite('manage_capabilities'), false)
|
|
244
|
-
assert.equal(shouldAllowToolForDirectMemoryWrite('files'), false)
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
it('treats long remember-and-confirm turns as narrow direct memory writes', () => {
|
|
248
|
-
assert.equal(
|
|
249
|
-
isNarrowDirectMemoryWriteTurn('Remember that my favorite programming language is Rust and I prefer functional programming patterns. Then confirm what you just stored.'),
|
|
250
|
-
true,
|
|
251
|
-
)
|
|
252
|
-
assert.equal(
|
|
253
|
-
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".'),
|
|
254
|
-
true,
|
|
255
|
-
)
|
|
256
|
-
assert.equal(
|
|
257
|
-
isNarrowDirectMemoryWriteTurn('Remember that my favorite programming language is Rust, then write a file summarizing it and send it to me.'),
|
|
258
|
-
false,
|
|
259
|
-
)
|
|
260
|
-
})
|
|
261
|
-
|
|
262
218
|
it('canonicalizes required tool names when checking completion', () => {
|
|
263
219
|
// The requiredToolsPending filter must canonicalize tool names so that
|
|
264
220
|
// alias names (e.g. ask_human) match canonical names from LangGraph events.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import { HumanMessage, AIMessage } from '@langchain/core/messages'
|
|
3
3
|
import { createReactAgent } from '@langchain/langgraph/prebuilt'
|
|
4
|
+
import { MemorySaver } from '@langchain/langgraph'
|
|
4
5
|
import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/runtime/heartbeat-defaults'
|
|
5
6
|
import { buildSessionTools } from '@/lib/server/session-tools'
|
|
6
7
|
import { buildChatModel } from '@/lib/server/build-llm'
|
|
@@ -51,31 +52,43 @@ import {
|
|
|
51
52
|
import type { ContinuationType } from '@/lib/server/chat-execution/stream-continuation'
|
|
52
53
|
import { dedup, errorMessage, sleep } from '@/lib/shared-utils'
|
|
53
54
|
import { perf } from '@/lib/server/runtime/perf'
|
|
54
|
-
import { getCheckpointSaver } from '@/lib/server/langgraph-checkpoint'
|
|
55
55
|
import {
|
|
56
56
|
compactThreadRecallText,
|
|
57
57
|
getExplicitRequiredToolNames,
|
|
58
58
|
getWalletApprovalBoundaryAction,
|
|
59
|
-
isNarrowDirectMemoryWriteTurn,
|
|
60
59
|
isWalletSimulationResult,
|
|
61
60
|
resolveToolAction,
|
|
62
|
-
shouldAllowToolForDirectMemoryWrite,
|
|
63
|
-
shouldAllowToolForCurrentThreadRecall,
|
|
64
61
|
shouldForceExternalServiceSummary,
|
|
65
62
|
shouldTerminateOnSuccessfulMemoryMutation,
|
|
66
63
|
updateStreamedToolEvents,
|
|
67
64
|
} from '@/lib/server/chat-execution/chat-streaming-utils'
|
|
68
65
|
import { LangGraphToolEventTracker } from '@/lib/server/chat-execution/tool-event-tracker'
|
|
69
66
|
|
|
67
|
+
// LangGraph's streamEvents leaves dangling internal promises when the for-await
|
|
68
|
+
// loop exits early (break on tool loop detection, execution boundary, etc.).
|
|
69
|
+
// These promises may later reject with GraphRecursionError or AbortError.
|
|
70
|
+
// Register a permanent handler to prevent process crashes from these expected
|
|
71
|
+
// background rejections. Only LangGraph-specific errors (identified by
|
|
72
|
+
// pregelTaskId or lc_error_code) are suppressed; all other rejections propagate
|
|
73
|
+
// normally.
|
|
74
|
+
process.on('unhandledRejection', (err: unknown) => {
|
|
75
|
+
if (
|
|
76
|
+
err && typeof err === 'object'
|
|
77
|
+
&& ('pregelTaskId' in err
|
|
78
|
+
|| (err instanceof Error && (err.name === 'AbortError' || err.name === 'GraphRecursionError'))
|
|
79
|
+
|| (err as Record<string, unknown>).lc_error_code === 'GRAPH_RECURSION_LIMIT')
|
|
80
|
+
) {
|
|
81
|
+
// Silently suppress — expected background rejection from LangGraph
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
70
86
|
// Re-export continuation functions so existing consumers don't need to change imports
|
|
71
87
|
export {
|
|
72
88
|
getExplicitRequiredToolNames,
|
|
73
|
-
isNarrowDirectMemoryWriteTurn,
|
|
74
89
|
isWalletSimulationResult,
|
|
75
90
|
looksLikeOpenEndedDeliverableTask,
|
|
76
91
|
shouldForceRecoverableToolErrorFollowthrough,
|
|
77
|
-
shouldAllowToolForDirectMemoryWrite,
|
|
78
|
-
shouldAllowToolForCurrentThreadRecall,
|
|
79
92
|
shouldForceExternalExecutionFollowthrough,
|
|
80
93
|
shouldForceDeliverableFollowthrough,
|
|
81
94
|
shouldForceExternalServiceSummary,
|
|
@@ -101,9 +114,6 @@ interface StreamAgentChatOpts {
|
|
|
101
114
|
|
|
102
115
|
// LangGraph uses this internal configurable key to bypass subgraph lookup when
|
|
103
116
|
// resolving state from a namespaced checkpoint. It is not exported publicly in
|
|
104
|
-
// @langchain/langgraph 1.x, so keep the literal here instead of importing a
|
|
105
|
-
// non-exported symbol that breaks Next compilation.
|
|
106
|
-
const LANGGRAPH_CHECKPOINTER_CONFIG_KEY = '__pregel_checkpointer'
|
|
107
117
|
const CONTEXT_WARNING_OVERHEAD_TOKENS = 192
|
|
108
118
|
|
|
109
119
|
/** Extract HTTP status code and Retry-After from provider error objects (OpenAI SDK, etc.) */
|
|
@@ -341,7 +351,6 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
341
351
|
const pluginLines = buildPluginCapabilityLines(opts.enabledPlugins, { platformAssignScope: opts.platformAssignScope })
|
|
342
352
|
const toolDisciplineLines = buildToolDisciplineLines(opts.enabledPlugins)
|
|
343
353
|
const hasMemoryTools = opts.enabledPlugins.some((toolId) => (canonicalizePluginId(toolId) || toolId) === 'memory')
|
|
344
|
-
const directMemoryWriteOnlyTurn = Boolean(opts.userMessage && isNarrowDirectMemoryWriteTurn(opts.userMessage))
|
|
345
354
|
|
|
346
355
|
const parts: string[] = []
|
|
347
356
|
|
|
@@ -371,10 +380,6 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
371
380
|
'Do not use `manage_tasks`, `manage_agents`, or `delegate` as a substitute for a direct memory write or recall step.',
|
|
372
381
|
)
|
|
373
382
|
}
|
|
374
|
-
if (hasMemoryTools && directMemoryWriteOnlyTurn) {
|
|
375
|
-
parts.push(buildDirectMemoryWriteBlock())
|
|
376
|
-
}
|
|
377
|
-
|
|
378
383
|
if (opts.hasAttachmentContext) {
|
|
379
384
|
parts.push(
|
|
380
385
|
'## Attachments',
|
|
@@ -393,6 +398,7 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
393
398
|
'Execute by default — only confirm on high-risk actions.',
|
|
394
399
|
'If a tool errors, retry or explain the blocker. Never claim success without evidence.',
|
|
395
400
|
'Keep responses concise. Bullet points over prose. After file operations, confirm the result briefly (path and status) without echoing the full file contents.',
|
|
401
|
+
'Do not end every reply with a question. Only ask when a specific missing detail blocks progress. When a task is done, state the result and stop.',
|
|
396
402
|
opts.responseStyle === 'concise'
|
|
397
403
|
? `IMPORTANT: Be extremely concise.${opts.responseMaxChars ? ` Keep responses under ${opts.responseMaxChars} characters.` : ' Target under 500 characters.'} Lead with the answer, skip preamble.`
|
|
398
404
|
: opts.responseStyle === 'detailed'
|
|
@@ -458,16 +464,6 @@ function buildCurrentThreadRecallBlock(history: Message[]): string {
|
|
|
458
464
|
return lines.join('\n')
|
|
459
465
|
}
|
|
460
466
|
|
|
461
|
-
function buildDirectMemoryWriteBlock(): string {
|
|
462
|
-
return [
|
|
463
|
-
'## Direct Memory Write',
|
|
464
|
-
'This turn is a direct request to remember, store, or correct a durable fact.',
|
|
465
|
-
'Call `memory_store` or `memory_update` immediately, then confirm the stored value succinctly.',
|
|
466
|
-
'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.',
|
|
467
|
-
'Do not inspect skills, browse the workspace, request capabilities, manage tasks, manage agents, or delegate before the direct memory write is complete.',
|
|
468
|
-
].join('\n')
|
|
469
|
-
}
|
|
470
|
-
|
|
471
467
|
export interface StreamAgentChatResult {
|
|
472
468
|
/** All text accumulated across every LLM turn (for SSE / web UI history). */
|
|
473
469
|
fullText: string
|
|
@@ -543,8 +539,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
543
539
|
|
|
544
540
|
const promptParts: string[] = []
|
|
545
541
|
const hasProvidedSystemPrompt = typeof systemPrompt === 'string' && systemPrompt.trim().length > 0
|
|
546
|
-
const
|
|
547
|
-
const currentThreadRecallRequest = !directMemoryWriteOnlyTurn && isCurrentThreadRecallRequest(message)
|
|
542
|
+
const currentThreadRecallRequest = isCurrentThreadRecallRequest(message)
|
|
548
543
|
const hasAttachmentContext = Boolean(
|
|
549
544
|
imagePath
|
|
550
545
|
|| attachedFiles?.length
|
|
@@ -653,8 +648,8 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
653
648
|
try {
|
|
654
649
|
const pluginContextParts = await getPluginManager().collectAgentContext(session, sessionPlugins, message, history)
|
|
655
650
|
promptParts.push(...pluginContextParts)
|
|
656
|
-
} catch {
|
|
657
|
-
|
|
651
|
+
} catch (err: unknown) {
|
|
652
|
+
console.error('[stream-agent-chat] Plugin context injection failed:', err instanceof Error ? err.message : String(err))
|
|
658
653
|
}
|
|
659
654
|
|
|
660
655
|
if (!hasProvidedSystemPrompt && activeProjectContext.projectId) {
|
|
@@ -757,7 +752,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
757
752
|
|
|
758
753
|
// Proactive memory recall: inject relevant memories into context before LLM invocation
|
|
759
754
|
// Skips heartbeat polls, very short messages, and thread-recall requests (which use chat history instead)
|
|
760
|
-
if (session.agentId && !
|
|
755
|
+
if (session.agentId && !currentThreadRecallRequest && message.length > 12) {
|
|
761
756
|
try {
|
|
762
757
|
const agents = loadAgents()
|
|
763
758
|
const agentForMemory = agents[session.agentId]
|
|
@@ -794,23 +789,6 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
794
789
|
memoryScopeMode: agentMemoryScopeMode,
|
|
795
790
|
})
|
|
796
791
|
endToolBuildPerf({ toolCount: tools.length })
|
|
797
|
-
const toolsForTurn = currentThreadRecallRequest
|
|
798
|
-
? tools.filter((tool) => {
|
|
799
|
-
const toolName = typeof (tool as { name?: unknown }).name === 'string'
|
|
800
|
-
? String((tool as { name?: unknown }).name)
|
|
801
|
-
: ''
|
|
802
|
-
return shouldAllowToolForCurrentThreadRecall(toolName)
|
|
803
|
-
})
|
|
804
|
-
: directMemoryWriteOnlyTurn
|
|
805
|
-
? tools.filter((tool) => {
|
|
806
|
-
const toolName = typeof (tool as { name?: unknown }).name === 'string'
|
|
807
|
-
? String((tool as { name?: unknown }).name)
|
|
808
|
-
: ''
|
|
809
|
-
return shouldAllowToolForDirectMemoryWrite(toolName)
|
|
810
|
-
})
|
|
811
|
-
: tools
|
|
812
|
-
const checkpointNamespace = `chat:${startTs}`
|
|
813
|
-
const checkpointSaver = getCheckpointSaver()
|
|
814
792
|
const recursionLimit = getAgentLoopRecursionLimit(runtime)
|
|
815
793
|
|
|
816
794
|
// Build message history for context
|
|
@@ -997,11 +975,17 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
997
975
|
// Warning failure is non-critical
|
|
998
976
|
}
|
|
999
977
|
|
|
978
|
+
// Use a fresh in-memory checkpointer instead of the SQLite one. We manage
|
|
979
|
+
// conversation history externally via langchainMessages — each iteration
|
|
980
|
+
// receives full history, so no cross-iteration checkpoint state is needed.
|
|
981
|
+
// MemorySaver avoids the SQLite serde round-trip that dropped tool_call IDs
|
|
982
|
+
// or ToolMessages, causing OpenAI to reject with "tool_calls must be
|
|
983
|
+
// followed by tool messages" errors.
|
|
1000
984
|
const agent = createReactAgent({
|
|
1001
985
|
llm,
|
|
1002
|
-
tools
|
|
986
|
+
tools,
|
|
1003
987
|
prompt,
|
|
1004
|
-
checkpointer:
|
|
988
|
+
checkpointer: new MemorySaver(),
|
|
1005
989
|
})
|
|
1006
990
|
|
|
1007
991
|
const langchainMessages: Array<HumanMessage | AIMessage> = []
|
|
@@ -1018,7 +1002,6 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1018
1002
|
const currentContent = await buildLangChainContent(message, imagePath, attachedFiles)
|
|
1019
1003
|
langchainMessages.push(new HumanMessage({ content: currentContent }))
|
|
1020
1004
|
let pendingGraphMessages = [...langchainMessages]
|
|
1021
|
-
let currentCheckpointId: string | undefined
|
|
1022
1005
|
|
|
1023
1006
|
let fullText = ''
|
|
1024
1007
|
let lastSegment = ''
|
|
@@ -1073,7 +1056,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1073
1056
|
MAX_TOOL_SUMMARY_RETRIES = 1
|
|
1074
1057
|
MAX_UNFINISHED_TOOL_FOLLOWTHROUGHS = 1
|
|
1075
1058
|
}
|
|
1076
|
-
const REQUIRED_TOOL_KICKOFF_TIMEOUT_MS =
|
|
1059
|
+
const REQUIRED_TOOL_KICKOFF_TIMEOUT_MS = runtime.requiredToolKickoffMs
|
|
1077
1060
|
let autoContinueCount = 0
|
|
1078
1061
|
let transientRetryCount = 0
|
|
1079
1062
|
let pendingRetryAfterMs: number | null = null
|
|
@@ -1154,7 +1137,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1154
1137
|
idleTimer = setTimeout(() => {
|
|
1155
1138
|
idleTimedOut = true
|
|
1156
1139
|
iterationController.abort()
|
|
1157
|
-
},
|
|
1140
|
+
}, runtime.streamIdleStallMs)
|
|
1158
1141
|
}
|
|
1159
1142
|
|
|
1160
1143
|
const armRequiredToolKickoff = () => {
|
|
@@ -1175,23 +1158,21 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1175
1158
|
const toolPerfEnds = new Map<string, (extra?: Record<string, unknown>) => number>()
|
|
1176
1159
|
const iterationInputMessages = pendingGraphMessages
|
|
1177
1160
|
let iterationSucceeded = false
|
|
1161
|
+
const eventStream = agent.streamEvents(
|
|
1162
|
+
{ messages: iterationInputMessages },
|
|
1163
|
+
{
|
|
1164
|
+
version: 'v2',
|
|
1165
|
+
recursionLimit,
|
|
1166
|
+
signal: iterationController.signal,
|
|
1167
|
+
configurable: {
|
|
1168
|
+
thread_id: `${session.id}:${startTs}:${iteration}`,
|
|
1169
|
+
},
|
|
1170
|
+
},
|
|
1171
|
+
)
|
|
1178
1172
|
|
|
1179
1173
|
try {
|
|
1180
1174
|
armIdleWatchdog()
|
|
1181
1175
|
armRequiredToolKickoff()
|
|
1182
|
-
const eventStream = agent.streamEvents(
|
|
1183
|
-
{ messages: iterationInputMessages },
|
|
1184
|
-
{
|
|
1185
|
-
version: 'v2',
|
|
1186
|
-
recursionLimit,
|
|
1187
|
-
signal: iterationController.signal,
|
|
1188
|
-
configurable: {
|
|
1189
|
-
thread_id: session.id,
|
|
1190
|
-
checkpoint_ns: checkpointNamespace,
|
|
1191
|
-
...(currentCheckpointId ? { checkpoint_id: currentCheckpointId } : {}),
|
|
1192
|
-
},
|
|
1193
|
-
},
|
|
1194
|
-
)
|
|
1195
1176
|
|
|
1196
1177
|
for await (const event of eventStream) {
|
|
1197
1178
|
const kind = event.event
|
|
@@ -1434,7 +1415,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1434
1415
|
} catch (innerErr: unknown) {
|
|
1435
1416
|
const errName = innerErr instanceof Error ? innerErr.constructor.name : ''
|
|
1436
1417
|
const errMsg = idleTimedOut
|
|
1437
|
-
?
|
|
1418
|
+
? `Model stream stalled without emitting text or tool results for ${Math.trunc(runtime.streamIdleStallMs / 1000)} seconds.`
|
|
1438
1419
|
: requiredToolKickoffTimedOut
|
|
1439
1420
|
? `The turn did not start the required workspace tool step within ${Math.trunc(REQUIRED_TOOL_KICKOFF_TIMEOUT_MS / 1000)} seconds.`
|
|
1440
1421
|
: errorMessage(innerErr)
|
|
@@ -1539,24 +1520,6 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1539
1520
|
abortController.signal.removeEventListener('abort', onParentAbort)
|
|
1540
1521
|
}
|
|
1541
1522
|
|
|
1542
|
-
if (iterationSucceeded) {
|
|
1543
|
-
try {
|
|
1544
|
-
const state = await agent.getState({
|
|
1545
|
-
configurable: {
|
|
1546
|
-
thread_id: session.id,
|
|
1547
|
-
checkpoint_ns: checkpointNamespace,
|
|
1548
|
-
[LANGGRAPH_CHECKPOINTER_CONFIG_KEY]: checkpointSaver,
|
|
1549
|
-
},
|
|
1550
|
-
}) as { config?: { configurable?: { checkpoint_id?: unknown } } } | null
|
|
1551
|
-
const latestCheckpointId = state?.config?.configurable?.checkpoint_id
|
|
1552
|
-
if (typeof latestCheckpointId === 'string' && latestCheckpointId.trim()) {
|
|
1553
|
-
currentCheckpointId = latestCheckpointId
|
|
1554
|
-
}
|
|
1555
|
-
} catch (checkpointErr) {
|
|
1556
|
-
console.warn('[stream-agent-chat] Failed to refresh latest checkpoint state:', errorMessage(checkpointErr))
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
1523
|
if (reachedExecutionBoundary) break
|
|
1561
1524
|
|
|
1562
1525
|
if (!shouldContinue
|
|
@@ -1808,7 +1771,9 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1808
1771
|
const promptMessage = new HumanMessage({ content: continuationPrompt })
|
|
1809
1772
|
langchainMessages.push(promptMessage)
|
|
1810
1773
|
continuationMessages.push(promptMessage)
|
|
1811
|
-
|
|
1774
|
+
// Provide full conversation history since the agent has no checkpointer
|
|
1775
|
+
// and each iteration starts with only the messages we explicitly pass.
|
|
1776
|
+
pendingGraphMessages = [...langchainMessages]
|
|
1812
1777
|
lastSegment = ''
|
|
1813
1778
|
} else if (shouldContinue === 'transient') {
|
|
1814
1779
|
// Exponential backoff before retrying transient errors; respect Retry-After if present
|
|
@@ -1896,7 +1861,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
1896
1861
|
const totalTokens = totalInputTokens + totalOutputTokens
|
|
1897
1862
|
if (totalTokens > 0) {
|
|
1898
1863
|
const cost = estimateCost(session.model, totalInputTokens, totalOutputTokens)
|
|
1899
|
-
const pluginDefinitionCosts = buildPluginDefinitionCosts(
|
|
1864
|
+
const pluginDefinitionCosts = buildPluginDefinitionCosts(tools, toolToPluginMap)
|
|
1900
1865
|
const usageRecord: UsageRecord = {
|
|
1901
1866
|
sessionId: session.id,
|
|
1902
1867
|
messageIndex: history.length,
|
|
@@ -100,7 +100,7 @@ function looksLikeIncompleteDeliverableResponse(text: string): boolean {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
const ARTIFACT_PATH_EXT_RE = /\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|ts|tsx|js|jsx|mjs|cjs|py|sql|sh)$/i
|
|
103
|
-
const EXPLICIT_ARTIFACT_OUTPUT_RE = /\b(?:save|write|output|export)\b[^.!?\n]{0,80}\b(?:to|as|at|in)\b[^.!?\n]{0,60}(\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|~\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|\.\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|[a-z0-9._/-]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)\b)/i
|
|
103
|
+
const EXPLICIT_ARTIFACT_OUTPUT_RE = /\b(?:save|write|output|export|create|generate)\b[^.!?\n]{0,80}\b(?:to|as|at|in)\b[^.!?\n]{0,60}(\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|~\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|\.\/[^\s,'"]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)|[a-z0-9._/-]+\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|py|ts|tsx|js|jsx|mjs|cjs|sql|sh)\b)/i
|
|
104
104
|
|
|
105
105
|
function hasExplicitFileOutputRequest(text: string): boolean {
|
|
106
106
|
const normalized = text.toLowerCase()
|
|
@@ -1534,7 +1534,7 @@ async function routeMessage(connector: Connector, msg: InboundMessage): Promise<
|
|
|
1534
1534
|
const threadContextBlock = buildConnectorThreadContextBlock(msg, { isFirstThreadTurn: wasCreated })
|
|
1535
1535
|
if (threadContextBlock) promptParts.push(threadContextBlock)
|
|
1536
1536
|
// Add connector context
|
|
1537
|
-
promptParts.push(`\nYou are receiving messages via ${msg.platform}. The user "${msg.senderName}" is messaging from channel "${msg.channelName || msg.channelId}". Respond naturally and conversationally.
|
|
1537
|
+
promptParts.push(`\nYou are receiving messages via ${msg.platform}. The user "${msg.senderName}" (ID: ${msg.senderId}) is messaging from channel "${msg.channelName || msg.channelId}". Respond naturally and conversationally.
|
|
1538
1538
|
|
|
1539
1539
|
## Response Style
|
|
1540
1540
|
Be action-first and autonomous: when the user gives an instruction, execute it instead of asking routine follow-up questions.
|
|
@@ -2,7 +2,6 @@ import test from 'node:test'
|
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
3
|
import {
|
|
4
4
|
inferAutomaticMemoryCategory,
|
|
5
|
-
isDirectMemoryWriteRequest,
|
|
6
5
|
isCurrentThreadRecallRequest,
|
|
7
6
|
normalizeMemoryCategory,
|
|
8
7
|
shouldAutoCaptureMemory,
|
|
@@ -42,17 +41,6 @@ test('isCurrentThreadRecallRequest detects same-thread recall without matching s
|
|
|
42
41
|
)
|
|
43
42
|
})
|
|
44
43
|
|
|
45
|
-
test('isDirectMemoryWriteRequest detects remember-and-confirm turns without matching recall questions', () => {
|
|
46
|
-
assert.equal(
|
|
47
|
-
isDirectMemoryWriteRequest('Remember that my favorite programming language is Rust and I prefer functional programming patterns. Then confirm what you just stored.'),
|
|
48
|
-
true,
|
|
49
|
-
)
|
|
50
|
-
assert.equal(
|
|
51
|
-
isDirectMemoryWriteRequest('What preferences did I tell you earlier in this conversation?'),
|
|
52
|
-
false,
|
|
53
|
-
)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
44
|
test('shouldAutoCaptureMemory filters noisy turns', () => {
|
|
57
45
|
assert.equal(shouldAutoCaptureMemory({ message: 'thanks', response: 'Happy to help with that.' }), false)
|
|
58
46
|
assert.equal(shouldAutoCaptureMemory({ message: 'Please save this to memory', response: 'Stored memory "note".' }), false)
|
|
@@ -63,13 +51,15 @@ test('shouldAutoCaptureMemory filters noisy turns', () => {
|
|
|
63
51
|
}), true)
|
|
64
52
|
})
|
|
65
53
|
|
|
66
|
-
test('inferAutomaticMemoryCategory
|
|
54
|
+
test('inferAutomaticMemoryCategory falls back to knowledge/facts without content-sniffing', () => {
|
|
55
|
+
// Content-sniffing regex removed — the agent picks categories via guidance.
|
|
56
|
+
// inferAutomaticMemoryCategory (called with category "note") should fall through.
|
|
67
57
|
assert.equal(
|
|
68
58
|
inferAutomaticMemoryCategory('The user prefers direct status updates.', 'I will keep future updates terse and direct.'),
|
|
69
|
-
'
|
|
59
|
+
'knowledge/facts',
|
|
70
60
|
)
|
|
71
61
|
assert.equal(
|
|
72
62
|
inferAutomaticMemoryCategory('We decided to ship the GitHub import first.', 'Decision locked for the next milestone.'),
|
|
73
|
-
'
|
|
63
|
+
'knowledge/facts',
|
|
74
64
|
)
|
|
75
65
|
})
|
|
@@ -6,7 +6,7 @@ const MEMORY_META_RE = /\b(?:remember|memory|memorize|store this|save this|forge
|
|
|
6
6
|
const LOW_SIGNAL_RESPONSE_RE = /^(?:HEARTBEAT_OK|NO_MESSAGE)\b/i
|
|
7
7
|
const CURRENT_THREAD_RECALL_MARKER_RE = /\b(?:this conversation|this chat|this thread|current conversation|current chat|current thread|same thread|same chat|same conversation|earlier in (?:this )?(?:conversation|chat|thread)|from (?:this|our) (?:conversation|chat|thread)|you just stored|you just said|we just discussed|we just decided)\b/i
|
|
8
8
|
const CURRENT_THREAD_RECALL_INTENT_RE = /\b(?:what|which|who|when|where|did|remind|recap|summarize|repeat|list|tell me|answer|confirm|recall|mention)\b/i
|
|
9
|
-
const DIRECT_MEMORY_WRITE_MARKER_RE = /\b(?:remember|memorize|store|save|write to memory|add to memory|update.*memory|correct.*memory)\b/i
|
|
9
|
+
const DIRECT_MEMORY_WRITE_MARKER_RE = /\b(?:remember|memorize|store (?:this|that|the fact|it)|save (?:this|that|the fact|it) (?:to|in) memory|write to memory|add to memory|update.*memory|correct.*memory)\b/i
|
|
10
10
|
const DIRECT_MEMORY_WRITE_FOLLOWUP_RE = /\b(?:confirm|recap|repeat|summarize|what you just stored|what you saved|what you updated)\b/i
|
|
11
11
|
|
|
12
12
|
function normalizeWhitespace(value: string): string {
|
|
@@ -36,16 +36,6 @@ export function isCurrentThreadRecallRequest(message: string): boolean {
|
|
|
36
36
|
return CURRENT_THREAD_RECALL_INTENT_RE.test(trimmed) || /\?\s*$/.test(trimmed)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export function isDirectMemoryWriteRequest(message: string): boolean {
|
|
40
|
-
const trimmed = normalizeWhitespace(message)
|
|
41
|
-
if (!trimmed) return false
|
|
42
|
-
const directWriteLike = DIRECT_MEMORY_WRITE_MARKER_RE.test(trimmed)
|
|
43
|
-
if (!directWriteLike) return false
|
|
44
|
-
if (/\?\s*$/.test(trimmed) && !DIRECT_MEMORY_WRITE_FOLLOWUP_RE.test(trimmed)) return false
|
|
45
|
-
if (isCurrentThreadRecallRequest(trimmed) && !DIRECT_MEMORY_WRITE_FOLLOWUP_RE.test(trimmed)) return false
|
|
46
|
-
return true
|
|
47
|
-
}
|
|
48
|
-
|
|
49
39
|
export function shouldAutoCaptureMemoryTurn(message: string, response: string): boolean {
|
|
50
40
|
const normalizedMessage = normalizeWhitespace(message)
|
|
51
41
|
const normalizedResponse = normalizeWhitespace(response)
|
|
@@ -67,19 +57,20 @@ export function shouldAutoCaptureMemory(
|
|
|
67
57
|
return shouldAutoCaptureMemoryTurn(input.message || '', input.response || '')
|
|
68
58
|
}
|
|
69
59
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
title: string | null | undefined,
|
|
73
|
-
content: string | null | undefined,
|
|
74
|
-
): string {
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
61
|
+
export function normalizeMemoryCategory(input: string | null | undefined, _title?: string | null, _content?: string | null): string {
|
|
75
62
|
const explicit = lower(input)
|
|
76
|
-
const sample = `${lower(title)}\n${lower(content)}`
|
|
77
63
|
|
|
78
64
|
const mapExplicit = (value: string): string | null => {
|
|
79
65
|
if (!value || value === 'note' || value === 'notes') return null
|
|
80
66
|
if (['preference', 'preferences', 'likes', 'dislikes'].includes(value)) return 'identity/preferences'
|
|
81
67
|
if (['identity', 'profile', 'persona'].includes(value)) return 'identity/profile'
|
|
82
68
|
if (['relationship', 'relationships', 'people'].includes(value)) return 'identity/relationships'
|
|
69
|
+
if (['contact', 'contacts'].includes(value)) return 'identity/contacts'
|
|
70
|
+
if (['routine', 'routines', 'schedule', 'habit', 'habits'].includes(value)) return 'identity/routines'
|
|
71
|
+
if (['event', 'events', 'life event', 'life events', 'significant', 'milestone'].includes(value)) return 'identity/events'
|
|
72
|
+
if (['goal', 'goals', 'objective', 'objectives', 'target', 'targets'].includes(value)) return 'identity/goals'
|
|
73
|
+
if (['instruction', 'instructions', 'directive', 'directives', 'standing order', 'rule', 'rules'].includes(value)) return 'knowledge/instructions'
|
|
83
74
|
if (['decision', 'decisions', 'choice'].includes(value)) return 'projects/decisions'
|
|
84
75
|
if (['learning', 'learnings', 'lesson', 'lessons'].includes(value)) return 'projects/learnings'
|
|
85
76
|
if (['project', 'projects', 'task', 'tasks'].includes(value)) return 'projects/context'
|
|
@@ -94,30 +85,9 @@ export function normalizeMemoryCategory(
|
|
|
94
85
|
const explicitMapped = mapExplicit(explicit)
|
|
95
86
|
if (explicitMapped) return explicitMapped
|
|
96
87
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (/\b(?:wife|husband|partner|friend|manager|teammate|client|customer|relationship)\b/.test(sample)) {
|
|
101
|
-
return 'identity/relationships'
|
|
102
|
-
}
|
|
103
|
-
if (/\b(?:decided|decision|approved|picked|selected|going with|will use)\b/.test(sample)) {
|
|
104
|
-
return 'projects/decisions'
|
|
105
|
-
}
|
|
106
|
-
if (/\b(?:learned|lesson|fixed|solved|root cause|failure|bug|regression|postmortem)\b/.test(sample)) {
|
|
107
|
-
return 'projects/learnings'
|
|
108
|
-
}
|
|
109
|
-
if (/\b(?:error|incident|stack trace|exception|crash)\b/.test(sample)) {
|
|
110
|
-
return 'execution/errors'
|
|
111
|
-
}
|
|
112
|
-
if (/\b(?:project|repo|repository|ticket|task|milestone|deadline|roadmap)\b/.test(sample)) {
|
|
113
|
-
return 'projects/context'
|
|
114
|
-
}
|
|
115
|
-
if (/\b(?:config|credential|endpoint|workspace|path|env var|environment|docker|sandbox)\b/.test(sample)) {
|
|
116
|
-
return 'operations/environment'
|
|
117
|
-
}
|
|
118
|
-
if (/\b(?:fact|documentation|reference|api|schema)\b/.test(sample)) {
|
|
119
|
-
return 'knowledge/facts'
|
|
120
|
-
}
|
|
88
|
+
// No content-sniffing regex — the agent picks the category via the guidance
|
|
89
|
+
// in its memory policy block. We just normalize explicit aliases above and
|
|
90
|
+
// fall back to knowledge/facts for uncategorized entries.
|
|
121
91
|
return explicit && explicit !== 'note' && explicit !== 'notes' ? explicit : 'knowledge/facts'
|
|
122
92
|
}
|
|
123
93
|
|
|
@@ -404,3 +404,49 @@ describe('buildAgentHeartbeatPrompt', () => {
|
|
|
404
404
|
assert.ok(result.includes('Another active task'))
|
|
405
405
|
})
|
|
406
406
|
})
|
|
407
|
+
|
|
408
|
+
// ── lightContext config ─────────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
describe('heartbeatConfigForSession lightContext', () => {
|
|
411
|
+
it('defaults to false when not set', () => {
|
|
412
|
+
const cfg = mod.heartbeatConfigForSession(
|
|
413
|
+
{ id: 's1' },
|
|
414
|
+
{ heartbeatIntervalSec: 60 },
|
|
415
|
+
{},
|
|
416
|
+
)
|
|
417
|
+
assert.equal(cfg.lightContext, false)
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
it('inherits from global settings', () => {
|
|
421
|
+
const cfg = mod.heartbeatConfigForSession(
|
|
422
|
+
{ id: 's1' },
|
|
423
|
+
{ heartbeatIntervalSec: 60, heartbeatLightContext: true },
|
|
424
|
+
{},
|
|
425
|
+
)
|
|
426
|
+
assert.equal(cfg.lightContext, true)
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('agent overrides global', () => {
|
|
430
|
+
const agents: Record<string, Record<string, unknown>> = {
|
|
431
|
+
'a1': { heartbeatLightContext: true },
|
|
432
|
+
}
|
|
433
|
+
const cfg = mod.heartbeatConfigForSession(
|
|
434
|
+
{ id: 's1', agentId: 'a1' },
|
|
435
|
+
{ heartbeatIntervalSec: 60, heartbeatLightContext: false },
|
|
436
|
+
agents,
|
|
437
|
+
)
|
|
438
|
+
assert.equal(cfg.lightContext, true)
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
it('agent false overrides global true', () => {
|
|
442
|
+
const agents: Record<string, Record<string, unknown>> = {
|
|
443
|
+
'a1': { heartbeatLightContext: false },
|
|
444
|
+
}
|
|
445
|
+
const cfg = mod.heartbeatConfigForSession(
|
|
446
|
+
{ id: 's1', agentId: 'a1' },
|
|
447
|
+
{ heartbeatIntervalSec: 60, heartbeatLightContext: true },
|
|
448
|
+
agents,
|
|
449
|
+
)
|
|
450
|
+
assert.equal(cfg.lightContext, false)
|
|
451
|
+
})
|
|
452
|
+
})
|