Compare commits
2 commits
33d133b2da
...
596d5b7ed2
| Author | SHA1 | Date | |
|---|---|---|---|
| 596d5b7ed2 | |||
| 5d674fe61b |
3 changed files with 141 additions and 15 deletions
|
|
@ -1,9 +1,15 @@
|
|||
# Ulysses Improvement Tasks
|
||||
|
||||
Last updated: 2026-07-07
|
||||
Last updated: 2026-07-08
|
||||
|
||||
## Priority: HIGH
|
||||
|
||||
- [ ] **Agent toolcalling loop: model narrates actions but never emits fenced blocks**
|
||||
Gemma-4-12b repeatedly narrates intent ("I'm calling list_vault now") without emitting the ```tool fenced block. The model clearly understands what it should do but the fenced block never arrives. This blocks agent workflows entirely when it occurs (~25% of tool calls). The anti-hallucination system prompt rule drafted yesterday is staged but not live (container not restarted). Need to investigate whether this is: (a) the missing system prompt rule, (b) a regex/parsing issue in parse_tool_blocks(), (c) the compact API-model prompt confusing non-tool-native models, or (d) a context/truncation issue preventing the fenced block from reaching the parser.
|
||||
|
||||
- [ ] **Chat deletion dropdown broken — does not appear**
|
||||
Clicking the arrow on a chat in the left drawer no longer shows the dropdown menu (rename/delete/etc.). Cannot delete chats. Likely a JS event listener or DOM positioning issue — possibly zoom-related getBoundingClientRect() skew or a regression from recent dropdown/popup fixes.
|
||||
|
||||
- [ ] **AUTO_START_LOCAL: persist & auto-restore last running model on restart**
|
||||
New `AUTO_START_LOCAL` env var (bool, default false). On graceful shutdown/restart, persist the currently running model + all launch parameters (engine, quantization, context length, GPU layers, etc.) to a file in `/app/data/`. On startup, if the file exists and AUTO_START_LOCAL is true, automate the full launch pipeline: resolve the saved preset, spin up the engine, and register the endpoint — skipping the manual cookbook → Launch → pick model → verify params → launch → wait → refresh cycle the user currently endures on every restart.
|
||||
|
||||
|
|
|
|||
|
|
@ -2912,6 +2912,54 @@ async def stream_agent_loop(
|
|||
r"\b[^.\n]{0,140}",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
# Map intent verbs → likely tool names, filtered against available tools.
|
||||
# Used by the intent-without-action supervisor to give the model a concrete
|
||||
# tool name instead of a vague "DO IT NOW."
|
||||
_INTENT_TOOL_HINTS: Dict[str, List[str]] = {
|
||||
"list": ["ls", "list_vault", "list_served_models", "list_sessions", "manage_documents"],
|
||||
"scan": ["ls", "list_vault", "grep"],
|
||||
"read": ["read_file", "manage_documents", "view_document"],
|
||||
"view": ["read_file", "manage_documents", "view_document"],
|
||||
"see": ["read_file", "manage_documents", "view_document"],
|
||||
"look": ["read_file", "grep", "web_search"],
|
||||
"show": ["read_file", "manage_documents"],
|
||||
"search": ["web_search", "grep", "search_documents"],
|
||||
"find": ["grep", "web_search", "search_documents"],
|
||||
"query": ["web_search", "grep"],
|
||||
"grep": ["grep"],
|
||||
"write": ["write_file", "create_document"],
|
||||
"create": ["create_document", "write_file"],
|
||||
"edit": ["edit_document", "write_file"],
|
||||
"tail": ["tail_serve_output", "list_served_models"],
|
||||
"check": ["read_file", "grep", "web_search", "list_served_models"],
|
||||
"investigate": ["read_file", "grep", "web_search", "bash"],
|
||||
"examine": ["read_file", "grep", "bash"],
|
||||
"debug": ["bash", "python", "read_file", "grep"],
|
||||
"run": ["bash", "python"],
|
||||
"execute": ["bash", "python"],
|
||||
"start": ["launch_model", "serve_model"],
|
||||
"launch": ["launch_model", "serve_model"],
|
||||
"call": [], # too generic — model usually names the tool explicitly
|
||||
"fetch": ["web_fetch", "read_file"],
|
||||
"pull": ["web_fetch", "read_file"],
|
||||
"inspect": ["read_file", "grep", "bash"],
|
||||
"verify": ["read_file", "grep", "bash"],
|
||||
"diagnose": ["bash", "read_file", "grep"],
|
||||
"capture": ["read_file", "grep", "bash"],
|
||||
"grab": ["read_file", "web_fetch"],
|
||||
"test": ["bash", "python"],
|
||||
"use": [], # too generic
|
||||
"do": [], # too generic
|
||||
"perform": [], # too generic
|
||||
"serve": ["serve_model", "list_served_models"],
|
||||
"register": ["register_endpoint"],
|
||||
"adopt": ["register_endpoint"],
|
||||
"kill": ["bash"],
|
||||
"stop": ["bash"],
|
||||
"restart": ["bash"],
|
||||
"kick": ["bash"],
|
||||
"trigger": ["bash", "python"],
|
||||
}
|
||||
_awaiting_user = False # set by ask_user → end the turn and wait for a choice
|
||||
|
||||
# Document streaming state (persists across rounds)
|
||||
|
|
@ -3433,24 +3481,94 @@ async def stream_agent_loop(
|
|||
"session_id from the serve/list result. Never answer with "
|
||||
"\"check logs\" when those tools are available."
|
||||
)
|
||||
# Find the most specific tool hint from the intent phrase.
|
||||
_hint_tools: List[str] = []
|
||||
_avail = _relevant_tools or set()
|
||||
for _verb, _tools in _INTENT_TOOL_HINTS.items():
|
||||
if f" {_verb}" in f" {_lower_phrase}" and _tools:
|
||||
_hint_tools = [t for t in _tools if t in _avail][:3]
|
||||
if _hint_tools:
|
||||
break
|
||||
# Also check if the model named a specific tool
|
||||
_named_match = re.search(
|
||||
r'(?:call|run|use|trigger)\s+(?:the\s+)?`?(\w[\w_-]+)`?',
|
||||
_matched_phrase, re.IGNORECASE,
|
||||
)
|
||||
if _named_match:
|
||||
_named_tool = _named_match.group(1).lower()
|
||||
if _named_tool in _avail:
|
||||
_hint_tools.insert(0, _named_tool)
|
||||
_tool_hint = ""
|
||||
if _hint_tools:
|
||||
_tool_list = "`, `".join(_hint_tools[:3])
|
||||
_tool_hint = f" Call `{_tool_list}`. "
|
||||
|
||||
messages.append({
|
||||
"role": "system",
|
||||
"content": (
|
||||
f"You just wrote: \"{_matched_phrase}\" — but ended the "
|
||||
"turn without making the actual tool call. The user can "
|
||||
"see you announced the action but didn't run it, which "
|
||||
"is the most frustrating thing you can do. "
|
||||
"DO IT NOW: emit the actual function call this turn. "
|
||||
f"{_cookbook_log_hint}"
|
||||
"turn without making the actual tool call." + _tool_hint +
|
||||
" The user can see you announced the action but didn't run "
|
||||
"it, which is the most frustrating thing you can do. "
|
||||
"DO IT NOW: emit the function call this turn. "
|
||||
+ _cookbook_log_hint +
|
||||
"If you decided not to do it after all, say so plainly in "
|
||||
"one sentence instead of restating the plan."
|
||||
),
|
||||
})
|
||||
# Visible signal in the stream so the user knows we caught it.
|
||||
yield f'data: {json.dumps({"type": "agent_step", "round": round_num + 1})}\n\n'
|
||||
continue
|
||||
break # no tools — done
|
||||
|
||||
# ── Wrong-tool detector ───────────────────────────────────────
|
||||
# When the model DID emit a tool call but the prose describes a
|
||||
# DIFFERENT action. Common on smaller models: says "I'll list the
|
||||
# vault" but emits rename_session. Compare intent against actual
|
||||
# tool blocks and flag mismatches.
|
||||
_intent_text_wt = _strip_think_blocks(cleaned_round).strip()
|
||||
_intent_match_wt = _INTENT_RE.search(_intent_text_wt) if _intent_text_wt else None
|
||||
if _intent_match_wt and tool_blocks and not guide_only and not _force_answer:
|
||||
_matched_wt = _intent_match_wt.group(0).strip().lower()
|
||||
_actual_tools = {b.tool_type for b in tool_blocks}
|
||||
_suggested: Set[str] = set()
|
||||
for _verb, _tools in _INTENT_TOOL_HINTS.items():
|
||||
if f" {_verb}" in f" {_matched_wt}":
|
||||
_suggested.update(_tools)
|
||||
_named_wt = re.search(
|
||||
r'(?:call|run|use|trigger)\s+(?:the\s+)?`?(\w[\w_-]+)`?',
|
||||
_matched_wt, re.IGNORECASE,
|
||||
)
|
||||
if _named_wt:
|
||||
_suggested.add(_named_wt.group(1).lower())
|
||||
if _suggested and not (_suggested & _actual_tools):
|
||||
logger.info(
|
||||
f"[agent] wrong-tool-detect round {round_num}: "
|
||||
f"intent suggests {sorted(_suggested)[:5]}, actual={sorted(_actual_tools)}"
|
||||
)
|
||||
_avail_wt = _relevant_tools or set()
|
||||
_candidates = sorted(_suggested & _avail_wt)[:3]
|
||||
if _candidates:
|
||||
_cand_list = "`, `".join(_candidates)
|
||||
logger.info(
|
||||
f"[agent] wrong-tool: {sorted(_actual_tools)} ≠ {_candidates} — "
|
||||
f"skipping execution, injecting correction"
|
||||
)
|
||||
messages.append({
|
||||
"role": "system",
|
||||
"content": (
|
||||
f"You just called `{'`, `'.join(sorted(_actual_tools))}` "
|
||||
f"but your description said you wanted to: \"{_matched_wt}\". "
|
||||
f"These are not the same thing. The tool you likely need is "
|
||||
f"`{_cand_list}`. Call that instead."
|
||||
),
|
||||
})
|
||||
_empty_round = round_response.strip() or "(model attempted wrong tool call)"
|
||||
_append_tool_results(
|
||||
messages, _empty_round, [], [], [], False, round_num, round_reasoning,
|
||||
)
|
||||
yield f'data: {json.dumps({"type": "agent_step", "round": round_num + 1})}\n\n'
|
||||
continue
|
||||
|
||||
# ── Loop-breaker (Terminus-style stall detector) ──────────────
|
||||
# Stall detector for repeated no-progress tool loops.
|
||||
# A round is "useless" ONLY when it re-issues a recent tool call AND
|
||||
|
|
|
|||
|
|
@ -825,17 +825,18 @@ function createSessionItem(s) {
|
|||
} else {
|
||||
// Position the dropdown using viewport coords
|
||||
const rect = menuBtn.getBoundingClientRect();
|
||||
const z = parseFloat(getComputedStyle(document.documentElement).zoom) || 1;
|
||||
dropdown.style.left = '';
|
||||
dropdown.style.right = (window.innerWidth - rect.right) + 'px';
|
||||
dropdown.style.right = (window.innerWidth - rect.right / z) + 'px';
|
||||
// Show off-screen first to measure height
|
||||
dropdown.style.top = '-9999px';
|
||||
dropdown.style.display = 'block';
|
||||
const ddRect = dropdown.getBoundingClientRect();
|
||||
// Flip above if not enough room below
|
||||
if (rect.bottom + 2 + ddRect.height > window.innerHeight) {
|
||||
dropdown.style.top = Math.max(2, rect.top - ddRect.height - 2) + 'px';
|
||||
if (rect.bottom / z + 2 + ddRect.height / z > window.innerHeight) {
|
||||
dropdown.style.top = Math.max(2, rect.top / z - ddRect.height / z - 2) + 'px';
|
||||
} else {
|
||||
dropdown.style.top = rect.bottom + 2 + 'px';
|
||||
dropdown.style.top = rect.bottom / z + 2 + 'px';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -2669,14 +2670,15 @@ function _showDropdown(anchorEl, items) {
|
|||
|
||||
// Position using viewport coords (same pattern as session menus)
|
||||
const rect = anchorEl.getBoundingClientRect();
|
||||
dd.style.right = (window.innerWidth - rect.right) + 'px';
|
||||
const z = parseFloat(getComputedStyle(document.documentElement).zoom) || 1;
|
||||
dd.style.right = (window.innerWidth - rect.right / z) + 'px';
|
||||
dd.style.top = '-9999px';
|
||||
dd.style.display = 'block';
|
||||
const ddRect = dd.getBoundingClientRect();
|
||||
if (rect.bottom + 2 + ddRect.height > window.innerHeight) {
|
||||
dd.style.top = Math.max(2, rect.top - ddRect.height - 2) + 'px';
|
||||
if (rect.bottom / z + 2 + ddRect.height / z > window.innerHeight) {
|
||||
dd.style.top = Math.max(2, rect.top / z - ddRect.height / z - 2) + 'px';
|
||||
} else {
|
||||
dd.style.top = (rect.bottom + 2) + 'px';
|
||||
dd.style.top = (rect.bottom / z + 2) + 'px';
|
||||
}
|
||||
|
||||
function close() { dd.remove(); }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue