create-walle 0.9.13 → 0.9.14

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.
Files changed (58) hide show
  1. package/README.md +6 -1
  2. package/bin/create-walle.js +195 -30
  3. package/bin/mcp-inject.js +18 -53
  4. package/package.json +3 -1
  5. package/template/claude-task-manager/approval-agent.js +7 -0
  6. package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
  7. package/template/claude-task-manager/git-utils.js +111 -3
  8. package/template/claude-task-manager/lib/session-history.js +144 -16
  9. package/template/claude-task-manager/lib/session-standup.js +409 -0
  10. package/template/claude-task-manager/lib/standup-attention.js +200 -0
  11. package/template/claude-task-manager/lib/status-hooks.js +8 -2
  12. package/template/claude-task-manager/lib/update-telemetry.js +114 -0
  13. package/template/claude-task-manager/lib/walle-default-model.js +55 -0
  14. package/template/claude-task-manager/lib/walle-mcp-auto-config.js +62 -0
  15. package/template/claude-task-manager/lib/walle-supervisor.js +83 -19
  16. package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
  17. package/template/claude-task-manager/providers/codex-mcp.js +104 -0
  18. package/template/claude-task-manager/providers/index.js +2 -0
  19. package/template/claude-task-manager/public/css/setup.css +2 -1
  20. package/template/claude-task-manager/public/css/walle.css +5 -0
  21. package/template/claude-task-manager/public/index.html +1596 -283
  22. package/template/claude-task-manager/public/js/session-search-utils.js +171 -1
  23. package/template/claude-task-manager/public/js/setup.js +62 -19
  24. package/template/claude-task-manager/public/js/stream-view.js +55 -6
  25. package/template/claude-task-manager/public/js/walle-session.js +73 -16
  26. package/template/claude-task-manager/public/js/walle.js +34 -2
  27. package/template/claude-task-manager/server.js +780 -177
  28. package/template/claude-task-manager/session-integrity.js +58 -15
  29. package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
  30. package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
  31. package/template/package.json +1 -1
  32. package/template/wall-e/agent.js +36 -7
  33. package/template/wall-e/api-walle.js +72 -20
  34. package/template/wall-e/coding/stream-processor.js +22 -2
  35. package/template/wall-e/coding-orchestrator.js +26 -6
  36. package/template/wall-e/eval/agent-runner.js +16 -4
  37. package/template/wall-e/eval/benchmark-generator.js +21 -1
  38. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
  39. package/template/wall-e/eval/codex-cli-baseline.js +633 -0
  40. package/template/wall-e/eval/eval-orchestrator.js +3 -3
  41. package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
  42. package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
  43. package/template/wall-e/lib/mcp-integration.js +220 -0
  44. package/template/wall-e/llm/ollama.js +47 -8
  45. package/template/wall-e/llm/ollama.plugin.json +1 -1
  46. package/template/wall-e/llm/tool-adapter.js +1 -0
  47. package/template/wall-e/loops/ingest.js +42 -8
  48. package/template/wall-e/mcp-server.js +272 -10
  49. package/template/wall-e/memory/ctm-session-context.js +910 -0
  50. package/template/wall-e/server.js +26 -1
  51. package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
  52. package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
  53. package/template/wall-e/skills/skill-planner.js +52 -3
  54. package/template/wall-e/tools/builtin-middleware.js +55 -2
  55. package/template/wall-e/tools/shell-policy.js +1 -1
  56. package/template/wall-e/tools/slack-owner.js +104 -0
  57. package/template/website/index.html +2 -2
  58. package/template/builder-journal.md +0 -17
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ try {
5
+ const envPath = require('path').resolve(__dirname, '..', '..', '.env');
6
+ const lines = require('fs').readFileSync(envPath, 'utf8').split('\n');
7
+ for (const line of lines) {
8
+ const match = line.match(/^([A-Z_]+)=(.*)$/);
9
+ if (match && !process.env[match[1]]) process.env[match[1]] = match[2];
10
+ }
11
+ } catch {}
12
+
13
+ const crypto = require('crypto');
14
+ const path = require('path');
15
+ process.chdir(path.join(__dirname, '..'));
16
+
17
+ const benchmarks = require('./benchmarks/coding-agent.json');
18
+ const {
19
+ DEFAULT_RESULTS_DIR,
20
+ runCodexCliBaselineBenchmark,
21
+ storeBaselineResult,
22
+ summarizeBaselineResults,
23
+ writeBaselineArtifact,
24
+ } = require('./codex-cli-baseline');
25
+
26
+ async function main() {
27
+ const args = parseArgs(process.argv.slice(2));
28
+ if (args.help) {
29
+ printHelp();
30
+ return;
31
+ }
32
+
33
+ const selected = selectBenchmarks({
34
+ id: args.id,
35
+ limit: args.limit ? Number(args.limit) : null,
36
+ difficulty: args.difficulty,
37
+ });
38
+ if (!selected.length) {
39
+ console.error('No benchmarks selected');
40
+ process.exitCode = 1;
41
+ return;
42
+ }
43
+
44
+ const timeoutMs = args.timeout ? Number(args.timeout) : 600_000;
45
+ const runId = crypto.randomUUID();
46
+ const dryRun = !!args['dry-run'];
47
+ const model = args.model || null;
48
+ const resultsDir = args['results-dir'] || DEFAULT_RESULTS_DIR;
49
+ const brain = initBrain();
50
+
51
+ console.log('=== Codex CLI Baseline Runner ===');
52
+ console.log(`Run: ${runId}`);
53
+ console.log(`Benchmarks: ${selected.length}/${benchmarks.length}`);
54
+ console.log(`Mode: ${dryRun ? 'dry-run' : 'codex exec baseline'}`);
55
+ console.log(`Model: ${model || 'codex default'}`);
56
+ console.log(`Auth: ${args['use-env-openai-key'] ? 'env OPENAI_API_KEY allowed' : 'ChatGPT/Codex auth preferred (OPENAI_API_KEY stripped)'}`);
57
+ console.log('');
58
+
59
+ const results = [];
60
+ for (const benchmark of selected) {
61
+ console.log(`--- ${benchmark.id} (${benchmark.difficulty || 'unknown'}) ---`);
62
+ console.log(` Prompt: ${String(benchmark.prompt || '').replace(/\s+/g, ' ').slice(0, 120)}`);
63
+ console.log(` Fixture: ${benchmark.agentExpectations?.projectFixture || 'express-basic'}`);
64
+
65
+ const started = Date.now();
66
+ const result = await runCodexCliBaselineBenchmark(benchmark, {
67
+ dryRun,
68
+ model,
69
+ timeoutMs,
70
+ keepFailures: !!args['keep-failures'],
71
+ useEnvOpenAIKey: !!args['use-env-openai-key'],
72
+ dangerouslyBypassSandbox: !!args['dangerously-bypass-sandbox'],
73
+ allowMcp: !!args['allow-mcp'],
74
+ disableMcpServers: normalizeListArg(args['disable-mcp']),
75
+ });
76
+ result.runId = runId;
77
+ result.timestamp = new Date().toISOString();
78
+ results.push(result);
79
+
80
+ if (args.record) {
81
+ result.artifactPath = writeBaselineArtifact(result, { resultsDir });
82
+ }
83
+ storeBaselineResult({ brain, runId, benchmark, result, model, timeoutMs });
84
+
85
+ console.log(` Success: ${result.success}`);
86
+ if (result.status) console.log(` Status: ${result.status}`);
87
+ if (result.score) console.log(` Score: ${(result.score.composite || 0).toFixed(3)}`);
88
+ console.log(` Files: ${(result.actualFileChanges || []).join(', ') || 'none'}`);
89
+ console.log(` Tests: ${result.testsPassed == null ? 'N/A' : result.testsPassed}`);
90
+ console.log(` Time: ${((Date.now() - started) / 1000).toFixed(1)}s`);
91
+ if (result.error) console.log(` Error: ${String(result.error).split('\n')[0]}`);
92
+ if (result.sandboxDir) console.log(` Kept sandbox: ${result.sandboxDir}`);
93
+ if (result.artifactPath) console.log(` Artifact: ${result.artifactPath}`);
94
+ console.log('');
95
+ }
96
+
97
+ const summary = summarizeBaselineResults(results);
98
+ console.log('=== SUMMARY ===');
99
+ console.log(`Passed: ${summary.passed}/${summary.total}`);
100
+ console.log(`Average composite score: ${summary.avgComposite.toFixed(3)}`);
101
+ console.log(`Failures: ${JSON.stringify(summary.failures)}`);
102
+ if (!dryRun && summary.failed > 0) process.exitCode = 1;
103
+ }
104
+
105
+ function initBrain() {
106
+ try {
107
+ const brain = require('../brain');
108
+ brain.initDb();
109
+ return brain;
110
+ } catch (err) {
111
+ console.warn(`Brain not available: ${err.message}`);
112
+ return null;
113
+ }
114
+ }
115
+
116
+ function selectBenchmarks({ id, limit, difficulty } = {}) {
117
+ let selected = benchmarks;
118
+ if (id) selected = selected.filter((benchmark) => benchmark.id === id);
119
+ if (difficulty) selected = selected.filter((benchmark) => benchmark.difficulty === difficulty);
120
+ if (limit) selected = selected.slice(0, limit);
121
+ return selected;
122
+ }
123
+
124
+ function parseArgs(argv) {
125
+ const out = {};
126
+ for (let i = 0; i < argv.length; i += 1) {
127
+ const arg = argv[i];
128
+ if (!arg.startsWith('--')) continue;
129
+ const key = arg.slice(2);
130
+ if (['dry-run', 'record', 'keep-failures', 'use-env-openai-key', 'dangerously-bypass-sandbox', 'allow-mcp', 'help'].includes(key)) {
131
+ out[key] = true;
132
+ } else {
133
+ if (out[key] === undefined) out[key] = argv[i + 1];
134
+ else if (Array.isArray(out[key])) out[key].push(argv[i + 1]);
135
+ else out[key] = [out[key], argv[i + 1]];
136
+ i += 1;
137
+ }
138
+ }
139
+ return out;
140
+ }
141
+
142
+ function normalizeListArg(value) {
143
+ if (value == null) return null;
144
+ const values = Array.isArray(value) ? value : [value];
145
+ return values
146
+ .flatMap((entry) => String(entry || '').split(','))
147
+ .map((entry) => entry.trim())
148
+ .filter(Boolean);
149
+ }
150
+
151
+ function printHelp() {
152
+ console.log(`Usage:
153
+ node eval/run-codex-cli-baseline.js --id agent-001 --model gpt-5.5
154
+ node eval/run-codex-cli-baseline.js --limit 5 --model gpt-5-codex --record
155
+ node eval/run-codex-cli-baseline.js --dry-run --difficulty easy
156
+
157
+ Options:
158
+ --id <id> Run one benchmark id
159
+ --limit <n> Limit selected benchmarks
160
+ --difficulty <easy|medium|hard>
161
+ --model <id> Forwarded verbatim to: codex exec -m <id>
162
+ --timeout <ms> Per-benchmark timeout (default 600000)
163
+ --record Write result JSON artifacts
164
+ --results-dir <path> Artifact directory
165
+ --keep-failures Keep failed sandbox directories
166
+ --use-env-openai-key Let codex inherit OPENAI_API_KEY instead of preferring ChatGPT auth
167
+ --allow-mcp Let codex load enabled MCP servers from local config
168
+ --disable-mcp <name[,name]> Explicit MCP server names to disable; defaults to auto-discover enabled servers
169
+ --dangerously-bypass-sandbox Pass codex's bypass flag. Use only inside an external sandbox.
170
+ --dry-run Verify fixture setup only; does not invoke codex
171
+ `);
172
+ }
173
+
174
+ main().catch((err) => {
175
+ console.error(err.stack || err.message);
176
+ process.exit(1);
177
+ });
@@ -0,0 +1,220 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const http = require('http');
5
+ const path = require('path');
6
+
7
+ const WALLE_SERVER_NAME = 'wall-e';
8
+
9
+ const MCP_TARGETS = Object.freeze([
10
+ { id: 'claude-code', tool: 'Claude Code', configPath: '.claude/mcp.json', detectPath: '.claude', kind: 'json' },
11
+ { id: 'claude-code-global', tool: 'Claude Code Global', configPath: '.claude.json', detectPath: '.claude.json', kind: 'json' },
12
+ { id: 'codex', tool: 'Codex', configPath: '.codex/config.toml', detectPath: '.codex', kind: 'codex-toml' },
13
+ { id: 'cursor', tool: 'Cursor', configPath: '.cursor/mcp.json', detectPath: '.cursor', kind: 'json' },
14
+ { id: 'windsurf', tool: 'Windsurf', configPath: '.codeium/windsurf/mcp_config.json', detectPath: '.codeium/windsurf', kind: 'json' },
15
+ { id: 'claude-desktop', tool: 'Claude Desktop', configPath: 'Library/Application Support/Claude/claude_desktop_config.json', detectPath: 'Library/Application Support/Claude', kind: 'json' },
16
+ ]);
17
+
18
+ function wallEMcpUrl(wallePort) {
19
+ return `http://localhost:${Number(wallePort) || 3457}/mcp`;
20
+ }
21
+
22
+ function wallEMcpConfig(wallePort) {
23
+ return { type: 'http', url: wallEMcpUrl(wallePort) };
24
+ }
25
+
26
+ function _home(homeDir) {
27
+ return homeDir || process.env.HOME || process.env.USERPROFILE || '';
28
+ }
29
+
30
+ function _targetPaths(target, homeDir) {
31
+ const home = _home(homeDir);
32
+ return {
33
+ detectPath: path.join(home, target.detectPath),
34
+ configPath: path.join(home, target.configPath),
35
+ };
36
+ }
37
+
38
+ function _isDetected(target, homeDir) {
39
+ const paths = _targetPaths(target, homeDir);
40
+ return fs.existsSync(paths.detectPath);
41
+ }
42
+
43
+ function _readJsonConfig(configPath) {
44
+ if (!fs.existsSync(configPath)) return { config: {}, exists: false };
45
+ try {
46
+ return { config: JSON.parse(fs.readFileSync(configPath, 'utf8')), exists: true };
47
+ } catch (err) {
48
+ return { config: null, exists: true, error: err };
49
+ }
50
+ }
51
+
52
+ function _writeJsonConfig(configPath, config) {
53
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
54
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
55
+ }
56
+
57
+ function _jsonStatus(target, configPath, wallePort) {
58
+ const loaded = _readJsonConfig(configPath);
59
+ if (loaded.error) {
60
+ return { tool: target.tool, status: 'invalid_config', configPath, error: loaded.error.message };
61
+ }
62
+ const entry = loaded.config?.mcpServers?.[WALLE_SERVER_NAME];
63
+ if (entry && entry.url === wallEMcpUrl(wallePort)) {
64
+ return { tool: target.tool, status: 'configured', configPath };
65
+ }
66
+ if (entry) return { tool: target.tool, status: 'wrong_port', configPath, url: entry.url || '' };
67
+ return { tool: target.tool, status: 'not_configured', configPath };
68
+ }
69
+
70
+ function _ensureJsonTarget(target, configPath, wallePort) {
71
+ const loaded = _readJsonConfig(configPath);
72
+ if (loaded.error) {
73
+ return { tool: target.tool, action: 'error', configPath, error: loaded.error.message };
74
+ }
75
+ const config = loaded.config || {};
76
+ if (!config.mcpServers || typeof config.mcpServers !== 'object' || Array.isArray(config.mcpServers)) {
77
+ config.mcpServers = {};
78
+ }
79
+ const existing = config.mcpServers[WALLE_SERVER_NAME];
80
+ if (existing && existing.url === wallEMcpUrl(wallePort)) {
81
+ return { tool: target.tool, action: 'already_configured', configPath };
82
+ }
83
+ const action = existing ? 'updated' : 'added';
84
+ config.mcpServers[WALLE_SERVER_NAME] = wallEMcpConfig(wallePort);
85
+ _writeJsonConfig(configPath, config);
86
+ return { tool: target.tool, action, configPath };
87
+ }
88
+
89
+ function _codexBlockRegex() {
90
+ return /(^|\n)\[mcp_servers\.(?:"wall-e"|wall-e)\]\n[\s\S]*?(?=\n\[[^\n]+\]|\s*$)/m;
91
+ }
92
+
93
+ function _extractCodexWallEBlock(text) {
94
+ const match = String(text || '').match(_codexBlockRegex());
95
+ return match ? match[0].replace(/^\n/, '') : '';
96
+ }
97
+
98
+ function _extractTomlStringValue(block, key) {
99
+ const re = new RegExp(`^\\s*${key}\\s*=\\s*"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"\\s*$`, 'm');
100
+ const match = String(block || '').match(re);
101
+ if (!match) return '';
102
+ return match[1].replace(/\\"/g, '"').replace(/\\\\/g, '\\');
103
+ }
104
+
105
+ function _codexStatus(target, configPath, wallePort) {
106
+ const text = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
107
+ const block = _extractCodexWallEBlock(text);
108
+ if (!block) return { tool: target.tool, status: 'not_configured', configPath };
109
+ const url = _extractTomlStringValue(block, 'url');
110
+ if (url === wallEMcpUrl(wallePort)) return { tool: target.tool, status: 'configured', configPath };
111
+ return { tool: target.tool, status: 'wrong_port', configPath, url };
112
+ }
113
+
114
+ function _upsertCodexWallEBlock(text, wallePort) {
115
+ const body = String(text || '').replace(_codexBlockRegex(), '').replace(/\s+$/g, '');
116
+ const block = `[mcp_servers."wall-e"]\nurl = "${wallEMcpUrl(wallePort)}"\n`;
117
+ return (body ? `${body}\n\n` : '') + block;
118
+ }
119
+
120
+ function _ensureCodexTarget(target, configPath, wallePort) {
121
+ const existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
122
+ const status = _codexStatus(target, configPath, wallePort);
123
+ if (status.status === 'configured') return { tool: target.tool, action: 'already_configured', configPath };
124
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
125
+ fs.writeFileSync(configPath, _upsertCodexWallEBlock(existing, wallePort));
126
+ return { tool: target.tool, action: status.status === 'wrong_port' ? 'updated' : 'added', configPath };
127
+ }
128
+
129
+ function detectMcpIntegrations(wallePort, opts = {}) {
130
+ const homeDir = opts.homeDir;
131
+ return MCP_TARGETS.map((target) => {
132
+ const paths = _targetPaths(target, homeDir);
133
+ if (!_isDetected(target, homeDir)) return { tool: target.tool, status: 'not_installed' };
134
+ if (target.kind === 'codex-toml') return _codexStatus(target, paths.configPath, wallePort);
135
+ return _jsonStatus(target, paths.configPath, wallePort);
136
+ });
137
+ }
138
+
139
+ function ensureMcpIntegrations(wallePort, opts = {}) {
140
+ const homeDir = opts.homeDir;
141
+ return MCP_TARGETS.map((target) => {
142
+ const paths = _targetPaths(target, homeDir);
143
+ if (!_isDetected(target, homeDir)) return { tool: target.tool, action: 'not_installed' };
144
+ if (target.kind === 'codex-toml') return _ensureCodexTarget(target, paths.configPath, wallePort);
145
+ return _ensureJsonTarget(target, paths.configPath, wallePort);
146
+ });
147
+ }
148
+
149
+ function injectMcpConfigs(wallePort, homeDir) {
150
+ return ensureMcpIntegrations(wallePort, { homeDir });
151
+ }
152
+
153
+ function _postMcp(port, payload, timeoutMs) {
154
+ return new Promise((resolve, reject) => {
155
+ const body = JSON.stringify(payload);
156
+ const req = http.request({
157
+ host: '127.0.0.1',
158
+ port: Number(port) || 3457,
159
+ path: '/mcp',
160
+ method: 'POST',
161
+ timeout: timeoutMs,
162
+ headers: {
163
+ 'Content-Type': 'application/json',
164
+ 'Content-Length': Buffer.byteLength(body),
165
+ },
166
+ }, (res) => {
167
+ const chunks = [];
168
+ res.on('data', chunk => chunks.push(chunk));
169
+ res.on('end', () => {
170
+ const raw = Buffer.concat(chunks).toString();
171
+ let json = null;
172
+ try { json = raw ? JSON.parse(raw) : null; } catch {}
173
+ resolve({ status: res.statusCode || 0, json, body: raw });
174
+ });
175
+ });
176
+ req.on('timeout', () => req.destroy(new Error('Timed out testing Wall-E MCP')));
177
+ req.on('error', reject);
178
+ req.write(body);
179
+ req.end();
180
+ });
181
+ }
182
+
183
+ async function testWallEMcpEndpoint(wallePort, opts = {}) {
184
+ const timeoutMs = opts.timeoutMs || 2000;
185
+ try {
186
+ const init = await _postMcp(wallePort, {
187
+ jsonrpc: '2.0',
188
+ id: 1,
189
+ method: 'initialize',
190
+ params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'wall-e-self-test', version: '1.0.0' } },
191
+ }, timeoutMs);
192
+ if (init.status !== 200 || !init.json?.result?.serverInfo) {
193
+ return { ok: false, status: init.status, error: 'initialize failed' };
194
+ }
195
+ const tools = await _postMcp(wallePort, {
196
+ jsonrpc: '2.0',
197
+ id: 2,
198
+ method: 'tools/list',
199
+ params: {},
200
+ }, timeoutMs);
201
+ const list = tools.json?.result?.tools;
202
+ if (tools.status !== 200 || !Array.isArray(list)) {
203
+ return { ok: false, status: tools.status, error: 'tools/list failed' };
204
+ }
205
+ return { ok: true, server: init.json.result.serverInfo, toolCount: list.length };
206
+ } catch (err) {
207
+ return { ok: false, error: err.message };
208
+ }
209
+ }
210
+
211
+ module.exports = {
212
+ MCP_TARGETS,
213
+ WALLE_SERVER_NAME,
214
+ detectMcpIntegrations,
215
+ ensureMcpIntegrations,
216
+ injectMcpConfigs,
217
+ testWallEMcpEndpoint,
218
+ wallEMcpConfig,
219
+ wallEMcpUrl,
220
+ };
@@ -2,6 +2,37 @@
2
2
 
