Replace the legacy long-goal skill contract with command-scoped goal tools and runtime guidance. Keep goal state durable across continuations while restricting create and replace mutations to explicit user /goal turns.
Treat max_messages as a last-resort replay guard now that consolidation and idle auto-compact own normal history reduction. Raising the default avoids frequent sliding-window prefix churn in moderate conversations without adding a new cache-policy knob.
Add fail_on_tool_error to AgentDefaults and wire it through
AgentLoop -> SubagentManager -> AgentRunSpec.
Previously hardcoded to True in SubagentManager._run_subagent.
Now configurable via config.json with default True for backward
compatibility. When set to False, subagents can retry on minor
tool errors instead of immediately failing.
Changes:
- nanobot/config/schema.py: add fail_on_tool_error field (default True)
- nanobot/agent/subagent.py: accept and forward fail_on_tool_error
- nanobot/agent/loop.py: pass config through to SubagentManager
- tests/agent/test_subagent.py: add regression test
Signed-off-by: axelray-dev <110029405+axelray-dev@users.noreply.github.com>
Document that SDK callers always go through run() → process_direct(ephemeral=False),
so per_call hooks are intentionally excluded from ephemeral turns per chengyongru
review feedback.
The test_run_populates_tools_used_across_iterations,
test_run_populates_final_messages, and
test_run_user_hooks_still_fire_alongside_capture tests were reading
bot._loop._extra_hooks directly in their fake_process_direct mocks,
but the contextvar change moved per-call hooks to _per_call_hooks.
Also fixes the ephemeral guard: per_call hooks should not be used in
ephemeral mode (the original code incorrectly applied them regardless).
Nanobot.run() previously mutated the shared self._loop._extra_hooks
attribute under a try/finally. When two run() calls with different
session_keys execute concurrently, they overwrite each other's hook
lists — the second call saves the first call's hooks as 'prev', and
the finally block restores stale state.
Use a contextvars.ContextVar instead, which is per-task in asyncio.
This gives each concurrent run() call its own hook list without
changing any function signatures. Falls back to self._extra_hooks
when no per-call hooks are set (non-SDK usage).
Treat the current live user message as the replay boundary for normal user turns, while keeping user-turn extension for history and consolidation paths that need it. Add regression coverage for the user-triggered long tool-turn case.
The non-streaming retry called process_direct again with the same
content, persisting a duplicate user turn. Pass persist_user_message=False
so the retry recovers a response without re-recording the user message.