freddie 0.0.126 → 0.0.127
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/plugins/edit/handler.js +7 -5
- package/plugins/grep/handler.js +2 -1
- package/plugins/gui-chat/plugin.js +28 -3
- package/plugins/read/handler.js +6 -4
- package/plugins/write/handler.js +5 -4
- package/src/agent/llm_resolver.js +6 -1
- package/src/agent/machine.js +14 -2
- package/src/cli/doctor.js +3 -1
- package/src/host/index.js +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freddie",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.127",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Open JS agent harness built on pi-mono, floosie, xstate, and anentrypoint-design",
|
|
6
6
|
"bin": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"acptoapi": "^1.0.144",
|
|
29
29
|
"anentrypoint-design": "latest",
|
|
30
30
|
"commander": "^14.0.0",
|
|
31
|
+
"dotenv": "^16.6.1",
|
|
31
32
|
"express": "^5.0.0",
|
|
32
33
|
"flatspace": "^1.0.18",
|
|
33
34
|
"floosie": "^0.6.14",
|
package/plugins/edit/handler.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
2
3
|
export const _tool = ({
|
|
3
4
|
name: 'edit',
|
|
4
5
|
toolset: 'core',
|
|
@@ -16,14 +17,15 @@ export const _tool = ({
|
|
|
16
17
|
required: ['path', 'old_string', 'new_string'],
|
|
17
18
|
},
|
|
18
19
|
},
|
|
19
|
-
handler: async ({ path: p, old_string, new_string, replace_all = false }) => {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
handler: async ({ path: p, old_string, new_string, replace_all = false }, ctx = {}) => {
|
|
21
|
+
const resolved = ctx.cwd && !path.isAbsolute(p) ? path.join(ctx.cwd, p) : p
|
|
22
|
+
if (!fs.existsSync(resolved)) return { error: `not found: ${resolved}` }
|
|
23
|
+
const src = fs.readFileSync(resolved, 'utf8')
|
|
22
24
|
const occurrences = src.split(old_string).length - 1
|
|
23
25
|
if (occurrences === 0) return { error: 'old_string not found' }
|
|
24
26
|
if (occurrences > 1 && !replace_all) return { error: `old_string matches ${occurrences} times; pass replace_all=true` }
|
|
25
27
|
const out = replace_all ? src.split(old_string).join(new_string) : src.replace(old_string, new_string)
|
|
26
|
-
fs.writeFileSync(
|
|
27
|
-
return { path:
|
|
28
|
+
fs.writeFileSync(resolved, out, 'utf8')
|
|
29
|
+
return { path: resolved, replacements: replace_all ? occurrences : 1 }
|
|
28
30
|
},
|
|
29
31
|
})
|
package/plugins/grep/handler.js
CHANGED
|
@@ -18,7 +18,8 @@ export const _tool = ({
|
|
|
18
18
|
required: ['pattern'],
|
|
19
19
|
},
|
|
20
20
|
},
|
|
21
|
-
handler: async ({ pattern, path: root = '.', head_limit = 200, ignore_case = false, glob }) => {
|
|
21
|
+
handler: async ({ pattern, path: root = '.', head_limit = 200, ignore_case = false, glob }, ctx = {}) => {
|
|
22
|
+
if (ctx.cwd && !path.isAbsolute(root)) root = path.join(ctx.cwd, root)
|
|
22
23
|
const re = new RegExp(pattern, ignore_case ? 'i' : '')
|
|
23
24
|
const out = []
|
|
24
25
|
const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', '.cache'])
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import { runTurn } from '../../src/agent/machine.js'
|
|
2
|
-
import { createSession } from '../../src/sessions.js'
|
|
2
|
+
import { createSession, getMessages, appendMessage } from '../../src/sessions.js'
|
|
3
|
+
|
|
4
|
+
// A client-supplied sessionId continues that conversation: prior turns'
|
|
5
|
+
// messages are reloaded and fed back into runTurn (which otherwise starts a
|
|
6
|
+
// brand-new xstate machine with no memory of anything), and the new turn's
|
|
7
|
+
// messages are persisted back so the NEXT call in this session sees them too.
|
|
8
|
+
// Without this, sessionId was purely a display label -- runTurn never saw it
|
|
9
|
+
// and every call was independently stateless regardless of continuity intent.
|
|
10
|
+
async function loadPriorMessages(sessionId) {
|
|
11
|
+
if (!sessionId) return []
|
|
12
|
+
try {
|
|
13
|
+
const rows = await getMessages(sessionId)
|
|
14
|
+
return rows.map(r => ({ role: r.role, content: r.content, ...(r.tool_calls ? { tool_calls: r.tool_calls } : {}), ...(r.tool_call_id ? { tool_call_id: r.tool_call_id } : {}) }))
|
|
15
|
+
} catch (_) { return [] }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function persistNewMessages(sessionId, allMessages, priorCount) {
|
|
19
|
+
if (!sessionId) return
|
|
20
|
+
for (const m of allMessages.slice(priorCount)) {
|
|
21
|
+
try { await appendMessage(sessionId, { role: m.role, content: m.content, toolCalls: m.tool_calls || null, toolCallId: m.tool_call_id || null }) } catch (_) {}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
3
25
|
export default {
|
|
4
26
|
name: 'gui-chat', surfaces: 'gui',
|
|
5
27
|
register({ gui }) {
|
|
@@ -18,11 +40,13 @@ export default {
|
|
|
18
40
|
sessionId = await createSession({ platform: 'web', title: prompt.slice(0, 80), cwd: cwd || null, skill: skill || null, model: model || null })
|
|
19
41
|
} catch (_) { sessionId = null }
|
|
20
42
|
}
|
|
43
|
+
const priorMessages = await loadPriorMessages(sessionId)
|
|
21
44
|
|
|
22
45
|
if (!wantsSse) {
|
|
23
46
|
try {
|
|
24
|
-
const out = await runTurn({ prompt, timeoutMs: 120000, cwd, skill, provider, model })
|
|
47
|
+
const out = await runTurn({ prompt, messages: priorMessages, sessionKey: sessionId || undefined, timeoutMs: 120000, cwd, skill, provider, model })
|
|
25
48
|
if (out.error) return res.status(500).json({ error: out.error, sessionId })
|
|
49
|
+
await persistNewMessages(sessionId, out.messages || [], priorMessages.length)
|
|
26
50
|
return res.json({ result: out.result || '', messages: out.messages || [], iterations: out.iterations, sessionId })
|
|
27
51
|
} catch (e) {
|
|
28
52
|
return res.status(500).json({ error: String(e.message || e), sessionId })
|
|
@@ -35,8 +59,9 @@ export default {
|
|
|
35
59
|
const send = (event, data) => res.write('event: ' + event + '\ndata: ' + JSON.stringify(data) + '\n\n')
|
|
36
60
|
send('start', { ts: Date.now(), sessionId })
|
|
37
61
|
try {
|
|
38
|
-
const out = await runTurn({ prompt, timeoutMs: 120000, cwd, skill, provider, model })
|
|
62
|
+
const out = await runTurn({ prompt, messages: priorMessages, sessionKey: sessionId || undefined, timeoutMs: 120000, cwd, skill, provider, model })
|
|
39
63
|
if (out.error) { send('error', { error: out.error }); res.end(); return }
|
|
64
|
+
await persistNewMessages(sessionId, out.messages || [], priorMessages.length)
|
|
40
65
|
for (const m of out.messages) send('message', m)
|
|
41
66
|
send('done', { result: out.result || '', iterations: out.iterations, sessionId })
|
|
42
67
|
} catch (e) { send('error', { error: String(e.message || e) }) }
|
package/plugins/read/handler.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
2
3
|
export const _tool = ({
|
|
3
4
|
name: 'read',
|
|
4
5
|
toolset: 'core',
|
|
@@ -15,10 +16,11 @@ export const _tool = ({
|
|
|
15
16
|
required: ['path'],
|
|
16
17
|
},
|
|
17
18
|
},
|
|
18
|
-
handler: async ({ path: p, offset = 0, limit = 2000 }) => {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
handler: async ({ path: p, offset = 0, limit = 2000 }, ctx = {}) => {
|
|
20
|
+
const resolved = ctx.cwd && !path.isAbsolute(p) ? path.join(ctx.cwd, p) : p
|
|
21
|
+
if (!fs.existsSync(resolved)) return { error: `not found: ${resolved}` }
|
|
22
|
+
const lines = fs.readFileSync(resolved, 'utf8').split('\n')
|
|
21
23
|
const slice = lines.slice(offset, offset + limit)
|
|
22
|
-
return { path:
|
|
24
|
+
return { path: resolved, total: lines.length, content: slice.map((l, i) => `${(offset + i + 1).toString().padStart(6)}\t${l}`).join('\n') }
|
|
23
25
|
},
|
|
24
26
|
})
|
package/plugins/write/handler.js
CHANGED
|
@@ -15,9 +15,10 @@ export const _tool = ({
|
|
|
15
15
|
required: ['path', 'content'],
|
|
16
16
|
},
|
|
17
17
|
},
|
|
18
|
-
handler: async ({ path: p, content }) => {
|
|
19
|
-
|
|
20
|
-
fs.
|
|
21
|
-
|
|
18
|
+
handler: async ({ path: p, content }, ctx = {}) => {
|
|
19
|
+
const resolved = ctx.cwd && !path.isAbsolute(p) ? path.join(ctx.cwd, p) : p
|
|
20
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
|
21
|
+
fs.writeFileSync(resolved, content, 'utf8')
|
|
22
|
+
return { path: resolved, bytes: Buffer.byteLength(content, 'utf8') }
|
|
22
23
|
},
|
|
23
24
|
})
|
|
@@ -116,7 +116,12 @@ export function resolveCallLLM({ provider, model } = {}) {
|
|
|
116
116
|
// Without this, comma-separated model lists default to ['error'] and
|
|
117
117
|
// rate-limited / empty / timed-out responses don't trigger chain
|
|
118
118
|
// fallback — the request just throws.
|
|
119
|
-
|
|
119
|
+
// max_tokens: acptoapi passes an unset value through to some providers
|
|
120
|
+
// as a very high implicit default (witnessed: 65536), which a
|
|
121
|
+
// low-credit free-tier account (e.g. openrouter) rejects outright with
|
|
122
|
+
// a 402 before even trying the request at a smaller size. 4096 matches
|
|
123
|
+
// acptoapi-bridge.js's own in-process default (see callLLM above).
|
|
124
|
+
const opts = { model: m, messages: toMsgs(input.messages), tools: toTools(input.tools), max_tokens: input.max_tokens || 4096, onFallback: input.onFallback, output: 'openai', fallbackOn: ['error', 'rate_limit', 'timeout', 'empty'] }
|
|
120
125
|
if (/^queue\//.test(m)) opts.queuesMap = getConfigValue('agent.model_queues', {}) || {}
|
|
121
126
|
if (m.includes(',') || /^queue\//.test(m)) opts.matrixSource = process.env.FREDDIE_MATRIX_URL || MATRIX_FILE
|
|
122
127
|
|
package/src/agent/machine.js
CHANGED
|
@@ -263,7 +263,12 @@ export async function runTurn({ prompt, messages = [], model, provider, callLLM,
|
|
|
263
263
|
try {
|
|
264
264
|
const { autoRecall, projectNamespace } = await import('../learn/gm-learn.js')
|
|
265
265
|
const hits = await autoRecall(prompt, { limit: 5, namespace: await projectNamespace() })
|
|
266
|
-
|
|
266
|
+
// Weak models were witnessed answering FROM this block instead of the new
|
|
267
|
+
// user message below it (asked to remember a number, answered a prior
|
|
268
|
+
// turn's unrelated question instead) -- the plain "Relevant memories:"
|
|
269
|
+
// label gave no signal that this is background reference material, not
|
|
270
|
+
// the current instruction. Explicit priority framing fixes it.
|
|
271
|
+
if (hits.length) sysParts.push('Background context from past conversations (gm rs-learn) -- for reference only, does not describe the current task:\n' + hits.map(h => '- ' + h.text).join('\n') + '\n\nThe user\'s actual request for THIS turn follows below and takes priority over the above.')
|
|
267
272
|
} catch (_) {}
|
|
268
273
|
if (sysParts.length) initMessages.unshift({ role: 'user', content: sysParts.join('\n\n') })
|
|
269
274
|
const inbound = await h.hooks.invoke('onMessageInbound', { content: prompt })
|
|
@@ -272,7 +277,14 @@ export async function runTurn({ prompt, messages = [], model, provider, callLLM,
|
|
|
272
277
|
// Persist the turn snapshot under kind=agent so an interrupted turn (process
|
|
273
278
|
// refresh mid-tool-call) resumes exactly where it stopped via resumeTurn.
|
|
274
279
|
const key = sessionKey || randomUUID()
|
|
275
|
-
|
|
280
|
+
// cwd must reach file-path tool handlers (write/read/edit) via toolCtx, not
|
|
281
|
+
// just the system-prompt text above -- those handlers resolve relative paths
|
|
282
|
+
// with bare fs calls against process.cwd(), so without this every relative
|
|
283
|
+
// path silently lands in the freddie server's own cwd instead of the
|
|
284
|
+
// caller's intended project directory (only `bash` was safe, since it takes
|
|
285
|
+
// cwd as an explicit tool argument the model was told to pass).
|
|
286
|
+
const mergedToolCtx = cwd ? { cwd, ...(toolCtx || {}) } : toolCtx
|
|
287
|
+
const machine = createAgentMachine({ model, provider, callLLM, enabledToolsets, disabledToolsets, maxIterations, events, sessionKey: key, toolCtx: mergedToolCtx, tool_choice })
|
|
276
288
|
const pa = await createPersistentActor(machine, { kind: 'agent', key, input: { messages: initMessages } })
|
|
277
289
|
pa.actor.send({ type: 'SUBMIT', prompt })
|
|
278
290
|
return await driveAgentActor({ pa, h, events, prompt, provider, model, skill, cwd, witnessPath, timeoutMs, sessionKey: key })
|
package/src/cli/doctor.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
4
5
|
import { getFreddieHome } from '../home.js'
|
|
6
|
+
const _require = createRequire(import.meta.url)
|
|
5
7
|
const CHECKS = [
|
|
6
8
|
{ name: 'freddie-home', run: () => fs.existsSync(getFreddieHome()) ? { ok: true } : { ok: false, fix: 'mkdir -p ' + getFreddieHome() } },
|
|
7
9
|
{ name: 'node-version', run: () => { const v = process.versions.node; const major = Number(v.split('.')[0]); return major >= 20 ? { ok: true, value: v } : { ok: false, fix: 'install node >=20', value: v } } },
|
|
8
|
-
{ name: '
|
|
10
|
+
{ name: '@libsql/client', run: () => { try { _require.resolve('@libsql/client'); return { ok: true } } catch { return { ok: false, fix: 'npm install' } } } },
|
|
9
11
|
{ name: 'gh-cli', run: () => { const r = spawnSync('gh', ['--version'], { encoding: 'utf8' }); return r.status === 0 ? { ok: true, value: r.stdout.split('\n')[0] } : { ok: false, fix: 'install gh CLI' } } },
|
|
10
12
|
{ name: 'git', run: () => { const r = spawnSync('git', ['--version'], { encoding: 'utf8' }); return r.status === 0 ? { ok: true, value: r.stdout.trim() } : { ok: false, fix: 'install git' } } },
|
|
11
13
|
{ name: 'config-file', run: () => { const p = path.join(getFreddieHome(), 'config.yaml'); return fs.existsSync(p) ? { ok: true } : { ok: false, fix: 'freddie setup' } } },
|
package/src/host/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import dotenv from 'dotenv'
|
|
3
4
|
import { createHost, discoverPlugins } from './host.js'
|
|
4
5
|
import { getFreddieHome } from '../home.js'
|
|
5
6
|
import { applyActiveProjectFromRegistry } from '../projects.js'
|
|
@@ -10,6 +11,13 @@ let _loadPromise = null
|
|
|
10
11
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
11
12
|
const REPO_PLUGINS = path.resolve(__dirname, '..', '..', 'plugins')
|
|
12
13
|
|
|
14
|
+
// Every process entrypoint (bin/freddie.js, src/web/server.js, src/acp/server.js,
|
|
15
|
+
// src/gateway/run.js) calls bootHost() before touching a provider key, so this
|
|
16
|
+
// is the one place a `.env` in the invoking cwd reaches process.env for every
|
|
17
|
+
// downstream reader (acptoapi's own process.env.GROQ_API_KEY etc reads, pi-ai's
|
|
18
|
+
// findEnvKeys/getEnvApiKey). Silent no-op when no .env file exists.
|
|
19
|
+
dotenv.config()
|
|
20
|
+
|
|
13
21
|
export function host() {
|
|
14
22
|
if (!_host) _host = createHost({ surfaces: ['pi', 'gui'] })
|
|
15
23
|
return _host
|