@swarmclawai/swarmclaw 1.5.33 → 1.5.34
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 +20 -3
- package/package.json +2 -2
- package/src/app/api/secrets/[id]/route.ts +4 -2
- package/src/lib/ollama-mode.test.ts +18 -1
- package/src/lib/ollama-mode.ts +8 -0
- package/src/lib/providers/openai.ts +5 -1
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +10 -8
- package/src/lib/server/chat-execution/prompt-sections.ts +2 -2
- package/src/lib/server/chat-execution/stream-agent-chat.ts +2 -2
- package/src/lib/server/ollama-runtime.ts +2 -2
- package/src/lib/server/provider-health.ts +3 -1
- package/src/lib/server/runtime/heartbeat-service.ts +13 -10
- package/src/lib/server/session-tools/index.ts +11 -1
- package/src/proxy.ts +7 -1
package/README.md
CHANGED
|
@@ -39,6 +39,10 @@ Extension tutorial: https://swarmclaw.ai/docs/extension-tutorial
|
|
|
39
39
|
<td align="center"><img src="doc/assets/logos/codex.svg" width="32" alt="Codex"><br><sub>Codex</sub></td>
|
|
40
40
|
<td align="center"><img src="doc/assets/logos/gemini-cli.svg" width="32" alt="Gemini CLI"><br><sub>Gemini CLI</sub></td>
|
|
41
41
|
<td align="center"><img src="doc/assets/logos/opencode.svg" width="32" alt="OpenCode"><br><sub>OpenCode</sub></td>
|
|
42
|
+
<td align="center"><img src="doc/assets/logos/copilot-cli.svg" width="32" alt="Copilot CLI"><br><sub>Copilot</sub></td>
|
|
43
|
+
<td align="center"><img src="doc/assets/logos/cursor-cli.svg" width="32" alt="Cursor Agent CLI"><br><sub>Cursor</sub></td>
|
|
44
|
+
<td align="center"><img src="doc/assets/logos/qwen-code-cli.svg" width="32" alt="Qwen Code CLI"><br><sub>Qwen Code</sub></td>
|
|
45
|
+
<td align="center"><img src="doc/assets/logos/goose.svg" width="32" alt="Goose"><br><sub>Goose</sub></td>
|
|
42
46
|
<td align="center"><img src="doc/assets/logos/anthropic.svg" width="32" alt="Anthropic"><br><sub>Anthropic</sub></td>
|
|
43
47
|
<td align="center"><img src="doc/assets/logos/openai.svg" width="32" alt="OpenAI"><br><sub>OpenAI</sub></td>
|
|
44
48
|
<td align="center"><img src="public/provider-logos/openrouter.png" width="32" alt="OpenRouter"><br><sub>OpenRouter</sub></td>
|
|
@@ -61,7 +65,7 @@ Extension tutorial: https://swarmclaw.ai/docs/extension-tutorial
|
|
|
61
65
|
- Node.js 22.6+ (`nvm use` will pick up the repo's `.nvmrc`, which matches CI)
|
|
62
66
|
- npm 10+ or another supported package manager
|
|
63
67
|
- Docker Desktop is recommended for sandbox browser execution
|
|
64
|
-
- Optional provider CLIs if you want delegated CLI backends such as Claude Code, Codex, OpenCode, or
|
|
68
|
+
- Optional provider CLIs if you want delegated CLI backends such as Claude Code, Codex, OpenCode, Gemini, Copilot, Cursor Agent, Qwen Code, or Goose
|
|
65
69
|
|
|
66
70
|
## Quick Start
|
|
67
71
|
|
|
@@ -147,10 +151,10 @@ Full hosted deployment guides live at https://swarmclaw.ai/docs/deployment
|
|
|
147
151
|
|
|
148
152
|
## Core Capabilities
|
|
149
153
|
|
|
150
|
-
- **Providers**:
|
|
154
|
+
- **Providers**: 23 built-in — Claude Code CLI, Codex CLI, OpenCode CLI, Gemini CLI, Copilot CLI, Cursor Agent CLI, Qwen Code CLI, Goose, Anthropic, OpenAI, OpenRouter, Google Gemini, DeepSeek, Groq, Together, Mistral, xAI, Fireworks, Nebius, DeepInfra, Ollama, OpenClaw, and Hermes Agent, plus compatible custom endpoints.
|
|
151
155
|
- **OpenRouter**: <img src="public/provider-logos/openrouter.png" alt="OpenRouter logo" width="20" height="20" /> Use OpenRouter as a first-class built-in provider with its standard OpenAI-compatible endpoint and routed model IDs such as `openai/gpt-4.1-mini`.
|
|
152
156
|
- **Hermes Agent**: <img src="public/provider-logos/hermes-agent.png" alt="Hermes Agent logo" width="20" height="20" /> Connect Hermes through its OpenAI-compatible API server, locally or through a reachable remote `/v1` endpoint.
|
|
153
|
-
- **Delegation**: built-in delegation to Claude Code, Codex CLI, OpenCode CLI, Gemini CLI, and native SwarmClaw subagents.
|
|
157
|
+
- **Delegation**: built-in delegation to Claude Code, Codex CLI, OpenCode CLI, Gemini CLI, Cursor Agent CLI, Qwen Code CLI, and native SwarmClaw subagents.
|
|
154
158
|
- **Autonomy**: heartbeat loops, schedules, background jobs, task execution, supervisor recovery, and agent wakeups.
|
|
155
159
|
- **Orchestration**: durable structured execution with branching, repeat loops, parallel branches, explicit joins, restart-safe run state, and contextual launch from chats, chatrooms, tasks, schedules, and API flows.
|
|
156
160
|
- **Structured Sessions**: reusable bounded runs with templates, facilitators, participants, hidden live rooms, chatroom `/breakout`, durable transcripts, outputs, operator controls, and a visible protocols template gallery plus visual builder.
|
|
@@ -371,6 +375,12 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
371
375
|
|
|
372
376
|
## Releases
|
|
373
377
|
|
|
378
|
+
### v1.5.34 Highlights
|
|
379
|
+
|
|
380
|
+
- **Ollama Cloud auth fix**: SwarmClaw now normalizes `api.ollama.com` and `www.ollama.com` to `ollama.com` before making authenticated requests, avoiding the redirect that was dropping authorization headers and causing false provider-health/runtime failures.
|
|
381
|
+
- **Chat execution context hardening**: tool invocation now resolves names case-insensitively, oversized tool results are truncated before they are fed back into the model, and proactive grounding/heartbeat prompts stay smaller under pressure to reduce avoidable context blowouts.
|
|
382
|
+
- **API compatibility fixes**: OpenAI-compatible streaming now captures reasoning deltas from providers that emit them outside `delta.content`, and A2A endpoints are exempt from the main proxy access-key gate so they can rely on their own auth scheme.
|
|
383
|
+
|
|
374
384
|
### v1.5.33 Highlights
|
|
375
385
|
|
|
376
386
|
- **CLI global flag compatibility**: legacy-routed commands now honor the documented `--access-key` and `--base-url` aliases even when they appear after the subcommand, so authenticated CLI automation works the same across binary entry points.
|
|
@@ -380,6 +390,13 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
380
390
|
|
|
381
391
|
- **Fix Docker first-run crash**: resolved `EISDIR: illegal operation on a directory, read` error when running `docker compose up` without a pre-existing `.env.local` file. Docker was creating a directory mount instead of a file, which crashed Next.js on startup. Replaced the file bind mount with `env_file` directive using `required: false`.
|
|
382
392
|
|
|
393
|
+
### v1.5.4 Highlights
|
|
394
|
+
|
|
395
|
+
- **Cursor Agent CLI built-in provider**: Cursor Agent CLI is now a first-class worker provider with session continuity, headless execution, and delegation support.
|
|
396
|
+
- **Qwen Code CLI built-in provider**: Qwen Code CLI is now available as a built-in worker provider and delegation backend with structured headless execution support.
|
|
397
|
+
- **Goose built-in provider**: Goose is now supported as a runtime-managed worker provider, using its own local auth and provider configuration while preserving SwarmClaw session continuity.
|
|
398
|
+
- **CLI setup and health parity**: setup flows, provider checks, setup doctor, and provider-facing UI now recognize Cursor, Qwen Code, and Goose alongside the existing CLI-backed providers.
|
|
399
|
+
|
|
383
400
|
### v1.5.3 Highlights
|
|
384
401
|
|
|
385
402
|
- **Copilot CLI v1.x compatibility**: the `copilot-cli` provider now handles the current event format (`assistant.message_delta`, `assistant.message`, updated `result` payload) while keeping backward compatibility with the legacy format. Also fixes `--resume` flag syntax. (Community contribution by [@borislavnnikolov](https://github.com/borislavnnikolov) -- PR #36)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.34",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
75
75
|
"test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
|
|
76
76
|
"test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
|
|
77
|
-
"test:runtime": "tsx --test src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/app/api/approvals/route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/tts/route.test.ts",
|
|
77
|
+
"test:runtime": "tsx --test src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/tts/route.test.ts",
|
|
78
78
|
"test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
|
|
79
79
|
"test:e2e": "tsx .workbench/browser-e2e/run.ts",
|
|
80
80
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
@@ -12,7 +12,8 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
12
12
|
const secret = secrets[id]
|
|
13
13
|
if (!secret) return notFound()
|
|
14
14
|
// Never expose the encrypted value
|
|
15
|
-
const
|
|
15
|
+
const safe = { ...(secret as Record<string, unknown>) }
|
|
16
|
+
delete safe.encryptedValue
|
|
16
17
|
return NextResponse.json(safe)
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -36,6 +37,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
36
37
|
return secret
|
|
37
38
|
})
|
|
38
39
|
if (!result) return notFound()
|
|
39
|
-
const
|
|
40
|
+
const safe = { ...(result as Record<string, unknown>) }
|
|
41
|
+
delete safe.encryptedValue
|
|
40
42
|
return NextResponse.json(safe)
|
|
41
43
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import test from 'node:test'
|
|
3
|
-
import { isOllamaCloudEndpoint, normalizeOllamaMode, resolveStoredOllamaMode } from './ollama-mode'
|
|
3
|
+
import { isOllamaCloudEndpoint, normalizeOllamaCloudEndpoint, normalizeOllamaMode, resolveStoredOllamaMode } from './ollama-mode'
|
|
4
4
|
|
|
5
5
|
test('normalizeOllamaMode only accepts explicit local and cloud values', () => {
|
|
6
6
|
assert.equal(normalizeOllamaMode('local'), 'local')
|
|
@@ -31,3 +31,20 @@ test('resolveStoredOllamaMode falls back to endpoint only for legacy records', (
|
|
|
31
31
|
assert.equal(resolveStoredOllamaMode({ apiEndpoint: 'http://localhost:11434' }), 'local')
|
|
32
32
|
assert.equal(resolveStoredOllamaMode({}), 'local')
|
|
33
33
|
})
|
|
34
|
+
|
|
35
|
+
test('normalizeOllamaCloudEndpoint rewrites api.ollama.com to ollama.com', () => {
|
|
36
|
+
assert.equal(normalizeOllamaCloudEndpoint('https://api.ollama.com'), 'https://ollama.com')
|
|
37
|
+
assert.equal(normalizeOllamaCloudEndpoint('https://api.ollama.com/v1'), 'https://ollama.com/v1')
|
|
38
|
+
assert.equal(normalizeOllamaCloudEndpoint('http://api.ollama.com'), 'http://ollama.com')
|
|
39
|
+
assert.equal(normalizeOllamaCloudEndpoint('https://www.ollama.com'), 'https://ollama.com')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('normalizeOllamaCloudEndpoint preserves correct ollama.com URLs', () => {
|
|
43
|
+
assert.equal(normalizeOllamaCloudEndpoint('https://ollama.com'), 'https://ollama.com')
|
|
44
|
+
assert.equal(normalizeOllamaCloudEndpoint('https://ollama.com/v1'), 'https://ollama.com/v1')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('normalizeOllamaCloudEndpoint does not mangle non-Ollama endpoints', () => {
|
|
48
|
+
assert.equal(normalizeOllamaCloudEndpoint('http://localhost:11434'), 'http://localhost:11434')
|
|
49
|
+
assert.equal(normalizeOllamaCloudEndpoint('https://api.openai.com/v1'), 'https://api.openai.com/v1')
|
|
50
|
+
})
|
package/src/lib/ollama-mode.ts
CHANGED
|
@@ -18,6 +18,14 @@ export function isOllamaCloudEndpoint(endpoint: string | null | undefined): bool
|
|
|
18
18
|
return /^https?:\/\/(?:www\.|api\.)?ollama\.com(?:\/|$)/i.test(normalized)
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Normalize an Ollama Cloud endpoint to avoid the api.ollama.com -> ollama.com
|
|
23
|
+
* 301 redirect which drops the Authorization header.
|
|
24
|
+
*/
|
|
25
|
+
export function normalizeOllamaCloudEndpoint(endpoint: string): string {
|
|
26
|
+
return endpoint.replace(/^(https?:\/\/)(?:www\.|api\.)?ollama\.com/i, '$1ollama.com')
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
export function resolveStoredOllamaMode(input: {
|
|
22
30
|
ollamaMode?: string | null
|
|
23
31
|
apiEndpoint?: string | null
|
|
@@ -165,7 +165,11 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
|
|
|
165
165
|
if (data === '[DONE]') continue
|
|
166
166
|
try {
|
|
167
167
|
const parsed = JSON.parse(data)
|
|
168
|
-
const
|
|
168
|
+
const choice = parsed.choices?.[0]?.delta
|
|
169
|
+
const delta = choice?.content
|
|
170
|
+
// Thinking/reasoning models (kimi-k2, etc.) put output in reasoning fields
|
|
171
|
+
|| choice?.reasoning_content
|
|
172
|
+
|| choice?.reasoning
|
|
169
173
|
if (delta) {
|
|
170
174
|
fullResponse += delta
|
|
171
175
|
writeSSE(write, 'd', delta)
|
|
@@ -453,12 +453,13 @@ async function invokeSessionTool(
|
|
|
453
453
|
})
|
|
454
454
|
|
|
455
455
|
try {
|
|
456
|
-
const
|
|
456
|
+
const lowerName = toolName.toLowerCase()
|
|
457
|
+
const directTool = tools.find((t) => t?.name === toolName || t?.name?.toLowerCase() === lowerName) as StructuredToolInterface | undefined
|
|
457
458
|
const availableToolNames = tools.map((candidate) => candidate?.name).filter(Boolean)
|
|
458
459
|
const translated = directTool
|
|
459
|
-
? { toolName, args }
|
|
460
|
+
? { toolName: directTool.name, args }
|
|
460
461
|
: translateRequestedToolInvocation(toolName, args, ctx.message, availableToolNames)
|
|
461
|
-
const selectedTool = directTool || tools.find((t) => t?.name === translated.toolName) as StructuredToolInterface | undefined
|
|
462
|
+
const selectedTool = directTool || tools.find((t) => t?.name === translated.toolName || t?.name?.toLowerCase() === translated.toolName.toLowerCase()) as StructuredToolInterface | undefined
|
|
462
463
|
if (!selectedTool?.invoke) {
|
|
463
464
|
const resolvedName = translated.toolName !== toolName ? translated.toolName : null
|
|
464
465
|
const unavailableReason = resolvedName === 'delegate'
|
|
@@ -469,18 +470,19 @@ async function invokeSessionTool(
|
|
|
469
470
|
return { invoked: false, responseOverride: null, unavailableReason }
|
|
470
471
|
}
|
|
471
472
|
|
|
473
|
+
const resolvedToolName = selectedTool.name
|
|
472
474
|
const toolCallId = genId()
|
|
473
|
-
ctx.emit({ t: 'tool_call', toolName, toolInput: JSON.stringify(translated.args), toolCallId })
|
|
475
|
+
ctx.emit({ t: 'tool_call', toolName: resolvedToolName, toolInput: JSON.stringify(translated.args), toolCallId })
|
|
474
476
|
const toolOutput = await selectedTool.invoke(translated.args)
|
|
475
477
|
const outputText = typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput)
|
|
476
|
-
ctx.emit({ t: 'tool_result', toolName, toolOutput: outputText, toolCallId })
|
|
478
|
+
ctx.emit({ t: 'tool_result', toolName: resolvedToolName, toolOutput: outputText, toolCallId })
|
|
477
479
|
|
|
478
480
|
const delegateResponse = (
|
|
479
|
-
|
|
480
|
-
||
|
|
481
|
+
resolvedToolName === 'delegate'
|
|
482
|
+
|| resolvedToolName.startsWith('delegate_to_')
|
|
481
483
|
) ? extractDelegateResponse(outputText) : null
|
|
482
484
|
|
|
483
|
-
calledNames.add(
|
|
485
|
+
calledNames.add(resolvedToolName)
|
|
484
486
|
|
|
485
487
|
if (delegateResponse) {
|
|
486
488
|
return { invoked: true, responseOverride: delegateResponse, toolOutputText: outputText }
|
|
@@ -475,7 +475,7 @@ export async function buildProactiveMemorySection(
|
|
|
475
475
|
)
|
|
476
476
|
sections.push(`## Recalled Context\nRelevant memories from previous interactions:\n${recalledLines.join('\n')}`)
|
|
477
477
|
if (knowledgeTrace?.hits.length) {
|
|
478
|
-
const groundingLines = knowledgeTrace.hits.map((hit) =>
|
|
478
|
+
const groundingLines = knowledgeTrace.hits.slice(0, 10).map((hit) =>
|
|
479
479
|
`- [${hit.chunkIndex + 1}/${hit.chunkCount}] ${hit.sourceTitle}: ${hit.snippet}`,
|
|
480
480
|
)
|
|
481
481
|
sections.push(`## Source Grounding\nSource-backed knowledge retrieved for this turn:\n${groundingLines.join('\n')}`)
|
|
@@ -488,7 +488,7 @@ export async function buildProactiveMemorySection(
|
|
|
488
488
|
}
|
|
489
489
|
|
|
490
490
|
if (knowledgeTrace?.hits.length) {
|
|
491
|
-
const groundingLines = knowledgeTrace.hits.map((hit) =>
|
|
491
|
+
const groundingLines = knowledgeTrace.hits.slice(0, 10).map((hit) =>
|
|
492
492
|
`- [${hit.chunkIndex + 1}/${hit.chunkCount}] ${hit.sourceTitle}: ${hit.snippet}`,
|
|
493
493
|
)
|
|
494
494
|
return {
|
|
@@ -695,7 +695,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
695
695
|
(result.summaryAdded ? ' (LLM summary)' : ' (sliding window fallback)'),
|
|
696
696
|
)
|
|
697
697
|
}
|
|
698
|
-
} catch {
|
|
698
|
+
} catch (compactErr) { log.warn(TAG, `Auto-compaction failed for ${session.id}:`, compactErr) }
|
|
699
699
|
|
|
700
700
|
// Truncate oversized assistant messages in history to prevent context blowout
|
|
701
701
|
const HISTORY_MSG_MAX_CHARS = 8_000
|
|
@@ -787,7 +787,7 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
|
|
|
787
787
|
if (warning) {
|
|
788
788
|
prompt = joinPromptSegments(warning, prompt)
|
|
789
789
|
}
|
|
790
|
-
} catch {
|
|
790
|
+
} catch (degradeErr) { log.warn(TAG, `Context degradation check failed for ${session.id}:`, degradeErr) }
|
|
791
791
|
|
|
792
792
|
await runCapabilityHook(
|
|
793
793
|
'llmInput',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stripOllamaCloudModelSuffix } from '@/lib/ollama-model'
|
|
2
|
-
import { isOllamaCloudEndpoint, resolveStoredOllamaMode } from '@/lib/ollama-mode'
|
|
2
|
+
import { isOllamaCloudEndpoint, normalizeOllamaCloudEndpoint, resolveStoredOllamaMode } from '@/lib/ollama-mode'
|
|
3
3
|
import { PROVIDER_DEFAULTS } from '@/lib/providers/provider-defaults'
|
|
4
4
|
|
|
5
5
|
const OLLAMA_CLOUD_KEY_ENV_VARS = ['OLLAMA_API_KEY', 'OLLAMA_CLOUD_API_KEY'] as const
|
|
@@ -41,7 +41,7 @@ export function resolveOllamaRuntimeConfig(input: {
|
|
|
41
41
|
const cloudApiKey = resolveOllamaCloudApiKey(explicitApiKey)
|
|
42
42
|
const useCloud = ollamaMode === 'cloud'
|
|
43
43
|
const endpoint = useCloud
|
|
44
|
-
? (isOllamaCloudEndpoint(explicitEndpoint) ? explicitEndpoint! : PROVIDER_DEFAULTS.ollamaCloud)
|
|
44
|
+
? normalizeOllamaCloudEndpoint(isOllamaCloudEndpoint(explicitEndpoint) ? explicitEndpoint! : PROVIDER_DEFAULTS.ollamaCloud)
|
|
45
45
|
: (explicitEndpoint && !isOllamaCloudEndpoint(explicitEndpoint) ? explicitEndpoint : PROVIDER_DEFAULTS.ollama)
|
|
46
46
|
|
|
47
47
|
return {
|
|
@@ -305,8 +305,10 @@ export async function pingAnthropic(apiKey: string): Promise<{ ok: boolean; mess
|
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
export async function pingOllama(endpoint: string, apiKey?: string): Promise<{ ok: boolean; message: string }> {
|
|
308
|
-
|
|
308
|
+
let normalizedEndpoint = (endpoint || 'http://localhost:11434').replace(/\/+$/, '')
|
|
309
309
|
const useCloud = isOllamaCloudEndpoint(normalizedEndpoint)
|
|
310
|
+
// Normalize api.ollama.com -> ollama.com to avoid 301 redirect that drops auth
|
|
311
|
+
if (useCloud) normalizedEndpoint = normalizedEndpoint.replace(/^(https?:\/\/)(?:www\.|api\.)?ollama\.com/i, '$1ollama.com')
|
|
310
312
|
if (useCloud && !apiKey) {
|
|
311
313
|
return { ok: false, message: 'Ollama Cloud requires an API key.' }
|
|
312
314
|
}
|
|
@@ -319,10 +319,11 @@ export function buildAgentHeartbeatPrompt(
|
|
|
319
319
|
agent: HeartbeatPromptAgent | null | undefined,
|
|
320
320
|
fallbackPrompt: string,
|
|
321
321
|
heartbeatFileContent: string,
|
|
322
|
-
opts?: { approvals?: Record<string, ApprovalRequest>; chatrooms?: Record<string, Chatroom
|
|
322
|
+
opts?: { approvals?: Record<string, ApprovalRequest>; chatrooms?: Record<string, Chatroom>; lightContext?: boolean },
|
|
323
323
|
): string {
|
|
324
324
|
if (!agent) return fallbackPrompt
|
|
325
325
|
|
|
326
|
+
const light = opts?.lightContext === true
|
|
326
327
|
const sections: string[] = []
|
|
327
328
|
|
|
328
329
|
// ── Phase 1: Identity context ──
|
|
@@ -331,11 +332,11 @@ export function buildAgentHeartbeatPrompt(
|
|
|
331
332
|
const identityContext = buildIdentityContext(session, agent)
|
|
332
333
|
const continuityContext = buildIdentityContinuityContext(session, agent)
|
|
333
334
|
if (identityContext) sections.push(identityContext)
|
|
334
|
-
if (continuityContext) sections.push(continuityContext)
|
|
335
|
+
if (!light && continuityContext) sections.push(continuityContext)
|
|
335
336
|
const description = agent.description || ''
|
|
336
337
|
const soul = agent.soul || ''
|
|
337
338
|
if (description) sections.push(`Description: ${description}`)
|
|
338
|
-
if (soul) sections.push(`Persona: ${soul.slice(0, 300)}`)
|
|
339
|
+
if (!light && soul) sections.push(`Persona: ${soul.slice(0, 300)}`)
|
|
339
340
|
|
|
340
341
|
// ── Phase 2: Pending approvals ──
|
|
341
342
|
const agentId = agent.id || session.agentId || ''
|
|
@@ -346,7 +347,7 @@ export function buildAgentHeartbeatPrompt(
|
|
|
346
347
|
(a) => a.status === 'pending' && a.agentId === agentId,
|
|
347
348
|
)
|
|
348
349
|
if (pending.length > 0) {
|
|
349
|
-
const approvalLines = pending.slice(0, 5).map(
|
|
350
|
+
const approvalLines = pending.slice(0, light ? 2 : 5).map(
|
|
350
351
|
(a) => `- [${a.category}] ${a.title}${a.description ? `: ${a.description.slice(0, 100)}` : ''}`,
|
|
351
352
|
)
|
|
352
353
|
sections.push(`### Pending Approvals (${pending.length})\n${approvalLines.join('\n')}`)
|
|
@@ -366,7 +367,7 @@ export function buildAgentHeartbeatPrompt(
|
|
|
366
367
|
const dynamicGoal = agent.heartbeatGoal || ''
|
|
367
368
|
const dynamicNextAction = agent.heartbeatNextAction || ''
|
|
368
369
|
const systemPrompt = agent.systemPrompt || ''
|
|
369
|
-
const goalSummary = systemPrompt.slice(0, 500)
|
|
370
|
+
const goalSummary = systemPrompt.slice(0, light ? 200 : 500)
|
|
370
371
|
|
|
371
372
|
if (dynamicGoal) {
|
|
372
373
|
sections.push(`Current goal (self-set): ${dynamicGoal}`)
|
|
@@ -377,12 +378,13 @@ export function buildAgentHeartbeatPrompt(
|
|
|
377
378
|
|
|
378
379
|
const strippedContent = stripBlockedItems(heartbeatFileContent)
|
|
379
380
|
const effectiveFileContent = isHeartbeatContentEffectivelyEmpty(strippedContent) ? '' : strippedContent
|
|
380
|
-
if (effectiveFileContent) sections.push(`\nHEARTBEAT.md contents:\n${effectiveFileContent.slice(0, 2000)}`)
|
|
381
|
+
if (effectiveFileContent) sections.push(`\nHEARTBEAT.md contents:\n${effectiveFileContent.slice(0, light ? 500 : 2000)}`)
|
|
381
382
|
|
|
383
|
+
const messageCount = light ? 2 : 5
|
|
382
384
|
const recentMessages = (
|
|
383
385
|
Array.isArray(session.messages)
|
|
384
|
-
? session.messages.slice(-
|
|
385
|
-
: (session.id ? getRecentMessages(session.id,
|
|
386
|
+
? session.messages.slice(-messageCount)
|
|
387
|
+
: (session.id ? getRecentMessages(session.id, messageCount) : [])
|
|
386
388
|
) as HeartbeatPromptMessage[]
|
|
387
389
|
const recentContext = recentMessages
|
|
388
390
|
.map((m) => {
|
|
@@ -395,8 +397,8 @@ export function buildAgentHeartbeatPrompt(
|
|
|
395
397
|
.join('\n')
|
|
396
398
|
if (recentContext) sections.push(`Recent conversation:\n${recentContext}`)
|
|
397
399
|
|
|
398
|
-
// ── Phase 4b: Chatroom mentions since last heartbeat ──
|
|
399
|
-
try {
|
|
400
|
+
// ── Phase 4b: Chatroom mentions since last heartbeat (skip in light mode) ──
|
|
401
|
+
if (!light) try {
|
|
400
402
|
const chatrooms = Object.values(opts?.chatrooms ?? loadChatrooms()) as Chatroom[]
|
|
401
403
|
const myChatrooms = chatrooms.filter((c) => !c.archivedAt && c.agentIds?.includes(agentId))
|
|
402
404
|
if (myChatrooms.length > 0) {
|
|
@@ -718,6 +720,7 @@ export async function tickHeartbeats() {
|
|
|
718
720
|
const baseHeartbeatMessage = buildAgentHeartbeatPrompt(session, agent, cfg.prompt, heartbeatFileContent, {
|
|
719
721
|
approvals: sharedApprovals,
|
|
720
722
|
chatrooms: sharedChatrooms,
|
|
723
|
+
lightContext: cfg.lightContext,
|
|
721
724
|
})
|
|
722
725
|
let heartbeatMessage = isMainSession(session)
|
|
723
726
|
? buildMainLoopHeartbeatPrompt(session, baseHeartbeatMessage)
|
|
@@ -6,6 +6,8 @@ import { loadSettings, loadSession, loadAgent, loadMcpServers, patchAgent, patch
|
|
|
6
6
|
import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
|
|
7
7
|
import { log } from '../logger'
|
|
8
8
|
import { resolveSessionToolPolicy } from '../tool-capability-policy'
|
|
9
|
+
import { truncateToolResultText, calculateMaxToolResultChars } from '../chat-execution/tool-result-guard'
|
|
10
|
+
import { getContextWindowSize } from '../context-manager'
|
|
9
11
|
import { expandExtensionIds } from '../tool-aliases'
|
|
10
12
|
import type { ToolContext, SessionToolsResult, ToolBuildContext, AbortSignalRef } from './context'
|
|
11
13
|
|
|
@@ -455,9 +457,17 @@ export async function buildSessionTools(cwd: string, enabledExtensions: string[]
|
|
|
455
457
|
}
|
|
456
458
|
const effectiveArgs = hookResult.input ?? guardedArgs
|
|
457
459
|
const result = await candidate.invoke(effectiveArgs ?? {})
|
|
458
|
-
const
|
|
460
|
+
const rawOutput = typeof result === 'string' ? result : JSON.stringify(result)
|
|
461
|
+
// Truncate oversized tool outputs before LangGraph feeds them back to
|
|
462
|
+
// the LLM. Without this, a single large tool result (e.g. shell dump,
|
|
463
|
+
// large web fetch) can blow out the context window inside LangGraph's
|
|
464
|
+
// internal state, which the auto-compaction system cannot observe.
|
|
465
|
+
const currentSession = resolveCurrentSession()
|
|
466
|
+
const maxChars = calculateMaxToolResultChars(getContextWindowSize(currentSession?.provider || '', currentSession?.model || ''))
|
|
467
|
+
const outputText = truncateToolResultText(rawOutput, maxChars)
|
|
459
468
|
setSpanAttributes(span, {
|
|
460
469
|
'swarmclaw.tool.output_bytes': Buffer.byteLength(outputText, 'utf-8'),
|
|
470
|
+
...(rawOutput.length !== outputText.length ? { 'swarmclaw.tool.truncated_from': rawOutput.length } : {}),
|
|
461
471
|
})
|
|
462
472
|
await runCapabilityHook(
|
|
463
473
|
'afterToolExec',
|
package/src/proxy.ts
CHANGED
|
@@ -87,13 +87,19 @@ export function proxy(request: NextRequest) {
|
|
|
87
87
|
})
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
//
|
|
90
|
+
// A2A endpoints use their own authentication (Authorization: Bearer / x-a2a-access-key)
|
|
91
|
+
const isA2ARoute = pathname === '/api/a2a'
|
|
92
|
+
|| pathname.startsWith('/api/a2a/')
|
|
93
|
+
|| pathname === '/api/.well-known/agent-card'
|
|
94
|
+
|
|
95
|
+
// Only protect API routes (not auth, inbound webhooks, or A2A)
|
|
91
96
|
if (
|
|
92
97
|
!pathname.startsWith('/api/')
|
|
93
98
|
|| pathname === '/api/auth'
|
|
94
99
|
|| pathname === '/api/healthz'
|
|
95
100
|
|| isWebhookTrigger
|
|
96
101
|
|| isConnectorWebhook
|
|
102
|
+
|| isA2ARoute
|
|
97
103
|
) {
|
|
98
104
|
return NextResponse.next()
|
|
99
105
|
}
|