71 Commits

Author SHA1 Message Date
CoreRasurae 4fe14c5b77 feat(agents): add offset-based pagination to file_read
CI / lint (pull_request) Successful in 1m40s
CI / quality (pull_request) Successful in 2m7s
CI / typecheck (pull_request) Successful in 3m23s
CI / build (pull_request) Successful in 1m6s
CI / security (pull_request) Successful in 4m51s
CI / integration_tests (pull_request) Successful in 3m14s
CI / unit_tests (pull_request) Successful in 5m24s
CI / coverage (pull_request) Successful in 6m51s
CI / status-check (pull_request) Successful in 7s
CI / benchmark (pull_request) Failing after 17m20s
CI / lint (push) Successful in 59s
CI / typecheck (push) Successful in 2m21s
CI / security (push) Successful in 1m16s
CI / quality (push) Successful in 51s
CI / build (push) Successful in 1m29s
CI / integration_tests (push) Successful in 3m28s
CI / unit_tests (push) Successful in 4m37s
CI / coverage (push) Successful in 4m58s
CI / status-check (push) Successful in 7s
CI / benchmark (push) Failing after 18m9s
`file_read` always read from position 0; `max_chars` (ADR-2030 D-3) only
bounded where the returned content stopped, so once a large file was
truncated there was no way to retrieve the next chunk short of falling
back to the `shell` tool (`tail -c +N`, `sed -n`, `dd skip=...`) — the
same class of awkward, error-prone detour ADR-2030 D-4 already eliminated
for file paths mistaken for shell commands.

Adds an optional `offset` character-position argument (default 0,
byte-for-byte identical output when omitted) to both the tool schema
(`llm_tools._BUILTIN_TOOL_SCHEMAS["file_read"]`) and the implementation
(`ToolAgent._file_read_tool`). Combined with `max_chars` it returns the
window `[offset, offset + max_chars)`, and the `[FILE_READ_SUCCESS]`
header now signals continuation: `MORE_CONTENT_AT_OFFSET: N` when more
content remains (N is the exact next offset, so callers never compute
`offset + max_chars` themselves), or `NO_MORE_CONTENT_AT_OFFSET: N` when
the requested offset is at or beyond end-of-file — a success response
with empty content, not an error, since that is the expected terminal
state of a correctly-paginating caller. Negative or non-integer offsets
raise `ExecutionError`, mirroring the existing `max_chars` validation
pattern.

See docs/adr/ADR-2033-file-read-offset-pagination.md for the full set of
design decisions, including why directory-listing pagination and
byte-level seeking are deferred to a future issue.

ISSUES CLOSED: #83
2026-07-31 08:53:31 +00:00
CoreRasurae 4b354f5b2d feat(agents): surface timeout budget in tool schemas and errors
CI / lint (pull_request) Successful in 50s
CI / quality (pull_request) Successful in 55s
CI / security (pull_request) Successful in 1m25s
CI / typecheck (pull_request) Successful in 1m34s
CI / build (pull_request) Successful in 1m34s
CI / integration_tests (pull_request) Successful in 5m18s
CI / unit_tests (pull_request) Successful in 6m38s
CI / benchmark (pull_request) Failing after 42m6s
CI / lint (push) Successful in 47s
CI / typecheck (push) Successful in 1m26s
CI / security (push) Successful in 1m9s
CI / quality (push) Successful in 50s
CI / build (push) Successful in 1m36s
CI / integration_tests (push) Successful in 3m45s
CI / unit_tests (push) Successful in 5m42s
CI / coverage (push) Successful in 4m44s
CI / status-check (push) Successful in 6s
CI / benchmark (push) Successful in 44m16s
CI / coverage (pull_request) Successful in 4m57s
CI / status-check (pull_request) Successful in 7s
The `shell` and `http_request` built-in tools are the only two whose
execution actually honors the tool agent's `timeout` config field
(ToolAgent._execute_shell_command, ToolAgent._http_request_tool), but
their LLM-facing schemas (_BUILTIN_TOOL_SCHEMAS in llm_tools.py) never
told the model a timeout applied, and neither tool's timeout error
named the config field that controls it.

- _BUILTIN_TOOL_SCHEMAS["shell"] and ["http_request"] descriptions now
  state that execution is bounded by the agent's configured `timeout`
  (seconds), matching the existing precedent of documenting `file_read`'s
  `max_chars` behavior in its schema.
- _execute_shell_command's timeout ExecutionError now names `timeout`
  as the adjustable config field, in addition to the elapsed seconds it
  already reported.
- _http_request_tool now catches asyncio.TimeoutError distinctly from
  the generic `except Exception`, so a request timeout produces a
  message naming the config field instead of the previous generic
  "HTTP request failed" text (which gave no indication a timeout was
  the cause). Non-timeout HTTP failures are unaffected — same
  exception handling, same message shape as before.

No change to enforcement mechanics: `self.timeout` remains the sole
config field (default 1s, Actor Configuration Standard §4.5). This is
schema/message content only, within the extension precedent already
established by ADR-2030 (D-1 schema registry, D-4/D-6 error-quality
improvements) — no new ADR was needed for this change.

Tests: extended features/tool_agent.feature (updated the two existing
shell/http_request timeout scenarios to also assert the new config-field
hint) and features/llm_tools_coverage.feature (new scenarios asserting
both schema descriptions mention the timeout budget). Full suite:
141 features / 2782 scenarios / 13172 steps passed; coverage 96.8%
(>=96.5% threshold). Robot integration_tests: 27/27 suites passed.
ASV benchmark_regression: no significant change, as expected for a
messaging-only change.

ISSUES CLOSED: #82
2026-07-30 19:29:54 +01:00
CoreRasurae a8f68b8262 fix(graph): enforce USD budget at each agent invocation with pre-flight gate
CI / lint (pull_request) Successful in 50s
CI / typecheck (pull_request) Successful in 55s
CI / security (pull_request) Successful in 53s
CI / quality (pull_request) Successful in 38s
CI / unit_tests (pull_request) Successful in 3m15s
CI / integration_tests (pull_request) Successful in 1m14s
CI / build (pull_request) Successful in 39s
CI / coverage (pull_request) Successful in 3m16s
CI / status-check (pull_request) Successful in 3s
CI / lint (push) Successful in 37s
CI / typecheck (push) Successful in 56s
CI / security (push) Successful in 54s
CI / quality (push) Successful in 37s
CI / unit_tests (push) Successful in 3m15s
CI / integration_tests (push) Successful in 1m14s
CI / build (push) Successful in 39s
CI / coverage (push) Successful in 3m16s
CI / status-check (push) Successful in 3s
Enforces the USD budget (max_cost_usd) as a hard pre-flight gate before
every node execution and at each main-agent ainvoke round inside the
multi-turn tool loop, with pruning passes silently skipped when the
budget is exhausted.

Pre-flight gate (pure_graph.py):
  _check_budget_pre_flight() raises ExecutionError(kind='cost',
  reason='budget_exhausted') immediately before node execution in both
  _execute_from_node and all three branches of _stream_from_node.

In-node budget gating (llm.py):
  _check_budget_before_invoke() gates every _retry_ainvoke() call
  inside _execute_tool_loop, process_message, and stream_message.
  _should_skip_pruning() skips pruning passes when budget exhausted.
  _accumulate_cost_after_invoke() computes USD cost from token counts
  using the pricing table and advances current_accumulated_cost so
  subsequent budget checks see the updated cost.

ContextVar plumbing (retry.py, pure_graph.py):
  current_accumulated_cost, current_max_cost_usd, and current_pricing
  ContextVars carry per-node budget state from PureLangGraph into
  LLMAgent. Set before node.execute() via _set_budget_context_for_node,
  reset in finally blocks via _reset_budget_context.

Reviewer fixes (rui.hu #80):
  - Move cost accumulation inside try block in terminal streaming branch
    to prevent ContextVar leak (was set after finally reset).
  - Standardize log precision to $%.6f across all budget messages.
  - Remove orphaned 'already exhausted' step (criterion unreachable).
  - Add dedicated streaming budget enforcement scenario.
  - Remove redundant = None default on _reset_budget_context pricing_token.

ISSUES CLOSED: #76
2026-07-12 19:51:39 +01:00
CoreRasurae 2072bc3ad1 feat(langgraph): implement real type:tool graph node execution via ToolAgent dispatch
CI / lint (pull_request) Successful in 51s
CI / typecheck (pull_request) Successful in 58s
CI / security (pull_request) Successful in 55s
CI / quality (pull_request) Successful in 38s
CI / unit_tests (pull_request) Successful in 3m14s
CI / integration_tests (pull_request) Successful in 1m13s
CI / build (pull_request) Successful in 39s
CI / coverage (pull_request) Successful in 3m16s
CI / status-check (pull_request) Successful in 3s
CI / lint (push) Successful in 38s
CI / typecheck (push) Successful in 53s
CI / security (push) Successful in 52s
CI / quality (push) Successful in 36s
CI / unit_tests (push) Successful in 3m12s
CI / integration_tests (push) Successful in 1m9s
CI / build (push) Successful in 37s
CI / coverage (push) Successful in 3m12s
CI / status-check (push) Successful in 4s
Wires NodeConfig.tool and NodeConfig.static_config through to
ToolAgent.invoke() so type: tool graph nodes execute real tool
calls instead of returning no-ops.

- NodeConfig: adds tool (str | None) and static_config (dict)
- Node._execute_tool: creates ToolAgent, invokes with static config
  merged with threaded previous output, returns ToolMessage
- runtime_dispatch: auto-creates ToolAgent for type: tool nodes
  without pre-registered agents
- pure_graph: parses tool:/config: YAML keys into NodeConfig
- BDD: 11 scenarios, 33 steps, all passing

Closes #75
2026-07-09 14:06:59 +00:00
hurui200320 9ec578f229 feat(agents): add tool_agent_class parameter to AgentFactory and create_executor
CI / lint (pull_request) Successful in 44s
CI / typecheck (pull_request) Successful in 49s
CI / security (pull_request) Successful in 47s
CI / quality (pull_request) Successful in 33s
CI / unit_tests (pull_request) Successful in 3m4s
CI / integration_tests (pull_request) Successful in 1m2s
CI / build (pull_request) Successful in 33s
CI / coverage (pull_request) Successful in 3m5s
CI / status-check (pull_request) Successful in 2s
CI / lint (push) Successful in 31s
CI / typecheck (push) Successful in 46s
CI / security (push) Successful in 46s
CI / quality (push) Successful in 31s
CI / unit_tests (push) Successful in 3m2s
CI / integration_tests (push) Successful in 1m2s
CI / build (push) Successful in 31s
CI / coverage (push) Successful in 3m4s
CI / status-check (push) Successful in 6s
Extends AgentFactory.__init__ and create_executor() with a keyword-only
tool_agent_class: type[ToolAgent] = ToolAgent parameter so callers can
inject a custom ToolAgent subclass at runtime without monkey-patching
module globals (resolves the five-module setattr loop in cleveragents-webapp).

**AgentFactory** (agents/factory.py):
- Accepts tool_agent_class keyword argument; validates it is a ToolAgent
  subclass at construction time (fail-fast, CONTRIBUTING.md §Argument
  Validation).
- Stores as self._tool_agent_class and uses it to populate
  self.agent_types['tool'], so every create_agent('tool') call
  instantiates the supplied subclass.
- Forwards tool_agent_class to LLMAgent when creating 'llm'-typed agents
  so the LLM tool-calling loop honours the injected subclass.
- Default ToolAgent is preserved, so all existing callers are unaffected.

**LLMAgent** (agents/llm.py):
- Accepts a keyword-only tool_agent_class: type[ToolAgent] = ToolAgent
  argument, validated as a ToolAgent subclass at construction time.
- The multi-turn tool-call loop (_execute_tool_loop) constructs its
  ephemeral tool executors via self._tool_agent_class(...) across all
  three dispatch sites (regular loop, budget-exhaustion synthesis round,
  and stuck-model synthesis round), so the LLM tool-calling path no
  longer falls back to the module-level ToolAgent. This closes the gap
  that previously prevented removing the webapp monkey-patch for the LLM
  tool-calling path.

**Executor / create_executor** (runtime.py):
- Executor.__init__ accepts and validates tool_agent_class; stores as
  self.tool_agent_class.
- create_executor() accepts tool_agent_class and forwards it to Executor.

**Dispatch paths** (runtime_dispatch.py):
- _execute_tool: uses executor.tool_agent_class(...) instead of the
  module-level ToolAgent(...) so the direct-construction path (bypassing
  AgentFactory) also honours the injected subclass.
- _execute_graph and _execute_graph_stream: pass
  tool_agent_class=executor.tool_agent_class to AgentFactory so graph
  nodes using tool-typed agents pick up the subclass.
- _execute_llm and _execute_llm_stream: pass
  tool_agent_class=executor.tool_agent_class to AgentFactory so the
  single-LLM actor's internally-created LLMAgent (and its tool-calling
  loop) honours the injected subclass. Without this, a single LLM actor
  that makes tool calls silently fell back to the base ToolAgent.
