Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions dashboard/src/components/GeneratorProfileSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@ export function GeneratorProfileSelector() {

// Fetch agent list once for scope options
useEffect(() => {
void getStats().then((s) => {
setAgentNames(s.top_agents.map((a) => a.name).filter(Boolean));
});
void getStats()
.then((s) => {
setAgentNames(s.top_agents.map((a) => a.name).filter(Boolean));
})
.catch((err: unknown) => {
// Surface via the existing ErrorBanner so the user sees a signal rather
// than silently falling back to the global "all" scope only.
setError(err instanceof Error ? err.message : String(err));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: The ErrorBanner rendered at line 88 wires onRetry={() => void loadProfile(scope)}, but the error you just set comes from getStats() in this useEffect, not from loadProfile. Clicking Retry will (a) re-fetch the profile (which may not be the failing call) and (b) immediately clear the error via setError(null) inside loadProfile, but agentNames will remain empty and getStats() will not be retried. The user sees a misleading "retry" affordance that doesn't address the underlying failure.

Suggested fix: track which fetch errored and route retry accordingly (e.g., retryFn local state, or split the error banner so getStats failures have a separate retry handler that re-runs the useEffect).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

});
}, []);

const scopeOptions = useMemo(() => {
Expand Down
2 changes: 1 addition & 1 deletion taosmd/catalog_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ async def index_day(
# The memory model is a system-wide setting. When configured it
# overrides the auto-detected model; when unset, the detected model
# (prior behaviour) is kept as the fallback.
model = resolve_memory_model(model)
model = resolve_memory_model(fallback=model)

# --- Stage 1b: Intake Classification (taxonomy filing) ---
classify_results = []
Expand Down
12 changes: 8 additions & 4 deletions taosmd/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,11 @@ def _generator_profile_set(profile_id: str, agent=None, data_dir=None) -> int:
print(f"error: unknown profile {profile_id!r}", file=sys.stderr)
return 1
if agent:
agents.set_agent_generator_profile(agent, profile_id, data_dir=data_dir)
try:
agents.set_agent_generator_profile(agent, profile_id, data_dir=data_dir)
except agents.AgentNotFoundError:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Inconsistent with http_server.py:1048 which catches both AgentNotFoundError and ValueError. Today set_agent_generator_profile only raises AgentNotFoundError, but if a future validation (e.g., profile_id name rules) raises ValueError, the CLI will surface an uncaught traceback — which is exactly what the PR set out to fix for the unknown-agent case. Consider catching (agents.AgentNotFoundError, ValueError) here too for symmetry.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

print(f"error: agent {agent!r} is not registered", file=sys.stderr)
return 1
print(f"agent {agent}: generator profile = {profile_id}")
else:
config.set_generator_profile(profile_id, data_dir=data_dir)
Expand Down Expand Up @@ -1776,11 +1780,11 @@ def main(argv: list[str] | None = None) -> int:

if args.cmd == "generator-profile":
if args.generator_profile_cmd == "list":
return _generator_profile_list()
return _generator_profile_list(data_dir=args.data_dir)
if args.generator_profile_cmd == "show":
return _generator_profile_show(args.profile_id)
return _generator_profile_show(args.profile_id, data_dir=args.data_dir)
if args.generator_profile_cmd == "set":
return _generator_profile_set(args.profile_id, agent=args.agent)
return _generator_profile_set(args.profile_id, agent=args.agent, data_dir=args.data_dir)

parser.print_help()
return 1
Expand Down
6 changes: 4 additions & 2 deletions taosmd/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,15 +253,17 @@ def get_runtime_overrides(data_dir=None) -> dict:
return out


def resolve_memory_model(fallback: str | None = None, data_dir=None) -> str | None:
def resolve_memory_model(fallback: str | None = None, agent: str | None = None, data_dir=None) -> str | None:
"""Resolve the active generator model: pin > profile(tier) > fallback.

When agent is provided, a per-agent generator profile is consulted before
the global profile, mirroring the resolution order in resolve_generator.
Delegates to generator_profiles.resolve_generator (lazy import to avoid a
cycle). Returns None when resolution yields the empty (retrieval-only)
value AND no fallback was given, preserving the historical None contract.
"""
from . import generator_profiles # lazy: avoids config<->profiles cycle
resolved = generator_profiles.resolve_generator(fallback=fallback, data_dir=data_dir)
resolved = generator_profiles.resolve_generator(agent=agent, fallback=fallback, data_dir=data_dir)
return resolved or None


Expand Down
2 changes: 1 addition & 1 deletion taosmd/emem_event_lift.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ async def lift_edu_to_triples(
if not model:
from .config import resolve_memory_model # noqa: PLC0415

model = resolve_memory_model("llama3.1:8b")
model = resolve_memory_model(fallback="llama3.1:8b")
messages = [
{"role": "system", "content": _system_prompt()},
{"role": "user", "content": _ONESHOT_INPUT},
Expand Down
5 changes: 4 additions & 1 deletion taosmd/generator_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ def resolve_generator(agent: str | None = None, *, fallback: str | None = None,
return pin
pid = None
if agent:
pid = _agents.get_agent_generator_profile(agent, data_dir=data_dir)
try:
pid = _agents.get_agent_generator_profile(agent, data_dir=data_dir)
except _agents.AgentNotFoundError:
pid = None
pid = pid or _config.get_generator_profile(data_dir) or default_profile_id()
prof = get_profile(pid)
if prof is not None:
Expand Down
2 changes: 1 addition & 1 deletion taosmd/http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1045,7 +1045,7 @@ def _handle_generator_profile_post(self) -> None:
_agents.set_agent_generator_profile(agent, pid, data_dir=data_dir)
else:
_cfg.set_generator_profile(pid, data_dir=data_dir)
except Exception as exc: # e.g. AgentNotFoundError
except (_agents.AgentNotFoundError, ValueError) as exc:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: ValueError is a very broad catch — this can swallow programmer errors (e.g., a KeyError re-raised as ValueError, bad config types). The PR description says this was narrowed from a bare except Exception, which is good, but consider catching the specific validation error the underlying call raises rather than all of ValueError. If set_agent_generator_profile is the only relevant caller and it only raises AgentNotFoundError, the ValueError arm may be dead code.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

self._send_json(400, {"error": str(exc)})
return
profiles = [
Expand Down
2 changes: 1 addition & 1 deletion taosmd/memory_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ async def process_conversation_turn(

resolved_model = extraction_model
if not resolved_model or resolved_model == "default":
resolved_model = resolve_memory_model() or "default"
resolved_model = resolve_memory_model(agent=agent_name or "default") or "default"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: agent_name or "default" will almost never match a registered agent, so resolve_generator silently falls back to the global profile anyway. The agent= kwarg only adds value when agent_name is truthy. Consider resolve_memory_model(agent=agent_name) if agent_name else resolve_memory_model() so the per-agent lookup only runs when the caller actually has a registered agent, and document the fallback semantics.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

facts = await extract_facts_with_llm(
text, llm_url, http_client,
agent_name=agent_name or "default",
Expand Down

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion taosmd/webui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="taOSmd — local memory inspector and A2A channel monitor" />
<title>taOSmd</title>
<script type="module" crossorigin src="./assets/index-D0AgbSmp.js"></script>
<script type="module" crossorigin src="./assets/index-BYD--bJq.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-C48QvJab.css">
</head>
<body>
Expand Down
4 changes: 2 additions & 2 deletions tests/test_config_memory_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def test_corrupt_file_treated_as_unset(data_dir):

def test_resolve_prefers_global_over_fallback(data_dir):
config.set_memory_model("ollama:qwen3:4b")
assert config.resolve_memory_model("fallback-model") == "ollama:qwen3:4b"
assert config.resolve_memory_model(fallback="fallback-model") == "ollama:qwen3:4b"


def test_resolve_uses_fallback_when_unset(data_dir, monkeypatch):
Expand All @@ -80,7 +80,7 @@ def test_resolve_uses_fallback_when_unset(data_dir, monkeypatch):
from taosmd import generator_profiles as gp
monkeypatch.setattr(gp.recipes, "local_probe", lambda: {"host": {}})
monkeypatch.setattr(gp.recipes, "tier_of", lambda info: "unknown-tier")
assert config.resolve_memory_model("fallback-model") == "fallback-model"
assert config.resolve_memory_model(fallback="fallback-model") == "fallback-model"


def test_resolve_none_fallback_when_unset(data_dir, monkeypatch):
Expand Down
5 changes: 4 additions & 1 deletion tests/test_generator_profile_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
def test_agent_generator_profile_roundtrip(tmp_path):
# register_agent (module-level) does not take data_dir; use the registry
# directly against tmp_path, exactly like tests/test_agents.py does.
agents.AgentRegistry(tmp_path).register_agent("alice")
registry = agents.AgentRegistry(tmp_path)
registry.register_agent("alice")
assert agents.get_agent_generator_profile("alice", data_dir=tmp_path) is None
agents.set_agent_generator_profile("alice", "factual-recall", data_dir=tmp_path)
assert agents.get_agent_generator_profile("alice", data_dir=tmp_path) == "factual-recall"
agents.set_agent_generator_profile("alice", None, data_dir=tmp_path)
assert agents.get_agent_generator_profile("alice", data_dir=tmp_path) is None
# field must be absent from the stored record, not just None-valued
assert "generator_profile_id" not in registry.get_agent("alice")


def test_set_agent_generator_profile_unknown_agent_raises(tmp_path):
Expand Down
15 changes: 15 additions & 0 deletions tests/test_generator_profile_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,18 @@ def test_cli_list_and_set(tmp_path, capsys):
def test_cli_set_rejects_unknown(tmp_path):
rc = cli._generator_profile_set("nope", agent=None, data_dir=tmp_path)
assert rc != 0


def test_cli_set_per_agent(tmp_path):
from taosmd import agents
agents.AgentRegistry(tmp_path).register_agent("alice")
rc = cli._generator_profile_set("factual-recall", agent="alice", data_dir=tmp_path)
assert rc == 0
assert agents.get_agent_generator_profile("alice", data_dir=tmp_path) == "factual-recall"


def test_cli_set_unknown_agent_returns_nonzero(tmp_path, capsys):
rc = cli._generator_profile_set("balanced", agent="ghost", data_dir=tmp_path)
assert rc != 0
err = capsys.readouterr().err
assert "ghost" in err
15 changes: 15 additions & 0 deletions tests/test_generator_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,18 @@ def test_per_agent_beats_global(at_12gb, tmp_path):
agents.AgentRegistry(tmp_path).register_agent("bob")
agents.set_agent_generator_profile("bob", "factual-recall", data_dir=tmp_path)
assert gp.resolve_generator("bob", data_dir=tmp_path) == "ollama:gemma4:12b"


def test_resolve_memory_model_uses_per_agent_profile(at_12gb, tmp_path):
from taosmd import config, agents
# global profile is balanced (qwen3.5:9b at gpu-12gb)
config.set_generator_profile("balanced", data_dir=tmp_path)
agents.AgentRegistry(tmp_path).register_agent("carol")
# per-agent profile is factual-recall (gemma4:12b at gpu-12gb)
agents.set_agent_generator_profile("carol", "factual-recall", data_dir=tmp_path)
result = config.resolve_memory_model(agent="carol", data_dir=tmp_path)
# must differ from global default (qwen3.5:9b) and match factual-recall
assert result == "ollama:gemma4:12b"
global_result = config.resolve_memory_model(data_dir=tmp_path)
assert global_result == "ollama:qwen3.5:9b"
assert result != global_result