Files
hurui200320 2664ebfd75
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
feat(ActorResult): implement ActorResult and NodeUsage types; capture per-node token counts from LangChain responses
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
..