- _execute_multi_actor: passes tool_agent_class=executor.tool_agent_class
  to the sub-Executor so every actor inside a multi-actor bundle (tool,
  graph, or LLM) honours the injected subclass. Without this, all
  dispatch paths reachable through a multi-actor bundle fell back to the
  base ToolAgent.
- Removed the now-unused module-level ToolAgent import; the _execute_tool
  docstring cross-reference is fully qualified so it still resolves.
  All six dispatch paths that construct or dispatch tool agents now
  forward the injected subclass.

**ReactiveCleverAgentsApp** (core/application.py):
- Removed the redundant register_agent_type('tool', ToolAgent) call from
  load_configuration and the dead type_map re-registration block in
  _create_agents; AgentFactory.__init__ already owns the 'tool'
  registration. This drops the last hardcoded module-level ToolAgent
  references flagged in the issue #73 audit.

**Tests** (features/tool_agent_class_injection.feature + steps):
- 17 BDD scenarios covering: default class when parameter omitted;
  custom subclass stored on factory and executor; invalid class rejected
  with ConfigurationError (factory, executor, and LLMAgent); factory
  create_agent returns custom instance; _execute_tool dispatch
  instantiates custom subclass; the LLM tool loop instantiates the
  injected subclass; isinstance LSP compatibility through the factory;
  the _execute_llm dispatch path forwarding tool_agent_class to
  AgentFactory (single-LLM actor making a tool call); and the
  _execute_multi_actor dispatch path forwarding tool_agent_class to the
  sub-Executor. The two new dispatch-path scenarios were verified to
  fail without the source fixes (instantiation_count=0), confirming
  they guard the injection contract for the single-LLM and multi-actor
  paths.

**Updated tests** (features/steps/runtime_coverage_steps.py,
features/steps/runtime_extended_coverage_steps.py):
- Removed the now-dead patch('cleveractors.runtime_dispatch.ToolAgent')
  mocks (and their mock instances) that no longer intercept _execute_tool
  after it switched to executor.tool_agent_class. The tool-actor scenario
  continues to pass via the real ToolAgent; the normal-coverage branch
  uses patch.object(ToolAgent, 'process_message', ...) to stub tool
  execution.

**Updated test** (features/steps/credential_executor_paths_steps.py):
- The _execute_multi_actor credentials-forwarding scenario intercepts
  Executor.__init__ with a capturing_init to assert the credentials
  dict reaches the sub-Executor. Now that _execute_multi_actor forwards
  tool_agent_class to the sub-Executor, capturing_init accepts and
  forwards that keyword so the assertion continues to pass.

**Removed obsolete test** (features/application_coverage_gaps.feature):
- Dropped the '_create_agents registers built-in agent types' scenario
  and its step definitions; the behaviour it asserted (redundant
  re-registration) was removed from _create_agents.

