create-walle 0.9.25 → 0.9.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/bin/create-walle.js +815 -45
- package/package.json +2 -2
- package/template/bin/ctm-dev-cleanup.js +90 -4
- package/template/bin/ctm-launch.sh +49 -1
- package/template/bin/dev.sh +45 -1
- package/template/bin/ensure-stable-node.js +132 -0
- package/template/bin/install-service.sh +9 -0
- package/template/claude-task-manager/api-prompts.js +899 -119
- package/template/claude-task-manager/approval-agent.js +360 -40
- package/template/claude-task-manager/bin/ctm-disclaim.c +42 -0
- package/template/claude-task-manager/bin/ctm-hotkey.swift +67 -81
- package/template/claude-task-manager/bin/ctm-screen-auth.swift +37 -0
- package/template/claude-task-manager/bin/install-hotkey.sh +97 -49
- package/template/claude-task-manager/bin/restart-ctm.sh +14 -0
- package/template/claude-task-manager/db.js +399 -48
- package/template/claude-task-manager/docs/approval-hook-sandbox.md +84 -0
- package/template/claude-task-manager/docs/codex-app-server-approvals.md +72 -0
- package/template/claude-task-manager/docs/codex-native-sandbox.md +47 -0
- package/template/claude-task-manager/docs/prompt-editing-tree-design.md +18 -1
- package/template/claude-task-manager/lib/approval-hook.js +200 -0
- package/template/claude-task-manager/lib/approval-self-adapt.js +1 -0
- package/template/claude-task-manager/lib/auth-rules.js +11 -0
- package/template/claude-task-manager/lib/background-llm.js +32 -4
- package/template/claude-task-manager/lib/codesign-identity.js +140 -0
- package/template/claude-task-manager/lib/codex-app-server-client.js +119 -0
- package/template/claude-task-manager/lib/codex-approval-bridge.js +118 -0
- package/template/claude-task-manager/lib/codex-history-terminal-renderer.js +571 -0
- package/template/claude-task-manager/lib/codex-paths.js +73 -0
- package/template/claude-task-manager/lib/codex-rollout-snapshot.js +164 -0
- package/template/claude-task-manager/lib/codex-rollout-tail.js +72 -0
- package/template/claude-task-manager/lib/codex-sandbox-args.js +47 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +118 -71
- package/template/claude-task-manager/lib/command-targets.js +163 -0
- package/template/claude-task-manager/lib/conversation-tail-merge.js +61 -19
- package/template/claude-task-manager/lib/db-owner-worker-client.js +29 -1
- package/template/claude-task-manager/lib/escalation-review.js +80 -3
- package/template/claude-task-manager/lib/flow-control.js +52 -0
- package/template/claude-task-manager/lib/fs-watcher.js +24 -15
- package/template/claude-task-manager/lib/ingest-cooldown.js +68 -0
- package/template/claude-task-manager/lib/jsonl-conversation-parser.js +8 -4
- package/template/claude-task-manager/lib/launchd-recovery.js +92 -0
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +207 -52
- package/template/claude-task-manager/lib/mobile-push-store.js +7 -0
- package/template/claude-task-manager/lib/model-overview-brain-fallback.js +102 -1
- package/template/claude-task-manager/lib/model-overview-cache.js +1 -0
- package/template/claude-task-manager/lib/oauth-proxy-supervisor.js +2 -1
- package/template/claude-task-manager/lib/perf-tracker.js +29 -2
- package/template/claude-task-manager/lib/permission-match.js +146 -16
- package/template/claude-task-manager/lib/project-slug.js +33 -0
- package/template/claude-task-manager/lib/prompt-intent.js +51 -4
- package/template/claude-task-manager/lib/read-pool-client.js +48 -3
- package/template/claude-task-manager/lib/real-node.js +73 -0
- package/template/claude-task-manager/lib/runtime-work-registry.js +131 -14
- package/template/claude-task-manager/lib/session-content-backfill.js +24 -5
- package/template/claude-task-manager/lib/session-diagnostics-batch.js +87 -0
- package/template/claude-task-manager/lib/session-history.js +5 -7
- package/template/claude-task-manager/lib/session-host-manager.js +19 -0
- package/template/claude-task-manager/lib/session-jobs.js +6 -0
- package/template/claude-task-manager/lib/session-message-response-cache.js +89 -0
- package/template/claude-task-manager/lib/session-messages-page.js +211 -0
- package/template/claude-task-manager/lib/session-messages-projection.js +170 -0
- package/template/claude-task-manager/lib/session-standup.js +8 -0
- package/template/claude-task-manager/lib/session-timeline-summary.js +16 -2
- package/template/claude-task-manager/lib/session-token-usage.js +30 -8
- package/template/claude-task-manager/lib/session-workspace-binding.js +29 -15
- package/template/claude-task-manager/lib/storage-migration.js +2 -1
- package/template/claude-task-manager/lib/transcript-store.js +179 -12
- package/template/claude-task-manager/lib/walle-ctm-history.js +298 -11
- package/template/claude-task-manager/lib/walle-permission-reply.js +49 -0
- package/template/claude-task-manager/lib/walle-session-cache.js +22 -1
- package/template/claude-task-manager/lib/walle-supervisor.js +42 -3
- package/template/claude-task-manager/package.json +5 -2
- package/template/claude-task-manager/prompt-harvest.js +31 -11
- package/template/claude-task-manager/providers/claude-code.js +29 -1
- package/template/claude-task-manager/providers/codex.js +13 -1
- package/template/claude-task-manager/public/css/setup.css +11 -0
- package/template/claude-task-manager/public/css/walle-session.css +132 -4
- package/template/claude-task-manager/public/css/walle.css +89 -0
- package/template/claude-task-manager/public/icon-16.png +0 -0
- package/template/claude-task-manager/public/icon-32.png +0 -0
- package/template/claude-task-manager/public/icon-512.png +0 -0
- package/template/claude-task-manager/public/index.html +2483 -165
- package/template/claude-task-manager/public/js/activation-render-check.js +55 -0
- package/template/claude-task-manager/public/js/flow-control-policy.js +52 -0
- package/template/claude-task-manager/public/js/message-renderer.js +60 -1
- package/template/claude-task-manager/public/js/prompts.js +13 -1
- package/template/claude-task-manager/public/js/session-status-precedence.js +9 -3
- package/template/claude-task-manager/public/js/setup.js +54 -10
- package/template/claude-task-manager/public/js/stream-resize-policy.js +80 -0
- package/template/claude-task-manager/public/js/stream-view.js +78 -0
- package/template/claude-task-manager/public/js/terminal-reconciler.js +52 -2
- package/template/claude-task-manager/public/js/tool-state.js +155 -0
- package/template/claude-task-manager/public/js/walle-session.js +887 -326
- package/template/claude-task-manager/public/js/walle.js +306 -195
- package/template/claude-task-manager/public/m/app.css +1 -0
- package/template/claude-task-manager/public/m/app.js +33 -3
- package/template/claude-task-manager/queue-engine.js +45 -1
- package/template/claude-task-manager/server.js +3367 -540
- package/template/claude-task-manager/workers/approval-blocklist.js +130 -17
- package/template/claude-task-manager/workers/db-owner-worker.js +31 -1
- package/template/claude-task-manager/workers/read-pool-worker.js +92 -5
- package/template/claude-task-manager/workers/session-host-process.js +10 -0
- package/template/claude-task-manager/workers/state-detectors/codex.js +58 -7
- package/template/package.json +2 -3
- package/template/shared/icons/AppIcon-ctm.icns +0 -0
- package/template/shared/icons/AppIcon-walle.icns +0 -0
- package/template/wall-e/agent.js +139 -18
- package/template/wall-e/api-walle.js +201 -22
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +1981 -0
- package/template/wall-e/brain.js +1049 -39
- package/template/wall-e/chat.js +427 -86
- package/template/wall-e/coding/acceptance-contract.js +26 -1
- package/template/wall-e/coding/action-memory-policy.js +353 -0
- package/template/wall-e/coding/action-memory-store.js +814 -0
- package/template/wall-e/coding/initial-messages.js +197 -0
- package/template/wall-e/coding/no-progress-guard.js +327 -0
- package/template/wall-e/coding/permission-service.js +88 -22
- package/template/wall-e/coding/session-workspaces.js +81 -0
- package/template/wall-e/coding/shell-sandbox.js +124 -0
- package/template/wall-e/coding/stream-processor.js +63 -2
- package/template/wall-e/coding/tool-execution-controller.js +14 -1
- package/template/wall-e/coding/tool-registry.js +1 -1
- package/template/wall-e/coding/transcript-writer.js +3 -0
- package/template/wall-e/coding-orchestrator.js +636 -35
- package/template/wall-e/coding-prompts.js +51 -2
- package/template/wall-e/docs/model-routing-policy.md +59 -0
- package/template/wall-e/docs/walle-shell-sandbox.md +61 -0
- package/template/wall-e/extraction/knowledge-extractor.js +76 -23
- package/template/wall-e/http/chat-api.js +30 -12
- package/template/wall-e/http/model-admin.js +93 -1
- package/template/wall-e/lib/background-lanes.js +133 -0
- package/template/wall-e/lib/boot-profile.js +11 -0
- package/template/wall-e/lib/brain-owner-worker-client.js +324 -0
- package/template/wall-e/lib/brain-read-pool-client.js +311 -0
- package/template/wall-e/lib/diagnostics-flags.js +87 -0
- package/template/wall-e/lib/event-loop-monitor.js +74 -3
- package/template/wall-e/lib/mcp-integration.js +7 -1
- package/template/wall-e/lib/real-node.js +98 -0
- package/template/wall-e/lib/runtime-health.js +206 -0
- package/template/wall-e/lib/runtime-worker-pool.js +101 -0
- package/template/wall-e/lib/scheduler-worker-jobs.js +231 -0
- package/template/wall-e/lib/scheduler.js +446 -17
- package/template/wall-e/lib/service-health.js +61 -2
- package/template/wall-e/lib/service-readiness.js +258 -0
- package/template/wall-e/lib/usage.js +152 -0
- package/template/wall-e/lib/worker-thread-pool.js +389 -0
- package/template/wall-e/llm/client.js +81 -4
- package/template/wall-e/llm/default-fallback.js +54 -8
- package/template/wall-e/llm/mlx.js +536 -73
- package/template/wall-e/llm/mlx.plugin.json +1 -1
- package/template/wall-e/llm/ollama.js +342 -43
- package/template/wall-e/llm/provider-error.js +18 -1
- package/template/wall-e/llm/provider-health-state.js +176 -0
- package/template/wall-e/llm/routing-policy.js +796 -0
- package/template/wall-e/llm/supported-models.js +5 -0
- package/template/wall-e/loops/tasks.js +60 -14
- package/template/wall-e/loops/think.js +89 -24
- package/template/wall-e/mcp-server.js +192 -28
- package/template/wall-e/server.js +32 -7
- package/template/wall-e/skills/script-skill-runner.js +8 -1
- package/template/wall-e/skills/skill-planner.js +64 -1
- package/template/wall-e/tools/builtin-middleware.js +67 -2
- package/template/wall-e/tools/local-tools.js +116 -26
- package/template/wall-e/tools/permission-checker.js +52 -4
- package/template/wall-e/tools/permission-rules.js +36 -0
- package/template/wall-e/tools/shell-analyzer.js +46 -1
- package/template/wall-e/training/gemma-e4b-qlora.js +314 -0
- package/template/wall-e/training/real-trajectory-miner.js +2617 -0
- package/template/wall-e/training/replay-eval-analysis.js +151 -0
- package/template/wall-e/training/run-shell-command-selector.js +277 -0
- package/template/wall-e/training/tool-sft-dataset.js +312 -0
- package/template/wall-e/training/tool-sft-renderers.js +144 -0
- package/template/wall-e/training/tool-trace-harvester.js +1440 -0
- package/template/wall-e/training/trajectory-action-selector.js +364 -0
- package/template/wall-e/weather-runtime.js +232 -0
- package/template/wall-e/workers/brain-owner-worker.js +162 -0
- package/template/wall-e/workers/brain-read-worker.js +148 -0
- package/template/wall-e/workers/runtime-worker.js +145 -0
|
@@ -6,6 +6,21 @@ function isGemma4Model(model) {
|
|
|
6
6
|
return /^gemma4:/i.test(String(model || ''));
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
function isFastGemma4Model(model) {
|
|
10
|
+
return /^gemma4:e4b(?:$|[-:])/i.test(String(model || ''));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function envPositiveInt(name, fallback) {
|
|
14
|
+
const raw = process.env[name];
|
|
15
|
+
const n = Number(raw);
|
|
16
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function defaultNumCtxForModel(model) {
|
|
20
|
+
if (isGemma4Model(model)) return envPositiveInt('WALLE_OLLAMA_GEMMA_NUM_CTX', 16384);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
function isReasoningCapableOllamaModel(model) {
|
|
10
25
|
return /^(gemma4:|gpt-oss\b|deepseek-r1\b|qwen3\b|qwq\b|phi-?4.*reason)/i.test(String(model || ''));
|
|
11
26
|
}
|
|
@@ -40,26 +55,201 @@ function buildOllamaOptions({ model, maxTokens, temperature, options } = {}) {
|
|
|
40
55
|
if (maxTokens != null) out.num_predict = maxTokens;
|
|
41
56
|
if (temperature != null) out.temperature = temperature;
|
|
42
57
|
if (isGemma4Model(model) && out.num_ctx == null) {
|
|
43
|
-
out.num_ctx =
|
|
58
|
+
out.num_ctx = defaultNumCtxForModel(model);
|
|
44
59
|
}
|
|
45
60
|
return Object.keys(out).length ? out : undefined;
|
|
46
61
|
}
|
|
47
62
|
|
|
63
|
+
function compactError(err) {
|
|
64
|
+
const msg = String(err?.message || err || '').replace(/\s+/g, ' ').trim();
|
|
65
|
+
if (/aborted|timeout/i.test(msg)) return 'request_timeout';
|
|
66
|
+
if (/ECONNREFUSED|fetch failed|connection/i.test(msg)) return 'daemon_unreachable';
|
|
67
|
+
return msg.slice(0, 160) || 'unknown_error';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function durationNsToMs(value) {
|
|
71
|
+
const n = Number(value || 0);
|
|
72
|
+
return Number.isFinite(n) && n > 0 ? Math.round(n / 1e6) : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function usageFromOpenAICompat(raw, latencyMs) {
|
|
76
|
+
const base = raw?.usage
|
|
77
|
+
? {
|
|
78
|
+
input: raw.usage.prompt_tokens ?? raw.usage.prompt_eval_count ?? 0,
|
|
79
|
+
output: raw.usage.completion_tokens ?? raw.usage.eval_count ?? 0,
|
|
80
|
+
}
|
|
81
|
+
: { input: 0, output: 0 };
|
|
82
|
+
const evalDurationNs = raw?.eval_duration || raw?.usage?.eval_duration || 0;
|
|
83
|
+
const evalCount = raw?.eval_count || raw?.usage?.eval_count || base.output || 0;
|
|
84
|
+
const promptEvalDurationNs = raw?.prompt_eval_duration || raw?.usage?.prompt_eval_duration || 0;
|
|
85
|
+
const promptEvalCount = raw?.prompt_eval_count || raw?.usage?.prompt_eval_count || base.input || 0;
|
|
86
|
+
const genTokPerSec = evalDurationNs > 0 ? evalCount / (evalDurationNs / 1e9) : null;
|
|
87
|
+
const prefillTokPerSec = promptEvalDurationNs > 0 ? promptEvalCount / (promptEvalDurationNs / 1e9) : null;
|
|
88
|
+
return {
|
|
89
|
+
...base,
|
|
90
|
+
requestLatencyMs: latencyMs,
|
|
91
|
+
openAiCompat: true,
|
|
92
|
+
genTokPerSec,
|
|
93
|
+
prefillTokPerSec,
|
|
94
|
+
evalDurationMs: durationNsToMs(evalDurationNs),
|
|
95
|
+
promptEvalDurationMs: durationNsToMs(promptEvalDurationNs),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function textFromContent(content) {
|
|
100
|
+
if (content == null) return '';
|
|
101
|
+
if (typeof content === 'string') return content;
|
|
102
|
+
if (Array.isArray(content)) {
|
|
103
|
+
return content.map((part) => {
|
|
104
|
+
if (!part) return '';
|
|
105
|
+
if (typeof part === 'string') return part;
|
|
106
|
+
return part.text || part.content || '';
|
|
107
|
+
}).filter(Boolean).join('\n');
|
|
108
|
+
}
|
|
109
|
+
if (typeof content === 'object') {
|
|
110
|
+
if (content.text != null) return textFromContent(content.text);
|
|
111
|
+
if (content.content != null) return textFromContent(content.content);
|
|
112
|
+
try { return JSON.stringify(content); } catch { return String(content); }
|
|
113
|
+
}
|
|
114
|
+
return String(content);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function messagesToOllamaNative(messages = []) {
|
|
118
|
+
const out = [];
|
|
119
|
+
const toolNamesById = new Map();
|
|
120
|
+
|
|
121
|
+
for (const msg of messages || []) {
|
|
122
|
+
if (!msg || !msg.role) continue;
|
|
123
|
+
if (typeof msg.content === 'string') {
|
|
124
|
+
const converted = { role: msg.role, content: msg.content };
|
|
125
|
+
if (msg.role === 'assistant' && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
|
|
126
|
+
converted.tool_calls = msg.tool_calls.map((call, index) => {
|
|
127
|
+
const id = call.id || call.toolCallId || call.tool_use_id || `ollama_tool_${index}`;
|
|
128
|
+
const name = call.name || call.tool || call.function?.name || '';
|
|
129
|
+
if (id && name) toolNamesById.set(id, name);
|
|
130
|
+
return { function: { name, arguments: call.input || call.arguments || call.function?.arguments || {} } };
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (msg.role === 'tool') converted.tool_name = msg.tool_name || msg.name || toolNamesById.get(msg.tool_call_id) || '';
|
|
134
|
+
out.push(converted);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!Array.isArray(msg.content)) {
|
|
139
|
+
out.push({ role: msg.role, content: textFromContent(msg.content) });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const toolResults = msg.content.filter((part) => part?.type === 'tool_result');
|
|
144
|
+
if (toolResults.length > 0) {
|
|
145
|
+
for (const result of toolResults) {
|
|
146
|
+
const id = result.tool_use_id || result.id || result.toolCallId || '';
|
|
147
|
+
out.push({
|
|
148
|
+
role: 'tool',
|
|
149
|
+
content: textFromContent(result.content),
|
|
150
|
+
tool_name: result.tool_name || result.name || toolNamesById.get(id) || '',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const textParts = msg.content.filter((part) => part?.type === 'text');
|
|
157
|
+
const reasoningParts = msg.content.filter((part) => part?.type === 'reasoning');
|
|
158
|
+
const toolUses = msg.content.filter((part) => part?.type === 'tool_use');
|
|
159
|
+
const converted = {
|
|
160
|
+
role: msg.role,
|
|
161
|
+
content: textParts.map((part) => part.text || part.content || '').filter(Boolean).join('\n') || '',
|
|
162
|
+
};
|
|
163
|
+
if (reasoningParts.length > 0) {
|
|
164
|
+
converted.thinking = reasoningParts.map((part) => part.text || part.content || '').filter(Boolean).join('\n');
|
|
165
|
+
}
|
|
166
|
+
if (toolUses.length > 0) {
|
|
167
|
+
converted.tool_calls = toolUses.map((call, index) => {
|
|
168
|
+
const id = call.id || call.tool_use_id || call.toolCallId || `ollama_tool_${index}`;
|
|
169
|
+
if (id && call.name) toolNamesById.set(id, call.name);
|
|
170
|
+
return { function: { name: call.name || '', arguments: call.input || {} } };
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
out.push(converted);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function responseFromOllamaNative(raw, latencyMs) {
|
|
179
|
+
const msg = raw?.message || {};
|
|
180
|
+
const toolCalls = (msg.tool_calls || []).map((call, index) => ({
|
|
181
|
+
id: call.id || call.toolCallId || call.tool_use_id || `ollama_tool_${index}`,
|
|
182
|
+
name: call.name || call.tool || call.function?.name || '',
|
|
183
|
+
input: call.input || call.arguments || call.function?.arguments || {},
|
|
184
|
+
})).filter((call) => call.name);
|
|
185
|
+
const evalDurationNs = raw?.eval_duration || 0;
|
|
186
|
+
const evalCount = raw?.eval_count || 0;
|
|
187
|
+
const promptEvalDurationNs = raw?.prompt_eval_duration || 0;
|
|
188
|
+
const promptEvalCount = raw?.prompt_eval_count || 0;
|
|
189
|
+
const genTokPerSec = evalDurationNs > 0 ? evalCount / (evalDurationNs / 1e9) : null;
|
|
190
|
+
const prefillTokPerSec = promptEvalDurationNs > 0 ? promptEvalCount / (promptEvalDurationNs / 1e9) : null;
|
|
191
|
+
return {
|
|
192
|
+
content: msg.content || null,
|
|
193
|
+
reasoningContent: msg.thinking || msg.reasoning || msg.reasoning_content || null,
|
|
194
|
+
toolCalls,
|
|
195
|
+
stopReason: toolCalls.length > 0 ? 'tool_use' : 'end_turn',
|
|
196
|
+
usage: {
|
|
197
|
+
input: promptEvalCount,
|
|
198
|
+
output: evalCount,
|
|
199
|
+
genTokPerSec,
|
|
200
|
+
prefillTokPerSec,
|
|
201
|
+
evalDurationMs: evalDurationNs / 1e6,
|
|
202
|
+
promptEvalDurationMs: promptEvalDurationNs / 1e6,
|
|
203
|
+
requestLatencyMs: latencyMs,
|
|
204
|
+
},
|
|
205
|
+
latencyMs,
|
|
206
|
+
raw,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function fetchJson(url, opts = {}) {
|
|
211
|
+
const timeoutMs = opts.timeoutMs || 5000;
|
|
212
|
+
const init = { ...opts };
|
|
213
|
+
delete init.timeoutMs;
|
|
214
|
+
if (!init.signal && timeoutMs > 0) init.signal = AbortSignal.timeout(timeoutMs);
|
|
215
|
+
const resp = await fetch(url, init);
|
|
216
|
+
const text = await resp.text();
|
|
217
|
+
let json = null;
|
|
218
|
+
try { json = text ? JSON.parse(text) : null; } catch {}
|
|
219
|
+
if (!resp.ok) {
|
|
220
|
+
const detail = json?.error || text || `HTTP ${resp.status}`;
|
|
221
|
+
const err = new Error(`Ollama API error ${resp.status}: ${detail}`);
|
|
222
|
+
err.status = resp.status;
|
|
223
|
+
err.body = text;
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
return json;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function ollamaModelName(model) {
|
|
230
|
+
return String(model?.name || model?.model || '').trim();
|
|
231
|
+
}
|
|
232
|
+
|
|
48
233
|
/**
|
|
49
234
|
* Create an Ollama local-model LLM provider.
|
|
50
235
|
* Uses Ollama's OpenAI-compatible API — no npm dependency needed.
|
|
51
236
|
* @param {{ baseUrl?: string }} config
|
|
52
237
|
*/
|
|
53
238
|
function createOllamaProvider(config = {}) {
|
|
54
|
-
const baseUrl = (config.baseUrl || 'http://
|
|
239
|
+
const baseUrl = (config.baseUrl || 'http://127.0.0.1:11434').replace(/\/+$/, '');
|
|
55
240
|
const defaultThink = config.think ?? false;
|
|
241
|
+
const defaultKeepAlive = config.keepAlive ?? process.env.OLLAMA_KEEP_ALIVE ?? '30m';
|
|
56
242
|
|
|
57
243
|
return {
|
|
58
244
|
type: 'ollama',
|
|
59
245
|
|
|
60
|
-
async chat({ model, messages, tools, system, maxTokens, temperature, signal, thinking, reasoningEffort, options } = {}) {
|
|
246
|
+
async chat({ model, messages, tools, system, maxTokens, temperature, signal, thinking, reasoningEffort, options, keepAlive } = {}) {
|
|
61
247
|
const modelId = model || 'gemma4:e4b';
|
|
62
248
|
const openaiMessages = [];
|
|
249
|
+
const nativeMessages = messagesToOllamaNative([
|
|
250
|
+
...(system ? [{ role: 'system', content: system }] : []),
|
|
251
|
+
...((messages && messages.length > 0) ? messages : []),
|
|
252
|
+
]);
|
|
63
253
|
|
|
64
254
|
// System prompt as first message
|
|
65
255
|
if (system) {
|
|
@@ -71,28 +261,51 @@ function createOllamaProvider(config = {}) {
|
|
|
71
261
|
openaiMessages.push(...messagesToOpenAI(messages));
|
|
72
262
|
}
|
|
73
263
|
|
|
74
|
-
//
|
|
75
|
-
|
|
264
|
+
// Native Ollama API honors options/keep_alive and returns eval_duration
|
|
265
|
+
// timing. Keep tool calls on the OpenAI-compatible path by default: it is
|
|
266
|
+
// currently more reliable for Gemma4 coding-agent tool selection. Native
|
|
267
|
+
// tools remain available for explicit probes via config.useNativeTools.
|
|
268
|
+
const useNativeApi = !tools || tools.length === 0 || config.useNativeTools === true;
|
|
76
269
|
|
|
77
270
|
if (useNativeApi) {
|
|
78
271
|
const resolvedThink = resolveThinkValue({ model: modelId, thinking, reasoningEffort });
|
|
79
272
|
const nativeBody = {
|
|
80
273
|
model: modelId,
|
|
81
|
-
messages:
|
|
274
|
+
messages: nativeMessages,
|
|
82
275
|
stream: false,
|
|
83
276
|
think: resolvedThink !== undefined ? resolvedThink : defaultThink,
|
|
84
277
|
};
|
|
278
|
+
const resolvedKeepAlive = keepAlive ?? defaultKeepAlive;
|
|
279
|
+
if (resolvedKeepAlive != null && resolvedKeepAlive !== '') nativeBody.keep_alive = resolvedKeepAlive;
|
|
85
280
|
const nativeOptions = buildOllamaOptions({ model: modelId, maxTokens, temperature, options });
|
|
86
281
|
if (nativeOptions) nativeBody.options = nativeOptions;
|
|
282
|
+
const openaiTools = toOpenAI(tools);
|
|
283
|
+
if (openaiTools && openaiTools.length > 0) nativeBody.tools = openaiTools;
|
|
87
284
|
|
|
88
285
|
const start = Date.now();
|
|
89
|
-
|
|
286
|
+
let resp = await fetch(`${baseUrl}/api/chat`, {
|
|
90
287
|
method: 'POST',
|
|
91
288
|
headers: { 'Content-Type': 'application/json' },
|
|
92
289
|
body: JSON.stringify(nativeBody),
|
|
93
290
|
signal,
|
|
94
291
|
});
|
|
95
292
|
|
|
293
|
+
if (!resp.ok && resp.status === 400 && nativeBody.tools) {
|
|
294
|
+
const errText = await resp.text().catch(() => '');
|
|
295
|
+
if (errText.includes('does not support tools')) {
|
|
296
|
+
console.log(`[ollama] ${model} does not support tools — retrying without`);
|
|
297
|
+
delete nativeBody.tools;
|
|
298
|
+
resp = await fetch(`${baseUrl}/api/chat`, {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
headers: { 'Content-Type': 'application/json' },
|
|
301
|
+
body: JSON.stringify(nativeBody),
|
|
302
|
+
signal,
|
|
303
|
+
});
|
|
304
|
+
} else {
|
|
305
|
+
throw new Error(`Ollama API error ${resp.status}: ${errText}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
96
309
|
if (!resp.ok) {
|
|
97
310
|
const text = await resp.text().catch(() => '');
|
|
98
311
|
throw new Error(`Ollama API error ${resp.status}: ${text}`);
|
|
@@ -100,34 +313,10 @@ function createOllamaProvider(config = {}) {
|
|
|
100
313
|
|
|
101
314
|
const raw = await resp.json();
|
|
102
315
|
const latencyMs = Date.now() - start;
|
|
103
|
-
const msg = raw.message || {};
|
|
104
|
-
const reasoningContent = msg.thinking || msg.reasoning || msg.reasoning_content || null;
|
|
105
|
-
|
|
106
|
-
// Extract accurate timing from native API (durations are in nanoseconds)
|
|
107
|
-
const evalDurationNs = raw.eval_duration || 0;
|
|
108
|
-
const evalCount = raw.eval_count || 0;
|
|
109
|
-
const promptEvalDurationNs = raw.prompt_eval_duration || 0;
|
|
110
|
-
const promptEvalCount = raw.prompt_eval_count || 0;
|
|
111
|
-
const genTokPerSec = evalDurationNs > 0 ? evalCount / (evalDurationNs / 1e9) : null;
|
|
112
|
-
const prefillTokPerSec = promptEvalDurationNs > 0 ? promptEvalCount / (promptEvalDurationNs / 1e9) : null;
|
|
113
|
-
|
|
114
316
|
return {
|
|
115
|
-
|
|
116
|
-
reasoningContent,
|
|
117
|
-
toolCalls: [],
|
|
118
|
-
stopReason: 'end_turn',
|
|
119
|
-
usage: {
|
|
120
|
-
input: promptEvalCount,
|
|
121
|
-
output: evalCount,
|
|
122
|
-
genTokPerSec,
|
|
123
|
-
prefillTokPerSec,
|
|
124
|
-
evalDurationMs: evalDurationNs / 1e6,
|
|
125
|
-
promptEvalDurationMs: promptEvalDurationNs / 1e6,
|
|
126
|
-
},
|
|
127
|
-
latencyMs,
|
|
317
|
+
...responseFromOllamaNative(raw, latencyMs),
|
|
128
318
|
model: modelId,
|
|
129
319
|
provider: 'ollama',
|
|
130
|
-
raw,
|
|
131
320
|
};
|
|
132
321
|
}
|
|
133
322
|
|
|
@@ -145,7 +334,6 @@ function createOllamaProvider(config = {}) {
|
|
|
145
334
|
|
|
146
335
|
if (maxTokens != null) body.max_tokens = maxTokens;
|
|
147
336
|
if (temperature != null) body.temperature = temperature;
|
|
148
|
-
|
|
149
337
|
const start = Date.now();
|
|
150
338
|
let resp = await fetch(`${baseUrl}/v1/chat/completions`, {
|
|
151
339
|
method: 'POST',
|
|
@@ -178,21 +366,54 @@ function createOllamaProvider(config = {}) {
|
|
|
178
366
|
|
|
179
367
|
const raw = await resp.json();
|
|
180
368
|
const normalized = responseFromOpenAI(raw);
|
|
369
|
+
const latencyMs = Date.now() - start;
|
|
181
370
|
|
|
182
371
|
let stopReason = 'end_turn';
|
|
183
372
|
if (normalized.stopReason === 'tool_calls') stopReason = 'tool_use';
|
|
184
373
|
else if (normalized.stopReason === 'stop' || !normalized.stopReason) stopReason = 'end_turn';
|
|
185
374
|
|
|
186
|
-
return {
|
|
375
|
+
return {
|
|
376
|
+
...normalized,
|
|
377
|
+
usage: usageFromOpenAICompat(raw, latencyMs),
|
|
378
|
+
stopReason,
|
|
379
|
+
latencyMs,
|
|
380
|
+
model: modelId,
|
|
381
|
+
provider: 'ollama',
|
|
382
|
+
raw,
|
|
383
|
+
};
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
async prewarm({ model, keepAlive, timeoutMs = 120000 } = {}) {
|
|
387
|
+
const modelId = model || config.model || 'gemma4:e4b';
|
|
388
|
+
const body = {
|
|
389
|
+
model: modelId,
|
|
390
|
+
messages: [{ role: 'user', content: 'Reply OK.' }],
|
|
391
|
+
stream: false,
|
|
392
|
+
think: false,
|
|
393
|
+
keep_alive: keepAlive ?? defaultKeepAlive,
|
|
394
|
+
options: { num_predict: 8, num_ctx: Math.min(defaultNumCtxForModel(modelId) || 2048, 2048) },
|
|
395
|
+
};
|
|
396
|
+
const started = Date.now();
|
|
397
|
+
const raw = await fetchJson(`${baseUrl}/api/chat`, {
|
|
398
|
+
method: 'POST',
|
|
399
|
+
headers: { 'Content-Type': 'application/json' },
|
|
400
|
+
body: JSON.stringify(body),
|
|
401
|
+
timeoutMs,
|
|
402
|
+
});
|
|
403
|
+
return {
|
|
404
|
+
ok: Boolean(raw?.message?.content),
|
|
405
|
+
latencyMs: Date.now() - started,
|
|
406
|
+
totalDurationMs: durationNsToMs(raw?.total_duration),
|
|
407
|
+
loadDurationMs: durationNsToMs(raw?.load_duration),
|
|
408
|
+
promptEvalCount: raw?.prompt_eval_count ?? null,
|
|
409
|
+
evalCount: raw?.eval_count ?? null,
|
|
410
|
+
raw,
|
|
411
|
+
};
|
|
187
412
|
},
|
|
188
413
|
|
|
189
414
|
async listModels() {
|
|
190
415
|
try {
|
|
191
|
-
const
|
|
192
|
-
signal: AbortSignal.timeout(3000),
|
|
193
|
-
});
|
|
194
|
-
if (!resp.ok) return [];
|
|
195
|
-
const data = await resp.json();
|
|
416
|
+
const data = await fetchJson(`${baseUrl}/api/tags`, { timeoutMs: 3000 });
|
|
196
417
|
return (data.models || [])
|
|
197
418
|
// Drop embedding models — they're not chat-capable.
|
|
198
419
|
.filter((m) => !/embed|embedding/i.test(m.name || ''))
|
|
@@ -209,14 +430,91 @@ function createOllamaProvider(config = {}) {
|
|
|
209
430
|
|
|
210
431
|
async isAvailable() {
|
|
211
432
|
try {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
});
|
|
215
|
-
return resp.ok;
|
|
433
|
+
await fetchJson(`${baseUrl}/api/tags`, { timeoutMs: 3000 });
|
|
434
|
+
return true;
|
|
216
435
|
} catch {
|
|
217
436
|
return false;
|
|
218
437
|
}
|
|
219
438
|
},
|
|
439
|
+
|
|
440
|
+
async probeConnection({ model, timeoutMs = 25000, chatTimeoutMs, keepAlive } = {}) {
|
|
441
|
+
const started = Date.now();
|
|
442
|
+
const modelId = model || config.model || 'gemma4:e4b';
|
|
443
|
+
const probe = {
|
|
444
|
+
ok: false,
|
|
445
|
+
provider: 'ollama',
|
|
446
|
+
model: modelId,
|
|
447
|
+
base_url: baseUrl,
|
|
448
|
+
checked_at: new Date().toISOString(),
|
|
449
|
+
daemon_ok: false,
|
|
450
|
+
model_present: false,
|
|
451
|
+
loaded: null,
|
|
452
|
+
diagnostics: {},
|
|
453
|
+
};
|
|
454
|
+
try {
|
|
455
|
+
const tags = await fetchJson(`${baseUrl}/api/tags`, { timeoutMs: Math.min(timeoutMs, 5000) });
|
|
456
|
+
probe.daemon_ok = true;
|
|
457
|
+
const models = Array.isArray(tags.models) ? tags.models : [];
|
|
458
|
+
probe.models = models
|
|
459
|
+
.map((m) => ollamaModelName(m))
|
|
460
|
+
.filter(Boolean)
|
|
461
|
+
.filter((name) => !/embed|embedding/i.test(name));
|
|
462
|
+
probe.model_present = probe.models.includes(modelId);
|
|
463
|
+
if (!probe.model_present) {
|
|
464
|
+
probe.error_type = 'model_missing';
|
|
465
|
+
probe.error = `Ollama model "${modelId}" is not installed.`;
|
|
466
|
+
probe.latency_ms = Date.now() - started;
|
|
467
|
+
return probe;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
const ps = await fetchJson(`${baseUrl}/api/ps`, { timeoutMs: Math.min(timeoutMs, 5000) });
|
|
472
|
+
const loadedModels = Array.isArray(ps.models) ? ps.models : [];
|
|
473
|
+
const loaded = loadedModels.find((m) => ollamaModelName(m) === modelId) || null;
|
|
474
|
+
probe.loaded = loaded ? {
|
|
475
|
+
loaded: true,
|
|
476
|
+
context_length: loaded.context_length ?? loaded.details?.context_length ?? null,
|
|
477
|
+
expires_at: loaded.expires_at || null,
|
|
478
|
+
size_vram: loaded.size_vram ?? null,
|
|
479
|
+
} : { loaded: false };
|
|
480
|
+
} catch (err) {
|
|
481
|
+
probe.loaded = { loaded: null, error: compactError(err) };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const chatStart = Date.now();
|
|
485
|
+
const chat = await fetchJson(`${baseUrl}/api/chat`, {
|
|
486
|
+
method: 'POST',
|
|
487
|
+
headers: { 'Content-Type': 'application/json' },
|
|
488
|
+
body: JSON.stringify({
|
|
489
|
+
model: modelId,
|
|
490
|
+
messages: [{ role: 'user', content: 'Reply with OK.' }],
|
|
491
|
+
stream: false,
|
|
492
|
+
think: false,
|
|
493
|
+
keep_alive: keepAlive ?? defaultKeepAlive,
|
|
494
|
+
options: { num_predict: 8, num_ctx: 2048 },
|
|
495
|
+
}),
|
|
496
|
+
timeoutMs: chatTimeoutMs || timeoutMs,
|
|
497
|
+
});
|
|
498
|
+
const content = String(chat?.message?.content || '').trim();
|
|
499
|
+
probe.ok = content.length > 0;
|
|
500
|
+
probe.error_type = probe.ok ? '' : 'empty_response';
|
|
501
|
+
probe.error = probe.ok ? '' : 'Ollama returned an empty probe response.';
|
|
502
|
+
probe.latency_ms = Date.now() - started;
|
|
503
|
+
probe.diagnostics = {
|
|
504
|
+
chat_latency_ms: Date.now() - chatStart,
|
|
505
|
+
total_duration_ms: durationNsToMs(chat?.total_duration),
|
|
506
|
+
load_duration_ms: durationNsToMs(chat?.load_duration),
|
|
507
|
+
prompt_eval_count: chat?.prompt_eval_count ?? null,
|
|
508
|
+
eval_count: chat?.eval_count ?? null,
|
|
509
|
+
};
|
|
510
|
+
return probe;
|
|
511
|
+
} catch (err) {
|
|
512
|
+
probe.error_type = compactError(err);
|
|
513
|
+
probe.error = String(err?.message || err || 'Ollama probe failed').slice(0, 240);
|
|
514
|
+
probe.latency_ms = Date.now() - started;
|
|
515
|
+
return probe;
|
|
516
|
+
}
|
|
517
|
+
},
|
|
220
518
|
};
|
|
221
519
|
}
|
|
222
520
|
|
|
@@ -224,6 +522,7 @@ module.exports = {
|
|
|
224
522
|
createOllamaProvider,
|
|
225
523
|
buildOllamaOptions,
|
|
226
524
|
resolveThinkValue,
|
|
525
|
+
isFastGemma4Model,
|
|
227
526
|
isReasoningCapableOllamaModel,
|
|
228
527
|
capabilitiesForOllamaModel,
|
|
229
528
|
};
|
|
@@ -114,6 +114,11 @@ function classifyType(status, lower) {
|
|
|
114
114
|
) return 'auth_error';
|
|
115
115
|
if (status === 404 || /model .*not found|not_found_error|model_not_found|does not exist|unknown model/i.test(lower)) return 'model_unavailable';
|
|
116
116
|
if (/context length exceeded|context_length_exceeded|maximum context|token limit/i.test(lower)) return 'context_window';
|
|
117
|
+
// Connect-phase failures mean the endpoint was never reached (DNS, routing,
|
|
118
|
+
// VPN-only gateways). They must classify as 'network' even though undici
|
|
119
|
+
// spells them "Connect Timeout Error" — the generic 'timeout' advice
|
|
120
|
+
// ("switch provider/model") is wrong when nothing ever connected.
|
|
121
|
+
if (/und_err_connect_timeout|connecttimeouterror|connect timeout|ehostunreach|enetunreach|ehostdown/i.test(lower)) return 'network';
|
|
117
122
|
if (status === 408 || /timeout|timed out|etimedout|aborterror|aborted/i.test(lower)) return 'timeout';
|
|
118
123
|
if (status === 529 || status === 503 || /overloaded|service unavailable|temporarily unavailable|unavailable/i.test(lower)) return 'provider_unavailable';
|
|
119
124
|
if (/fetch failed|network|eai_again|enotfound|econnreset|socket hang up|connection refused|econnrefused/i.test(lower)) return 'network';
|
|
@@ -157,7 +162,7 @@ function messageForType(type) {
|
|
|
157
162
|
case 'network':
|
|
158
163
|
return {
|
|
159
164
|
title: 'AI provider network error',
|
|
160
|
-
body: 'Wall-E could not reach the provider endpoint. Check connectivity, base URL, or local provider status, then retry.',
|
|
165
|
+
body: 'Wall-E could not reach the provider endpoint. Check connectivity (VPN if the endpoint is internal), the base URL, or local provider status, then retry.',
|
|
161
166
|
};
|
|
162
167
|
case 'provider_unavailable':
|
|
163
168
|
return {
|
|
@@ -258,6 +263,18 @@ function toApiPayload(err, context = {}) {
|
|
|
258
263
|
function recordProviderFailureAlert(providerError, brain) {
|
|
259
264
|
if (!providerError) return { alerted: false };
|
|
260
265
|
try {
|
|
266
|
+
try {
|
|
267
|
+
const { recordProviderHealthStatus } = require('./provider-health-state');
|
|
268
|
+
recordProviderHealthStatus({
|
|
269
|
+
provider: providerError.provider || 'default',
|
|
270
|
+
model: providerError.model || '',
|
|
271
|
+
ok: false,
|
|
272
|
+
source: 'provider-error',
|
|
273
|
+
error_type: providerError.type || 'provider_error',
|
|
274
|
+
error: providerError.rawMessage || providerError.userMessage || providerError.title || '',
|
|
275
|
+
checked_at: providerError.createdAt || new Date().toISOString(),
|
|
276
|
+
}, { brain, clearTransientAlerts: false });
|
|
277
|
+
} catch {}
|
|
261
278
|
const planner = require('../skills/skill-planner');
|
|
262
279
|
const actionUrl = providerError.type === 'request_invariant'
|
|
263
280
|
? null
|