freddie 0.0.116 → 0.0.118
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
CHANGED
|
@@ -62,9 +62,15 @@ export default {
|
|
|
62
62
|
let model = opts.model || undefined
|
|
63
63
|
if (!provider && model && /^[a-z][a-z0-9-]*\//.test(model)) { provider = model.split('/')[0]; model = model.slice(provider.length + 1) }
|
|
64
64
|
const out = await runTurn({ prompt: opts.prompt, provider, model, skill: opts.skill || undefined, cwd: opts.cwd || undefined, timeoutMs: Number(opts.timeout), witnessPath: opts.witness || undefined })
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
process.exit(
|
|
65
|
+
console.log(out.error ? '' : (out.result || out.messages?.at(-1)?.content || ''))
|
|
66
|
+
if (out.error) console.error('error:', out.error)
|
|
67
|
+
// Tear down cleanly instead of process.exit(): force-closing the
|
|
68
|
+
// process while undici keep-alive sockets and a pending setImmediate
|
|
69
|
+
// are still live makes libuv double-close its async handle and assert
|
|
70
|
+
// UV_HANDLE_CLOSING on Windows. Close the HTTP dispatcher's sockets,
|
|
71
|
+
// then set exitCode and let the event loop drain on its own.
|
|
72
|
+
try { const u = await import('undici'); await u.getGlobalDispatcher()?.close?.() } catch {}
|
|
73
|
+
process.exitCode = out.error ? 1 : 0
|
|
68
74
|
} })
|
|
69
75
|
C({ name: 'cron', description: 'Manage cron jobs', args: [{ name: 'action', default: 'list' }, { name: 'a1' }, { name: 'a2' }], action: async (action, a1, a2) => {
|
|
70
76
|
const { listJobs, createJob, cancelJob, deleteJob, tick } = await import('../../src/cron/scheduler.js')
|
|
@@ -2,6 +2,32 @@ import { logger } from '../observability/log.js'
|
|
|
2
2
|
|
|
3
3
|
const log = logger('acptoapi')
|
|
4
4
|
|
|
5
|
+
// acptoapi may take minutes to answer when it serially walks dead providers
|
|
6
|
+
// (each ~90s timeout) before a live one responds. Node's default undici
|
|
7
|
+
// headersTimeout/bodyTimeout (~300s) would throw UND_ERR_HEADERS_TIMEOUT
|
|
8
|
+
// mid-walk, so we raise them to 0 (disabled) on the global dispatcher and let
|
|
9
|
+
// our AbortController be the single source of truth for the overall deadline
|
|
10
|
+
// (default 240s, override via FREDDIE_LLM_TIMEOUT_MS). Use setGlobalDispatcher
|
|
11
|
+
// once rather than a per-request Agent so we don't leak a dispatcher per call.
|
|
12
|
+
const ACPTOAPI_TIMEOUT_MS = Number(process.env.FREDDIE_LLM_TIMEOUT_MS) || 240000
|
|
13
|
+
let _dispatcherSet = false
|
|
14
|
+
async function ensureLongTimeoutDispatcher() {
|
|
15
|
+
if (_dispatcherSet) return
|
|
16
|
+
_dispatcherSet = true
|
|
17
|
+
try {
|
|
18
|
+
const undici = await import('undici')
|
|
19
|
+
// headersTimeout/bodyTimeout 0: tolerate acptoapi's minutes-long walk.
|
|
20
|
+
// keepAlive*Timeout 1ms: close the socket right after the response so no
|
|
21
|
+
// keep-alive socket lingers between calls.
|
|
22
|
+
undici.setGlobalDispatcher(new undici.Agent({
|
|
23
|
+
headersTimeout: 0,
|
|
24
|
+
bodyTimeout: 0,
|
|
25
|
+
keepAliveTimeout: 1,
|
|
26
|
+
keepAliveMaxTimeout: 1,
|
|
27
|
+
}))
|
|
28
|
+
} catch { /* undici not available — rely on AbortController + defaults */ }
|
|
29
|
+
}
|
|
30
|
+
|
|
5
31
|
export function getAcptoapiUrl() {
|
|
6
32
|
return process.env.FREDDIE_LLM_URL || 'http://127.0.0.1:4800/v1'
|
|
7
33
|
}
|
|
@@ -36,8 +62,9 @@ export async function callLLM({ messages, tools = [], model } = {}) {
|
|
|
36
62
|
// Network Access headers so cross-origin loopback (gh-pages → localhost)
|
|
37
63
|
// succeeds when acptoapi is running. The earlier preemptive loopback
|
|
38
64
|
// refusal caused false negatives on reachable endpoints.
|
|
65
|
+
await ensureLongTimeoutDispatcher()
|
|
39
66
|
const _ac = new AbortController()
|
|
40
|
-
const _tid = setTimeout(() => _ac.abort(new Error('acptoapi fetch timeout')),
|
|
67
|
+
const _tid = setTimeout(() => _ac.abort(new Error('acptoapi fetch timeout')), ACPTOAPI_TIMEOUT_MS)
|
|
41
68
|
let res
|
|
42
69
|
try {
|
|
43
70
|
res = await fetch(base.replace(/\/$/, '') + '/chat/completions', {
|
|
@@ -94,7 +121,7 @@ function adaptResponse(r) {
|
|
|
94
121
|
|
|
95
122
|
function tryParseJson(s) { try { return typeof s === 'string' ? JSON.parse(s) : (s || {}) } catch { return {} } }
|
|
96
123
|
|
|
97
|
-
export async function isReachable(timeoutMs =
|
|
124
|
+
export async function isReachable(timeoutMs = 10000) {
|
|
98
125
|
try {
|
|
99
126
|
const controller = new AbortController()
|
|
100
127
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
|
package/src/agent/machine.js
CHANGED
|
@@ -55,7 +55,18 @@ export function createAgentMachine({ provider, model, maxIterations = 90, callLL
|
|
|
55
55
|
input: ({ context }) => ({ messages: context.messages, model: context.model, provider: context.provider, enabledToolsets: context.enabledToolsets, disabledToolsets: context.disabledToolsets }),
|
|
56
56
|
onDone: [
|
|
57
57
|
{ guard: ({ event }) => Array.isArray(event.output?.tool_calls) && event.output.tool_calls.length > 0, target: 'tool_calls', actions: assign({ messages: ({ context, event }) => [...context.messages, { role: 'assistant', content: event.output.content || '', tool_calls: event.output.tool_calls }] }) },
|
|
58
|
-
{ target: 'done', actions: assign({ messages: ({ context, event }) => [...context.messages, { role: 'assistant', content: event.output.content || '' }], lastResult: ({ event }) =>
|
|
58
|
+
{ target: 'done', actions: assign({ messages: ({ context, event }) => [...context.messages, { role: 'assistant', content: event.output.content || '' }], lastResult: ({ context, event }) => {
|
|
59
|
+
// Prefer this turn's content, but if the model ended with empty
|
|
60
|
+
// text (it may have put its answer in an earlier turn alongside a
|
|
61
|
+
// tool_call), fall back to the last non-empty assistant message so
|
|
62
|
+
// the caller never gets an empty result after a successful run.
|
|
63
|
+
if (event.output.content && event.output.content.trim()) return event.output.content;
|
|
64
|
+
for (let i = context.messages.length - 1; i >= 0; i--) {
|
|
65
|
+
const m = context.messages[i];
|
|
66
|
+
if (m.role === 'assistant' && typeof m.content === 'string' && m.content.trim()) return m.content;
|
|
67
|
+
}
|
|
68
|
+
return event.output.content || '';
|
|
69
|
+
} }) },
|
|
59
70
|
],
|
|
60
71
|
onError: { target: 'done', actions: assign({ error: ({ event }) => String(event.error?.message || event.error) }) },
|
|
61
72
|
},
|
|
@@ -172,16 +183,22 @@ export async function runTurn({ prompt, messages = [], model, provider, callLLM,
|
|
|
172
183
|
const machine = createAgentMachine({ model, provider, callLLM, enabledToolsets, disabledToolsets, maxIterations, events })
|
|
173
184
|
const actor = createActor(machine, { input: { messages: initMessages } }); actor.start(); actor.send({ type: 'SUBMIT', prompt })
|
|
174
185
|
return await new Promise((resolve, reject) => {
|
|
175
|
-
|
|
176
|
-
|
|
186
|
+
let sub
|
|
187
|
+
const cleanup = () => { try { sub?.unsubscribe() } catch {} try { actor.stop() } catch {} }
|
|
188
|
+
const t = setTimeout(() => { cleanup(); reject(new Error('agent turn timeout')) }, timeoutMs)
|
|
189
|
+
sub = actor.subscribe(snap => { if (snap.status !== 'done') return; clearTimeout(t)
|
|
177
190
|
;(async () => {
|
|
178
191
|
const out = snap.output
|
|
179
192
|
const outbound = await h.hooks.invoke('onMessageOutbound', { content: out?.result || '' })
|
|
180
193
|
if (outbound?.systemMessage || outbound?.additionalContext) out.messages = mergeHookExtras(out.messages || [], outbound, 'onMessageOutbound')
|
|
181
194
|
await h.hooks.invoke('onSessionEnd', { reason: out?.error ? 'error' : 'ok', iterations: out?.iterations })
|
|
182
195
|
const errorStack = out?.error ? (events.find(e => e.type === 'llm_call' && !e.ok)?.stack || null) : null
|
|
183
|
-
await writeTrajectory(out, { prompt, provider, model, skill, cwd, events, errorStack, witnessPath })
|
|
184
|
-
|
|
196
|
+
await writeTrajectory(out, { prompt, provider, model, skill, cwd, events, errorStack, witnessPath })
|
|
197
|
+
// Unsubscribe + stop the actor once the turn is done — a finished
|
|
198
|
+
// actor should not be left running with live subscriptions/handles.
|
|
199
|
+
cleanup()
|
|
200
|
+
resolve(out)
|
|
201
|
+
})().catch(e => { cleanup(); reject(e) })
|
|
185
202
|
})
|
|
186
203
|
})
|
|
187
204
|
}
|