ISSUES CLOSED: #73
2026-07-08 09:23:34 +00:00
CoreRasurae ceb577e405 fix(nodes): propagate ExecutionError from retry mechanism instead of swallowing it
CI / lint (pull_request) Successful in 46s
CI / typecheck (pull_request) Successful in 51s
CI / security (pull_request) Successful in 50s
CI / quality (pull_request) Successful in 34s
CI / unit_tests (pull_request) Successful in 3m7s
CI / integration_tests (pull_request) Successful in 1m8s
CI / build (pull_request) Successful in 34s
CI / coverage (pull_request) Successful in 3m8s
CI / status-check (pull_request) Successful in 3s
CI / lint (push) Successful in 46s
CI / typecheck (push) Successful in 51s
CI / security (push) Successful in 51s
CI / quality (push) Successful in 34s
CI / unit_tests (push) Successful in 3m7s
CI / integration_tests (push) Successful in 1m8s
CI / build (push) Successful in 35s
CI / coverage (push) Successful in 3m9s
CI / status-check (push) Successful in 3s
When all LLM communication retries are exhausted, call_with_retry() raises
ExecutionError(kind=\"timeout\"). Two broad except Exception handlers in
Node._execute_agent() and Node.execute() systematically caught this,
converting the timeout into an error string in graph state or — in the
streaming path — an empty response.

Added targeted except ExecutionError as e handlers that check
e.kind and re-raise. Non-timeout ExecutionError instances
(tool errors, etc.) continue to be caught and converted to error strings
for graceful graph continuation.

Extend _is_http_comms_error() in retry.py to recognize provider SDK
connection errors (APIConnectionError, APITimeoutError) by class name,
so they are retried with exponential backoff instead of propagating
immediately.

Add safety net in process_message() to wrap non-retried connection
errors as ExecutionError(kind='timeout') for correct propagation
through the node layer (issue #71).

Two new BDD scenarios verify:
- ExecutionError(kind=\"timeout\") propagates with correct kind/reason
- Non-timeout ExecutionError is still caught gracefully

ISSUES CLOSED: #71
2026-07-07 13:47:02 +01:00
CoreRasurae 52451b2550 test(agents): disable retry in error-path tests to avoid unnecessary delays
CI / lint (pull_request) Successful in 36s
CI / typecheck (pull_request) Successful in 52s
CI / security (pull_request) Successful in 50s
CI / quality (pull_request) Successful in 34s
CI / unit_tests (pull_request) Successful in 3m9s
CI / integration_tests (pull_request) Successful in 1m3s
CI / build (pull_request) Successful in 35s
CI / coverage (pull_request) Successful in 3m9s
CI / status-check (pull_request) Successful in 4s
CI / lint (push) Successful in 46s
CI / typecheck (push) Successful in 51s
CI / security (push) Successful in 50s
CI / quality (push) Successful in 34s
CI / unit_tests (push) Successful in 3m8s
CI / build (push) Successful in 37s
CI / coverage (push) Successful in 3m8s
CI / integration_tests (push) Successful in 1m12s
CI / status-check (push) Successful in 2s
Pre-existing tests that inject error-raising mock chat models (raising
LangChainException or RuntimeError) were triggering the full
exponential-backoff retry loop (~63.5s per test). With 6 such tests,
this added minutes to the suite.

Set max_retries=0 in each error-path step definition so the retry loop
fails fast on the first attempt, preserving the test contract while
avoiding unnecessary sleep delays.
2026-07-03 16:00:20 +01:00
CoreRasurae 55734cd512 feat(agents): add LLM agent retry mechanisms with exponential backoff
- Documents the retry feature implementation (issue #69, ADR-2032)

Introduce configurable retry mechanisms (ADR-2032) for all LLM agent
communications (main agent and pruning agent) with exponential backoff.

- New config fields: max_retries (default 7), max_retry_time (default 60s)
  -1 sentinel disables the corresponding guard (infinite retries/time)
- Exponential backoff: initial 0.5s, doubled each retry
- Termination: whichever comes first (max_retries or max_retry_time),
  with -1 disabling the respective check
- Counter and accumulated wait time reset on success
- Timeout ExecutionError includes URL, actor graph name, and retry stats
- Uniform scope: applies to both main agent and pruning agent
- Timeout raises ExecutionError(kind="timeout") with URL, actor graph
  name, retry count, and accumulated wait time in the message (D-7)
- Same client/connection reused across retries, no re-establishment (D-6)
- Uniform scope: applies to both main agent and pruning agent (D-5)
- Graph name threaded through state metadata and ContextVar for error
  reporting
- BDD tests (7 scenarios) for retry behaviour including -1 sentinel cases
- Reject max_retries/max_retry_time values < -1 with ConfigurationError
- Reset current_graph_name ContextVar after node execution
- Wrap no-tools astream() with per-chunk granular retry via call_with_retry
- Update ADR D-7 with symmetrical disabled-guard example
- Update CHANGELOG to reflect full feature scope

ISSUES CLOSED: #69
2026-07-03 16:00:11 +01:00
hurui200320 306f114b8a feat(agents): add tool-call support to LLMAgent.stream_message
CI / lint (pull_request) Successful in 47s
CI / typecheck (pull_request) Successful in 51s
CI / security (pull_request) Successful in 50s
CI / quality (pull_request) Successful in 33s
CI / unit_tests (pull_request) Successful in 3m7s
CI / integration_tests (pull_request) Successful in 1m10s
CI / build (pull_request) Successful in 35s
CI / coverage (pull_request) Successful in 3m9s
CI / status-check (pull_request) Successful in 3s
CI / lint (push) Successful in 34s
CI / typecheck (push) Successful in 51s
CI / security (push) Successful in 50s
CI / quality (push) Successful in 33s
CI / unit_tests (push) Successful in 3m6s
CI / integration_tests (push) Successful in 1m7s
CI / build (push) Successful in 33s
CI / coverage (push) Successful in 3m7s
CI / status-check (push) Successful in 3s
Extract the entire multi-turn tool-call orchestration from the inline
loop inside process_message() into a new shared private method
_execute_tool_loop().  Both process_message() and stream_message() now
delegate to this single implementation, eliminating the previous
capability gap where stream_message() had no tool-calling support.

Design:

- _ToolLoopResult dataclass: carries final_response, accumulated_prompt,
  accumulated_completion, budget_exhausted, and synthesis_was_run.  Both
  callers read token counts from this object instead of local sentinels.

- _ToolLoopError exception: wraps any exception raised inside the helper
  and carries partial accumulated token counts + any_invocation_made flag.
  Callers catch this, set their _captured_prompt/_captured_completion
  sentinels (billing integrity), then re-raise the original cause.

- _execute_tool_loop(): extracted verbatim from process_message().
  Contains the full ainvoke() loop, per-tool ToolAgent dispatch, token
  accumulation across rounds, token-budget exhaustion + synthesis path
  (§4.4.7 D-4), stuck-model synthesis prompt, and tool-output pruning
  pass (§4.4.8).  On exception, wraps in _ToolLoopError for the caller.

process_message() refactor:

- Replaces ~490 lines of inline tool loop with a 15-line delegation.
  For tool-configured actors: awaits _execute_tool_loop(), extracts
  response text and token counts from the result.
  For no-tool actors: single plain ainvoke() (unchanged behaviour).
  Billing-integrity sentinels (_captured_prompt/_captured_completion)
  are set from _ToolLoopError on exception and from the result on
  success.  All existing Behave scenarios continue to pass unmodified.

stream_message() integration:

- When tools are configured: awaits _execute_tool_loop() and yields the
  final response as a single content chunk.  Token counts come from the
  result's accumulated_prompt/accumulated_completion fields (spanning
  all tool rounds + pruning passes).  Memory update is performed before
  the generator returns.  Billing sentinels are updated from
  _ToolLoopError on exception.
- When no tools are configured: the existing astream() token-by-token
  path runs completely unchanged.

Single-chunk trade-off: when tools are involved, the user already waits
for tool execution before the final answer begins, so yielding that
answer as one chunk vs. token-by-token has minimal UX impact.  The
alternative (re-calling astream() after ainvoke() produced the final
response) would double LLM cost for every tool-using request.

Webapp workaround: the actor_config_has_tools fallback in
cleveragents/cleveragents-webapp#328 (stream_fallback_to_non_stream in
_execute_via_cleveractors_stream / _generate_actor_sse) can now be
removed.

Tests added:
- features/llm_agent_tool_loop.feature + steps: 10 Behave scenarios
  covering _execute_tool_loop() internals independently (single round,
  multi-round, max_rounds exhaustion, token accumulation, budget
  exhaustion, stuck-model synthesis, pruning pass, tool dispatch error,
  exception path with partial counts, pre-invocation ConfigurationError).
- features/llm_agent_stream_tool_calls.feature + steps: 7 Behave
  scenarios for stream_message() with tool-call support (single chunk
  output, two-round token sum, final text, no-tools astream unchanged,
  no-tools token counts, exception billing integrity, memory update).
- robot/llm_tool_calling.robot: two new Robot integration tests:
  'Streaming Path Exercises Tool Loop Via Execute Stream' and
  'Streaming Path Accumulates Tokens Across Two Tool Rounds'.
- robot/ToolCallingTestLib.py: new keywords and
  create_executor_with_multi_round_tool_calling_agent factory.

Quality gates: format ✓  lint ✓  typecheck ✓  security_scan ✓
unit_tests ✓  integration_tests ✓  coverage_report ✓ (97.0%)

ISSUES CLOSED: #67
2026-07-02 07:47:39 +00:00
CoreRasurae 8a7e353767 fix(actors): llm token consumption undercounted
CI / lint (pull_request) Successful in 35s
CI / typecheck (pull_request) Successful in 50s
CI / security (pull_request) Successful in 49s
CI / quality (pull_request) Successful in 33s
CI / unit_tests (pull_request) Successful in 3m5s
CI / integration_tests (pull_request) Successful in 1m19s
CI / build (pull_request) Successful in 35s
CI / coverage (pull_request) Successful in 3m7s
CI / status-check (pull_request) Successful in 3s
CI / lint (push) Successful in 33s
CI / typecheck (push) Successful in 50s
CI / security (push) Successful in 50s
CI / quality (push) Successful in 33s
CI / integration_tests (push) Successful in 1m11s
CI / build (push) Successful in 34s
CI / unit_tests (push) Failing after 14m51s
CI / coverage (push) Has been cancelled
CI / status-check (push) Has been cancelled
- enforces pruning model token-tracking metadata availability for cost tracking
- also temporarily enforces pruning-model match

ISSUES CLOSED: #65
2026-07-01 14:55:58 +01:00
CoreRasurae bcce5dc42a fix(agents): validate tool name against declared tools list before dispatch
CI / lint (pull_request) Successful in 43s
CI / typecheck (pull_request) Successful in 47s
CI / security (pull_request) Successful in 46s
CI / quality (pull_request) Successful in 31s
CI / unit_tests (pull_request) Successful in 3m1s
CI / integration_tests (pull_request) Successful in 1m1s
CI / build (pull_request) Successful in 31s
CI / coverage (pull_request) Successful in 3m2s
CI / status-check (pull_request) Successful in 2s
CI / lint (push) Successful in 31s
CI / typecheck (push) Successful in 46s
CI / security (push) Successful in 47s
CI / quality (push) Successful in 30s
CI / unit_tests (push) Successful in 3m9s
CI / integration_tests (push) Successful in 1m7s
CI / build (push) Successful in 37s
CI / coverage (push) Successful in 3m11s
CI / status-check (push) Successful in 3s
ISSUES CLOSED: #63
2026-06-30 12:06:05 +01:00
CoreRasurae 964e87cc0f feat(tool-loop): add token-budget awareness and tool output pruning
CI / lint (pull_request) Successful in 1m31s
CI / typecheck (pull_request) Successful in 50s
CI / security (pull_request) Successful in 50s
CI / quality (pull_request) Successful in 33s
CI / unit_tests (pull_request) Successful in 3m5s
CI / integration_tests (pull_request) Successful in 1m7s
CI / build (pull_request) Successful in 34s
CI / coverage (pull_request) Successful in 3m7s
CI / status-check (pull_request) Successful in 3s
CI / lint (push) Successful in 33s
CI / typecheck (push) Successful in 50s
CI / security (push) Successful in 50s
CI / quality (push) Successful in 33s
CI / integration_tests (push) Successful in 1m12s
CI / build (push) Successful in 33s
CI / unit_tests (push) Successful in 3m5s
CI / coverage (push) Successful in 3m6s
CI / status-check (push) Successful in 6s
Implement token-budget awareness (§4.4.7) and tool output pruning
(§4.4.8-9) for the multi-turn tool-call loop to prevent context-window
exhaustion.

Token budget: tracks estimated token count before each ainvoke(), emits
warning at 75% budget, injects synthesis prompt at exhaustion with one
final tool-call round. Estimation via chat_model.get_num_tokens() with
len(content)//4 heuristic fallback. Config key: token_budget_percent.

Tool output pruning: implicit LLM extraction pass scoped to file_read
calls. Schema augmentation injects output_prune (bool) and
output_prune_context (string) meta-arguments. Pruning model responds
with [PRUNE_INFO_START/END] and [PRUNE_OUTPUT_START/END] markers.
Config keys: allow_tool_output_pruning, pruning_model.

- Capture output_prune value in budget synthesis flow with proper guard
- Fix ADR D-4: synthesis prompt is HumanMessage not SystemMessage
- Fix Robot tests: replace /dev/null with temp file path

ADR-2031 documents all specification extensions. 21 new BDD scenarios,
5 Robot integration tests, 16 ASV benchmarks.

ISSUES CLOSED: #61
2026-06-26 18:21:14 +01:00
CoreRasurae 227e2feece feat(agents): implement multi-turn tool-call loop and sandbox improvements
CI / build (pull_request) Successful in 45s
CI / quality (pull_request) Successful in 58s
CI / lint (pull_request) Successful in 1m15s
CI / typecheck (pull_request) Successful in 1m15s
CI / security (pull_request) Successful in 1m15s
CI / integration_tests (pull_request) Successful in 1m24s
CI / unit_tests (pull_request) Successful in 3m25s
CI / coverage (pull_request) Successful in 3m10s
CI / status-check (pull_request) Successful in 6s
CI / lint (push) Successful in 42s
CI / quality (push) Successful in 48s
CI / typecheck (push) Successful in 51s
CI / security (push) Successful in 49s
CI / build (push) Successful in 47s
CI / integration_tests (push) Successful in 1m10s
CI / unit_tests (push) Successful in 3m9s
CI / coverage (push) Successful in 3m7s
CI / status-check (push) Successful in 3s
Implements the complete LLM agent tool-calling pipeline — the tool-calling
path was broken in multiple ways: tools from agent config were not passed
to the LLM, tool execution was single-pass (the model could not see tool
results or make follow-up calls), tool errors were discarded, and shell/
python_exec tools were never registered.

Key changes:

- Multi-turn tool-call loop: passes tools to the LLM via ainvoke(tools=...)
  and re-invokes after each batch of ToolResults, with a configurable
  round limit (tool_max_rounds config / TOOL_MAX_ROUNDS env var, default 20).

- Tool schema module (llm_tools.py): normalize_tool_entry() converts
  string tool names and config dicts to OpenAI function-calling format
  with full parameter schemas and LLM-facing guidance (max_chars for
  file_read, sandbox builtins for python_exec, etc.).

- file_read enhancements: directory listing with file sizes and type
  indicators, max_chars truncation to prevent context overflow, shell
  command detection with redirect to shell tool, file-not-found
  recovery hints with parent directory listing.

- python_exec sandbox: stdout capture so print() produces visible
  output, NameError guidance directing LLM to file_read/file_write
  instead of using open().

- shell/python_exec tool registration in builtin_tools when
  allow_shell / exec_python is enabled.

- create_subprocess_shell for shell commands (was create_subprocess_exec,
  which blocks pipes/redirections/chains).

- Tool error propagation: ExecutionError/ConfigurationError details
  included in ToolMessages for LLM self-correction.

- Stuck-model recovery: injects synthesizing prompt when model
  exhausts tool rounds without producing content.

- LLM provider error details preserved in ExecutionError messages
  instead of generic "LLM processing failed".

- Empty message filtering in _prepare_conversation_history() prevents
  whitespace-only messages from polluting downstream conversation
  history in multi-agent pipelines.

Closes #59
2026-06-23 12:39:20 +01:00
CoreRasurae 8f986c1e31 test(coverage): add coverage scenarios for pre-existing code paths
Additional BDD scenarios covering registry resolver errors, cache
TTL expiry, runtime dispatch normalization, template base edge cases,
validation actor coverage gaps, and YAML Jinja loader deferred rendering.
2026-06-23 10:49:16 +01:00
hurui200320 ed9c4dc95d fix(llmagent): do not close shared cached httpx clients in cleanup()
CI / quality (pull_request) Successful in 34s
CI / build (pull_request) Successful in 57s
CI / integration_tests (pull_request) Successful in 1m26s
CI / unit_tests (pull_request) Successful in 3m28s
CI / lint (pull_request) Successful in 1m1s
CI / typecheck (pull_request) Successful in 1m0s
CI / security (pull_request) Successful in 59s
CI / coverage (pull_request) Successful in 3m7s
CI / status-check (pull_request) Successful in 3s
CI / lint (push) Successful in 40s
CI / quality (push) Successful in 40s
CI / build (push) Successful in 40s
CI / typecheck (push) Successful in 1m5s
CI / security (push) Successful in 1m5s
CI / integration_tests (push) Successful in 1m20s
CI / unit_tests (push) Successful in 3m14s
CI / coverage (push) Successful in 3m2s
CI / status-check (push) Successful in 3s
LLMAgent.cleanup() previously iterated over hard-coded provider SDK client
attributes (root_async_client, root_client, _async_client, _client) and
called close() on each. Recent langchain-anthropic and langchain-openai
versions cache their default httpx clients via module-level lru_cache
functions. Closing those clients poisoned the cache: every subsequent
ChatAnthropic/ChatOpenAI instance in the same process received the same
closed httpx client and failed with a connection error.

Fix: cleanup() now only releases the agent's own reference to the chat
model (self._chat_model = None). The removed _KNOWN_CLIENT_ATTRS class
variable has been deleted and ClassVar removed from the typing import.

The concurrent-idempotency guarantee is preserved: the lock is still
acquired before nulling _chat_model, so two concurrent cleanup() calls
cannot both see a non-None model and attempt conflicting operations.

The four provider SDK client-closing scenarios in credential_injection.feature
and llm_missing_coverage.feature are removed as they tested the old (buggy)
behaviour. Their step definitions are removed from credential_cleanup_steps.py
(now only carries the resolve_class_ref patch step) and
llm_missing_coverage_steps.py is updated with the corrected assertions.

Six new regression BDD scenarios tagged @tdd_issue @tdd_issue_57 are added
in features/llm_cleanup_shared_client.feature, covering all four provider
SDK client attribute paths (Anthropic _async_client/_client, OpenAI
root_async_client/root_client) and two end-to-end two-agent scenarios that
prove a second agent can run successfully after the first is cleaned up.

ISSUES CLOSED: #57
2026-06-17 17:19:44 +00:00
CoreRasurae 57b1a2e70e test(unit): increase BDD coverage to 97% for registry, LLM, YAML template error branches
Adds 4 Behave BDD feature files with 15 scenarios covering
previously untested error-path branches:

- registry_resolver_errors.feature (7 scenarios):
  Invalid semver, empty version lists, alias mismatches,
  ID/local/unknown reference error handling
- llm_agent_config_validation.feature (4 scenarios):
  Empty name, non-dict config, non-TemplateRenderer,
  non-str credential key validation
- llm_provider_url_canonicalization.feature (2 scenarios):
  URL canonicalization with explicit port and scheme lowercasing
- yaml_template_non_dict_errors.feature (2 scenarios):
  Non-dict YAML parse results in template engine and inline Jinja handler

Coverage: 96.8% -> 97.1%
2026-06-15 23:13:46 +01:00
CoreRasurae 517dfb4cce feat(registry): implement local namespace reference resolution
CI / lint (pull_request) Successful in 35s
CI / quality (pull_request) Successful in 45s
CI / integration_tests (pull_request) Successful in 1m2s
CI / typecheck (pull_request) Successful in 1m10s
CI / security (pull_request) Successful in 1m10s
CI / build (pull_request) Successful in 42s
CI / unit_tests (pull_request) Successful in 3m21s
CI / coverage (pull_request) Successful in 3m7s
CI / status-check (pull_request) Successful in 3s
CI / quality (push) Successful in 50s
CI / security (push) Successful in 51s
CI / lint (push) Successful in 54s
CI / typecheck (push) Successful in 58s
CI / integration_tests (push) Successful in 1m0s
CI / build (push) Successful in 43s
CI / unit_tests (push) Successful in 3m23s
CI / coverage (push) Successful in 3m14s
CI / status-check (push) Successful in 3s
Implements the local:<path> reference scheme per Package Registry Standard v1.0.0 §5.3.

Closes #46
2026-06-12 17:11:35 +01:00
hurui200320 d93c32ccee feat(streaming): add Executor.execute_stream() returning AsyncIterator[str] for token-by-token delivery
CI / lint (pull_request) Successful in 43s
CI / quality (pull_request) Successful in 49s
CI / build (pull_request) Successful in 47s
CI / typecheck (pull_request) Successful in 1m9s
CI / security (pull_request) Successful in 1m9s
CI / integration_tests (pull_request) Successful in 1m8s
CI / unit_tests (pull_request) Successful in 3m9s
CI / coverage (pull_request) Successful in 3m7s
CI / status-check (pull_request) Successful in 3s
CI / lint (push) Successful in 55s
CI / typecheck (push) Successful in 56s
CI / security (push) Successful in 57s
CI / quality (push) Successful in 48s
CI / build (push) Successful in 48s
CI / integration_tests (push) Successful in 1m10s
CI / unit_tests (push) Successful in 3m26s
CI / coverage (push) Failing after 13m5s
CI / status-check (push) Has been cancelled
Implements the full streaming execution path for the CleverThis router:

- **LLMAgent.stream_message(message, context)** (agents/llm.py): Async generator
  calling self.chat_model.astream(messages) and yielding str(chunk.content) per
  chunk. Captures token counts from the final chunk's usage_metadata using the
  same _safe_int() fallback chain as process_message(). Sets _last_token_usage
  and last_token_usage_var after exhaustion.

- **Node.stream_agent(state)** (langgraph/nodes.py): Async generator that mirrors
  _execute_agent() but uses agent.stream_message() for LLMAgent instances and
  falls back to process_message() for non-LLM agents. Captures per-node token
  usage into self._last_stream_usage after stream exhaustion.

- **PureLangGraph.execute_stream()** (langgraph/pure_graph.py): Async generator
  mirroring execute(). Calls _stream_from_node() which uses astream() for all
  AGENT nodes (buffering tokens for intermediate nodes, yielding only from the
  terminal node) and ainvoke() for non-AGENT nodes. Stores final state and node
  usages in _last_stream_state / _last_stream_node_usages for post-stream access.
  Same limit enforcement (timeout_ms, max_depth, max_model_calls, max_tool_calls)
  as _execute_from_node().

- **_execute_llm_stream() / _execute_graph_stream()** (runtime_dispatch.py):
  Async generator dispatch functions mirroring _execute_llm() / _execute_graph().
  Set executor.last_result after stream exhaustion using collected token usage.

- **Executor.execute_stream(message)** (runtime.py): Public async generator that
  dispatches to _execute_llm_stream() for llm actors and _execute_graph_stream()
  for graph actors. Raises ConfigurationError for tool/multi_actor types.
  Resets last_result to None at entry; callers must exhaust the iterator.

- **Executor.last_result** (runtime.py): ActorResult | None attribute set by
  execute_stream() after the iterator is exhausted.

- Intermediate AGENT nodes use astream() internally (tokens buffered, not yielded
  to caller); terminal AGENT nodes yield buffered tokens. This avoids double-
  running the terminal LLM while satisfying AC2.
- Non-AGENT (FUNCTION/TOOL) nodes always use ainvoke() via node.execute().
- timeout_ms in execute_stream() buffers all tokens via asyncio.wait_for() around
  _collect_stream_tokens(), then yields them — this is simpler than wrapping an
  async generator directly.
- Tool and multi_actor actor types raise ConfigurationError (no streaming path).

- 60 new BDD scenarios in features/execute_stream.feature covering all acceptance
  criteria: token delivery, last_result population, token count extraction,
  limit enforcement (timeout, model_calls, tool_calls), error paths, graph
  streaming, non-AGENT node paths, parallel execution paths, and edge cases.
- All 2395 existing unit tests continue to pass.
- Coverage: 96.5% (meets threshold).

ISSUES CLOSED: #16
2026-06-12 10:08:12 +00:00
CoreRasurae e8bd348c77 feat(registry): implement RegistryError hierarchy with typed exceptions
CI / lint (pull_request) Successful in 41s
CI / typecheck (pull_request) Successful in 56s
CI / security (pull_request) Successful in 51s
CI / quality (pull_request) Successful in 53s
CI / integration_tests (pull_request) Successful in 1m10s
CI / build (pull_request) Successful in 43s
CI / unit_tests (pull_request) Successful in 3m30s
CI / coverage (pull_request) Successful in 3m22s
CI / status-check (pull_request) Successful in 4s
CI / quality (push) Successful in 45s
CI / security (push) Successful in 48s
CI / lint (push) Successful in 49s
CI / typecheck (push) Successful in 50s
CI / build (push) Successful in 49s
CI / integration_tests (push) Successful in 1m9s
CI / unit_tests (push) Successful in 3m13s
CI / coverage (push) Successful in 3m7s
CI / status-check (push) Successful in 3s
Add RegistryError base with message, details, and original_reference fields.
Implement all 9 typed exception types per Package Registry Standard §13.2:
PackageNotFoundError, InvalidPackageIdError, InvalidPackageReferenceError,
VersionNotFoundError, ValidationError, AuthenticationRequiredError,
AccessDeniedError, ConflictError, and RegistryNetworkError.

RegistryNetworkError carries status_code and url for transport failures.
__str__ includes original_reference when available.
exception_for_status() maps HTTP codes to typed exceptions.
_ERROR_TYPE_MAP enables error-type parsing from JSON error bodies.

23 Behave BDD scenarios in features/registry_exceptions.feature.
12 Robot Framework integration tests in robot/exceptions.robot.
Existing registry_http_client tests updated for InternalServerError removal.

ISSUES CLOSED: #29
2026-06-11 20:44:54 +01:00
CoreRasurae 054b432197 feat(registry): implement RegistryCache with LRU eviction and TTL
CI / lint (pull_request) Successful in 56s
CI / typecheck (pull_request) Successful in 56s
CI / security (pull_request) Successful in 1m2s
CI / quality (pull_request) Successful in 41s
CI / integration_tests (pull_request) Successful in 1m11s
CI / build (pull_request) Successful in 45s
CI / unit_tests (pull_request) Successful in 3m29s
CI / coverage (pull_request) Successful in 3m14s
CI / status-check (pull_request) Successful in 4s
CI / lint (push) Successful in 48s
CI / typecheck (push) Successful in 58s
CI / security (push) Successful in 1m1s
CI / quality (push) Successful in 39s
CI / integration_tests (push) Successful in 1m15s
CI / build (push) Successful in 41s
CI / unit_tests (push) Successful in 3m31s
CI / coverage (push) Successful in 3m27s
CI / status-check (push) Successful in 3s
- Add RegistryCache with LRU eviction, TTL expiry, SHA-1 content validation
 - Add CacheFactory for centralised cache configuration (Factory Method pattern)
 - Add CacheStats dataclass for hit/miss/eviction observability
 - Implement singleflight coalescing for concurrent-miss protection
 - Use tuple keys for resolve store to prevent separator collision
 - Fix cancellation-safe singleflight via plain Future + asyncio.shield
 - Independent max_size budgets per store (content + resolve)
 - Concurrent test assertions strengthened
 - All test imports moved to module level per CONTRIBUTING.md
 - Unused dead code removed from tests

 ISSUES CLOSED: #28
2026-06-11 19:26:04 +01:00
hurui200320 17d99abce7 feat(execution-limits): add structured ExecutionError kind/reason fields; enforce all 5 execution limits in PureLangGraph
CI / quality (pull_request) Successful in 36s
CI / lint (pull_request) Successful in 39s
CI / security (pull_request) Successful in 58s
CI / build (pull_request) Successful in 58s
CI / typecheck (pull_request) Successful in 1m2s
CI / integration_tests (pull_request) Successful in 1m14s
CI / unit_tests (pull_request) Successful in 3m35s
CI / coverage (pull_request) Successful in 3m34s
CI / status-check (pull_request) Successful in 3s
CI / lint (push) Successful in 47s
CI / typecheck (push) Successful in 46s
CI / build (push) Successful in 53s
CI / quality (push) Successful in 1m4s
CI / security (push) Successful in 1m7s
CI / integration_tests (push) Successful in 1m13s
CI / unit_tests (push) Successful in 3m43s
CI / coverage (push) Successful in 3m42s
CI / status-check (push) Successful in 3s
AC1: ExecutionError gains kind (categorical: depth, model_calls, tool_calls,
timeout, cost) and reason (sub-code: budget_exhausted or
missing_pricing_entry) fields, both defaulting to empty string. All existing
raise ExecutionError(msg) call sites are backward-compatible.

AC2: PureLangGraph.__init__ accepts limits: dict[str, Any] and
pricing: dict[str, Any] (both default to {}). When limits['max_depth'] is
supplied, a depth breach raises ExecutionError(kind='depth') instead of
silently returning the current message (the old behaviour was a silent
data-loss bug). When no max_depth limit is supplied, the legacy heuristic
(max(2000, len(nodes)*50)) is used with a silent cap for backward
compatibility with existing callers that do not pass limits.

AC3: max_model_calls is checked before each NodeType.AGENT execution;
counter increments after the check so the limit is enforced before the
(limit+1)-th invocation. Breach raises ExecutionError(kind='model_calls').

AC4: max_tool_calls is checked before each NodeType.TOOL execution; same
pattern as model_calls. Breach raises ExecutionError(kind='tool_calls').

AC5: execute() wraps _execute_from_node() in asyncio.wait_for() when
limits['timeout_ms'] is set. asyncio.TimeoutError is caught and re-raised
as ExecutionError(kind='timeout') so the router can map it to HTTP 429.

AC6+AC7: After each LLM node, cost is computed from the pricing table
(rates are USD per million tokens, matching ADR-2029 example values such
as gpt-4.1-mini prompt=/bin/zsh.15/1M). Cost breach raises
ExecutionError(kind='cost', reason='budget_exhausted'). A missing
provider or model entry raises ExecutionError(kind='cost',
reason='missing_pricing_entry') — proceeding with assumed zero cost is
forbidden per ADR-2029. An empty pricing dict ({}) disables all cost
checking (cost tracking was not requested).

AC8: ExecutionError re-exported from cleveractors/__init__.py and
added to __all__.

Wire: runtime_dispatch._execute_graph() now passes executor.limits and
executor.pricing to PureLangGraph so the limits reach the enforcement
layer.

ExecutionError is also re-raised (not swallowed) in _execute_from_node()'s
broad except block so limit errors always propagate to the caller.

New BDD tests (features/execution_limits.feature, 19 scenarios):
- ExecutionError defaults kind and reason to empty strings
- ExecutionError accepts kind and reason keyword arguments
- ExecutionError exported from cleveractors top-level package
- Graph exceeding max_depth raises ExecutionError(kind='depth')
- Two AGENT nodes with max_model_calls=1 raises on 2nd call
- Two TOOL nodes with max_tool_calls=1 raises on 2nd call
- Slow node with timeout_ms=50 raises ExecutionError(kind='timeout')
- LLM node exceeding max_cost_usd raises budget_exhausted
- Unknown provider/model in pricing raises missing_pricing_entry
- Empty pricing table skips cost calculation entirely
- Executor limits/pricing propagation verified

Quality gates: lint pass, typecheck 0 errors,
unit_tests 2317/2317 pass, integration_tests pass,
coverage 97.17% (threshold 96.5%).

ISSUES CLOSED: #15
2026-06-11 09:37:37 +00:00
hurui200320 2664ebfd75 feat(ActorResult): implement ActorResult and NodeUsage types; capture per-node token counts from LangChain responses
CI / lint (pull_request) Successful in 1m9s
CI / typecheck (pull_request) Successful in 1m10s
CI / security (pull_request) Successful in 1m8s
CI / quality (pull_request) Successful in 44s
CI / build (pull_request) Successful in 45s
CI / integration_tests (pull_request) Successful in 1m15s
CI / unit_tests (pull_request) Successful in 3m48s
CI / coverage (pull_request) Successful in 3m43s
CI / status-check (pull_request) Successful in 3s
CI / quality (push) Successful in 44s
CI / lint (push) Successful in 49s
CI / security (push) Successful in 47s
CI / typecheck (push) Successful in 48s
CI / build (push) Successful in 50s
CI / integration_tests (push) Successful in 1m16s
CI / unit_tests (push) Successful in 3m50s
CI / coverage (push) Successful in 3m35s
CI / status-check (push) Successful in 3s
AC1: Move NodeUsage and ActorResult from runtime.py into the spec-mandated
cleveractors/result.py module. runtime.py now imports and re-exports both
types for backward compatibility. result.py defines __all__, uses
dict[str, Any] | None (PEP 585) for the state field, and makes nodes a
required positional field (no default) so the contract that nodes is always
non-empty is statically enforced.

AC2: LLMAgent.process_message() extracts real token usage from
response.usage_metadata (primary) with isinstance(dict) guard, falling back
to response.response_metadata['token_usage'] with its own isinstance(dict)
guard. If neither is available, logs a WARNING with a cause parameter that
distinguishes the three failure conditions (empty usage_metadata, missing
response_metadata, or missing token_usage key) so operators can triage billing
discrepancies. Non-numeric token values are coerced via _safe_int() with
fallback to 0 and a warning log.

AC3: Token counts are available alongside the response string via the
side-channel attribute LLMAgent._last_token_usage, keeping process_message()
return type as str (preserving the Agent base-class contract unchanged).

AC4: Node._execute_agent() reads _last_token_usage, provider, and model from
any LLMAgent after execution and includes a _node_token_usage dict in the
state-updates return. PureLangGraph._node_usages accumulates one
(node_id, provider, model, prompt_tokens, completion_tokens) 5-tuple per LLM
node invocation during _execute_from_node(). PureLangGraph.execute() now
returns a 3-tuple: (response, final_state, node_usages). process_message()
unpacks and discards the extra elements.

AC5: Executor._execute_graph() uses the node_usages list from execute() to
build NodeUsage objects. Executor._execute_llm() reads _last_token_usage
directly from the LLMAgent instance instead of calling _estimate_tokens().
State isolation verified by a new test that calls execute() twice on the same
Executor with different mocked token counts and asserts independent results.

AC6: Aggregation invariant enforced in both _execute_llm() and _execute_graph()
via sum() over nodes list.

AC7: ActorResult and NodeUsage re-exported from cleveractors/__init__.py via
cleveractors.result (path updated from cleveractors.runtime).

Cleanup: Delete runtime_tokens.py (estimate_tokens, estimate_graph_tokens) and
the private _estimate_tokens() function in runtime.py — both are superseded by
real LangChain usage_metadata extraction. Remove unused import logging and
logger = logging.getLogger(__name__) from runtime.py (dead code after dispatch
logic was moved to runtime_dispatch.py). Move NodeUsageTuple type alias in
pure_graph.py to after the import blocks. Use tuple unpacking for last_usage
in runtime_dispatch._execute_llm().

New/updated BDD tests (actor_result_token_counting.feature, 25 scenarios):
- AC2: usage_metadata primary path (input_tokens/output_tokens)
- AC2: truthy non-dict usage_metadata treated as absent (isinstance guard)
- AC2: truthy non-dict response_metadata treated as absent (isinstance guard)
- AC2: response_metadata={} (empty dict) logs warning, counts 0
- AC2: response_metadata fallback path (prompt_tokens/completion_tokens)
- AC2: no usage data -> warning logged, counts 0
- AC2: response_metadata attribute present but None
- AC2: stale token reset before each call
- AC5: state isolation — execute() twice produces independent results
- AC6: aggregation invariant for single-node and multi-node actors
- AC6: placeholder node_id exact format assertion for _execute_graph
- AC6: placeholder node_id exact format assertion for _execute_multi_actor
- AC7: import correctness from cleveractors and cleveractors.result

CHANGELOG.md updated with Changed (real token extraction, result module move)
and Removed (runtime_tokens.py) entries under [Unreleased].

Quality gates: lint pass, typecheck 0 errors, unit_tests 2157/2157,
integration_tests 86/86, coverage 97.2% (threshold 97%).

ISSUES CLOSED: #14
2026-06-11 03:05:20 +00:00
CoreRasurae 284d2a9f00 feat(registry): extend TemplateType and integrate PackageReference into template system
CI / quality (pull_request) Successful in 37s
CI / lint (pull_request) Successful in 49s
CI / typecheck (pull_request) Successful in 50s
CI / build (pull_request) Successful in 50s
CI / security (pull_request) Successful in 55s
CI / integration_tests (pull_request) Successful in 59s
CI / unit_tests (pull_request) Successful in 3m38s
CI / coverage (pull_request) Successful in 3m35s
CI / status-check (pull_request) Successful in 4s
CI / lint (push) Successful in 33s
CI / quality (push) Successful in 58s
CI / typecheck (push) Successful in 1m1s
CI / security (push) Successful in 1m1s
CI / build (push) Successful in 52s
CI / integration_tests (push) Successful in 1m12s
CI / unit_tests (push) Successful in 3m41s
CI / coverage (push) Successful in 3m35s
CI / status-check (push) Successful in 3s
Extend TemplateType enum with TEMPLATE, SKILL, ACTOR, MCP, LSP.
Add optional package_ref field to ComponentReference for registry references.
Create ReferenceResolver with multi-server RegistryClient pool and caching.
Update InstantiationContext for registry-aware resolution with _original_reference.
Extend TemplateRegistry, EnhancedTemplateRegistry, and TemplateStore for 8 types.
Add registry reference detection in instantiate_from_config.
Add GenericTemplate for new package types without specialized template classes.

ISSUES CLOSED: #27
2026-06-10 12:50:38 +01:00
CoreRasurae 4e12902492 feat(registry): implement ReferenceResolver with version alias resolution
CI / quality (pull_request) Successful in 35s
CI / security (pull_request) Successful in 54s
CI / lint (pull_request) Successful in 58s
CI / build (pull_request) Successful in 42s
CI / typecheck (pull_request) Successful in 59s
CI / integration_tests (pull_request) Successful in 1m14s
CI / unit_tests (pull_request) Successful in 3m48s
CI / coverage (pull_request) Successful in 3m34s
CI / status-check (pull_request) Successful in 3s
CI / quality (push) Successful in 33s
CI / lint (push) Successful in 38s
CI / typecheck (push) Successful in 48s
CI / security (push) Successful in 48s
CI / build (push) Successful in 41s
CI / integration_tests (push) Successful in 1m15s
CI / unit_tests (push) Successful in 3m37s
CI / coverage (push) Successful in 3m34s
CI / status-check (push) Successful in 3s
Introduce ReferenceResolver implementing the Package Registry Standard v1.0.0
§5.3 and §4.2:

ReferenceResolver:
- parse(ref_str) → PackageReference handles all three reference schemes
  (registry, ID, local), wrapping ValueError into InvalidPackageReferenceError
- resolve(ref_str) → PackageId resolves references to concrete IDs:
  ID refs directly, registry refs via RegistryClient, local refs reserved
- Integrates with RegistryClient for remote HTTP resolution

Version resolution (§4.2):
- resolve_version(version, available_versions) resolves concrete (vX.Y.Z)
  and mutable aliases (latest, vx, x, vX.x, vX.Y.x)
- is_concrete_version() / is_version_alias() classification helpers

Integration tests: 35 Robot Framework test cases against fake registry server
with version alias resolution support

Benchmarks: 7 ASV benchmark classes covering parse, resolve, and version
resolution across small/medium/large version lists

ISSUES CLOSED: #26
2026-06-10 12:04:00 +01:00
CoreRasurae 8fa9e7652d feat(registry): implement Canonicalizer with NFC normalization and SHA-1 hashing
CI / quality (pull_request) Successful in 42s
CI / lint (pull_request) Successful in 46s
CI / security (pull_request) Successful in 47s
CI / build (pull_request) Successful in 50s
CI / typecheck (pull_request) Successful in 1m4s
CI / integration_tests (pull_request) Successful in 1m9s
CI / unit_tests (pull_request) Successful in 3m43s
CI / coverage (pull_request) Successful in 3m42s
CI / status-check (pull_request) Successful in 3s
CI / lint (push) Successful in 59s
CI / typecheck (push) Successful in 58s
CI / quality (push) Successful in 1m5s
CI / security (push) Successful in 1m9s
CI / build (push) Successful in 51s
CI / integration_tests (push) Successful in 1m40s
CI / unit_tests (push) Successful in 3m53s
CI / coverage (push) Successful in 3m35s
CI / status-check (push) Successful in 3s
Create Canonicalizer class implementing Package Registry Standard v1.0.0 §6:
- NFC normalization of all string values via unicodedata.normalize
- Lexicographic key sorting for deterministic output
- Lifecycle field stripping (version, release_date)
- RFC-8785 canonical JSON serialization (no whitespace, sorted keys)
- SHA-1 content-addressed hashing via compute_package_id()
- Internal reference resolution with configurable resolver callback

10 integration test cases covering:
- Complex content canonicalization with lifecycle stripping
- Deterministic output across multiple runs
- Key sorting enforcement
- Unicode NFC normalization
- PackageId computation for all 8 package types
- Different content produces different output

ISSUES CLOSED: #25
2026-06-10 11:33:50 +01:00
hurui200320 76c4c74201 feat(create_executor): implement create_executor() factory and Executor.execute() returning ActorResult
CI / lint (pull_request) Successful in 36s
CI / quality (pull_request) Successful in 40s
CI / build (pull_request) Successful in 44s
CI / security (pull_request) Successful in 51s
CI / typecheck (pull_request) Successful in 52s
CI / integration_tests (pull_request) Successful in 1m3s
CI / unit_tests (pull_request) Successful in 3m38s
CI / coverage (pull_request) Successful in 3m36s
CI / status-check (pull_request) Successful in 3s
CI / lint (push) Successful in 1m2s
CI / typecheck (push) Successful in 1m2s
CI / security (push) Successful in 1m2s
CI / quality (push) Successful in 40s
CI / integration_tests (push) Successful in 1m8s
CI / build (push) Successful in 49s
CI / unit_tests (push) Successful in 3m40s
CI / coverage (push) Successful in 3m36s
CI / status-check (push) Successful in 3s
- **`src/cleveractors/runtime_types.py`** — `ActorResult` and `NodeUsage` dataclasses
  extracted to break circular imports (CONTRIBUTING.md §Import Guidelines). 100% coverage.
- **`src/cleveractors/runtime.py`** — `create_executor()` factory, `Executor` class
  with `execute()` dispatching to module-level functions in `runtime_dispatch`.
  Imports `_execute_*` at module level (no function-local imports). 100% coverage.
- **`src/cleveractors/runtime_dispatch.py`** — Four dispatch functions: `_execute_llm`,
  `_execute_graph`, `_execute_tool`, `_execute_multi_actor`. All imports at module
  level except `from cleveractors.runtime import Executor` inside `_execute_multi_actor`
  (the only remaining circular dep — Executor calls _execute_multi_actor which creates
  a new Executor; cannot be resolved without further restructuring). 99.69% coverage
  (only the `if TYPE_CHECKING:` guard line is uncovered — not a real gap).
- **`src/cleveractors/runtime_tokens.py`** — Token estimation via tiktoken with
  heuristic fallback. 100% coverage.

Key design decisions and fixes:
- **Circular import resolution**: `ActorResult`/`NodeUsage` moved to `runtime_types.py`;
  both `runtime.py` and `runtime_dispatch.py` import from it at module level.
- **`messages` forwarded to `_execute_llm`**: builds `context={"conversation_history":[...]}`
  passed to `LLMAgent.process_message` for multi-turn support.
- **`parallel_execution` aligned with `PureGraphConfig` default (True)**:
  reads from legacy `route` dict or v2.0 `routes.main` dict; defaults to True.
- **Factory normalization**: always merges `actors` into `agents` (actors take
  precedence) so configs using both keys work correctly.
- **Generic exception in agent creation re-raised as `ConfigurationError`**:
  no double-logging; exception message preserved in `ConfigurationError`.
- **`cleveragents_block` guarded against `None`**: uses `or {}` coercion.
- **Type annotations added**: `top_provider`, `top_model`, `top_sp`,
  `temperature_raw`, `max_tokens_raw`, `config_block`, `tools` in dispatch functions.
- **No `finally` in `_execute_tool`**: documented with comment (ToolAgent has no cleanup).
- **Dead step removed**: `step_rxe_invalid_node_type` was unreferenced and incorrect.
- **BDD coverage**: `parallel_execution=False` override verified via `PureGraphConfig`
  constructor args; conversation history forwarding verified; AC7 immutability for
  multi-actor path verified.

-  `nox -e lint` — passes
-  `nox -e format` — passes
-  `nox -e typecheck` — passes (0 errors, 1 expected warning)
-  `nox -e unit_tests` — 2113 scenarios pass, 0 failures, 0 skipped
-  `nox -e integration_tests` — 76 tests pass
-  `nox -e coverage_report` — 97.21% (9870 covered, 283 missing, 10153 total)
  PR-modified files: `runtime_types.py` 100%, `runtime.py` 100%,
  `runtime_dispatch.py` 99.69% (1 uncoverable TYPE_CHECKING guard), `runtime_tokens.py` 100%

ISSUES CLOSED: #13
2026-06-10 09:41:39 +00:00
CoreRasurae 3fc0a3fa57 test(coverage): add BDD unit tests to close coverage gap toward 97%
CI / lint (pull_request) Successful in 35s
CI / quality (pull_request) Successful in 44s
CI / typecheck (pull_request) Successful in 47s
CI / security (pull_request) Successful in 56s
CI / unit_tests (pull_request) Successful in 3m33s
CI / integration_tests (pull_request) Successful in 56s
CI / build (pull_request) Successful in 34s
CI / coverage (pull_request) Successful in 3m34s
CI / status-check (pull_request) Successful in 3s
CI / quality (push) Successful in 49s
CI / lint (push) Successful in 51s
CI / typecheck (push) Successful in 52s
CI / build (push) Successful in 41s
CI / security (push) Successful in 58s
CI / integration_tests (push) Successful in 1m0s
CI / unit_tests (push) Successful in 3m37s
CI / coverage (push) Successful in 3m34s
CI / status-check (push) Successful in 5s
Add comprehensive BDD Behave unit tests across six modules to improve
line coverage. New feature files and step definitions for:

- validation/_actor: graph/llm/tool/multi_actor config validation
- runtime_tokens: token estimation with tiktoken and fallback heuristics
- dynamic_router: ContentBasedCondition, RouterNode, graph extension
- runtime: v2 route format, config blocks, conversation history, multi-actor
- progress: ProgressBarManager remaining count resolution
- route: MESSAGE_ROUTER rules to metadata in to_graph_config

Also fix credential executor and graph cleanup step mocks to return
proper tuple values matching updated runtime signatures.

Refs: #39
2026-06-09 16:06:32 +01:00
hurui200320 f281fa3b24 feat(credentials): refactor LLMAgent/AgentFactory for per-request credential injection and extended provider routing
CI / lint (pull_request) Successful in 35s
CI / typecheck (pull_request) Successful in 50s
CI / security (pull_request) Successful in 50s
CI / quality (pull_request) Successful in 32s
CI / unit_tests (pull_request) Successful in 3m32s
CI / integration_tests (pull_request) Successful in 59s
CI / build (pull_request) Successful in 34s
CI / lint (push) Successful in 33s
CI / build (push) Successful in 37s
CI / quality (push) Successful in 48s
CI / typecheck (push) Successful in 48s
CI / security (push) Successful in 1m4s
CI / integration_tests (push) Successful in 1m18s
CI / coverage (pull_request) Successful in 3m34s
CI / status-check (pull_request) Successful in 3s
CI / unit_tests (push) Successful in 3m45s
CI / coverage (push) Successful in 3m34s
CI / status-check (push) Successful in 3s
Implements Wave 3 of the cleveractors-core integration (ADR-2026, ADR-2028).

## AgentFactory
- Added optional credentials: dict[str, dict[str, str]] | None parameter to
  __init__. Stored as self.credentials.
- When creating LLMAgent instances, the full credentials dict is forwarded via
  a new credentials constructor kwarg, threading per-request API keys without
  touching the stored actor config_dict.
- Added explicit type annotation ReactiveAgentFactory: type[AgentFactory].
- _create_agent_instance raises ConfigurationError when credentials is not None
  but does not contain an entry for the requested provider (AC4 compliance).

## LLMAgent
- Added optional credentials: dict[str, str] | None parameter to __init__.
  Stored as self._credentials; config_dict is never modified.
- LangChain client construction deferred to first access (lazy init) via a
  chat_model property backed by _ensure_chat_model(). The property setter
  allows test-injection of mock models without going through the lazy-init path.
- _create_chat_model() dispatches to credential-injection / standalone paths.
- Extracted to llm_client.py and llm_providers.py for modularity.
- Defined shared module-level constants DEFAULT_MODEL, DEFAULT_TEMPERATURE,
  DEFAULT_MAX_TOKENS to eliminate duplicated magic numbers across llm.py,
  runtime.py, and factory.py.
- Added public credentials property exposing self._credentials.
- Added type narrowing guard in chat_model property to satisfy static analysis.
- Added temperature_override type validation in process_message.
- Guarded cleanup() null-assignment with _chat_model_lock (TOCTOU fix).

## SSRF Prevention (ADR-2028)
- _validate_base_url(): https-only, userinfo rejection, localhost rejection,
  raw IP literal rejection, numeric-hostname (dotted-hex/octal/compact) rejection,
  percent-decode loop with iteration cap (10), trailing-dot FQDN strip.
- DNS resolution intentionally omitted (blocking call in async path).
- Canonical URL reconstruction uses lowercased scheme.

## Credential Leak Prevention
- All logger.exception(...) replaced with logger.debug(..., exc_info=True).
- Error messages use type(e).__name__ instead of str(e).
- process_message exception chains broken with from None to prevent LangChain
  authentication errors from leaking via __cause__.

## Runtime fixes
- Executor._execute_llm: shallow copy for setdefault mutations.
- Executor._execute_graph: restructured try/finally for resource cleanup.
- Uses DEFAULT_MODEL, DEFAULT_TEMPERATURE, DEFAULT_MAX_TOKENS from llm.py.
- Defensive copy of credentials dict in Executor.__init__ to prevent caller
  mutation after construction.

## Test improvements
- Added BDD scenarios for llm_imports.py ImportError fallback branches using
  sys.modules manipulation with a custom import hook, exercised via subprocess.
- Added BDD scenario for AgentFactory.create_agents_from_config() credential
  threading to all created agents.
- Removed tautological dataclass test scenarios (NodeUsage, ActorResult) from
  runtime_coverage.feature.
- Fixed misleading scenario title in runtime_coverage.feature (exception chain
  suppressed, not chained) and updated step to assert __cause__ is None.
- Added CHANGELOG entry for ReactiveAgentFactory backward-compatibility alias.
- Removed duplicate log emission in _check_known_provider_domain.

ISSUES CLOSED: #12
2026-06-08 11:10:46 +00:00
CoreRasurae 85ebe29f8e feat(registry): add PackageType, PackageId, PackageReference, PackageContent core types
CI / quality (pull_request) Successful in 45s
CI / lint (pull_request) Successful in 53s
CI / typecheck (pull_request) Successful in 54s
CI / integration_tests (pull_request) Successful in 1m0s
CI / security (pull_request) Successful in 1m4s
CI / build (pull_request) Successful in 51s
CI / unit_tests (pull_request) Successful in 3m46s
CI / coverage (pull_request) Successful in 3m41s
CI / status-check (pull_request) Successful in 3s
CI / quality (push) Successful in 39s
CI / lint (push) Successful in 1m0s
CI / typecheck (push) Successful in 1m3s
CI / build (push) Successful in 1m3s
CI / security (push) Successful in 1m6s
CI / integration_tests (push) Failing after 1m3s
CI / unit_tests (push) Successful in 3m53s
CI / coverage (push) Successful in 3m43s
CI / status-check (push) Failing after 3s
Package Registry Standard v1.0.0 (§3, §5) data types:

- PackageType enum matching §3.2: ACTOR, GRAPH, STREAM, AGENT, TEMPLATE, SKILL, MCP, LSP
- PackageId: frozen/hashable value object parsing pkg_<type>_<40-hex-sha1> format
- PackageReference: frozen/hashable parser for REGISTRY, ID, and LOCAL reference formats
- PackageContent: frozen wrapper for fetched package content with metadata
- 34 Behave BDD scenarios covering valid parsing, error paths, hashability, str/repr

ISSUES CLOSED: #23
2026-06-06 19:21:22 +01:00
CoreRasurae ab17407576 test(runtime): add BDD coverage tests for runtime Executor API
CI / lint (pull_request) Successful in 40s
CI / typecheck (pull_request) Successful in 58s
CI / security (pull_request) Successful in 1m8s
CI / quality (pull_request) Successful in 1m7s
CI / integration_tests (pull_request) Successful in 1m20s
CI / build (pull_request) Successful in 39s
CI / unit_tests (pull_request) Successful in 3m47s
CI / coverage (pull_request) Successful in 3m44s
CI / status-check (pull_request) Successful in 4s
CI / quality (push) Successful in 39s
CI / lint (push) Successful in 47s
CI / security (push) Successful in 52s
CI / integration_tests (push) Successful in 1m2s
CI / typecheck (push) Successful in 1m5s
CI / build (push) Successful in 1m11s
CI / unit_tests (push) Successful in 4m27s
CI / coverage (push) Successful in 4m28s
CI / status-check (push) Successful in 5s
Add 16 Behave BDD scenarios covering:
- create_executor() factory function and Executor initialization
- Executor.execute() dispatch to LLM, graph, tool, multi-actor agents
- _execute_llm credential injection and error handling
- _execute_graph PureGraphConfig construction
- _execute_tool tool configuration
- _execute_multi_actor routing and error cases
- _build_factory_config credential/limit injection
- _estimate_tokens with tiktoken and fallback heuristic
- NodeUsage and ActorResult dataclasses

ISSUES CLOSED: #33
Refs: #33
2026-06-05 23:55:31 +01:00
CoreRasurae 92d5305a20 feat(registry): implement async RegistryClient using httpx
CI / quality (pull_request) Successful in 38s
CI / lint (pull_request) Successful in 44s
CI / build (pull_request) Successful in 35s
CI / typecheck (pull_request) Successful in 1m11s
CI / security (pull_request) Successful in 1m8s
CI / integration_tests (pull_request) Successful in 1m8s
CI / unit_tests (pull_request) Successful in 3m35s
CI / coverage (pull_request) Failing after 3m49s
CI / status-check (pull_request) Failing after 3s
CI / lint (push) Successful in 41s
CI / security (push) Successful in 54s
CI / typecheck (push) Successful in 56s
CI / quality (push) Successful in 1m3s
CI / build (push) Successful in 42s
CI / integration_tests (push) Successful in 56s
CI / unit_tests (push) Successful in 3m38s
CI / coverage (push) Failing after 3m42s
CI / status-check (push) Failing after 3s
Add async RegistryClient implementing the Package Registry Standard
v1.0.0 HTTP API endpoints (§8) using httpx.AsyncClient.

- GET /packages/{id} — retrieve package content by PackageId
- GET /{type}/{ns}/{name}?version= — resolve references with
  version alias resolution (latest, vx, vX.Y.x, vX.x per §4.2)
- GET /browse — discover published packages with type/namespace filters
- GET /.well-known/cleverthis-packages — registry metadata discovery
- Maps all 8 standard error types (§13.2) to typed exceptions
- Supports anonymous reads (§9.1) and optional API key auth (§9.2)
- Async context manager support (__aenter__/__aexit__)
- 32 Behave BDD scenarios, 7 Robot Framework integration tests,
  ASV benchmarks, 100% registry module coverage

ISSUES CLOSED: #24
2026-06-05 22:01:21 +00:00
hurui200320 149feae15f feat(validate_dict): expose public validate_dict(config_dict, platform_limits) API
CI / quality (pull_request) Successful in 41s
CI / lint (pull_request) Successful in 46s
CI / integration_tests (pull_request) Successful in 1m0s
CI / build (pull_request) Successful in 1m2s
CI / typecheck (pull_request) Successful in 1m6s
CI / security (pull_request) Successful in 1m5s
CI / unit_tests (pull_request) Successful in 3m39s
CI / coverage (pull_request) Failing after 3m35s
CI / status-check (pull_request) Failing after 3s
CI / lint (push) Successful in 40s
CI / typecheck (push) Successful in 1m4s
CI / security (push) Successful in 1m4s
CI / quality (push) Successful in 32s
CI / integration_tests (push) Successful in 51s
CI / build (push) Successful in 45s
CI / unit_tests (push) Successful in 3m38s
CI / coverage (push) Failing after 3m34s
CI / status-check (push) Failing after 3s
Implement validate_dict() in cleveractors/validation.py and export it from
cleveractors/__init__.py as the first router-facing API (ADR-2024, Wave 1).

validate_dict(config_dict, platform_limits) -> dict[str, Any]:

- Validates that both 'agents' and 'routes' top-level keys are present.
- Validates each agent's type against the spec-defined vocabulary:
  {llm, tool, composite, chain, template_instance}.
- Validates LLM agent 'provider' values against platform_limits['allowed_providers']
  when present, or the default allowlist {openai, anthropic, google} when absent.
- Validates route/edge field names: 'from'/'to' are rejected in favour of
  'source'/'target' as required by the spec-conformant format (ADR-2025).
- Validates graph node types against the spec vocabulary:
  {start, end, agent, function, tool, conditional, subgraph, message_router}.
- Validates stream operator types against the spec vocabulary (all defined types).
- Enforces structural platform limits from platform_limits when present:
  max_graph_depth (DFS longest-path check), max_subgraph_depth (recursive
  subgraph-reference depth), max_total_nodes (total nodes across all graph routes).
- Returns config_dict unchanged when all checks pass (identity contract).
- Pure static validator: no file I/O, no env-var reads, no app construction.

Review fixes (Cycle 1):
- M3: Added isinstance(node_name, str) guard for graph node keys in
  _validate_graph_route, with BDD scenario for non-string node key.
- m1: Added # pragma: no branch to _count_total_nodes defensive guard in
  _limits.py (matching the identical guards in _subgraph_children).
- m2: Added # pragma: no branch to dead defensive continue branches in
  _validate_graph_route edge validation loop.
- m3: Added dedicated BDD scenario and step for dangling source node
  (src not in nodes_raw branch), plus renamed existing scenario from
  "source node" to "target node" for accuracy.
- m4: Deleted 4 lines of commented-out InlineYAMLJinja setup code in
  features/environment.py.
- n1: Added depth <= 0 ValueError guard in build_subgraph_chain helper.
- n2: Renamed scenario "Edge referencing a non-existent source node" to
  "Edge referencing a non-existent target node" (the step tested a target).
- n3: Changed > to >= in both _MAX_DFS_STEPS and _MAX_SUBGRAPH_STEPS guards
  to prevent off-by-one allowing one extra iteration beyond documented ceiling.
- n4: Added adjacency target deduplication in _compute_graph_depth to avoid
  wasting DFS step budget on parallel edges.
- Spec review: M1 (entry_point optional) and M2 (start/end implicit injection)
  were verified against the authoritative spec at docs/specification.md in the
  cleveragents-webapp repo (develop branch). The spec does NOT support either
  claim; entry_point is required and start/end must be explicitly declared.
  No changes needed for M1/M2.

Review fixes (Cycle 2):
- M1: Deduplicated _subgraph_children yields with a seen set to prevent
  duplicate subgraph references from consuming DFS steps redundantly and
  causing false "too complex" errors.
- M2: Changed adjacency value type from list[str] to set[str] in
  _compute_graph_depth for O(1) deduplication, reducing worst case
  from O(N^3) to O(N^2) for dense graphs.
- m1: Added BDD scenario for LLM agent without provider field failing
  when default "openai" is not in custom allowlist.
- m2: Added positive BDD scenarios for valid node types start, tool,
  conditional, message_router, function.
- m3: Added positive BDD scenarios for additional stream operator types
  filter, transform, reduce.
- m5: Added depth_cache memoization to _compute_subgraph_depth to avoid
  recomputing depths across graph routes sharing subgraph trees.
- m6: Moved cleveractors.validation._limits import from module level into
  after_scenario, guarded by hasattr.
- n1: Split validate_dict_routes_steps.py (was 473 lines) into
  validate_dict_routes_steps.py (generic route steps) and
  validate_dict_graph_route_steps.py (graph-specific steps).
- n2: Updated _MAX_DFS_STEPS module comment to accurately state O(N+E)
  complexity and that dense graphs can exhaust the ceiling quickly.

BDD Behave scenarios cover all requirements.
Robot Framework integration tests (robot/validate_dict.robot) cover API usage.

ISSUES CLOSED: #10
2026-06-05 09:23:44 +00:00
hurui200320 6b80be2117 feat(merge_configs): expose public merge_configs(*dicts) API per §3.1 deep-merge algorithm
CI / lint (pull_request) Successful in 42s
CI / integration_tests (pull_request) Successful in 49s
CI / quality (pull_request) Successful in 53s
CI / build (pull_request) Successful in 50s
CI / typecheck (pull_request) Successful in 54s
CI / security (pull_request) Successful in 1m12s
CI / unit_tests (pull_request) Successful in 3m53s
CI / coverage (pull_request) Successful in 3m50s
CI / status-check (pull_request) Successful in 3s
CI / lint (push) Successful in 43s
CI / quality (push) Successful in 46s
CI / security (push) Successful in 47s
CI / typecheck (push) Successful in 48s
CI / build (push) Successful in 50s
CI / integration_tests (push) Successful in 54s
CI / unit_tests (push) Successful in 3m49s
CI / coverage (push) Successful in 3m42s
CI / status-check (push) Successful in 3s
Implement a new module-level merge_configs() function in
src/cleveractors/config_utils.py that exposes the §3.1 deep-merge
algorithm as a public, importable API.

Motivation: The internal ReactiveConfigParser._merge_configs() method
mutates its base dict in place and only accepts two dicts. The
CleverThis router (wave 2 of cleveragents/cleveragents-webapp#275)
needs a public merge_configs() to combine platform base configs with
actor configs from the database without side-effects.

Implementation:
- merge_configs(*dicts) accumulates into a fresh dict, applying each
  overlay using the §3.1 rules:
    * Key absent → add via deep copy.
    * Both mappings → deep-merge recursively (_apply_merge helper).
    * Both sequences → extend (existing + deep copy of new).
    * Otherwise → replace with deep copy of new value.
- Runtime type guard: non-dict arguments raise TypeError immediately
  (fail-fast per CONTRIBUTING.md argument validation guidelines).
- Zero-argument call returns {}.
- Inputs are never mutated (copy.deepcopy used for all stored values).
- Internal _merge_configs on ReactiveConfigParser is unchanged.

Exports: merge_configs added to cleveractors/__init__.py import and
__all__ so consumers can use 'from cleveractors import merge_configs'.

Tests:
- 15 Behave BDD scenarios in features/merge_configs.feature covering
  zero-args, single-dict deep copy (including nested identity),
  empty-dict arguments, TypeError on None, absent-key insertion,
  scalar override, recursive mapping merge, sequence append,
  three-dict chained merge, deep nesting, input mutation guard,
  result-mutation independence from inputs, and type-mismatch
  replacement.
- 4 Robot Framework integration tests in robot/config.robot with
  strengthened assertions (len check, overlay immutability).
- 5 ASV benchmarks with realistically sized three-way merge fixtures
  (50 keys each with overlap).

Project files: Added CONTRIBUTORS.md per CONTRIBUTING.md §PR Process
rule 8.

Coverage: 97% (config_utils.py: 100%).
Quality gates: ruff lint ✓, pyright strict ✓ (0 errors),
all BDD/Robot tests ✓.

ISSUES CLOSED: #11
2026-06-03 14:44:18 +00:00
CoreRasurae 9753b31c7e test: add coverage gap tests improving coverage from 81.4% to 96.90%
CI / quality (push) Successful in 36s
CI / lint (push) Successful in 38s
CI / typecheck (push) Successful in 49s
CI / security (push) Successful in 51s
CI / integration_tests (push) Successful in 55s
CI / unit_tests (push) Successful in 3m35s
CI / build (push) Successful in 33s
CI / coverage (push) Successful in 3m36s
CI / status-check (push) Successful in 3s
Add 13 BDD scenarios covering previously uncovered code paths:
- SAFE_BUILTINS validation (sandbox.py: 0% → 100%)
- ConfigurationError re-raise path (config.py: 99.3% → 100%)
- CLI hello/main functions (cli.py: 72.7% → 90.9%)
- GraphState message truncation (state.py: 98.7% → 100%)
- ProgressBarManager update/context rendering (progress.py: 0% → 87.7%)
- MessageRouter regex/exact/contains routing (message_router.py: 0% → 59.4%)
- RoutingAdapter parse_routing_command (routing_adapter.py)
- DynamicRouterNode pattern-based routing (dynamic_router.py)
- EnhancedTemplateRegistry unknown template type (enhanced_registry.py: 99.2%)
- CompositeAgent null-graph error path (composite.py: 97.8% → 98.6%)

Cover routing_adapter.py (17% → 100%): all GOTO/ROUTE patterns,
create_routing_node, create_conditional_router, dynamic config conversion.

Cover dynamic_router.py (21% → 87%): execute with empty/dict/string
messages, extract_message with colon parsing, config creation, graph
extension with edge generation.

Cover message_router.py (59% → 78%): regex, exact, contains, prefix,
suffix match types, invalid regex handling, non-string message, set_state.

Exercise Node._prepare_conversation_history with invalid configs,
_runtime error paths, _execute_message_router with rules,
_execute_agent with current_message and metadata propagation,
_execute_function with dynamic_router, and _execute_conditional
with content_contains/content_not_contains/content_starts_with
and custom condition types. Improves nodes.py from 71.3% to 72.5%.

Exercise PureGraphConfig, PureLangGraph init with dict/config,
RxPyLangGraphBridge registration/connection/lookup,
ReactiveStreamRouter operator creation and condition functions,
ReactiveConfigParser config/route/graph parsing,
ToolAgent tool execution with JSON, space-separated, single,
file_read, progress_bar invocations, and
ReactiveCleverAgentsApp init/dispose/visualization.
2026-06-02 20:02:01 +01:00
CleverThis Engineering df468f821f style: apply end-of-file and whitespace fixes across feature files 2026-05-27 21:41:03 +00:00
CleverThis Engineering e7816ad9c4 fix: resolve all bandit, semgrep, and vulture security findings
- Set jinja2 autoescape to select_autoescape(default_for_string=False) in all
  template engines to prevent HTML escaping of YAML config values while still
  configuring explicit autoescape behavior
- Replace silent try/except/pass with logging.debug in langgraph bridge
- Add logging before try/except/continue in langgraph nodes
- Replace assert with proper if/raise ValueError in reactive config parser
- Add # nosec annotations on intentional eval()/exec()/subprocess uses
  with justifications (sandboxed code execution for tool agent)
- Add # nosemgrep annotations for intentional eval()/exec() in sandboxed tools
- Create vulture whitelist for TYPE_CHECKING imports used in string annotations
- Fix unused lambda variables with _ prefix
2026-05-26 23:12:05 +00:00
CleverThis Engineering 5bbda89386 style: fix all lint errors across source and test files
- Fix bare excepts (replace with except Exception:)
- Remove wildcard star imports from test step files
- Add explicit imports for ConfigurationError, AgentCreationError
- Add 'from err' to raise statements in except blocks
- Fix loop variable binding with default arguments (B023)
- Organize imports, fix trailing whitespace and blank line whitespace
- Bump Python to 3.13 and update tool configurations
2026-05-26 22:21:50 +00:00
CleverThis Engineering 2223a16d48 feat: Minor code changes and linting and formatting issues fixes 2026-05-26 21:53:20 +00:00
CleverThis Engineering d827c93d83 Chore: RxPy routing is not garunteed to work in run mode so added some guardrails to prevent that. 2026-05-26 21:53:20 +00:00
CleverThis Engineering 1001020fa1 Chore: Moved any pytest tests over to behave and removed pytest entierly 2026-05-26 21:53:19 +00:00
CleverThis Engineering 656dbd5626 feat: added --load-context argument to run command 2026-05-26 21:53:19 +00:00
CleverThis Engineering 05aef2f41a Fix: JINJA2 templates are now being honored. 2026-05-26 21:53:19 +00:00
CleverThis Engineering aa75d0978f Fix: Verbosity wasnt being respected and errors were still leaking to the top, this is fixed 2026-05-26 21:53:19 +00:00
CleverThis Engineering c26197e90a Feat: added verbosity flag to control logging output 2026-05-26 21:53:19 +00:00
CleverThis Engineering 9a9aa04745 Fixed linting and typechecking, tox fully works now 2026-05-26 21:53:19 +00:00
CleverThis Engineering 3da2958a16 tests: some improved code coverage 2026-05-26 21:53:19 +00:00
CleverThis Engineering 44a53a16aa feat: Added temperature override for run and interactive commands for better testing 2026-05-26 21:53:19 +00:00
CleverThis Engineering 57254f57e5 fix: Fixed many of the remaining issues and got a mostly working integration test for scientific paper writer 2026-05-26 21:53:19 +00:00
CleverThis Engineering d6477288fe test: Added vetter coverage to network and decorators 2026-05-26 21:53:19 +00:00
CleverThis Engineering 82ea476c5f feat: Added context tracking when using via run, and added context command for managing it 2026-05-26 21:53:18 +00:00