@swarmclawai/swarmclaw 0.7.7 → 0.8.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 +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +249 -14
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { describe, it } from 'node:test'
|
|
7
|
+
|
|
8
|
+
import { stripMainLoopMetaForPersistence } from './main-agent-loop.ts'
|
|
9
|
+
|
|
10
|
+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
|
|
11
|
+
|
|
12
|
+
function runWithTempDataDir(script: string) {
|
|
13
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-loop-adv-'))
|
|
14
|
+
try {
|
|
15
|
+
const result = spawnSync(
|
|
16
|
+
process.execPath,
|
|
17
|
+
['--import', 'tsx', '--input-type=module', '--eval', script],
|
|
18
|
+
{
|
|
19
|
+
cwd: repoRoot,
|
|
20
|
+
env: {
|
|
21
|
+
...process.env,
|
|
22
|
+
DATA_DIR: tempDir,
|
|
23
|
+
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
24
|
+
SWARMCLAW_BUILD_MODE: '1',
|
|
25
|
+
},
|
|
26
|
+
encoding: 'utf-8',
|
|
27
|
+
timeout: 15000,
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
31
|
+
const lines = (result.stdout || '')
|
|
32
|
+
.trim()
|
|
33
|
+
.split('\n')
|
|
34
|
+
.map((line) => line.trim())
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
37
|
+
return JSON.parse(jsonLine || '{}') as Record<string, unknown>
|
|
38
|
+
} finally {
|
|
39
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Shared setup script that creates one agent and one heartbeat-enabled main session */
|
|
44
|
+
function sessionSetupScript(sessionOverrides?: string, extraSessions?: string): string {
|
|
45
|
+
return `
|
|
46
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
47
|
+
const storage = storageMod.default || storageMod['module.exports'] || storageMod
|
|
48
|
+
const mainLoopMod = await import('./src/lib/server/main-agent-loop.ts')
|
|
49
|
+
const mainLoop = mainLoopMod.default || mainLoopMod['module.exports'] || mainLoopMod
|
|
50
|
+
|
|
51
|
+
storage.saveAgents({
|
|
52
|
+
'agent-a': {
|
|
53
|
+
id: 'agent-a',
|
|
54
|
+
name: 'Agent A',
|
|
55
|
+
provider: 'openai',
|
|
56
|
+
model: 'gpt-test',
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
storage.saveSessions({
|
|
61
|
+
main: {
|
|
62
|
+
id: 'main',
|
|
63
|
+
name: 'Main Thread',
|
|
64
|
+
shortcutForAgentId: 'agent-a',
|
|
65
|
+
cwd: process.cwd(),
|
|
66
|
+
user: 'tester',
|
|
67
|
+
provider: 'openai',
|
|
68
|
+
model: 'gpt-test',
|
|
69
|
+
claudeSessionId: null,
|
|
70
|
+
messages: [
|
|
71
|
+
{ role: 'user', text: 'Deploy the system.', time: 1 },
|
|
72
|
+
],
|
|
73
|
+
createdAt: 1,
|
|
74
|
+
lastActiveAt: 1,
|
|
75
|
+
sessionType: 'human',
|
|
76
|
+
agentId: 'agent-a',
|
|
77
|
+
heartbeatEnabled: true,
|
|
78
|
+
${sessionOverrides || ''}
|
|
79
|
+
},
|
|
80
|
+
${extraSessions || ''}
|
|
81
|
+
})
|
|
82
|
+
`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function heartbeatMetaLine(status: string, goal: string, nextAction?: string, extraFields?: string): string {
|
|
86
|
+
const parts = [`"status":"${status}","goal":"${goal}"`]
|
|
87
|
+
if (nextAction) parts.push(`"next_action":"${nextAction}"`)
|
|
88
|
+
if (extraFields) parts.push(extraFields)
|
|
89
|
+
return `[AGENT_HEARTBEAT_META]{${parts.join(',')}}`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function makeRunResultCall(
|
|
93
|
+
index: number,
|
|
94
|
+
resultText: string,
|
|
95
|
+
opts?: { error?: string; inputTokens?: number; outputTokens?: number; estimatedCost?: number; source?: string },
|
|
96
|
+
): string {
|
|
97
|
+
const errorPart = opts?.error ? `error: '${opts.error}',` : ''
|
|
98
|
+
const inputTokens = opts?.inputTokens ?? 10
|
|
99
|
+
const outputTokens = opts?.outputTokens ?? 5
|
|
100
|
+
const estimatedCost = opts?.estimatedCost ?? 0
|
|
101
|
+
const source = opts?.source ?? 'heartbeat'
|
|
102
|
+
return `
|
|
103
|
+
const followup${index} = mainLoop.handleMainLoopRunResult({
|
|
104
|
+
sessionId: 'main',
|
|
105
|
+
message: 'Continue objective step ${index}.',
|
|
106
|
+
internal: true,
|
|
107
|
+
source: '${source}',
|
|
108
|
+
resultText: \`${resultText}\`,
|
|
109
|
+
${errorPart}
|
|
110
|
+
inputTokens: ${inputTokens},
|
|
111
|
+
outputTokens: ${outputTokens},
|
|
112
|
+
estimatedCost: ${estimatedCost},
|
|
113
|
+
})
|
|
114
|
+
const state${index} = mainLoop.getMainLoopStateForSession('main')
|
|
115
|
+
`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
describe('main-agent-loop advanced', () => {
|
|
119
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
120
|
+
// 1. Followup chain escalation and cap
|
|
121
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
122
|
+
it('followup chain escalates then resets at DEFAULT_MAX_FOLLOWUP_CHAIN=3', () => {
|
|
123
|
+
const meta = heartbeatMetaLine('progress', 'deploy', 'continue')
|
|
124
|
+
const output = runWithTempDataDir(`
|
|
125
|
+
${sessionSetupScript()}
|
|
126
|
+
|
|
127
|
+
${makeRunResultCall(1, `Working on deployment.\\n${meta}`)}
|
|
128
|
+
${makeRunResultCall(2, `Still deploying.\\n${meta}`)}
|
|
129
|
+
${makeRunResultCall(3, `Almost done.\\n${meta}`)}
|
|
130
|
+
${makeRunResultCall(4, `Finishing up.\\n${meta}`)}
|
|
131
|
+
${makeRunResultCall(5, `Final polish.\\n${meta}`)}
|
|
132
|
+
|
|
133
|
+
console.log(JSON.stringify({
|
|
134
|
+
chain1: state1?.followupChainCount ?? -1,
|
|
135
|
+
chain2: state2?.followupChainCount ?? -1,
|
|
136
|
+
chain3: state3?.followupChainCount ?? -1,
|
|
137
|
+
chain4: state4?.followupChainCount ?? -1,
|
|
138
|
+
chain5: state5?.followupChainCount ?? -1,
|
|
139
|
+
hasFollowup1: followup1 !== null,
|
|
140
|
+
hasFollowup2: followup2 !== null,
|
|
141
|
+
hasFollowup3: followup3 !== null,
|
|
142
|
+
hasFollowup4: followup4 !== null,
|
|
143
|
+
hasFollowup5: followup5 !== null,
|
|
144
|
+
}))
|
|
145
|
+
`)
|
|
146
|
+
|
|
147
|
+
// Chain increments 0→1→2→3, then at 3 the condition (3 < 3) is false → resets to 0
|
|
148
|
+
// Call 5 starts from 0 again → increments to 1
|
|
149
|
+
assert.equal(output.chain1, 1, 'first call increments to 1')
|
|
150
|
+
assert.equal(output.chain2, 2, 'second call increments to 2')
|
|
151
|
+
assert.equal(output.chain3, 3, 'third call increments to 3 (the cap)')
|
|
152
|
+
assert.equal(output.chain4, 0, 'fourth call resets to 0 because cap was reached')
|
|
153
|
+
assert.equal(output.chain5, 1, 'fifth call increments from 0 to 1 again')
|
|
154
|
+
assert.equal(output.hasFollowup1, true, 'followup returned for call 1')
|
|
155
|
+
assert.equal(output.hasFollowup2, true, 'followup returned for call 2')
|
|
156
|
+
assert.equal(output.hasFollowup3, true, 'followup returned for call 3')
|
|
157
|
+
assert.equal(output.hasFollowup4, false, 'no followup at cap boundary')
|
|
158
|
+
assert.equal(output.hasFollowup5, true, 'followup resumes after reset')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
162
|
+
// 2. Chain reset on terminal status (ok / HEARTBEAT_OK)
|
|
163
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
164
|
+
it('followup chain resets to 0 on terminal ok status', () => {
|
|
165
|
+
const progressMeta = heartbeatMetaLine('progress', 'deploy', 'keep going')
|
|
166
|
+
const output = runWithTempDataDir(`
|
|
167
|
+
${sessionSetupScript()}
|
|
168
|
+
|
|
169
|
+
${makeRunResultCall(1, `Step one.\\n${progressMeta}`)}
|
|
170
|
+
${makeRunResultCall(2, `Step two.\\n${progressMeta}`)}
|
|
171
|
+
|
|
172
|
+
// Now send a terminal ack
|
|
173
|
+
const followupOk = mainLoop.handleMainLoopRunResult({
|
|
174
|
+
sessionId: 'main',
|
|
175
|
+
message: 'Continue.',
|
|
176
|
+
internal: true,
|
|
177
|
+
source: 'heartbeat',
|
|
178
|
+
resultText: 'HEARTBEAT_OK',
|
|
179
|
+
})
|
|
180
|
+
const stateOk = mainLoop.getMainLoopStateForSession('main')
|
|
181
|
+
|
|
182
|
+
console.log(JSON.stringify({
|
|
183
|
+
chainBefore1: state1?.followupChainCount ?? -1,
|
|
184
|
+
chainBefore2: state2?.followupChainCount ?? -1,
|
|
185
|
+
chainAfterOk: stateOk?.followupChainCount ?? -1,
|
|
186
|
+
statusAfterOk: stateOk?.status ?? null,
|
|
187
|
+
followupOk: followupOk,
|
|
188
|
+
}))
|
|
189
|
+
`)
|
|
190
|
+
|
|
191
|
+
assert.equal(output.chainBefore1, 1)
|
|
192
|
+
assert.equal(output.chainBefore2, 2)
|
|
193
|
+
assert.equal(output.chainAfterOk, 0, 'chain resets on HEARTBEAT_OK')
|
|
194
|
+
assert.equal(output.statusAfterOk, 'ok', 'status becomes ok')
|
|
195
|
+
assert.equal(output.followupOk, null, 'no followup on terminal ack')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
199
|
+
// 3. Chain reset on error
|
|
200
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
201
|
+
it('followup chain resets to 0 when error is present', () => {
|
|
202
|
+
const progressMeta = heartbeatMetaLine('progress', 'deploy', 'next step')
|
|
203
|
+
const output = runWithTempDataDir(`
|
|
204
|
+
${sessionSetupScript()}
|
|
205
|
+
|
|
206
|
+
${makeRunResultCall(1, `Working.\\n${progressMeta}`)}
|
|
207
|
+
${makeRunResultCall(2, `More work.\\n${progressMeta}`)}
|
|
208
|
+
|
|
209
|
+
// Send error result
|
|
210
|
+
${makeRunResultCall(3, 'Something broke.', { error: 'Connection timeout' })}
|
|
211
|
+
|
|
212
|
+
console.log(JSON.stringify({
|
|
213
|
+
chain1: state1?.followupChainCount ?? -1,
|
|
214
|
+
chain2: state2?.followupChainCount ?? -1,
|
|
215
|
+
chain3: state3?.followupChainCount ?? -1,
|
|
216
|
+
status3: state3?.status ?? null,
|
|
217
|
+
followup3: followup3,
|
|
218
|
+
}))
|
|
219
|
+
`)
|
|
220
|
+
|
|
221
|
+
assert.equal(output.chain1, 1)
|
|
222
|
+
assert.equal(output.chain2, 2)
|
|
223
|
+
assert.equal(output.chain3, 0, 'chain resets on error')
|
|
224
|
+
assert.equal(output.status3, 'blocked', 'status becomes blocked on error')
|
|
225
|
+
assert.equal(output.followup3, null, 'no followup on error')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
229
|
+
// 4. Event fan-out to main sessions only
|
|
230
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
231
|
+
it('pushMainLoopEventToMainSessions only targets heartbeat-enabled sessions', () => {
|
|
232
|
+
const output = runWithTempDataDir(`
|
|
233
|
+
${sessionSetupScript(
|
|
234
|
+
'',
|
|
235
|
+
`'non-hb': {
|
|
236
|
+
id: 'non-hb',
|
|
237
|
+
name: 'Non-HB Thread',
|
|
238
|
+
cwd: process.cwd(),
|
|
239
|
+
user: 'tester',
|
|
240
|
+
provider: 'openai',
|
|
241
|
+
model: 'gpt-test',
|
|
242
|
+
claudeSessionId: null,
|
|
243
|
+
messages: [{ role: 'user', text: 'Hello.', time: 1 }],
|
|
244
|
+
createdAt: 1,
|
|
245
|
+
lastActiveAt: 1,
|
|
246
|
+
sessionType: 'human',
|
|
247
|
+
agentId: 'agent-a',
|
|
248
|
+
heartbeatEnabled: false,
|
|
249
|
+
},`
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
const count = mainLoop.pushMainLoopEventToMainSessions({
|
|
253
|
+
type: 'task_completed',
|
|
254
|
+
text: 'Deployment finished',
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
const mainState = mainLoop.getMainLoopStateForSession('main')
|
|
258
|
+
const nonHbState = mainLoop.getMainLoopStateForSession('non-hb')
|
|
259
|
+
|
|
260
|
+
console.log(JSON.stringify({
|
|
261
|
+
count,
|
|
262
|
+
mainPendingCount: mainState?.pendingEvents?.length ?? 0,
|
|
263
|
+
mainEventText: mainState?.pendingEvents?.[0]?.text ?? null,
|
|
264
|
+
nonHbState: nonHbState,
|
|
265
|
+
}))
|
|
266
|
+
`)
|
|
267
|
+
|
|
268
|
+
assert.equal(output.count, 1, 'only 1 session received the event')
|
|
269
|
+
assert.equal(output.mainPendingCount, 1, 'heartbeat-enabled session got the event')
|
|
270
|
+
assert.equal(output.mainEventText, 'Deployment finished')
|
|
271
|
+
assert.equal(output.nonHbState, null, 'non-heartbeat session has no state')
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
275
|
+
// 5. Pending events cap at MAX_PENDING_EVENTS=16
|
|
276
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
277
|
+
it('caps pending events at 16, keeping the most recent', () => {
|
|
278
|
+
const output = runWithTempDataDir(`
|
|
279
|
+
${sessionSetupScript()}
|
|
280
|
+
|
|
281
|
+
for (let i = 0; i < 20; i++) {
|
|
282
|
+
mainLoop.pushMainLoopEventToMainSessions({
|
|
283
|
+
type: 'update',
|
|
284
|
+
text: 'Event number ' + i,
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const state = mainLoop.getMainLoopStateForSession('main')
|
|
289
|
+
const firstEventText = state?.pendingEvents?.[0]?.text ?? null
|
|
290
|
+
const lastEventText = state?.pendingEvents?.[state.pendingEvents.length - 1]?.text ?? null
|
|
291
|
+
|
|
292
|
+
console.log(JSON.stringify({
|
|
293
|
+
pendingCount: state?.pendingEvents?.length ?? 0,
|
|
294
|
+
firstEventText,
|
|
295
|
+
lastEventText,
|
|
296
|
+
}))
|
|
297
|
+
`)
|
|
298
|
+
|
|
299
|
+
assert.equal(output.pendingCount, 16, 'capped at 16')
|
|
300
|
+
// The oldest events (0-3) should have been dropped; the most recent 16 (4-19) remain
|
|
301
|
+
assert.equal(output.firstEventText, 'Event number 4', 'oldest events are dropped')
|
|
302
|
+
assert.equal(output.lastEventText, 'Event number 19', 'newest events are kept')
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
306
|
+
// 6. Timeline accumulation and cap at MAX_TIMELINE_ITEMS=40
|
|
307
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
308
|
+
it('accumulates timeline entries with correct source/status and caps at 40', () => {
|
|
309
|
+
const output = runWithTempDataDir(`
|
|
310
|
+
${sessionSetupScript()}
|
|
311
|
+
|
|
312
|
+
// Push enough run results to exceed the timeline cap.
|
|
313
|
+
// Each handleMainLoopRunResult with substantive text appends at least 1 timeline entry.
|
|
314
|
+
// With followup chaining, each also appends a 'followup' timeline entry → ~2 per call.
|
|
315
|
+
// We need >40 entries total, so 25 calls should exceed 40.
|
|
316
|
+
const chainCounts = []
|
|
317
|
+
for (let i = 0; i < 25; i++) {
|
|
318
|
+
const meta = '[AGENT_HEARTBEAT_META]{"status":"progress","goal":"timeline test","next_action":"step ' + i + '"}'
|
|
319
|
+
mainLoop.handleMainLoopRunResult({
|
|
320
|
+
sessionId: 'main',
|
|
321
|
+
message: 'Continue timeline test ' + i + '.',
|
|
322
|
+
internal: true,
|
|
323
|
+
source: 'heartbeat',
|
|
324
|
+
resultText: 'Completed step ' + i + '.\\n' + meta,
|
|
325
|
+
inputTokens: 5,
|
|
326
|
+
outputTokens: 3,
|
|
327
|
+
})
|
|
328
|
+
const s = mainLoop.getMainLoopStateForSession('main')
|
|
329
|
+
chainCounts.push(s?.followupChainCount ?? -1)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const finalState = mainLoop.getMainLoopStateForSession('main')
|
|
333
|
+
const timelineLength = finalState?.timeline?.length ?? 0
|
|
334
|
+
const sources = [...new Set((finalState?.timeline || []).map(e => e.source))]
|
|
335
|
+
const hasProgressStatus = (finalState?.timeline || []).some(e => e.status === 'progress')
|
|
336
|
+
|
|
337
|
+
console.log(JSON.stringify({
|
|
338
|
+
timelineLength,
|
|
339
|
+
cappedAt40: timelineLength <= 40,
|
|
340
|
+
sources,
|
|
341
|
+
hasProgressStatus,
|
|
342
|
+
firstNote: finalState?.timeline?.[0]?.note ?? null,
|
|
343
|
+
lastNote: finalState?.timeline?.[timelineLength - 1]?.note ?? null,
|
|
344
|
+
}))
|
|
345
|
+
`)
|
|
346
|
+
|
|
347
|
+
assert.ok((output.timelineLength as number) > 0, 'timeline has entries')
|
|
348
|
+
assert.equal(output.cappedAt40, true, 'timeline is capped at 40')
|
|
349
|
+
assert.ok(
|
|
350
|
+
(output.sources as string[]).includes('heartbeat') || (output.sources as string[]).includes('followup'),
|
|
351
|
+
'timeline has expected source values',
|
|
352
|
+
)
|
|
353
|
+
assert.equal(output.hasProgressStatus, true, 'timeline includes progress status entries')
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
357
|
+
// 7. Working memory notes from tool events
|
|
358
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
359
|
+
it('appends working memory notes when tool events are present', () => {
|
|
360
|
+
const output = runWithTempDataDir(`
|
|
361
|
+
${sessionSetupScript()}
|
|
362
|
+
|
|
363
|
+
const meta = '[AGENT_HEARTBEAT_META]{"status":"progress","goal":"research","next_action":"analyze"}'
|
|
364
|
+
mainLoop.handleMainLoopRunResult({
|
|
365
|
+
sessionId: 'main',
|
|
366
|
+
message: 'Research step.',
|
|
367
|
+
internal: true,
|
|
368
|
+
source: 'heartbeat',
|
|
369
|
+
resultText: 'Found important data.\\n' + meta,
|
|
370
|
+
toolEvents: [
|
|
371
|
+
{ name: 'web_search', input: '{"query":"important finding about X"}' },
|
|
372
|
+
{ name: 'shell', input: '{"action":"execute","command":"ls"}' },
|
|
373
|
+
],
|
|
374
|
+
inputTokens: 20,
|
|
375
|
+
outputTokens: 10,
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
const state = mainLoop.getMainLoopStateForSession('main')
|
|
379
|
+
|
|
380
|
+
console.log(JSON.stringify({
|
|
381
|
+
workingMemoryNotes: state?.workingMemoryNotes ?? [],
|
|
382
|
+
hasToolNote: (state?.workingMemoryNotes ?? []).some(n => n.includes('web_search') || n.includes('shell')),
|
|
383
|
+
lastMemoryNoteAt: state?.lastMemoryNoteAt !== null,
|
|
384
|
+
}))
|
|
385
|
+
`)
|
|
386
|
+
|
|
387
|
+
assert.ok((output.workingMemoryNotes as string[]).length > 0, 'working memory has notes')
|
|
388
|
+
assert.equal(output.hasToolNote, true, 'working memory includes tool names')
|
|
389
|
+
assert.equal(output.lastMemoryNoteAt, true, 'lastMemoryNoteAt is set')
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
393
|
+
// 8. Meta strip for persistence (direct import — no subprocess needed)
|
|
394
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
395
|
+
it('stripMainLoopMetaForPersistence removes meta lines and preserves regular text', () => {
|
|
396
|
+
const input = [
|
|
397
|
+
'Here is a normal analysis of the system.',
|
|
398
|
+
'[AGENT_HEARTBEAT_META]{"status":"progress","goal":"test"}',
|
|
399
|
+
'Another regular line with findings.',
|
|
400
|
+
'[MAIN_LOOP_PLAN]{"steps":["step1","step2"],"current_step":"step1"}',
|
|
401
|
+
'Final observation about performance.',
|
|
402
|
+
'[MAIN_LOOP_REVIEW]{"note":"reviewed","confidence":0.8,"needs_replan":false}',
|
|
403
|
+
].join('\n')
|
|
404
|
+
|
|
405
|
+
const result = stripMainLoopMetaForPersistence(input)
|
|
406
|
+
|
|
407
|
+
assert.ok(!result.includes('[AGENT_HEARTBEAT_META]'), 'heartbeat meta removed')
|
|
408
|
+
assert.ok(!result.includes('[MAIN_LOOP_PLAN]'), 'plan meta removed')
|
|
409
|
+
assert.ok(!result.includes('[MAIN_LOOP_REVIEW]'), 'review meta removed')
|
|
410
|
+
assert.ok(result.includes('Here is a normal analysis of the system.'), 'first regular line preserved')
|
|
411
|
+
assert.ok(result.includes('Another regular line with findings.'), 'second regular line preserved')
|
|
412
|
+
assert.ok(result.includes('Final observation about performance.'), 'third regular line preserved')
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
it('stripMainLoopMetaForPersistence handles text with no meta lines', () => {
|
|
416
|
+
const input = 'Just a simple message with no meta.'
|
|
417
|
+
const result = stripMainLoopMetaForPersistence(input)
|
|
418
|
+
assert.equal(result, input)
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it('stripMainLoopMetaForPersistence handles text that is only meta', () => {
|
|
422
|
+
const input = '[AGENT_HEARTBEAT_META]{"status":"ok","goal":"done"}'
|
|
423
|
+
const result = stripMainLoopMetaForPersistence(input)
|
|
424
|
+
assert.equal(result, '')
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
428
|
+
// 9. Status transitions (direct import via subprocess for state access)
|
|
429
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
430
|
+
it('preserves all valid status values: idle, progress, blocked, ok', () => {
|
|
431
|
+
const output = runWithTempDataDir(`
|
|
432
|
+
${sessionSetupScript()}
|
|
433
|
+
|
|
434
|
+
const statuses = ['idle', 'progress', 'blocked', 'ok']
|
|
435
|
+
const results = {}
|
|
436
|
+
|
|
437
|
+
for (const status of statuses) {
|
|
438
|
+
const meta = '[AGENT_HEARTBEAT_META]{"status":"' + status + '","goal":"status test ' + status + '"}'
|
|
439
|
+
// For terminal statuses we need non-internal source to avoid chain logic
|
|
440
|
+
// Actually internal + heartbeat source + no error works for progress/blocked/idle
|
|
441
|
+
// For 'ok' status without HEARTBEAT_OK text, it should preserve the status
|
|
442
|
+
mainLoop.handleMainLoopRunResult({
|
|
443
|
+
sessionId: 'main',
|
|
444
|
+
message: 'Testing status ' + status + '.',
|
|
445
|
+
internal: true,
|
|
446
|
+
source: 'heartbeat',
|
|
447
|
+
resultText: 'Status is ' + status + '.\\n' + meta,
|
|
448
|
+
inputTokens: 5,
|
|
449
|
+
outputTokens: 3,
|
|
450
|
+
})
|
|
451
|
+
const s = mainLoop.getMainLoopStateForSession('main')
|
|
452
|
+
results[status] = s?.status ?? null
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
console.log(JSON.stringify({ results }))
|
|
456
|
+
`)
|
|
457
|
+
|
|
458
|
+
const results = output.results as Record<string, string>
|
|
459
|
+
assert.equal(results.idle, 'idle', 'idle status preserved')
|
|
460
|
+
assert.equal(results.progress, 'progress', 'progress status preserved')
|
|
461
|
+
assert.equal(results.blocked, 'blocked', 'blocked status preserved')
|
|
462
|
+
assert.equal(results.ok, 'ok', 'ok status preserved')
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
466
|
+
// 10. Mission cost accumulation
|
|
467
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
468
|
+
it('accumulates missionCostUsd and missionTokens across multiple runs', () => {
|
|
469
|
+
const output = runWithTempDataDir(`
|
|
470
|
+
${sessionSetupScript()}
|
|
471
|
+
|
|
472
|
+
const meta1 = '[AGENT_HEARTBEAT_META]{"status":"progress","goal":"cost test","next_action":"step 1"}'
|
|
473
|
+
const meta2 = '[AGENT_HEARTBEAT_META]{"status":"progress","goal":"cost test","next_action":"step 2"}'
|
|
474
|
+
const meta3 = '[AGENT_HEARTBEAT_META]{"status":"progress","goal":"cost test","next_action":"step 3"}'
|
|
475
|
+
|
|
476
|
+
mainLoop.handleMainLoopRunResult({
|
|
477
|
+
sessionId: 'main',
|
|
478
|
+
message: 'Cost step 1.',
|
|
479
|
+
internal: true,
|
|
480
|
+
source: 'heartbeat',
|
|
481
|
+
resultText: 'Step 1 complete.\\n' + meta1,
|
|
482
|
+
inputTokens: 100,
|
|
483
|
+
outputTokens: 50,
|
|
484
|
+
estimatedCost: 0.05,
|
|
485
|
+
})
|
|
486
|
+
const s1 = mainLoop.getMainLoopStateForSession('main')
|
|
487
|
+
|
|
488
|
+
mainLoop.handleMainLoopRunResult({
|
|
489
|
+
sessionId: 'main',
|
|
490
|
+
message: 'Cost step 2.',
|
|
491
|
+
internal: true,
|
|
492
|
+
source: 'heartbeat',
|
|
493
|
+
resultText: 'Step 2 complete.\\n' + meta2,
|
|
494
|
+
inputTokens: 200,
|
|
495
|
+
outputTokens: 100,
|
|
496
|
+
estimatedCost: 0.10,
|
|
497
|
+
})
|
|
498
|
+
const s2 = mainLoop.getMainLoopStateForSession('main')
|
|
499
|
+
|
|
500
|
+
mainLoop.handleMainLoopRunResult({
|
|
501
|
+
sessionId: 'main',
|
|
502
|
+
message: 'Cost step 3.',
|
|
503
|
+
internal: true,
|
|
504
|
+
source: 'heartbeat',
|
|
505
|
+
resultText: 'Step 3 complete.\\n' + meta3,
|
|
506
|
+
inputTokens: 300,
|
|
507
|
+
outputTokens: 150,
|
|
508
|
+
estimatedCost: 0.15,
|
|
509
|
+
})
|
|
510
|
+
const s3 = mainLoop.getMainLoopStateForSession('main')
|
|
511
|
+
|
|
512
|
+
console.log(JSON.stringify({
|
|
513
|
+
cost1: s1?.missionCostUsd ?? -1,
|
|
514
|
+
cost2: s2?.missionCostUsd ?? -1,
|
|
515
|
+
cost3: s3?.missionCostUsd ?? -1,
|
|
516
|
+
tokens1: s1?.missionTokens ?? -1,
|
|
517
|
+
tokens2: s2?.missionTokens ?? -1,
|
|
518
|
+
tokens3: s3?.missionTokens ?? -1,
|
|
519
|
+
}))
|
|
520
|
+
`)
|
|
521
|
+
|
|
522
|
+
assert.ok(
|
|
523
|
+
Math.abs((output.cost1 as number) - 0.05) < 0.001,
|
|
524
|
+
`cost after step 1 should be ~0.05, got ${output.cost1}`,
|
|
525
|
+
)
|
|
526
|
+
assert.ok(
|
|
527
|
+
Math.abs((output.cost2 as number) - 0.15) < 0.001,
|
|
528
|
+
`cost after step 2 should be ~0.15, got ${output.cost2}`,
|
|
529
|
+
)
|
|
530
|
+
assert.ok(
|
|
531
|
+
Math.abs((output.cost3 as number) - 0.30) < 0.001,
|
|
532
|
+
`cost after step 3 should be ~0.30, got ${output.cost3}`,
|
|
533
|
+
)
|
|
534
|
+
assert.equal(output.tokens1, 150, 'tokens after step 1: 100+50=150')
|
|
535
|
+
assert.equal(output.tokens2, 450, 'tokens after step 2: 150+200+100=450')
|
|
536
|
+
assert.equal(output.tokens3, 900, 'tokens after step 3: 450+300+150=900')
|
|
537
|
+
})
|
|
538
|
+
})
|