3
3
  const { toOpenAI, messagesToOpenAI, responseFromOpenAI } = require('./tool-adapter');
4
4
 
5
+ function isGemma4Model(model) {
6
+ return /^gemma4:/i.test(String(model || ''));
7
+ }
8
+
9
+ function resolveThinkValue({ model, thinking, reasoningEffort } = {}) {
10
+ if (thinking === false || thinking === 'disabled' || thinking === 'off') return false;
11
+ if (thinking === true || thinking === 'enabled' || thinking === 'on') return true;
12
+ if (['low', 'medium', 'high'].includes(thinking)) return thinking;
13
+
14
+ // Ollama's GPT-OSS models accept effort levels. Gemma4's thinking renderer
15
+ // accepts the boolean form, so keep Gemma4 on true when callers ask for any
16
+ // non-disabled reasoning effort.
17
+ if (reasoningEffort && reasoningEffort !== 'disabled') {
18
+ if (/^gpt-oss/i.test(String(model || '')) && ['low', 'medium', 'high'].includes(reasoningEffort)) {
19
+ return reasoningEffort;
20
+ }
21
+ return true;
22
+ }
23
+ return undefined;
24
+ }
25
+
26
+ function buildOllamaOptions({ model, maxTokens, temperature, options } = {}) {
27
+ const out = { ...(options || {}) };
28
+ if (maxTokens != null) out.num_predict = maxTokens;
29
+ if (temperature != null) out.temperature = temperature;
30
+ if (isGemma4Model(model) && out.num_ctx == null) {
31
+ out.num_ctx = 16384;
32
+ }
33
+ return Object.keys(out).length ? out : undefined;
34
+ }
35
+
5
36
  /**
6
37
  * Create an Ollama local-model LLM provider.
7
38
  * Uses Ollama's OpenAI-compatible API — no npm dependency needed.
@@ -9,11 +40,13 @@ const { toOpenAI, messagesToOpenAI, responseFromOpenAI } = require('./tool-adapt
9
40
  */
