- Restore backticks around `prompt` in `Agent.prompt()` comment.
- Drop two stray blank lines around `mergedRecent` in `summarizeMessages`.
- Collapse deliberation comments in the new context-strategy test to one line.
- Add JSDoc note on `contextStrategy.custom.compress` that it fires every turn including the first; implementations must self-gate.
Replace `slice(initialMessages.length)` with an explicit `newMessages` accumulator so summarize/custom/sliding-window strategies that shrink conversation history no longer drop newly generated turns. Drops the `turns > 1` gate so oversized initial prompts can trigger compaction before the first LLM call.
Fixes#152.
* feat: expose custom MemoryStore via TeamConfig.sharedMemoryStore (#156)
The MemoryStore interface was already public, and SharedMemory / TeamInfo
already used the interface internally. This adds the final user-config wire
so integrators can attach custom backends (Redis, Postgres, Engram, etc.)
without hacking SharedMemory private fields.
Priority: sharedMemoryStore > sharedMemory: true > no memory.
Fully backward-compatible: existing sharedMemory: true users see no change.
SDK-only: the CLI cannot pass runtime objects through JSON config.
Closes#156.
* fix: validate sharedMemoryStore shape and reject from CLI JSON
Addresses Codex P2 review on #157. Plain objects from untrusted JSON could
previously reach `new SharedMemory(plainObject)` and crash later on the first
`.set`/`.list` call with a cryptic TypeError.
Defense-in-depth:
- SharedMemory constructor performs a runtime shape check and throws a clear
TypeError if the provided store does not implement get/set/list/delete/clear.
- CLI `asTeamConfig` explicitly rejects `sharedMemoryStore` in JSON with a
message pointing to the SDK path, since this field is documented SDK-only.
Adds 4 tests covering malformed stores (plain object, partial interface,
null, Team constructor path).
* fix: route falsy-but-present sharedMemoryStore through shape check
Addresses Codex finding 2 on #157. The truthy gate silently skipped falsy
values (null, 0, ''), letting config bugs downgrade to the default in-memory
store or no memory instead of failing fast.
Switched to `!== undefined` so any present value reaches SharedMemory's
runtime shape check and throws a clear TypeError. Adds 2 tests: null as a
bogus store throws; omitting the field still honors `sharedMemory: true`.
- New AzureOpenAIAdapter using AzureOpenAI client from openai SDK
- Registered 'azure-openai' in SupportedProvider and createAdapter()
- model field is primary deployment name; AZURE_OPENAI_DEPLOYMENT as fallback
- Default api-version: 2024-10-21
- Example in examples/providers/azure-openai.ts
- 14 tests covering chat, stream, tool_use, deployment fallback, error path
- Updated README.md, README_zh.md, examples/README.md, src/cli/oma.ts
Adds `oma run --dashboard` to write a static post-execution DAG HTML to `oma-dashboards/runTeam-<timestamp>.html`.
- Pure `renderTeamRunDashboard(result: TeamRunResult)` in `src/dashboard/`, no FS/network I/O in the library
- `TeamRunResult` gains `goal` + `tasks: TaskExecutionRecord[]` (with `TaskExecutionMetrics`)
- `layoutTasks()` extracted as pure function with cycle detection
- XSS mitigations: `application/json` payload, `</script>` escape, `textContent`-only node rendering
- CLI awaits write before exit (no race with `process.exit`)
Closes#4
Co-authored-by: Ibrahim Kazimov <74775400+ibrahimkzmv@users.noreply.github.com>
Implements `delegate_to_agent` built-in tool (closes#63). Opt-in registration via `includeDelegateTool`; only wired up by `runTeam` / `runTasks` for pool workers. Guards: self-delegation, unknown target, cycle detection via `delegationChain`, depth cap (`maxDelegationDepth`, default 3), pool deadlock.
Delegation runs on ephemeral Agent instances via `AgentPool.runEphemeral` (pool semaphore only, no per-agent lock) so mutual delegation (A→B while B→A) can't deadlock. Delegated run `tokenUsage` surfaces via `ToolResult.metadata` and rolls into the parent runner's total before the next budget check; delegation tool_result blocks are exempt from `compressToolResults` and the `compact` strategy. Best-effort SharedMemory audit writes at `{caller}/delegation:{target}:{ts}-{rand}`.
Picks up @NamelessNATM's work from #84 and adds cycle detection, token aggregation, compression exemption, mutual-delegation deadlock fix (Codex P1), and tool_result-preservation on budget-exceeded (Codex P2).
Co-authored-by: NamelessNATM <hamzarstar@gmail.com>
* feat: add rule-based compact context strategy (#111)
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
* fix: remove redundant length guard and fix compact type indentation
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 tool output auto-truncation at framework level (#110)
Prevent context blowup from large tool outputs by adding opt-in
character-based truncation (head 70% + tail 30% with marker).
Agent-level `maxToolOutputChars` and per-tool `maxOutputChars`
with per-tool taking priority. Marker overhead is budgeted so
the result never exceeds the configured limit.
* fix: truncateToolOutput may exceed maxChars when limit < marker overhead
- Fall back to hard slice when maxChars is too small to fit the marker
- Fix misplaced JSDoc for outputSchema in AgentConfig
- Tighten test assertion to verify length <= maxChars
* 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.
The short-circuit block in runTeam() called this.runAgent(), which emits
its own agent_start/agent_complete events and increments completedTaskCount.
The short-circuit block then emitted the same events again, and
buildTeamRunResult() incremented the count a second time.
Fix: call buildAgent() + agent.run() directly, bypassing runAgent().
Events and counting are handled once by the short-circuit block and
buildTeamRunResult() respectively.
Addresses all five review points from @JackChen-me on PR #70:
1. Extract shared keyword helpers into src/utils/keywords.ts so the
short-circuit selector and Scheduler.capability-match cannot drift.
Both orchestrator.ts and scheduler.ts now import the same module.
2. selectBestAgent now mirrors Scheduler.capability-match exactly,
including the asymmetric use of agent.model: agentKeywords includes
model, agentText does not. This restores parity with the documented
capability-match behaviour.
3. Remove isSimpleGoal and selectBestAgent from the public barrel
(src/index.ts). They remain exported from orchestrator.ts for unit
tests but are no longer part of the package API surface.
4. Forward the AbortSignal from runTeam(options) through the
short-circuit path. runAgent() now accepts an optional
{ abortSignal } argument; runTeam's short-circuit branch passes
the caller's signal so cancellation works for simple goals too.
5. Tighten the collaborate/coordinate complexity regexes so they only
fire on imperative directives ("collaborate with X", "coordinate
the team") and not on descriptive uses ("explain how pods
coordinate", "what is microservice collaboration").
Also fixes a pre-existing test failure in token-budget.test.ts:
"enforces orchestrator budget in runTeam" was using "Do work" as its
goal which now short-circuits, so the coordinator path the test was
exercising never ran. Switched to a multi-step goal.
Adds 60 new tests across short-circuit.test.ts and the new
keywords.test.ts covering all five fixes.
Co-Authored-By: Claude <noreply@anthropic.com>
When a goal is short (<200 chars) and contains no multi-step or
coordination signals, runTeam() now dispatches directly to the
best-matching agent — skipping the coordinator decomposition and
synthesis round-trips. This saves ~2 LLM calls worth of tokens
and latency for genuinely simple goals.
Complexity detection uses regex patterns for sequencing markers
(first...then, step N, numbered lists), coordination language
(collaborate, coordinate, work together), parallel execution
signals, and multi-deliverable patterns.
Agent selection reuses the same keyword-affinity scoring as the
capability-match scheduler strategy to pick the most relevant
agent from the team roster.
- Add isSimpleGoal() and selectBestAgent() (exported for testing)
- Add 35 unit tests covering heuristic edge cases and integration
- Update existing runTeam tests to use complex goals
Co-Authored-By: Claude <noreply@anthropic.com>
AgentPool now maintains a per-agent Semaphore(1) that serializes
concurrent run() calls targeting the same Agent. This prevents
shared-state races on Agent.state (status, messages, tokenUsage)
when multiple independent tasks are assigned to the same agent.
Lock acquisition order: per-agent lock first, then pool semaphore,
so queued tasks don't waste pool slots while waiting.
Fixes#61
Thread AbortSignal from the top-level API through RunContext to
executeQueue(), enabling graceful cancellation in Express, Next.js,
serverless, and CLI scenarios.
Changes:
- Added optional to RunContext interface
- now accepts
- now accepts
- executeQueue() checks signal.aborted before each dispatch round
and skips remaining tasks when cancelled
- Signal is forwarded to coordinator's run() and per-task pool.run()
so in-flight LLM calls are also cancelled
- Full backward compatibility: both methods work without options
The abort infrastructure already existed at lower layers
(AgentRunner, Agent, AgentPool) — this commit bridges the last gap
at the orchestrator level.
Co-authored-by: JasonOA888 <JasonOA888@users.noreply.github.com>
- Add contract tests for Anthropic, OpenAI, Gemini, Copilot adapters
- Add optional E2E test suite (tests/e2e/, run with npm run test:e2e)
- Add shared test fixtures (tests/helpers/llm-fixtures.ts)
- Configure vitest to exclude e2e tests by default
- Add "files" field to package.json to reduce npm package size by 50%
- Align npm description with GitHub repo description
- Bump version to 1.0.1
* 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
* feat(orchestrator): add onApproval callback for human-in-the-loop (#32)
Add an optional `onApproval` callback to OrchestratorConfig that gates
between task execution rounds. After each batch of parallel tasks
completes, the callback receives the completed tasks and the tasks about
to start, returning true to continue or false to abort gracefully.
Key changes:
- Add 'skipped' to TaskStatus for user-initiated abort (distinct from 'failed')
- Add skip(), skipRemaining(), cascadeSkip() to TaskQueue
- Add 'task_skipped' to OrchestratorEvent for progress monitoring
- Approval gate in executeQueue() with try/catch for callback errors
- Synthesis prompt now includes skipped tasks section
- 17 new tests covering queue skip operations and orchestrator integration
Closes#32
* docs: clarify onApproval contract and add missing test scenarios
- Document skip() cascade semantics, skipRemaining() in-flight constraint,
and onApproval trigger conditions / mutation warning
- Add concurrency safety comment on completedThisRound
- Note task_skipped as breaking union addition on OrchestratorEvent
- Add 3 test scenarios: single-batch no-callback, mixed success/failure
batch, and onProgress task_skipped event relay
* feat(agent): add beforeRun / afterRun lifecycle hooks (#31)
Add optional hook callbacks to AgentConfig for cross-cutting concerns
(guardrails, logging, token budgets) without modifying framework internals.
- beforeRun: receives prompt + agent config, can modify or throw to abort
- afterRun: receives AgentRunResult, can modify or throw to fail
- Works with all three execution modes: run(), prompt(), stream()
- 15 test cases covering modify, throw, async, composition, and history integrity
* fix(agent): preserve non-text content blocks in beforeRun hook
- applyHookContext now replaces only text blocks, keeping images and
tool results intact (was silently stripping them)
- Use backward loop instead of reverse() + find() for efficiency
- Clarify JSDoc that only `prompt` is applied from hook return value
- Add test for mixed-content user messages
* fix(agent): address review feedback on beforeRun/afterRun hooks
- Normalize stream done event to always yield AgentRunResult
- Move transitionTo('completed') after afterRun to fix state ordering
- Strip hook functions from BeforeRunHookContext.agent to avoid self-references
- Pass originalPrompt to applyHookContext to avoid redundant message scan
- Clarify afterRun JSDoc: not called when the run throws
- Add tests: error-path skip, outputSchema+afterRun, ctx.agent shape, multi-turn hooks