Add `contextStrategy: 'compact'` as a zero-LLM-cost alternative to `summarize`.
Instead of making an LLM call to compress everything into prose, it selectively
compresses old turns using structural rules:
- Preserve tool_use blocks (agent decisions) and error tool_results
- Replace long tool_result content with compact markers including tool name
- Truncate long assistant text blocks with head excerpts
- Keep recent turns (configurable via preserveRecentTurns) fully intact
- Detect already-compressed markers from compressToolResults to avoid double-processing
Closes#111
When minChars is set low, compressed markers could be re-compressed
with incorrect char counts. Skip blocks whose content already starts
with the compression prefix.
Replace consumed tool results with compact markers before each LLM call,
freeing context budget in multi-turn agent runs. A tool result is
"consumed" once the assistant has produced a response after seeing it.
- Add `compressToolResults` option to AgentConfig / RunnerOptions
- Runs before contextStrategy (lightweight, no LLM calls)
- Error results and short results (< minChars, default 500) are skipped
- 9 test cases covering default off, compression, parallel tools,
4+ turn compounding, error exemption, custom threshold, and
contextStrategy coexistence
* feat: add customTools support to AgentConfig for orchestrator-level tool injection
Users can now pass custom ToolDefinition objects via AgentConfig.customTools,
which are registered alongside built-in tools in all orchestrator paths
(runAgent, runTeam, runTasks). Custom tools bypass allowlist/preset filtering
but can still be blocked by disallowedTools.
Ref #108
* test: add disallowedTools blocking custom tool test
* fix: apply disallowedTools filtering to runtime-added custom tools
Previously runtime-added tools bypassed all filtering including
disallowedTools, contradicting the documented behavior. Now custom
tools still bypass preset/allowlist but respect the denylist.
- #99: pass per-call effectiveAbortSignal to buildToolContext() so tools
receive the correct signal instead of the static runner-level one
- #100: replace manual pending-task loop with queue.skipRemaining() on
abort, fixing blocked tasks left non-terminal and missing events
- #101: forward abortSignal in Gemini adapter's buildConfig() so the
SDK can cancel in-flight API calls
- Add 8 targeted tests for all three fixes
run() only handled 'done' events from stream(), silently dropping
'error' events. This caused failed LLM calls to return an empty
RunResult that the caller treated as successful.
* feat(agent): add smart loop detection for stuck agents (#16)
Detect when agents repeat the same tool calls or text outputs in a
sliding window. Three modes: warn (inject nudge, terminate on 2nd hit),
terminate (immediate stop), or custom callback. Fully opt-in via
`loopDetection` on AgentConfig — zero overhead when unconfigured.
* fix(agent): support async onLoopDetected callbacks and prevent orphaned tool_use events
- Await onLoopDetected callback result so async functions work correctly
instead of silently falling through to 'continue'
- Move loop detection before yielding tool_use events so terminate mode
never emits tool_use without a matching tool_result
* fix(agent): reset loopWarned on recovery and rename maxRepeatedToolCalls to maxRepetitions
- Reset loopWarned flag when the agent stops repeating, so a future
loop gets a fresh warning cycle instead of immediate termination
- Rename maxRepeatedToolCalls → maxRepetitions since the threshold
applies to both tool call and text output repetition detection
* test(agent): add tests for async callback, warn recovery, and injected warning text
- Verify async onLoopDetected callback is awaited correctly
- Verify loopWarned resets after recovery, giving fresh warning cycle
- Verify WARNING TextBlock is injected into user message content
Local models (Ollama, vLLM) sometimes return tool calls as text instead
of using the native tool_calls wire format. This adds a safety-net
extractor that parses tool calls from model text output when native
tool_calls is empty.
- Add text-tool-extractor with support for bare JSON, code fences,
and Hermes <tool_call> tags
- Wire fallback into OpenAI adapter chat() and stream() paths
- Add onWarning callback when model ignores configured tools
- Add timeoutMs on AgentConfig for per-run abort (local models can
be slow)
- Add 26 tests for extractor and fallback behavior
- Document local model compatibility in README
Add lightweight onTrace callback to OrchestratorConfig that emits
structured span events (llm_call, tool_call, task, agent) with timing,
token usage, and runId correlation. Zero overhead when not subscribed.
Closes#18