mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 02:05:22 -04:00
cookbook agent debug loop: persistent log files, auto-adopt orphan tmux, Codex/Claude skill parity
Three converging fixes so the chat agent + external Codex/Claude skills can actually debug a crashed serve instead of staring at a post-crash neofetch banner:
* Serves now `tee` to /tmp/odysseus-tmux/SESSION.log on the host running them. Runner saves fds 3/4 before the tee and restores them right before `exec ${SHELL}`, so the post-crash interactive zsh banner does NOT pollute the log file.
* `tail_serve_output` (chat agent) and `/api/codex/cookbook/output/{sid}` (Codex+Claude skills) both prefer the persistent log file over the tmux pane. Pane is fallback for sessions predating the tee runner. Default tail bumped 150 -> 400.
* `list_served_models` "recent log" snippet seeks to the Traceback line instead of showing the last 6 lines (which was always the bash prompt).
Cookbook auto-adoption sweep on `/api/cookbook/tasks/status`: every 20s (rate-limited) the cookbook SSHes each configured server, finds `serve-*` / `cookbook-*` tmux sessions running an actual model process (vllm/python/llama-server/etc., filtered via `pane_current_command`), and writes them into state.tasks. So when the agent falls back to raw ssh+tmux, the session appears in the Cookbook UI on the next poll.
`serve_model` error path now reads `data["detail"]` in addition to `data["error"]` so the FastAPI HTTPException message ("Invalid characters in cmd") actually reaches the agent instead of being swallowed as a generic "Serve failed". Tool description updated to warn against `cd …`/`source …`/`&&` prefixes.
Intent-without-action supervisor in agent_loop: when the model writes "Let me tail the output" / "I'll check the logs" / "Let me investigate" and ends the turn without emitting a tool call, the loop injects a sharp system nudge ("You said you would X — DO IT NOW") and continues. Capped at 2 nudges per chat so a model that genuinely cannot use the tool does not pin the loop.
Codex/Claude skill parity: adds `/cookbook/cached`, `/cookbook/presets`, `/cookbook/preset/{name}`, `/cookbook/adopt` so external agents have the same surface as the chat agent. SKILL.md docs + odysseus_api.py wrapper updated for both bundles.
`adopt_served_model` promoted to the always-on tool set so the agent has a documented fallback when serve_model rejects a cmd.
Also various cookbook UI tweaks accumulated alongside the above (cookbook.js, cookbookRunning.js, cookbookServe.js, cookbook-diagnosis.js, settings.js, style.css).
This commit is contained in:
+73
-10
@@ -353,6 +353,15 @@ function _buildEnvPrefixWindows() {
|
||||
}
|
||||
|
||||
export function _buildServeCmd(f, modelName, backend) {
|
||||
// When a venv is configured on the chosen server, use the venv's binaries
|
||||
// by absolute path. Bare `vllm` / `python3` relies on PATH, and SSH non-
|
||||
// interactive sessions often leave a user-site install (~/.local/bin/vllm)
|
||||
// ahead of the venv's bin, so the WRONG vllm gets launched even with the
|
||||
// venv activated. Absolute path sidesteps the whole PATH question.
|
||||
const _isVenv = _envState.env === 'venv' && _envState.envPath;
|
||||
const _venvBin = _isVenv ? (_envState.envPath.replace(/\/+$/, '') + '/bin/') : '';
|
||||
const _vllmBin = _venvBin ? `${_venvBin}vllm` : 'vllm';
|
||||
const _py3Bin = _venvBin ? `${_venvBin}python3` : 'python3';
|
||||
let cmd = '';
|
||||
if (backend === 'vllm') {
|
||||
const gpuId = f.gpu_id?.trim() || '';
|
||||
@@ -361,7 +370,15 @@ export function _buildServeCmd(f, modelName, backend) {
|
||||
const _opts = _detectModelOptimizations(modelName);
|
||||
if (_opts.envVars.length) cmd += _opts.envVars.join(' ') + ' ';
|
||||
}
|
||||
cmd += `vllm serve ${modelName} --host 0.0.0.0 --port ${f.port || '8000'}`;
|
||||
// Pinned attention backend (Attention field). Empty = let vLLM pick.
|
||||
const _attn = (f.vllm_attn_backend ?? '').toString().trim();
|
||||
if (_attn) cmd += `VLLM_ATTENTION_BACKEND=${_attn} `;
|
||||
// Free-text "Env" field — verbatim KEY=VAL pairs (space-separated).
|
||||
// Collapse any pasted newlines/tabs so the backend allowlist (which
|
||||
// rejects \n / \r) doesn't trip on a multi-line paste from a model card.
|
||||
const _extraEnv = (f.extra_env ?? '').toString().replace(/\s+/g, ' ').trim();
|
||||
if (_extraEnv) cmd += _extraEnv + ' ';
|
||||
cmd += `${_vllmBin} serve ${modelName} --host 0.0.0.0 --port ${f.port || '8000'}`;
|
||||
cmd += ` --tensor-parallel-size ${f.tp || '1'}`;
|
||||
cmd += ` --max-model-len ${f.ctx || '8192'}`;
|
||||
cmd += ` --gpu-memory-utilization ${f.gpu_mem || '0.90'}`;
|
||||
@@ -389,7 +406,9 @@ export function _buildServeCmd(f, modelName, backend) {
|
||||
} else if (backend === 'sglang') {
|
||||
const gpuId = f.gpu_id?.trim() || '';
|
||||
if (gpuId) cmd += `CUDA_VISIBLE_DEVICES=${gpuId} `;
|
||||
cmd += `python3 -m sglang.launch_server --model-path ${modelName} --host 0.0.0.0 --port ${f.port || '30000'}`;
|
||||
const _extraEnv = (f.extra_env ?? '').toString().replace(/\s+/g, ' ').trim();
|
||||
if (_extraEnv) cmd += _extraEnv + ' ';
|
||||
cmd += `${_py3Bin} -m sglang.launch_server --model-path ${modelName} --host 0.0.0.0 --port ${f.port || '30000'}`;
|
||||
if (f.tp && f.tp !== '1') cmd += ` --tp ${f.tp}`;
|
||||
if (f.ctx) cmd += ` --context-length ${f.ctx}`;
|
||||
if (f.gpu_mem && f.gpu_mem !== '0.90') cmd += ` --mem-fraction-static ${f.gpu_mem}`;
|
||||
@@ -642,13 +661,20 @@ async function _fetchDependencies() {
|
||||
const winBlocked = !isLocal && _isWindows() && _winUnsupported.has(pkg.name);
|
||||
const note = pkg.status_note ? `<div class="memory-item-meta" style="font-size:10px;opacity:0.65;margin-top:3px;">${esc(pkg.status_note)}</div>` : '';
|
||||
const updateNote = pkg.installed && pkg.pip_update_available === false && pkg.update_note ? `<div class="memory-item-meta" style="font-size:10px;opacity:0.55;margin-top:3px;">${esc(pkg.update_note)}</div>` : '';
|
||||
// Inline "Rebuild" tag for the llama_cpp row only. Styled as a
|
||||
// .cookbook-dep-tag so it matches the LLM category tag's pill look,
|
||||
// and lives to the LEFT of the category tag (clear affordance before
|
||||
// the row "value").
|
||||
const _rebuildBtn = (pkg.name === 'llama_cpp')
|
||||
? `<button type="button" class="cookbook-dep-tag cookbook-dep-rebuild" id="cookbook-rebuild-engine" title="Clear the cached llama.cpp build so the next serve recompiles from source (use after installing a CUDA/ROCm toolkit to turn a CPU-only build into a GPU build).">Rebuild</button>`
|
||||
: '';
|
||||
// Inline rebuild/reinstall tag. Styled as a .cookbook-dep-tag so it
|
||||
// matches the LLM category tag's pill look, and lives to the LEFT of the
|
||||
// category tag. llama_cpp uses the /api/cookbook/rebuild-engine flow
|
||||
// (clear cached binary so next serve recompiles); vllm/sglang use the
|
||||
// diagnosis-style `_launchServeTask` with `pip install --force-reinstall`
|
||||
// so the user can watch the pip install in the Running tab.
|
||||
let _rebuildBtn = '';
|
||||
if (pkg.name === 'llama_cpp') {
|
||||
_rebuildBtn = `<button type="button" class="cookbook-dep-tag cookbook-dep-rebuild" id="cookbook-rebuild-engine" title="Clear the cached llama.cpp build so the next serve recompiles from source (use after installing a CUDA/ROCm toolkit to turn a CPU-only build into a GPU build).">Rebuild</button>`;
|
||||
} else if (pkg.name === 'vllm' && pkg.installed) {
|
||||
_rebuildBtn = `<button type="button" class="cookbook-dep-tag cookbook-dep-rebuild cookbook-dep-reinstall" data-reinstall-pkg="vllm" title="Force-reinstall vLLM (pulls a matching torch). Runs as a tmux task in the Running tab.">Reinstall</button>`;
|
||||
} else if (pkg.name === 'sglang' && pkg.installed) {
|
||||
_rebuildBtn = `<button type="button" class="cookbook-dep-tag cookbook-dep-rebuild cookbook-dep-reinstall" data-reinstall-pkg="sglang" title="Force-reinstall SGLang (pulls a matching torch). Runs as a tmux task in the Running tab.">Reinstall</button>`;
|
||||
}
|
||||
return `<div class="cookbook-dep-row${winBlocked ? ' cookbook-dep-blocked' : ''}" data-pkg-name="${esc(pkg.name)}" data-dep-pip="${esc(pkg.pip || '')}" data-dep-target="${isLocal ? 'local' : 'remote'}" data-dep-kind="${esc(pkg.kind || 'python')}">`
|
||||
+ `<div class="cookbook-dep-info">`
|
||||
+ `<div class="memory-item-title">${esc(pkg.name)}</div>`
|
||||
@@ -696,7 +722,18 @@ async function _fetchDependencies() {
|
||||
// for PEP-668-locked system pythons (Arch, newer Debian).
|
||||
const _inEnv = _envState.env === 'venv' || _envState.env === 'conda';
|
||||
const _pipFlags = (!_isWindows() && !_inEnv) ? ' --user --break-system-packages' : '';
|
||||
const _py = _isWindows() ? 'python' : 'python3';
|
||||
// Use the venv's python3 by absolute path when configured. Even with the
|
||||
// env_prefix sourcing activate, SSH non-interactive sessions sometimes
|
||||
// pick a `python3` ahead of the venv's bin on PATH, so the install
|
||||
// silently lands in the wrong site-packages.
|
||||
let _py;
|
||||
if (_isWindows()) {
|
||||
_py = 'python';
|
||||
} else if (_envState.env === 'venv' && _envState.envPath) {
|
||||
_py = `${_envState.envPath.replace(/\/+$/, '')}/bin/python3`;
|
||||
} else {
|
||||
_py = 'python3';
|
||||
}
|
||||
const cmd = `${_py} -m pip install${upgrade ? ' -U' : ''}${_pipFlags} "${pipName}"`;
|
||||
let envPrefix = '';
|
||||
if (_isWindows()) {
|
||||
@@ -1072,6 +1109,32 @@ function _wireTabEvents(body) {
|
||||
});
|
||||
}
|
||||
|
||||
// "Reinstall" buttons for pip-based serving stacks (vllm, sglang). The
|
||||
// deps list renders ASYNCHRONOUSLY after _fetchDependencies resolves, so
|
||||
// attaching listeners directly here would miss buttons that don't exist
|
||||
// yet. Use document-level delegation instead — the click always finds the
|
||||
// right .cookbook-dep-reinstall button no matter when it was painted.
|
||||
if (!document._cookbookReinstallWired) {
|
||||
document._cookbookReinstallWired = true;
|
||||
document.addEventListener('click', async (ev) => {
|
||||
const btn = ev.target.closest?.('.cookbook-dep-reinstall');
|
||||
if (!btn) return;
|
||||
const pkg = btn.dataset.reinstallPkg || '';
|
||||
if (!pkg) return;
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const sel = document.getElementById('hwfit-deps-server');
|
||||
if (sel) _applyServerSelection(sel.value);
|
||||
const host = _envState.remoteHost || '';
|
||||
const where = host || 'this server';
|
||||
if (!confirm(`Reinstall ${pkg} on ${where}?\n\nRuns "pip install --force-reinstall --no-deps ${pkg}" as a tmux task. Watch progress in the Running tab.`)) return;
|
||||
const _venvPy = (_envState.env === 'venv' && _envState.envPath)
|
||||
? `${_envState.envPath.replace(/\/+$/, '')}/bin/python3`
|
||||
: 'python3';
|
||||
_launchServeTask(`reinstall-${pkg}`, 'pip-reinstall', `${_venvPy} -m pip install --force-reinstall --no-deps ${pkg}`);
|
||||
}, true);
|
||||
}
|
||||
|
||||
// Serve sort
|
||||
const serveSort = document.getElementById('serve-sort');
|
||||
if (serveSort) {
|
||||
|
||||
Reference in New Issue
Block a user