freddie 0.0.87 → 0.0.88
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/CHANGELOG.md +12 -0
- package/package.json +2 -2
- package/plugins/core-cli/plugin.js +5 -3
- package/src/agent/llm_resolver.js +13 -19
- package/src/agent/machine.js +30 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
3
|
### Fixed
|
|
4
|
+
- `src/agent/llm_resolver.js`: assistant tool_calls returning to provider on the second turn were missing OpenAI's required `type:"function"` and `function:{name,arguments:string}` wrapping. Added `toOpenAIMessages()` that wraps assistant tool_calls and stringifies tool message content. Witnessed: mistral previously errored 422 `messages.2.tool_calls.0.type : property "type" is missing`; after fix returns `Emperor Penguin` through full PLAN → EXECUTE → VERIFY → COMPLETE loop. Trajectory artifact at `penguins/.freddie/trajectories/2026-05-12T15-54-49-902-...json`.
|
|
5
|
+
- `plugins/core-cli/plugin.js`: `freddie exec` now uses `resolveCallLLM` instead of hardcoded acptoapi `callLLM` — previously failed `fetch failed` when acptoapi daemon wasn't running, even with valid `--model` and provider key. Added `--provider`, `--skill`, `--cwd` flags; auto-parses `provider/model` from `--model`.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `src/agent/machine.js`: `writeTrajectory()` writes per-turn JSON to `$FREDDIE_HOME/trajectories/<ts>-<slug>.json` when `agent.save_trajectories=true`. Records prompt, provider, model, cwd, iterations, result, error, state_transitions (PLAN/EXECUTE/VERIFY/COMPLETE), tool_calls, full messages. Filled gap: config flag existed but had no implementation.
|
|
9
|
+
- `scripts/validate-llm-providers.js`: rewritten to dynamically enumerate `.env` keys × acptoapi `PROVIDER_KEYS`. Emits `.gm/llm-validation.log` + `.gm/llm-validation.json`. Live witnessed run: 5/15 REAL_OK across groq, mistral, openrouter, sambanova, claude-cli.
|
|
10
|
+
|
|
11
|
+
### Witnessed broken (documented honestly, not fixed)
|
|
12
|
+
- opencode `serve --port 4790` listens but every HTTP endpoint times out — likely needs `OPENCODE_SERVER_PASSWORD` or non-HTTP transport.
|
|
13
|
+
- kilo ACP `POST /session` returns 200 but `GET /event` hangs in SSE reader.
|
|
14
|
+
- nvidia/cerebras default models in `acptoapi/lib/auto-chain.js` are stale.
|
|
15
|
+
|
|
4
16
|
- `src/agent/llm_resolver.js`: fixed tool-calling for all openai-compat providers (groq, mistral, cerebras, openrouter, nvidia, etc.) — `sdk.chat()` was internally using `from:'openai'` format which stripped `url` and `apiKey` before sending to the provider, causing "Failed to parse URL from undefined"; replaced with direct `fetch()` for openai-compat, bypassing the sdk format pipeline entirely
|
|
5
17
|
- `src/agent/llm_resolver.js`: tool schemas (from `getEnabledToolSchemas`) were never passed to the LLM API — `tools: undefined` was hardcoded; now converts freddie tool schemas to OpenAI `{type:'function', function:{...}}` format and passes them in the request body
|
|
6
18
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freddie",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.88",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Open JS agent harness built on pi-mono, floosie, xstate, and anentrypoint-design",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"plugsdk": "^1.0.15",
|
|
27
27
|
"xstate": "^5.31.0",
|
|
28
28
|
"zod": "^4.0.0",
|
|
29
|
-
"anentrypoint-design": "^0.0.
|
|
29
|
+
"anentrypoint-design": "^0.0.94",
|
|
30
30
|
"acptoapi": "^1.0.51"
|
|
31
31
|
},
|
|
32
32
|
"optionalDependencies": {
|
|
@@ -49,10 +49,12 @@ export default {
|
|
|
49
49
|
try { ({ callLLM } = await import('../../src/agent/pi-bridge.js')) } catch {}
|
|
50
50
|
await interactive({ callLLM })
|
|
51
51
|
} })
|
|
52
|
-
C({ name: 'exec', description: 'Run a single prompt through the agent and exit', options: [{ flag: '--prompt <prompt>', required: true }, { flag: '--model <model>', default: '' }, { flag: '--timeout <ms>', default: '60000' }], action: async (opts) => {
|
|
52
|
+
C({ name: 'exec', description: 'Run a single prompt through the agent and exit', options: [{ flag: '--prompt <prompt>', required: true }, { flag: '--model <model>', default: '' }, { flag: '--provider <provider>', default: '' }, { flag: '--skill <skill>', default: '' }, { flag: '--cwd <cwd>', default: '' }, { flag: '--timeout <ms>', default: '60000' }], action: async (opts) => {
|
|
53
53
|
const { runTurn } = await import('../../src/agent/machine.js')
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
let provider = opts.provider || undefined
|
|
55
|
+
let model = opts.model || undefined
|
|
56
|
+
if (!provider && model && /^[a-z][a-z0-9-]*\//.test(model)) { provider = model.split('/')[0]; model = model.slice(provider.length + 1) }
|
|
57
|
+
const out = await runTurn({ prompt: opts.prompt, provider, model, skill: opts.skill || undefined, cwd: opts.cwd || undefined, timeoutMs: Number(opts.timeout) })
|
|
56
58
|
if (out.error) { console.error('error:', out.error); process.exit(1) }
|
|
57
59
|
console.log(out.result || out.messages?.at(-1)?.content || '')
|
|
58
60
|
process.exit(0)
|
|
@@ -65,8 +65,18 @@ function toOpenAITools(schemas) {
|
|
|
65
65
|
return schemas.map(s => ({ type: 'function', function: { name: s.name, description: s.description || '', parameters: s.parameters || { type: 'object', properties: {} } } }))
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
function toOpenAIMessages(messages) {
|
|
69
|
+
return messages.map(m => {
|
|
70
|
+
if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
|
71
|
+
return { role: 'assistant', content: m.content || '', tool_calls: m.tool_calls.map(tc => ({ id: tc.id, type: 'function', function: { name: tc.name || tc.function?.name, arguments: typeof (tc.arguments || tc.function?.arguments) === 'string' ? (tc.arguments || tc.function?.arguments) : JSON.stringify(tc.arguments || tc.function?.arguments || {}) } })) }
|
|
72
|
+
}
|
|
73
|
+
if (m.role === 'tool') return { role: 'tool', tool_call_id: m.tool_call_id, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) }
|
|
74
|
+
return m
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
async function directOpenAICompatChat(url, apiKey, model, messages, tools) {
|
|
69
|
-
const body = { model, messages, ...(tools?.length ? { tools } : {}) }
|
|
79
|
+
const body = { model, messages: toOpenAIMessages(messages), ...(tools?.length ? { tools } : {}) }
|
|
70
80
|
const res = await fetch(url, {
|
|
71
81
|
method: 'POST',
|
|
72
82
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
|
|
@@ -90,7 +100,7 @@ async function sdkChat(provider, model, input) {
|
|
|
90
100
|
result = await directOpenAICompatChat(r.url, apiKey, r.model, input.messages, openaiTools)
|
|
91
101
|
} else {
|
|
92
102
|
const { buffer: sdkBuffer } = sdk
|
|
93
|
-
result = await sdkBuffer({ from: null, to: 'openai', provider: r.provider, model: r.model, messages: input.messages, apiKey, ...(openaiTools ? { tools: openaiTools } : {}) })
|
|
103
|
+
result = await sdkBuffer({ from: null, to: 'openai', provider: r.provider, model: r.model, messages: toOpenAIMessages(input.messages), apiKey, ...(openaiTools ? { tools: openaiTools } : {}) })
|
|
94
104
|
}
|
|
95
105
|
const choice = result?.choices?.[0]?.message || {}
|
|
96
106
|
const content = typeof choice.content === 'string' ? choice.content : ''
|
|
@@ -156,23 +166,7 @@ export function resolveCallLLM({ provider, model } = {}) {
|
|
|
156
166
|
|
|
157
167
|
const preference = getConfigValue('agent.model_preference', [])
|
|
158
168
|
if (Array.isArray(preference) && preference.length > 0) {
|
|
159
|
-
|
|
160
|
-
for (const pref of preference) {
|
|
161
|
-
const p = pref.provider
|
|
162
|
-
const m = pref.model || model || input.model || defaultModel(p)
|
|
163
|
-
if (!await hasKey(p)) continue
|
|
164
|
-
if (!isAvailable(p)) continue
|
|
165
|
-
try {
|
|
166
|
-
return await sdkChat(p, m, input)
|
|
167
|
-
} catch (e) {
|
|
168
|
-
markFailed(p)
|
|
169
|
-
errors.push(`${p}: ${e.message}`)
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
if (errors.length) {
|
|
173
|
-
const status = getStatus().map(s => `${s.provider}(ok=${s.ok},fails=${s.failCount})`).join(', ')
|
|
174
|
-
throw new Error(`all preference providers failed: ${errors.join('; ')} | sampler: ${status}`)
|
|
175
|
-
}
|
|
169
|
+
try { return await tryChain(preference, input, model) } catch (e) { if (!/chain empty/.test(e.message)) throw e }
|
|
176
170
|
}
|
|
177
171
|
|
|
178
172
|
const links = sdk.buildAutoChain(model || input.model)
|
package/src/agent/machine.js
CHANGED
|
@@ -85,6 +85,35 @@ export function createAgentMachine({ provider, model, maxIterations = 90, callLL
|
|
|
85
85
|
})
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
async function writeTrajectory(out, { prompt, provider, model, skill, cwd }) {
|
|
89
|
+
try {
|
|
90
|
+
const { getConfigValue } = await import('../config.js')
|
|
91
|
+
if (!getConfigValue('agent.save_trajectories', false)) return
|
|
92
|
+
const { getFreddieHome } = await import('../home.js')
|
|
93
|
+
const fs = await import('node:fs')
|
|
94
|
+
const path = await import('node:path')
|
|
95
|
+
const dir = path.join(getFreddieHome(), 'trajectories')
|
|
96
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
97
|
+
const states = []
|
|
98
|
+
const toolCalls = []
|
|
99
|
+
for (const m of out.messages || []) {
|
|
100
|
+
if (m.role === 'assistant' && m.tool_calls?.length) { states.push('EXECUTE'); for (const tc of m.tool_calls) toolCalls.push({ name: tc.name || tc.function?.name, arguments: tc.arguments || tc.function?.arguments || {} }) }
|
|
101
|
+
else if (m.role === 'user') states.push('PLAN')
|
|
102
|
+
else if (m.role === 'assistant') states.push('COMPLETE')
|
|
103
|
+
else if (m.role === 'tool') states.push('VERIFY')
|
|
104
|
+
}
|
|
105
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').replace(/Z$/, '')
|
|
106
|
+
const slug = (prompt || 'turn').slice(0, 40).replace(/[^a-zA-Z0-9-]+/g, '-').replace(/^-+|-+$/g, '').toLowerCase()
|
|
107
|
+
const file = path.join(dir, `${ts}-${slug}.json`)
|
|
108
|
+
fs.writeFileSync(file, JSON.stringify({
|
|
109
|
+
ts, prompt, provider, model, skill, cwd,
|
|
110
|
+
iterations: out.iterations, result: out.result, error: out.error,
|
|
111
|
+
state_transitions: states, tool_calls: toolCalls,
|
|
112
|
+
messages: out.messages,
|
|
113
|
+
}, null, 2))
|
|
114
|
+
} catch (_) {}
|
|
115
|
+
}
|
|
116
|
+
|
|
88
117
|
export async function runTurn({ prompt, messages = [], model, provider, callLLM, enabledToolsets, disabledToolsets, maxIterations = 90, timeoutMs = 30000, cwd, skill } = {}) {
|
|
89
118
|
const initMessages = [...messages]
|
|
90
119
|
const systemParts = []
|
|
@@ -104,7 +133,7 @@ export async function runTurn({ prompt, messages = [], model, provider, callLLM,
|
|
|
104
133
|
actor.subscribe(snap => {
|
|
105
134
|
if (snap.status === 'done') {
|
|
106
135
|
clearTimeout(t)
|
|
107
|
-
resolve(snap.output)
|
|
136
|
+
writeTrajectory(snap.output, { prompt, provider, model, skill, cwd }).finally(() => resolve(snap.output))
|
|
108
137
|
}
|
|
109
138
|
})
|
|
110
139
|
})
|