10
41
  function createOllamaProvider(config = {}) {
11
42
  const baseUrl = (config.baseUrl || 'http://localhost:11434').replace(/\/+$/, '');
43
+ const defaultThink = config.think ?? false;
12
44
 
13
45
  return {
14
46
  type: 'ollama',
15
47
 
16
- async chat({ model, messages, tools, system, maxTokens, temperature, signal } = {}) {
48
+ async chat({ model, messages, tools, system, maxTokens, temperature, signal, thinking, reasoningEffort, options } = {}) {
49
+ const modelId = model || 'gemma4:e4b';
17
50
  const openaiMessages = [];
18
51
 
19
52
  // System prompt as first message
@@ -30,13 +63,15 @@ function createOllamaProvider(config = {}) {
30
63
  const useNativeApi = !tools || tools.length === 0;
31
64
 
32
65
  if (useNativeApi) {
66
+ const resolvedThink = resolveThinkValue({ model: modelId, thinking, reasoningEffort });
33
67
  const nativeBody = {
34
- model: model || 'gemma4:e4b',
68
+ model: modelId,
35
69
  messages: openaiMessages,
36
70
  stream: false,
71
+ think: resolvedThink !== undefined ? resolvedThink : defaultThink,
37
72
  };
38
- if (maxTokens != null) nativeBody.options = { ...nativeBody.options, num_predict: maxTokens };
39
- if (temperature != null) nativeBody.options = { ...nativeBody.options, temperature };
73
+ const nativeOptions = buildOllamaOptions({ model: modelId, maxTokens, temperature, options });
74
+ if (nativeOptions) nativeBody.options = nativeOptions;
40
75
 
41
76
  const start = Date.now();
42
77
  const resp = await fetch(`${baseUrl}/api/chat`, {
@@ -54,6 +89,7 @@ function createOllamaProvider(config = {}) {
54
89
  const raw = await resp.json();
55
90
  const latencyMs = Date.now() - start;
56
91
  const msg = raw.message || {};
92
+ const reasoningContent = msg.thinking || msg.reasoning || msg.reasoning_content || null;
57
93
 
58
94
  // Extract accurate timing from native API (durations are in nanoseconds)
59
95
  const evalDurationNs = raw.eval_duration || 0;
@@ -65,6 +101,7 @@ function createOllamaProvider(config = {}) {
65
101
 
66
102
  return {
67
103
  content: msg.content || null,
104
+ reasoningContent,
68
105
  toolCalls: [],
69
106
  stopReason: 'end_turn',
70
107
  usage: {
@@ -76,17 +113,19 @@ function createOllamaProvider(config = {}) {
76
113
  promptEvalDurationMs: promptEvalDurationNs / 1e6,
77
114
  },
78
115
  latencyMs,
79
- model: model || 'gemma4:e4b',
116
+ model: modelId,
80
117
  provider: 'ollama',
81
118
  raw,
82
119
  };
83
120
  }
84
121
 
85
122
  // Fall back to OpenAI-compat API for tool use
123
+ const resolvedThink = resolveThinkValue({ model: modelId, thinking, reasoningEffort });
86
124
  const body = {
87
- model: model || 'gemma4:e4b',
125
+ model: modelId,
88
126
  messages: openaiMessages,
89
127
  stream: false,
128
+ think: resolvedThink !== undefined ? resolvedThink : defaultThink,
90
129
  };
91
130
 
92
131
  const openaiTools = toOpenAI(tools);
@@ -132,7 +171,7 @@ function createOllamaProvider(config = {}) {
132
171
  if (normalized.stopReason === 'tool_calls') stopReason = 'tool_use';
133
172
  else if (normalized.stopReason === 'stop' || !normalized.stopReason) stopReason = 'end_turn';
134
173
 
135
- return { ...normalized, stopReason, latencyMs: Date.now() - start, model: model || 'gemma4:e4b', provider: 'ollama', raw };
174
+ return { ...normalized, stopReason, latencyMs: Date.now() - start, model: modelId, provider: 'ollama', raw };
136
175
  },
137
176
 
138
177
  async listModels() {
@@ -169,4 +208,4 @@ function createOllamaProvider(config = {}) {
169
208
  };
170
209
  }
171
210
 
172
- module.exports = { createOllamaProvider };
211
+ module.exports = { createOllamaProvider, buildOllamaOptions, resolveThinkValue };
@@ -13,7 +13,7 @@
13
13
  "chat": true,
14
14
  "tools": true,
15
15
  "vision": false,
16
- "thinking": false,
16
+ "thinking": true,
17
17
  "streaming": true,
18
18
  "local": true
19
19
  },
@@ -205,6 +205,7 @@ function responseFromOpenAI(resp) {
205
205
 
206
206
  return {
207
207
  content: msg.content || null,
208
+ reasoningContent: msg.reasoning_content || msg.reasoning || msg.thinking || null,
208
209
  toolCalls: (msg.tool_calls || []).map((tc, index) => ({
209
210
  id: tc.id || synthesizeOpenAIToolCallId(index),
210
211
  name: tc.function.name,
@@ -1,22 +1,39 @@
1
1
  const brain = require('../brain');
2
2
  const eventBus = require('../events/event-bus');
3
3
 
4
+ function checkpointName(adapter) {
5
+ const name = adapter?.name || adapter?.constructor?.name || 'unknown';
6
+ return `ingest:${name}`;
7
+ }
8
+
9
+ function newerTimestamp(a, b) {
10
+ if (!a) return b || null;
11
+ if (!b) return a;
12
+ return b > a ? b : a;
13
+ }
14
+
4
15
  async function runOnce(adapters) {
5
- const checkpoint = brain.getCheckpoint('ingest');
6
- const since = checkpoint?.last_memory_at || null;
16
+ const legacyCheckpoint = brain.getCheckpoint('ingest');
7
17
  let memoriesIngested = 0;
8
- let latestTimestamp = since;
18
+ let latestTimestamp = legacyCheckpoint?.last_memory_at || null;
19
+ const adapterResults = {};
9
20
 
10
21
  for (const adapter of adapters) {
22
+ const adapterCheckpointName = checkpointName(adapter);
23
+ const adapterCheckpoint = brain.getCheckpoint(adapterCheckpointName);
24
+ const since = adapterCheckpoint?.last_memory_at || null;
25
+ let adapterIngested = 0;
26
+ let adapterLatestTimestamp = since;
27
+
11
28
  try {
12
29
  const memories = await adapter.poll(since);
13
30
  for (const mem of memories) {
31
+ adapterLatestTimestamp = newerTimestamp(adapterLatestTimestamp, mem.timestamp);
32
+ latestTimestamp = newerTimestamp(latestTimestamp, mem.timestamp);
14
33
  const result = brain.insertMemory(mem);
15
34
  if (result) {
16
35
  memoriesIngested++;
17
- if (!latestTimestamp || mem.timestamp > latestTimestamp) {
18
- latestTimestamp = mem.timestamp;
19
- }
36
+ adapterIngested++;
20
37
  if (mem.importance >= 0.8 || mem.memory_type === 'message_received') {
21
38
  eventBus.emitHighImportance(result?.id, (mem.content || '').slice(0, 200));
22
39
  }
@@ -59,17 +76,34 @@ async function runOnce(adapters) {
59
76
  console.error('[ingest] Exchange chunking failed:', e.message);
60
77
  }
61
78
  }
79
+ brain.upsertCheckpoint(adapterCheckpointName, {
80
+ last_memory_at: adapterLatestTimestamp,
81
+ metadata: JSON.stringify({
82
+ adapter: adapter.name || adapter.constructor?.name || 'unknown',
83
+ memories_ingested: adapterIngested,
84
+ memories_seen: memories.length,
85
+ }),
86
+ });
87
+ adapterResults[adapter.name || adapter.constructor?.name || 'unknown'] = {
88
+ memoriesIngested: adapterIngested,
89
+ memoriesSeen: memories.length,
90
+ lastMemoryAt: adapterLatestTimestamp,
91
+ };
62
92
  } catch (err) {
63
93
  console.error(`[ingest] Adapter ${adapter.name} failed:`, err.message);
94
+ adapterResults[adapter.name || adapter.constructor?.name || 'unknown'] = {
95
+ error: err.message,
96
+ lastMemoryAt: adapterLatestTimestamp,
97
+ };
64
98
  }
65
99
  }
66
100
 
67
101
  brain.upsertCheckpoint('ingest', {
68
102
  last_memory_at: latestTimestamp,
69
- metadata: JSON.stringify({ memories_ingested: memoriesIngested }),
103
+ metadata: JSON.stringify({ memories_ingested: memoriesIngested, adapters: adapterResults }),
70
104
  });
71
105
 
72
106
  return { memoriesIngested };
73
107
  }
74
108
 
75
- module.exports = { runOnce };
109
+ module.exports = { checkpointName, runOnce };