Build autonomous AI agents in Python.
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
docs: Add CLAUDE.md for project overview, architecture, development commands, and testing structure | 9 个月前 | |
chore(ci): solve sync dev ci action | 1 个月前 | |
docs: translate benchmarks QUICKSTART.md to English | 4 个月前 | |
TECH-1588: agent retry + usage tracking refactor (#595) * TECH-1588: agent retry + usage tracking refactor Make AgentRunOutput the single source of truth for usage; rebuild the retry path so failed attempts no longer drop tokens from agent.usage. - Add Task.reset_run_state() (per-attempt state cleanup) and call it from task_start() and Agent.do_async's retry block. _run_id is deliberately not reset — do_async assigns it before InitStep. - Add TaskUsage.snapshot() / subtract() for delta-based accumulation. - Add _agent_usage_baseline on AgentRunOutput (with to_dict/from_dict round-trip) so cross-process resume after retry exhaustion does not double-count. - Flip _prepare_continuation_context to task._usage = output.usage (output is canonical) via output._ensure_usage(). - Make _finalize_agent_usage baseline-aware: delta = usage - baseline when a baseline is present. - Add @retryable exhaustion hook (sync + async); Agent implements _on_retries_exhausted to capture the last failed attempt's usage, snapshot the baseline, and idempotently upsert the output to storage (avoiding save_session_async's message-append side effect). - Capture each prior failed attempt's usage in the retry block before reset_run_state discards its TaskUsage. - Prefix injected test errors with 'INJECTED ERROR:' so test assertions distinguish injected vs real failures. - Delegate Chat retries entirely to Agent.retry: drop Chat(retry_attempts, retry_delay), _execute_with_retry, _is_retryable_error and the stream-retry wrappers. - New smoke tests: tests/smoke_tests/agent/test_retry_metrics.py (7 scenarios) and tests/smoke_tests/chat/test_chat_retry.py (3 scenarios). Stale unit tests for Chat's retry validation / _is_retryable_error removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(chat): use agent-first storage/memory resolution - Chat reuses agent.memory (and its storage) when present; storage= and memory-config kwargs apply only when the agent has no memory. - Realign Chat.session_id / user_id with agent.memory and emit UserWarning on mismatch. SessionManager now keys against the aligned ids. - AutonomousAgent.autonomous_memory / autonomous_storage become live properties tracking self.memory (no more stale snapshot after a Memory swap). - Add 12 smoke regressions across chat and autonomous_agent suites covering the resolution order and id alignment. * fix(tools,messages): tolerate gemma stream artifacts in HQ chat - shell_toolkit.run_command / run_python: @tool(timeout=None, max_retries=0) so the generic tool-layer asyncio.wait_for stops duplicating the subprocess's own timeout and leaking child processes on cancel (was triggering a 6x retry loop on legitimate run_command calls with no observable progress). - messages.ToolCallPart.args_as_dict: extend the existing trailing-junk fallback to also handle truncated / unterminated args (stream cut mid-string) by returning {} instead of tearing down the whole stream. - Trim verbose comment blocks in agent.py, tasks.py and run/agent/output.py to reflect the 'concise comments' preference. * docs(claude): track pending explanation/ doc syncs for TECH-1588 Three documents/ai/explanation/ files still describe pre-TECH-1588 contracts: - chat.md: agent-first storage/memory wiring and session_id / user_id alignment with UserWarning. - messages.md: ToolCallPart.args_as_dict() now falls back to {} on truncated/unterminated args instead of asserting. - tools.md: ShellToolKit overrides ToolConfig defaults with @tool(timeout=None, max_retries=0). Surface them in CLAUDE.md so a future session reconciles all three in one 'docs: sync explanation/' pass before relying on the old text. * docs: sync explanation/ docs with TECH-1588 contracts - chat.md: rewrite the storage/memory wiring section to describe the new agent-first policy (Chat reuses agent.memory and its storage when present; storage= and memory-config kwargs only apply when agent.memory is None) and the new session_id / user_id alignment + UserWarning. Update the dependency tree and integration notes that still said 'Chat replaces agent.memory'. - messages.md: ToolCallPart.args_as_dict() no longer asserts dict; document the trailing-junk + truncated-args fallback to {}. - tools.md: ToolConfig defaults table now flags the ShellToolKit override (timeout=None, max_retries=0) alongside the existing MCPTool override. - CLAUDE.md: drop the 'Outstanding documentation updates' TODO now that the three docs are in sync. * docs(claude): require documents/ai/explanation/ sync on contract changes Add a permanent rule under 'AI Operational Guides' so future sessions know: when a code change alters an observable contract (public API, default, side effect, error shape, new emission, conditional invariant), the matching documents/ai/explanation/<subsystem>.md must be resynced in the same commit/PR or an immediate follow-up 'docs: sync explanation/...' commit. Lists the common triggers and a 4-step how-to (grep, edit surgically, bundle with the change, surface what you found at the start of the reply). * refactor(pipeline): move error-injection scaffolding out of production The error-injection helpers used to live in src/upsonic/agent/pipeline/ step.py as a global _ERROR_INJECTIONS dict + inject/clear/check funcs + a try/except wrapper in Step.run. That's test-only code on the hot path; ship it from production and move it to tests/. - step.py: drop _ERROR_INJECTIONS, inject_error_into_step, clear_error_injection, check_error_injection, and the try/except around the check call in Step.run. Net -75 lines from runtime. - tests/_pipeline_injection.py: new shared helper with the same surface API (inject_error_into_step / clear_error_injection). Monkey-patches Step.run on first use and restores it once the registry is empty. Crucially, mirrors the old behaviour of finalizing an ERROR StepResult on the context before raising so get_problematic_step() and continue_run_async can still find a resume point. - tests/conftest.py: add project root to sys.path so suites can do 'from tests._pipeline_injection import …'. - test_retry_metrics.py, test_chat_retry.py, test_comprehensive_hitl.py, usage_durable_execution.py: change the import path only; test bodies untouched. (test_comprehensive_hitl also splits the StepResult/StepStatus import which stays under upsonic.agent.pipeline.) - documents/ai/explanation/agent/agent.md: resync the step.run description per the CLAUDE.md doc-sync rule — no more check_error_injection in production. Verified: 15 retry/chat-retry/task-metric tests + 34 HITL tests (comprehensive + usage_metrics + delay + storage_verification) all pass against the new helper. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> | 1 个月前 | |
fix: fix firecrawl tool attributes | 4 个月前 | |
chore(prebuilt): restore legacy template path for backward compatibility After #580 standardized the prebuilt layout, AppliedScientist's AGENT_FOLDER points at src/upsonic/prebuilt/applied_scientist/template in the *current* package. Older pip-installed clients still hard-code the pre-refactor path "prebuilt_autonomous_agents/applied_scientist" and run a sparse clone against this repo to fetch templates at runtime. Removing those files broke those clients with no possible fix on their end. Restore the legacy folder as a verbatim duplicate of the canonical template directory so old sparse-clone calls succeed. Future template edits should land at src/upsonic/prebuilt/applied_scientist/template/ (canonical) and be mirrored here for as long as we want to keep supporting the pre-refactor versions. These files are not packaged into the wheel (pyproject.toml builds only src/upsonic), so this adds zero overhead to installs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> | 2 个月前 | |
chore(master): release 0.77.3 | 1 个月前 | |
fix: TECH-1625 centralized usage registry (#602) * feat(usage-registry): Phase 0 foundation — UsageEntry / AggregatedUsage / UsageRegistry Adds src/upsonic/usage_registry/ as the in-memory backbone for a single source of truth for token / cost / timing across Agent, Task, Chat, Team, Workflow, and system (Memory / Reliability) execution scopes. - UsageEntry: append-only ledger row tagged with any subset of chat/agent/task/team/workflow/system_usage_id. Idempotent by entry_id — re-recording the same id replaces (not double-counts), removing the need for the baseline/snapshot/subtract arithmetic TaskUsage uses today to survive retries. - AggregatedUsage: read-only roll-up; shape mirrors TaskUsage so Phase 3 read-through wrappers drop in without changing callers. - UsageRegistry: thread-safe dict keyed by entry_id, scope filtering with AND semantics, convenience by_chat/by_agent/by_task/by_team/ by_workflow shortcuts. - new_usage_id(scope): uuid4 with a scope prefix for readable logs. - get_default_registry(): process-wide singleton for in-memory mode. No integration with existing usage.py yet — this commit only adds new code paths. 31 unit tests cover defaults, totals, idempotency, scope filtering AND-semantics, cost None-vs-zero distinction, model-order preservation, time-to-first-token earliest-non-None, thread safety (10×100 concurrent records, no drops), and id uniqueness. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): Phase 1a — add chat/agent/task/team usage_id fields Plumbing the per-scope identity surfaces the Phase-2 emission point needs. No reads or writes yet — these ids exist, are lazy-generated when not provided, and are independent of existing identity fields (agent_id, task_id, team.name). - Task: new ``task_usage_id_`` field + ``task_usage_id`` property; eager uuid generation via ``new_usage_id("task")`` in __init__, mirrors the ``task_id_`` / ``price_id_`` style. Serialised in to_dict / from_dict. - Agent: new ``agent_usage_id`` kw arg + lazy property. Distinct from ``agent_id`` so a recreated-per-request agent can carry a fresh ledger scope without losing the logical id. - Chat: new ``chat_usage_id`` kwarg + attribute, ``new_usage_id("chat")`` default. Deliberately NOT an alias of ``session_id`` — a session may legitimately span multiple chat scopes. - Team: new ``team_id`` and ``team_usage_id`` kwargs + attributes (Team had no identity field before — only ``name``). Eski ``price_id`` still untouched; its removal is the next commit so the diff stays reviewable. 17 unit tests cover auto-generation, prefix, stability across reads, explicit override honored, independence from sibling ids, and serialisation round-trip for Task. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry)!: Phase 1b — remove price_id machinery wholesale BREAKING: the price_id cost-tracking layer was a half-built early prototype that the new usage registry supersedes. Per the agreed plan no shim or deprecation alias is kept — call sites migrate to ``task.task_usage_id`` for scope and ``task.usage.{input_tokens, output_tokens,cost}`` (TaskUsage) for metrics. Removed: - ``Task.price_id_`` field, ``price_id`` property, eager uuid setter - ``Task.get_total_cost`` / ``total_cost`` / ``total_input_token`` / ``total_output_token`` properties (read TaskUsage directly) - ``upsonic.utils.printing``: ``price_id_summary`` global, ``print_price_id_summary``, ``get_price_id_total_cost``, plus the per-call price_id-tracking blocks in ``call_end`` and ``agent_end`` - ``Agent.do_async`` / ``astream`` per-run ``task.price_id_`` reseting - ``CallManagementStep`` / ``CallManager`` price_id printing wiring - ``ContextManager`` task-metadata dict's price_id / total_cost / total_input_tokens / total_output_tokens entries (replaced by ``task_usage_id``) - ``Direct.do`` / ``models/__init__.py`` price_id reset - ``upsonic.utils.__init__`` lazy export of ``print_price_id_summary`` Sub-task wiring: - ``ReliabilityLayer.validator_task`` and editor task now pass ``task_usage_id_=parent.task_usage_id`` (was ``price_id_=`` before). - ``Graph.execute`` reads cost from ``task._usage.cost`` directly. - ``eval.reliability`` totals now read from ``task._usage``. - ``utils/printing.format_step_metadata`` reads cost from ``task._usage`` when available. Tests migrated: - Unit: ``test_do.py`` price-id classes deleted (the feature is gone); ``test_pipeline.py``, ``test_context_managers.py``, ``test_agent_streaming.py`` mock fixtures use ``task_usage_id`` and read tokens from ``task.usage``. - Smoke: ``test_task_metrics.py``, ``test_task_agent_attributes.py``, ``test_agent_streaming.py``, ``test_smoke_task.py``, ``test_storage_agentsession_comprehensive.py`` rewritten in terms of ``task.usage`` and ``task.task_usage_id``. - Doc examples: ``results_example2/3/7.py`` updated. 395 unit tests in touched areas pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): Phase 1c — scope-tag contextvar plumbing Lays the rails the Phase-2 emission point will read at the model-call site. Each public entry into Chat / Agent / Team pushes its own ``*_usage_id`` onto a ``contextvars.ContextVar`` and resets on exit (including the exception path). Sub-tasks spawned underneath inherit via copy_context, so a memory / reliability sub-agent run inside the parent agent automatically sees chat_usage_id + task_usage_id active — matching the agreed default: "active ids get written, no separate scope structure". Added: - ``usage_registry/scope.py``: ContextVars per scope (chat, agent, task, team, workflow, system, run, user) + ``scope()`` context manager + ``current_scope_tags()`` snapshot reader. - Re-exported ``scope`` / ``current_scope_tags`` from ``upsonic.usage_registry``. Wired: - ``Agent.do_async`` / ``Agent.astream``: push agent_usage_id + task_usage_id at start, reset in finally (symmetric across normal, error, and HITL-pause exit paths — the agent state finally already has the right shape post-TECH-1588). - ``Chat._invoke_blocking_async``: push chat_usage_id + user_id, reset in finally. - ``Chat._invoke_streaming`` / ``_invoke_streaming_events``: push inside the inner ``_stream`` async generator so the contextvar lives precisely as long as the generator iterates, not for the caller's whole task. - ``Team.do_async``: push team_usage_id around ``multi_agent_async``. Tests (60 new): - ``test_scope.py``: nesting + restore-on-exit + restore-on-exception + None-doesn't-clear + asyncio inheritance + child-override isolation (8 cases). - ``test_scope_wiring.py``: mocked-model probes confirm Agent.do pushes agent + task scope; chat.invoke pushes chat + user + agent + task; Team.do_async pushes team scope; teardown happens after each entry point (7 cases). All 407 touched-area unit tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): Phase 2 — write-through at fresh response.usage sites Wires every place a model.request() result first surfaces through a new ``record_request_usage()`` helper. The helper reads the active scope contextvars (chat / agent / task / team / user — all pushed in Phase 1c), builds a ``UsageEntry``, and appends it to the default registry. The legacy ``TaskUsage.incr`` / ``AgentUsage.incr`` accumulation chain is NOT touched — both systems run in parallel so the Phase 3 read-through cutover can compare totals before swapping. Roll-up sites (``parent.usage.incr(sub_output.usage)``) are intentionally NOT hooked to keep the registry idempotent: the sub-agent's own pipeline already recorded its entries. New module: - ``usage_registry/recorder.py::record_request_usage`` — token + cost + scope-tag merger; returns ``None`` if no usage to record (zero-token / falsy input) so callers don't need a guard. Hooks added (kind="llm", pipeline_step distinguishes path): - ``direct.py`` after Direct.do's model.request — step="direct" - ``pipeline/steps.py`` ModelExecutionStep finally — step="model_call" - ``pipeline/steps.py`` policy feedback model.request — step="policy_feedback" - ``pipeline/steps.py`` stream finalization — step="model_call_stream" - ``agent.py`` retry path — step="model_call_retry" - ``agent.py`` final fallback path — step="model_call_final" - ``agent.py`` follow-up after tool-call round — step="model_call_follow_up" - ``agent.py`` summarization (context_management_middleware) — step="summarization" - ``agent.py`` guardrail validation path — step="guardrail" Per agreed default: memory/reliability sub-agents inherit the parent's chat_usage_id + task_usage_id via contextvars (Phase 1c plumbing), so no per-call passing is needed. The active scope IS the right scope. Tests (10 new, all 69 usage_registry tests pass): - ``test_recorder.py``: None / zero-token noop, basic recording, scope-tag inheritance, multi-record aggregation; caught a sneaky bug where ``registry or get_default_registry()`` short-circuited to the default because empty UsageRegistry is falsy via ``__len__`` — fixed to explicit ``is not None`` check. - ``test_parity.py``: registry.by_task(task.task_usage_id) totals match task.usage; registry.by_agent matches agent.usage across multiple do() calls; per-task entries do not bleed between tasks; Chat.invoke records under chat scope; two chats / two agents stay isolated (no crossover). 416/419 touched-area unit tests pass (3 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): Phase 3 — scope inheritance + Chat read-through Two related cuts: 3a. Sub-agent runs no longer override the parent's scope. Agent.do_async / astream push agent_usage_id and task_usage_id ONLY when the contextvars are not already set. Nested runs (memory summarisation, reliability validator / editor, tool- orchestration agents-as-tools) inherit the active ids. This is what makes the agreed default — "memory / reliability writes to the active id, no separate scope" — actually true at the ledger level. The finally branches now only reset the tokens they own (push-er pops, inherit-er leaves alone). 3b. Chat's public usage properties cut over to the registry. ``chat.input_tokens`` / ``output_tokens`` / ``total_tokens`` / ``total_cost`` / ``total_requests`` / ``total_tool_calls`` and ``chat.get_usage()`` now read from ``UsageRegistry.by_chat(chat.chat_usage_id)`` by default. The legacy SessionManager-backed path is reachable via ``UPSONIC_LEGACY_USAGE=1`` for the rollout window so users can diff the two views before the legacy field deletion in Phase 5. Why this matters for the user-visible total: the legacy session.usage path missed every spend a sub-agent (memory summary, reliability) made because those agents had no way to write into the parent session's RunUsage. The registry path picks them up automatically — the same contextvar inheritance that 3a guarantees also routes the entry to the parent chat's bill. Agent.usage / Task.usage were intentionally NOT touched here — they have many internal mutator sites (incr / start_timer / stop_timer / _finalize_agent_usage) that Phase 5 will rewrite alongside the deletion. For now they remain the legacy mutables; the registry is the source for chat-level totals. Tests (4 new + reused parity suite, 73 total in usage_registry): - ``test_scope_wiring.test_nested_agent_run_inherits_parent_scope``: outer.scope() + inner.do_async → inner's emission carries OUTER's agent_usage_id and task_usage_id. - ``test_read_through.py``: * chat.total_tokens / total_cost reflect a registry-only entry that legacy session.usage cannot see. * UPSONIC_LEGACY_USAGE=1 flips back to SessionManager reads. * chat.get_usage() returns an AggregatedUsage with the same shape callers used to get from RunUsage. 420/423 touched-area unit tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): Phase 4 — persistence (Storage layer + Chat resume) Lands the persistence half of the registry. The in-memory ledger built in Phase 0 now has a storage-backed twin: every ``record()`` writes through to the active backend's ``usage_entries`` table, and a Chat re-opening with the same chat_usage_id + storage rehydrates its history on construction so ``chat.total_cost`` survives process restarts and cross-worker resumes. Storage contracts (Phase 4a): - ``Storage`` and ``AsyncStorage`` gain three opt-in methods each: ``upsert_usage_entry`` / ``query_usage_entries`` / ``delete_usage_entries`` (with ``aupsert_`` / ``aquery_`` / ``adelete_`` async siblings). Default implementations raise NotImplementedError so a caller that depends on persistence gets a clear error instead of silent breakage on a backend that hasn't been ported. - ``usage_entry_table`` constructor kwarg + ``usage_entry_table_name`` attribute, defaulting to ``upsonic_usage_entries`` — the agreed separate table layout, not a session-JSON embedding. Backends ported (Phase 4b): - InMemoryStorage: dict keyed by entry_id, scope-tag AND filtering, delete-by-scope. - JSONStorage: ``upsonic_usage_entries.json`` next to the other per-table files; idempotent entry_id replace on upsert. - SqliteStorage: new ``usage_entries`` table schema with every scope tag indexed for cheap roll-up queries; SQLite-flavoured INSERT … ON CONFLICT DO UPDATE for idempotency. - Postgres / Mongo / Redis intentionally left unported in this commit — the base default raises a clear NotImplementedError so call sites that have a real persistence need fail loudly. Followup Phase-4b commits will fill them in. Registry → storage wiring (Phase 4c): - ``UsageRegistry(storage=...)`` constructor + ``attach_storage`` / ``detach_storage`` / ``flush_to_storage`` / ``load_from_storage``. - ``record()`` and ``record_many()`` write through to the attached storage when present, swallowing NotImplementedError so the in-memory mode keeps working on unported backends. - ``record_many`` now materialises its iterable once so a generator isn't exhausted by the in-memory loop before the storage loop sees it. - Chat constructor: after the storage / memory resolution settles, attach the resolved storage to the default registry and call ``load_from_storage(chat_usage_id=self.chat_usage_id)`` so the chat picks up any prior spend recorded against this scope. Failure is swallowed — registry wiring never breaks chat construction. Tests (13 new): - Round-trip suite (``_BackendRoundTripMixin``): upsert/query, idempotent entry_id replace, delete-by-scope. Run against InMemory and JSON; SQLite skipped when sqlalchemy isn't installed. - ``TestRegistryStorageAttach``: record() write-through; load_from_storage rehydrates a fresh registry; flush_to_storage back-fills storage from an in-memory ledger. - ``TestChatPersistsAndResumes``: invoke under a chat_usage_id, clear registry to simulate restart, open a fresh Chat with the same id + storage → chat.input_tokens / output_tokens / total_requests reflect the prior spend. 430/436 touched-area unit tests pass (6 skipped: pre-existing + the 3 SQLite cases gated on sqlalchemy). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): Phase 4b cont. — Postgres / Mongo / Redis backends Fills in the three storage backends Phase 4 left as NotImplementedError stubs. Every backend now exposes the same ``upsert_usage_entry`` / ``query_usage_entries`` / ``delete_usage_entries`` trio with idempotent entry_id semantics and every scope tag indexed. Postgres - ``USAGE_ENTRY_TABLE_SCHEMA`` added to postgres/schemas.py with one JSONB column for ``extra`` and individual indexes on every scope tag. - ``_get_usage_entry_table`` table cache field on the storage; tables list extended. - ``upsert_usage_entry`` uses PostgreSQL's INSERT … ON CONFLICT DO UPDATE on entry_id (matches the SQLite implementation Phase 4 already shipped). - query / delete share the same scope-filter loop pattern. Mongo - ``USAGE_ENTRY_COLLECTION_INDEXES`` added to mongo/schemas.py: entry_id unique + one index per scope tag + ``timestamp`` (desc) for recent-first queries. - ``_get_collection("usage_entries", ...)`` branch wired through the existing collection factory. - ``upsert_usage_entry`` uses ``replace_one({"entry_id": eid}, doc, upsert=True)`` for idempotency; query/delete use the AND-of-non-None filter the other backends share, with MongoDB's ``_id`` stripped on read so the docs roundtrip cleanly to ``UsageEntry.from_dict``. Redis - Layout: one ``upsonic:usage_entries:<entry_id>`` key per row, plus a Set per scope-tag value (``upsonic:usage_entries:idx:<field>:<value>``) to make AND-filtered queries an SINTER. Upserts diff old vs new tag values so a scope flip doesn't leave a phantom in the old Set. - ``_get_table_name`` learned the ``"usage_entries"`` branch so the helper plumbing already in this file (``_get_record`` / ``_get_all_records``) works for the new type. - Delete enforces "must specify at least one filter" — bare ``delete_usage_entries()`` is a no-op, matching the other backends. Tests - ``TestRedisBackend(_BackendRoundTripMixin)`` runs the same upsert / idempotency / delete-by-scope round-trip suite against a ``fakeredis.FakeRedis(decode_responses=True)`` client when fakeredis is installed. Postgres + Mongo are deferred — the smoke-test tier already exercises live instances and a mock-backed Mongo would need the optional ``mongomock`` dep we don't carry. 86/89 usage_registry unit tests pass (3 SQLite skipped — sqlalchemy not installed in the dev env). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry)!: Phase 5 — drop TECH-1588 baseline machinery The baseline / snapshot / subtract delta arithmetic introduced in TECH-1588 only existed to make the LEGACY ``TaskUsage.incr`` chain retry-safe and resume-safe: the same TaskUsage instance could be accumulated once by the retry exhaustion hook, then a second time by ``_finalize_agent_usage`` after a HITL resume, so a baseline snapshot let the second accumulation subtract the already-counted portion. With the Phase-2 usage registry that whole problem moves out of the incr chain. The ledger is keyed on ``entry_id`` (uuid per model call) and writes are upserts — recording the same call twice is a no-op, not a double-count. So: - ``TaskUsage.snapshot()`` and ``TaskUsage.subtract()`` — DELETED (77 lines). No callers remain. - ``AgentRunOutput._agent_usage_baseline`` field, its to_dict block, its from_dict round-trip and the ``_agent_usage_baseline=`` keyword in ``from_dict`` — DELETED (~20 lines). - ``Agent._on_retries_exhausted`` hook — DELETED (~65 lines). Its job was "capture the last failed attempt's usage so the user discarding the run still sees it in agent.usage". That capture now happens automatically: every model.request response has already been written to the registry under the active agent_usage_id by the Phase-2 emission hook, and ``agent.usage`` will become a registry view in a follow-up Phase-5 commit. The hook's secondary job (re-persist output state so the baseline survives cross-process resume) is also moot — the baseline is gone, and the registry's own ``upsert_usage_entry`` already persists every row idempotently. - ``Agent._finalize_agent_usage`` simplifies from "consider baseline, compute delta, refresh baseline" to a plain "accumulate usage into agent.usage" call. The retry block in ``do_async`` (lines ~3853) that pre-captures the failed attempt's usage into ``agent.usage`` BEFORE ``reset_run_state`` discards it stays put — that's still the only path that keeps the legacy ``agent.usage`` totals correct across in-process retries until ``agent.usage`` becomes a registry property (next Phase-5 commit). 433/439 touched-area unit tests pass (6 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): Phase 5/2 — agent.usage and task.usage read-through Cuts the public ``agent.usage`` and ``task.usage`` surfaces over to the usage registry, mirroring what Phase 3 did for Chat. The legacy mutable chain keeps running in parallel for one rollout window so a caller that needs the old shape can flip ``UPSONIC_LEGACY_USAGE=1`` and get the original ``AgentUsage`` / ``TaskUsage`` instance back. Agent: - ``self.usage`` (mutable field) renamed to ``self._usage_legacy``. - New ``@property usage`` returns ``get_default_registry().by_agent(self.agent_usage_id)`` — an :class:`AggregatedUsage` view, API-compatible with AgentUsage on every field we read internally (input_tokens, output_tokens, requests, cost, ...). - ``@usage.setter`` writes to ``_usage_legacy`` so the existing ``_accumulate_run_usage`` site (``self.usage = AgentUsage()``) doesn't crash. ``self.usage.incr(...)`` was rewritten to ``self._usage_legacy.incr(...)`` so the legacy chain still accumulates correctly. - Internal reads at ``Agent.cost`` and ``_create_promptlayer_workflow`` already used ``getattr`` so they work on both the legacy AgentUsage and the registry's AggregatedUsage. Task: - ``@property usage`` rewritten to query ``get_default_registry().by_task(self.task_usage_id_)``. Falls back to the live ``_usage`` mutable when (a) ``UPSONIC_LEGACY_USAGE=1`` is set or (b) the task has no task_usage_id_ yet (very early construction-time edge). - Internal mutators (``task._usage.incr(...)`` / ``start_timer`` / etc.) untouched — they're explicit and the property only governs the public surface. Key behavioural win: ``agent.usage`` and ``task.usage`` now pick up spend from memory / reliability sub-agents automatically. Those sub-agents inherit the parent's agent_usage_id + task_usage_id via Phase 1c+3a contextvars, write to the registry via Phase 2, and the registry roll-up surfaces them in the parent's totals — without the legacy ``incr`` chain knowing anything about them. Tests (6 new): - ``test_agent_task_read_through.py``: * agent.usage / task.usage reflect Phase-2 registry entries * external registry writes under agent_usage_id (memory / reliability sub-agent simulation) show up in agent.usage * task-per-task isolation * UPSONIC_LEGACY_USAGE=1 returns AgentUsage / TaskUsage instance 439/445 touched-area unit tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry)!: Phase 5/3 — delete legacy chain + UPSONIC_LEGACY_USAGE flag The Phase-3 and Phase-5/2 read-through wrappers kept a parallel legacy path alive behind ``UPSONIC_LEGACY_USAGE=1`` so callers could diff the two views during the rollout window. That window is over. Per explicit instruction, every "legacy" branch is gone — the registry is the only source of truth for ``agent.usage`` / ``task.usage`` / ``chat.total_*``. Removed ------- - ``Agent._usage_legacy`` field and the ``@usage.setter`` writeback — the agent has no mutable usage state any more. - ``Agent._accumulate_run_usage`` (the AgentUsage incr fold). - ``Agent._finalize_agent_usage`` and both its call sites in ``do_async`` / ``astream`` finally blocks. The pretty-print path (``print_agent_metrics`` for non-paused main tasks) is inlined into the same finally so user-visible behaviour is identical. - The "capture failed-attempt usage before reset" block in ``do_async``'s retry path — every model.request response was already written to the registry under the active agent_usage_id at emission time, so the legacy capture-then-fold was redundant. - ``UPSONIC_LEGACY_USAGE`` env-flag fallback on: * ``Agent.usage`` (was: return AgentUsage instance) * ``Task.usage`` (was: return live TaskUsage instance) * Every ``chat.input_tokens / output_tokens / total_tokens / total_cost / total_requests / total_tool_calls / get_usage`` — plus the ``Chat._use_legacy_usage`` helper. - The two test cases that asserted the env flag still flipped reads (``test_legacy_env_flag_returns_*_instance`` / ``test_legacy_env_flag_falls_back_to_session_manager``) — they have no behaviour left to test. - Docstring / comment references to ``_finalize_agent_usage`` in ``Task.reset_run_state`` and ``pipeline/manager.py``. Behaviour kept -------------- - ``Agent.usage`` / ``Task.usage`` always return an :class:`AggregatedUsage` (possibly empty for a fresh agent / task that has not run yet — was ``None`` previously). Code that did ``if agent.usage is None: return None`` would now return a zero dict, which is the cleaner shape going forward. - One smoke-test in ``test_retry_metrics.test_hitl_pause_resume_no_baseline_double_count`` retitled — its old assertion that ``agent.usage is None`` mid-pause was a property of the legacy chain (``_finalize_agent_usage`` skipped on pause). The new semantics: registry has the entry the moment the model.request fires, so the assertion was a behavioural artifact, not a contract. - ``task._usage`` field remains as the in-pipeline timer / scratchpad (``start_timer`` / ``add_model_execution_time``). Internal code that reads it stays unchanged; only the public ``task.usage`` surface is registry-derived. 436/442 unit tests pass in the touched areas. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry)!: Phase 5/4 — delete output.usage rollup patches Every sub-pipeline LLM call (culture, reliability validator/editor, reflection, agent / user / tool policy, cache LLM lookup, orchestrator sub-agents, agent-as-tool calls) is now recorded directly into the usage registry under the inherited scope tags via the Phase-1c contextvars + Phase-2 emission hook. The manual ``incr`` rollups onto ``_agent_run_output.usage`` (or the local ``context._ensure_usage()``) were parallel writes into the legacy mutable that nothing reads any more — every public surface (``agent.usage``, ``task.usage``, ``chat.total_*``) reads from the registry. Deleted rollup patches ---------------------- - ``agent.py``: culture-drain at ``_handle_model_response``; tool-policy usage drains in both ``_execute_tool_calls`` paths (sync + parallel); user-policy usage drain in the user-policy step. - ``agent.py::_drain_agent_tool_usage``: collapsed to a no-op call-site stub (the AgentTool's private ``_accumulated_usage`` staging is left alone; nothing consumes it). - ``context_managers/system_prompt_manager.py::aprepare``: culture-drain rollup onto agent's run output. - ``tools/orchestration.py::_propagate_sub_agent_usage``: collapsed to a no-op stub. - ``pipeline/steps.py``: * cache-LLM rollup in CacheStep (still clears the staging field). * reflection sub-agent rollup. * reliability sub-agent rollup (still clears ``task._reliability_sub_agent_usage``). * agent-policy rollup in the policy-feedback loop. Tests updated (4 cases, ``test_usage_tracking.py``) - ``test_propagate_sub_agent_usage_increments_parent_usage`` → ``test_propagate_sub_agent_usage_is_noop``: asserts parent usage stays untouched after a sub-agent rollup is attempted. - ``test_drain_agent_tool_usage_agent_tool_drains_into_run_output`` → ``test_drain_agent_tool_usage_is_noop``: same contract flip. - ``test_aprepare_drains_culture_usage_into_agent_run_output`` → ``test_aprepare_does_not_drain_culture_usage_onto_run_output``: ``drain_accumulated_usage`` is now not invoked at all. - ``test_aprepare_no_drain_when_culture_usage_none``: redundant after the contract flip; folded into the rewritten test above. What still uses the local ``output.usage`` field ------------------------------------------------ ``AgentRunOutput.usage`` remains a per-run snapshot populated by the 9 ``_ensure_usage().incr(response.usage)`` sites for the agent's own direct model calls. Sub-agent / sub-pipeline spend appears in the registry roll-up but NOT in this per-run snapshot — by design, since ``output.usage`` documents only this run's direct emissions. A future Phase will replace ``output.usage`` with a registry query keyed by ``run_id`` once run scope is plumbed through contextvars. 434/440 unit tests pass in the touched areas. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry)!: Phase 6 — drop dead accumulator infrastructure After Phase 5/4 deleted the manual ``output.usage.incr(...)`` rollup patches every sub-pipeline had upstream-side staging that no longer fed anything. This commit removes those write-only ``_accumulated_usage`` / ``_last_llm_usage`` fields, their drain methods, and the call sites that previously bridged them into the legacy chain. Sub-pipeline LLM calls keep landing in the usage registry under the inherited scope tags (Phase 1c + Phase 2) — that is now the only roll-up path. Removed ------- - ``Agent._drain_agent_tool_usage`` method and its two call sites in ``_execute_tool_calls``. - ``Orchestrator._propagate_sub_agent_usage`` method and its three call sites (analysis / revision / synthesis sub-agent runs). - ``AgentTool._accumulated_usage`` field, the staging ``.incr`` after ``do_async``, and ``drain_accumulated_usage`` method. - ``UpsonicLLMProvider._accumulated_usage`` field + the staging ``.incr`` it received from ``_accumulate_usage_from_output`` + ``drain_accumulated_usage``. The helper itself is kept as a no-op stub so the 10 sub-pipeline call sites stay valid. - ``CultureManager._last_llm_usage`` field, ``.incr`` after culture extraction, ``drain_accumulated_usage``. - ``CacheManager._last_llm_usage`` field + the ``.incr`` after the cache-comparison sub-agent + the now-redundant clear in the cache pipeline step. - ``PolicyManager.drain_accumulated_usage`` and ``ToolPolicyManager.drain_accumulated_usage`` methods — both walked every policy's ``base_llm`` / ``text_finder_llm`` / ``feedback_llm`` to harvest accumulators that no longer exist. - Memory layer drains in ``storage/memory/session/agent.py`` (summary generator) and ``storage/memory/user/user.py`` (schema + trait extractor): the ``_last_llm_usage`` aggregator + the ``.incr`` at every ``do_async`` site + the rollup onto ``output._ensure_usage()``. Tests ----- Deleted 4 test classes (~285 lines) that asserted behaviour of the now-deleted features: ``TestCultureManagerUsageTracking``, ``TestOrchestratorUsagePropagation``, ``TestAgentToolUsageTracking``, ``TestAgentDrainAgentToolUsage``, plus ``TestSystemPromptManagerCultureUsageDrain``. The remaining classes in ``test_usage_tracking.py`` cover the data classes themselves (``TestTaskUsageRequestUsage``, ``TestAgentUsageAggregation``, ``TestAgentRunOutputUsage``, ``TestTaskUsageAggregation``) and keep passing. 421/427 unit tests pass in the touched areas (6 pre-existing skipped for sqlalchemy / RAG infra). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): Phase 7 — run_id scope + safety_engine stub removal Two small follow-ups that close out the dead-state cleanup. run_id contextvar ----------------- ``Agent.do_async`` and ``Agent.astream`` now push the run's ``run_id`` onto the ``usage_registry.scope._run_id`` contextvar (next to the agent / task pushes) and reset it in the finally. Every UsageEntry recorded during the run therefore carries ``run_id`` automatically, which is what a future per-run roll-up (``AgentRunOutput.usage`` as a registry view) will key on. Inherit-don't-override is intentionally NOT applied to run_id — each agent.do_async invocation IS a distinct run, so we always push our own and reset on exit. safety_engine UpsonicLLMProvider -------------------------------- The Phase-6 no-op stub ``_accumulate_usage_from_output`` plus its 10 call sites in ``safety_engine/llm/upsonic_llm.py`` are gone. Each sub-agent's LLM call is recorded by the Phase-2 emission hook under the inherited scope tags; nothing needs to stage anything locally. 421/427 unit tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(smoke): rewrite retry_metrics suite for registry semantics The original suite was authored around TECH-1588's baseline / snapshot / subtract machinery and the ``_on_retries_exhausted`` hook — all of which are gone after the usage-registry refactor. Every test is restated in terms of what the registry guarantees: - one UsageEntry per model.request, idempotent on entry_id - ``agent.usage`` is a registry.by_agent(...) view, so retry / HITL / cross-process resume can never double-count (structural, not arithmetic) Removed assertions on ``_agent_usage_baseline``, the exhaustion-hook contract, and the ``final.usage - baseline`` delta. Replaced with the simpler "agent.usage covers every emission tagged with this agent's scope" invariant. Doc-comment header rewritten to match the new model. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: fix CI failures from registry-backed task.usage + missing api key Two CI breakages surfaced in PR #602: 1. ``test_scope_ids.py`` constructed agents with the literal ``Agent("openai/gpt-4o")`` string. After ``infer_model`` got eager-init semantics for the OpenAI provider, that path now opens an ``AsyncOpenAI`` client at construction time — which raises ``OpenAIError`` when ``OPENAI_API_KEY`` is unset in CI. Switched every agent build in this file to a ``MagicMock(model_name="mock-model")`` so the suite needs no key. 2. ``test_task_usage.py`` had four assertions that pre-dated the Phase-5/2 ``task.usage`` cut-over to a registry view: - ``assert task.usage is None`` (initial state) - ``assert task.usage is task._usage`` (identity) - ``assert task.usage.input_tokens == 80`` after mutating ``task._usage`` directly - ``isinstance(task.usage, TaskUsage)`` after from_dict Each is rewritten to assert the new contract: - ``task.usage`` is always a non-None ``AggregatedUsage`` (empty when nothing has been recorded). - ``task._usage`` is the in-pipeline timer scratchpad and is distinct from ``task.usage`` (which is a registry query). - Mutating ``_usage`` directly does NOT show up in ``task.usage``; callers must go through the Phase-2 ``record_request_usage`` emission hook (which the real pipeline does at every model.request). - ``from_dict`` round-trips ``_usage`` (the scratchpad with timer state) but does NOT auto-rehydrate registry entries — the storage layer does that explicitly via ``UsageRegistry.load_from_storage`` at session reopen. 494/500 unit tests pass with ``OPENAI_API_KEY`` unset (matches CI env). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: use spec=Model MagicMock so infer_model short-circuits Last commit switched ``test_scope_ids.py`` to ``MagicMock(model_name=...)`` agents, but ``MagicMock`` without ``spec=Model`` fails the ``isinstance(model, Model)`` short-circuit in ``infer_model`` — so construction still fell through to ``provider_factory("openai")`` → ``AsyncOpenAI(api_key=None)`` → ``OpenAIError`` in CI (no key). Caught locally only because ``tests/conftest.py`` auto-loads ``.env`` via ``python-dotenv``; with ``OPENAI_API_KEY`` set the path never exercised the no-key branch. Verified the new fix by temporarily moving ``.env`` aside: 17/17 pass with ``-u OPENAI_API_KEY``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry)!: Phase 8/1 — unify usage surface to .usage.X Removes the parallel ``chat.input_tokens`` / ``chat.total_cost`` / ``chat.total_requests`` / ``chat.total_tool_calls`` / ``chat.total_tokens`` / ``chat.run_duration`` / ``chat.time_to_first_token`` / ``chat.get_usage()`` / ``chat.get_session_metrics()`` / ``chat.get_session_summary()`` shortcuts AND ``agent.cost`` dict. Every entity now exposes one canonical surface — ``.usage`` returning an :class:`AggregatedUsage` — so the API is "look up ``something.usage.input_tokens`` / ``.cost`` / ``.requests`` / ``.duration``" no matter whether ``something`` is an Agent, a Task, or a Chat. Removed from ``Chat`` - properties: ``input_tokens``, ``output_tokens``, ``total_tokens``, ``total_cost``, ``total_requests``, ``total_tool_calls``, ``run_duration``, ``time_to_first_token`` - methods: ``get_usage()``, ``get_session_metrics()``, ``get_session_summary()`` - new property: ``chat.usage`` → ``registry.by_chat(chat_usage_id)`` (same body the deleted ``get_usage()`` had). - ``__repr__`` now reads ``self.usage.cost`` directly. Removed from ``Agent`` - ``agent.cost`` dict property (returned ``{input_tokens, output_tokens, total_tokens, estimated_cost, requests, tool_calls, cache_read_tokens, cache_write_tokens, reasoning_tokens}``). Read the equivalents off ``agent.usage`` — same names, no ``estimated_`` prefix on ``cost``. Removed from public exports - ``SessionMetrics`` dropped from ``upsonic.chat.__init__`` exports (the dataclass survives as a session_manager-internal helper). - ``tests/smoke_tests/agent/test_agent_cost_property.py`` deleted — the whole file tested the removed ``agent.cost`` property. Migrated call sites (sed-style ``chat.X → chat.usage.X``) - 13 doc-example chat scripts under ``tests/doc_examples/chat/`` - ``tests/smoke_tests/chat/test_chat.py`` (also drops the ``SessionMetrics`` / ``get_session_metrics`` / ``get_session_summary`` test block — those contracts are gone) - ``tests/unit_tests/chat/test_chat.py`` - ``tests/unit_tests/usage_registry/test_read_through.py``, ``test_parity.py``, ``test_persistence.py`` Notes - ``chat.total_cost`` previously returned ``0.0`` when the registry had no priced entries; the new ``chat.usage.cost`` returns ``None`` in that case. The sed migration writes ``(chat.usage.cost or 0.0)`` at every site that compared the value to a float, so observable behaviour stays the same. - ``chat.run_duration`` and ``chat.time_to_first_token`` used to come from session storage, not the registry. After the unification they come off ``chat.usage.duration`` and ``chat.usage.time_to_first_token`` — same data, registry-keyed. 494 / 500 unit tests pass with ``OPENAI_API_KEY`` unset (6 unrelated skipped: pre-existing dev failures + sqlalchemy-gated SQLite cases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): Phase 8/2 — add Team.usage canonical surface ``Team`` had a ``team_usage_id`` since Phase 1a but no public ``.usage`` reader, so users had to drop down to ``registry.by_team(...)`` to see what their team had spent — out of step with Agent / Task / Chat / AgentRunOutput which all expose a ``.usage`` property. Added the missing property: ``team.usage`` → ``get_default_registry().by_team(self.team_usage_id)``. Same canonical :class:`AggregatedUsage` shape as the other surfaces, so the unified ``something.usage.X`` rule now holds across every aggregate-able entity the framework exposes. Test (1 new in ``test_scope_ids.TestTeamIds``): - ``test_team_usage_property_returns_aggregated_view``: empty registry → zero-valued AggregatedUsage; record one entry under the team scope → ``team.usage`` reflects it (input/output/requests). Note: Phase 8/3 was attempted (convert ``AgentRunOutput.usage`` from a mutable ``TaskUsage`` field to a registry-view property keyed by ``run_id``) but reverted — it broke 54 tests across pipeline / streaming / parity because internal pipeline code mutates ``output.usage`` directly and many tests assert the mutable contract. Per-run snapshots are a different concept from scope-level roll-ups and a unified property would conflate them; leaving the per-run mutable in place for now. The shape is already AggregatedUsage- compatible (``input_tokens`` / ``output_tokens`` / ``cost`` / ``duration`` / ``requests`` / ``tool_calls``) so callers reading ``output.usage.X`` see the same fields as everywhere else. 495 / 501 unit tests pass with ``OPENAI_API_KEY`` unset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry)!: Phase 8/4 — drop SessionMetrics / session summary ``SessionMetrics`` was the last "second way to read usage": a dataclass that mirrored Chat counters (total_input_tokens / total_output_tokens / total_cost / total_requests / total_tool_calls) plus a few session- runtime fields (duration, message_count, average_response_time, messages_per_minute). Phase 8/1 removed its Chat-side wrappers and its public export; this commit deletes the underlying machinery too. Removed - ``upsonic.chat.session_manager.SessionMetrics`` dataclass. - ``SessionManager.get_session_metrics`` / ``aget_session_metrics`` / ``get_session_summary`` methods. Doc examples migrated (4 files) - ``chat_metrics_3.py``: prints fields off ``chat.usage`` directly + ``chat.duration`` and ``len(chat.all_messages)`` for the runtime-only metrics. - ``chat_metrics_4.py``: hand-rolls the summary string from ``chat.usage`` instead of the deleted helper. - ``chat_examples_complex_chat_1.py``: same — reads from ``chat.usage`` + ``chat.duration``. - ``chat_examples_complex_chat_2.py``: same. Note: ``average_response_time`` and ``messages_per_minute`` (derived metrics) had no callers outside SessionMetrics itself, so they go with the dataclass. If anyone needs them they're trivial to compute from ``chat.usage.duration`` + ``len(chat.all_messages)``. 495 / 501 unit tests pass with ``OPENAI_API_KEY`` unset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): plumb model_execution_time through emission hook Until now the registry recorded tokens / cost / scope tags for every model.request but not the per-call elapsed time, so ``task.usage.model_execution_time`` always read zero — even though the same number was sitting in ``task._usage.model_execution_time`` on the in-pipeline scratchpad. Reading the same metric through two surfaces gave two different answers, which violated the unified "every ``.usage.X`` reads from the registry" promise. This commit - adds ``model_execution_time`` and ``time_to_first_token`` kwargs to ``record_request_usage``; both flow into the resulting ``UsageEntry`` (model_execution_time also seeds ``duration`` so AggregatedUsage rolls up correctly). - passes the per-call elapsed time at 8 of the 9 emission sites (direct.py + steps.py model_call / policy_feedback / model_call_stream + agent.py retry / final / follow_up / guardrail). Every site already had the value in a local ``_*_model_elapsed`` variable; just adds it to the kwargs. - drops the summarization site's ``record_request_usage`` call — the context-management middleware's own model.request emits a UsageEntry under the inherited scope tags, so recording here was a silent double-count. Result: ``task.usage.model_execution_time``, ``task.model_execution_time``, and ``task._usage.model_execution_time`` all return the same number now. ``task.duration`` is the only distinct timing surface — it's the wall-clock from task_start to task_end (orchestration + tool + model time), genuinely different from the per-call sum. Verified live: ``Agent("openai/gpt-4o-mini").do(Task("Reply OK"))`` shows model_execution_time = 1.315s across all three reads, task.duration = 1.471s (wall-clock). 495 / 501 unit tests pass with ``OPENAI_API_KEY`` unset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry)!: drop Task.duration / model_execution_time / tool_execution_time / upsonic_execution_time The previous commit made ``task.usage.model_execution_time`` / ``task.usage.duration`` carry the per-call timer data correctly, which meant the four top-level Task timer properties were the last duplicates of "read the same number two different ways". Per the unification rule, one canonical surface only: ``task.usage.X``. Removed from Task - ``task.duration`` (was timer.duration with start_time/end_time fallback) - ``task.model_execution_time`` (was ``_usage.model_execution_time``) - ``task.tool_execution_time`` (was ``_usage.tool_execution_time``) - ``task.upsonic_execution_time`` (was ``_usage.upsonic_execution_time``) All four were thin delegations to either ``task._usage`` (the in-pipeline timer scratchpad) or to ``end_time - start_time``. Now: - Read timing from ``task.usage.duration`` / ``.model_execution_time`` / ``.tool_execution_time`` / ``.upsonic_execution_time``. Registry-derived, same number, same shape as every other ``.usage.X``. - Wall-clock task length: derive directly from ``task.end_time - task.start_time`` (both still on Task as raw fields). The old ``task.duration`` was misleading anyway — it returned ``_usage.duration`` (model time, not wall-clock) when both were set, with a start_time/end_time fallback only when the timer hadn't run. Migrated 8 call sites in ``src/`` (graph.py, printing.py, pipeline/steps.py, context_managers/context_manager.py) — sed-pattern ``\\btask\\.duration\\b`` → ``task.usage.duration`` etc. Migrated 7 test files (smoke + doc_examples + unit). Deleted ``TestTaskPropertyDelegation`` test class in ``test_task_usage.py`` — its whole point was the delegation contract that's now gone. ``test_do.py::test_task_duration_set_after_start_and_end`` rewritten to assert directly on ``task.end_time - task.start_time`` instead. Live verification (``Agent("openai/gpt-4o-mini").do(Task("Reply OK"))``): - ``task.usage.duration`` = 1.635s - ``task.usage.model_execution_time`` = 1.635s - ``task.usage.tool_execution_time`` = 0.0 - ``task.usage.upsonic_execution_time`` = 0.0 - ``hasattr(task, 'duration')`` = False - ``hasattr(task, 'model_execution_time')`` = False - ``hasattr(task, 'tool_execution_time')`` = False - ``hasattr(task, 'upsonic_execution_time')`` = False 487 / 493 unit tests pass with OPENAI_API_KEY unset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(usage-registry): collapse emission + scope-push boilerplate Two helper extractions, no behavioural change — just less code that all says the same thing. #1: ``record_response_usage`` helper (in ``usage_registry/recorder.py``) - Every fresh model.request emission site was repeating the same 6-8 lines: snapshot.incr(response.usage), compute cost via calculate_cost_from_usage, set_usage_cost, then forward to record_request_usage with the same kwargs. Wrapped into a single function so each call site is now five lines instead of fifteen, with the per-step pipeline_step + model_execution_time as the only per-site arguments. - Migrated 8 sites (4 in agent.py: retry / final / follow_up / guardrail; 3 in pipeline/steps.py: model_call / policy_feedback / model_call_stream; the summarization site stays — special, no cost compute). direct.py left alone (only 5 lines, simpler). #2: ``push_scope_tags`` + ``reset_scope_tags`` helpers (in ``usage_registry/scope.py``) - The "push contextvar tokens at entry, reset them in finally" pattern was repeated six times (Agent.do_async / Agent.astream / Chat._invoke_blocking_async / Chat._invoke_streaming / Chat._invoke_streaming_events / Team.do_async). Each call site had ~18 lines of manual token tracking + try/except. - The new helper accepts every scope tag as kwargs, returns a ``[(var, token), …]`` list, and ``reset_scope_tags`` unwinds them in reverse with per-token error swallowing. - ``inherit=True`` flag preserves the Phase-3a "sub-agent inherits parent's scope" behaviour: a tag that's already active in the current context is left untouched, so nested ``Agent.do_async`` doesn't overwrite a parent's ids. - Multiple ``push_scope_tags`` calls can append to one tokens list — used in ``Agent.do_async`` / ``Agent.astream`` where ``agent`` / ``task`` push at entry (inherit) and ``run_id`` pushes later (always, no inherit). Net effect - ~155 lines deleted, ~95 added (-60 net). Every emission / scope-push pattern reads the same way now; no more line-by-line duplication across the eight sites. - Live verification with ``gpt-4o-mini``: ``chat.usage == agent.usage`` (82 in / 1 out / $0.0000129), models = ['gpt-4o-mini'], every assertion the unification probe ran in the previous round still passes. 487 / 493 unit tests pass with OPENAI_API_KEY unset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> | 1 个月前 | |
fix: TECH-1625 centralized usage registry (#602) * feat(usage-registry): Phase 0 foundation — UsageEntry / AggregatedUsage / UsageRegistry Adds src/upsonic/usage_registry/ as the in-memory backbone for a single source of truth for token / cost / timing across Agent, Task, Chat, Team, Workflow, and system (Memory / Reliability) execution scopes. - UsageEntry: append-only ledger row tagged with any subset of chat/agent/task/team/workflow/system_usage_id. Idempotent by entry_id — re-recording the same id replaces (not double-counts), removing the need for the baseline/snapshot/subtract arithmetic TaskUsage uses today to survive retries. - AggregatedUsage: read-only roll-up; shape mirrors TaskUsage so Phase 3 read-through wrappers drop in without changing callers. - UsageRegistry: thread-safe dict keyed by entry_id, scope filtering with AND semantics, convenience by_chat/by_agent/by_task/by_team/ by_workflow shortcuts. - new_usage_id(scope): uuid4 with a scope prefix for readable logs. - get_default_registry(): process-wide singleton for in-memory mode. No integration with existing usage.py yet — this commit only adds new code paths. 31 unit tests cover defaults, totals, idempotency, scope filtering AND-semantics, cost None-vs-zero distinction, model-order preservation, time-to-first-token earliest-non-None, thread safety (10×100 concurrent records, no drops), and id uniqueness. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): Phase 1a — add chat/agent/task/team usage_id fields Plumbing the per-scope identity surfaces the Phase-2 emission point needs. No reads or writes yet — these ids exist, are lazy-generated when not provided, and are independent of existing identity fields (agent_id, task_id, team.name). - Task: new ``task_usage_id_`` field + ``task_usage_id`` property; eager uuid generation via ``new_usage_id("task")`` in __init__, mirrors the ``task_id_`` / ``price_id_`` style. Serialised in to_dict / from_dict. - Agent: new ``agent_usage_id`` kw arg + lazy property. Distinct from ``agent_id`` so a recreated-per-request agent can carry a fresh ledger scope without losing the logical id. - Chat: new ``chat_usage_id`` kwarg + attribute, ``new_usage_id("chat")`` default. Deliberately NOT an alias of ``session_id`` — a session may legitimately span multiple chat scopes. - Team: new ``team_id`` and ``team_usage_id`` kwargs + attributes (Team had no identity field before — only ``name``). Eski ``price_id`` still untouched; its removal is the next commit so the diff stays reviewable. 17 unit tests cover auto-generation, prefix, stability across reads, explicit override honored, independence from sibling ids, and serialisation round-trip for Task. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry)!: Phase 1b — remove price_id machinery wholesale BREAKING: the price_id cost-tracking layer was a half-built early prototype that the new usage registry supersedes. Per the agreed plan no shim or deprecation alias is kept — call sites migrate to ``task.task_usage_id`` for scope and ``task.usage.{input_tokens, output_tokens,cost}`` (TaskUsage) for metrics. Removed: - ``Task.price_id_`` field, ``price_id`` property, eager uuid setter - ``Task.get_total_cost`` / ``total_cost`` / ``total_input_token`` / ``total_output_token`` properties (read TaskUsage directly) - ``upsonic.utils.printing``: ``price_id_summary`` global, ``print_price_id_summary``, ``get_price_id_total_cost``, plus the per-call price_id-tracking blocks in ``call_end`` and ``agent_end`` - ``Agent.do_async`` / ``astream`` per-run ``task.price_id_`` reseting - ``CallManagementStep`` / ``CallManager`` price_id printing wiring - ``ContextManager`` task-metadata dict's price_id / total_cost / total_input_tokens / total_output_tokens entries (replaced by ``task_usage_id``) - ``Direct.do`` / ``models/__init__.py`` price_id reset - ``upsonic.utils.__init__`` lazy export of ``print_price_id_summary`` Sub-task wiring: - ``ReliabilityLayer.validator_task`` and editor task now pass ``task_usage_id_=parent.task_usage_id`` (was ``price_id_=`` before). - ``Graph.execute`` reads cost from ``task._usage.cost`` directly. - ``eval.reliability`` totals now read from ``task._usage``. - ``utils/printing.format_step_metadata`` reads cost from ``task._usage`` when available. Tests migrated: - Unit: ``test_do.py`` price-id classes deleted (the feature is gone); ``test_pipeline.py``, ``test_context_managers.py``, ``test_agent_streaming.py`` mock fixtures use ``task_usage_id`` and read tokens from ``task.usage``. - Smoke: ``test_task_metrics.py``, ``test_task_agent_attributes.py``, ``test_agent_streaming.py``, ``test_smoke_task.py``, ``test_storage_agentsession_comprehensive.py`` rewritten in terms of ``task.usage`` and ``task.task_usage_id``. - Doc examples: ``results_example2/3/7.py`` updated. 395 unit tests in touched areas pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): Phase 1c — scope-tag contextvar plumbing Lays the rails the Phase-2 emission point will read at the model-call site. Each public entry into Chat / Agent / Team pushes its own ``*_usage_id`` onto a ``contextvars.ContextVar`` and resets on exit (including the exception path). Sub-tasks spawned underneath inherit via copy_context, so a memory / reliability sub-agent run inside the parent agent automatically sees chat_usage_id + task_usage_id active — matching the agreed default: "active ids get written, no separate scope structure". Added: - ``usage_registry/scope.py``: ContextVars per scope (chat, agent, task, team, workflow, system, run, user) + ``scope()`` context manager + ``current_scope_tags()`` snapshot reader. - Re-exported ``scope`` / ``current_scope_tags`` from ``upsonic.usage_registry``. Wired: - ``Agent.do_async`` / ``Agent.astream``: push agent_usage_id + task_usage_id at start, reset in finally (symmetric across normal, error, and HITL-pause exit paths — the agent state finally already has the right shape post-TECH-1588). - ``Chat._invoke_blocking_async``: push chat_usage_id + user_id, reset in finally. - ``Chat._invoke_streaming`` / ``_invoke_streaming_events``: push inside the inner ``_stream`` async generator so the contextvar lives precisely as long as the generator iterates, not for the caller's whole task. - ``Team.do_async``: push team_usage_id around ``multi_agent_async``. Tests (60 new): - ``test_scope.py``: nesting + restore-on-exit + restore-on-exception + None-doesn't-clear + asyncio inheritance + child-override isolation (8 cases). - ``test_scope_wiring.py``: mocked-model probes confirm Agent.do pushes agent + task scope; chat.invoke pushes chat + user + agent + task; Team.do_async pushes team scope; teardown happens after each entry point (7 cases). All 407 touched-area unit tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): Phase 2 — write-through at fresh response.usage sites Wires every place a model.request() result first surfaces through a new ``record_request_usage()`` helper. The helper reads the active scope contextvars (chat / agent / task / team / user — all pushed in Phase 1c), builds a ``UsageEntry``, and appends it to the default registry. The legacy ``TaskUsage.incr`` / ``AgentUsage.incr`` accumulation chain is NOT touched — both systems run in parallel so the Phase 3 read-through cutover can compare totals before swapping. Roll-up sites (``parent.usage.incr(sub_output.usage)``) are intentionally NOT hooked to keep the registry idempotent: the sub-agent's own pipeline already recorded its entries. New module: - ``usage_registry/recorder.py::record_request_usage`` — token + cost + scope-tag merger; returns ``None`` if no usage to record (zero-token / falsy input) so callers don't need a guard. Hooks added (kind="llm", pipeline_step distinguishes path): - ``direct.py`` after Direct.do's model.request — step="direct" - ``pipeline/steps.py`` ModelExecutionStep finally — step="model_call" - ``pipeline/steps.py`` policy feedback model.request — step="policy_feedback" - ``pipeline/steps.py`` stream finalization — step="model_call_stream" - ``agent.py`` retry path — step="model_call_retry" - ``agent.py`` final fallback path — step="model_call_final" - ``agent.py`` follow-up after tool-call round — step="model_call_follow_up" - ``agent.py`` summarization (context_management_middleware) — step="summarization" - ``agent.py`` guardrail validation path — step="guardrail" Per agreed default: memory/reliability sub-agents inherit the parent's chat_usage_id + task_usage_id via contextvars (Phase 1c plumbing), so no per-call passing is needed. The active scope IS the right scope. Tests (10 new, all 69 usage_registry tests pass): - ``test_recorder.py``: None / zero-token noop, basic recording, scope-tag inheritance, multi-record aggregation; caught a sneaky bug where ``registry or get_default_registry()`` short-circuited to the default because empty UsageRegistry is falsy via ``__len__`` — fixed to explicit ``is not None`` check. - ``test_parity.py``: registry.by_task(task.task_usage_id) totals match task.usage; registry.by_agent matches agent.usage across multiple do() calls; per-task entries do not bleed between tasks; Chat.invoke records under chat scope; two chats / two agents stay isolated (no crossover). 416/419 touched-area unit tests pass (3 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): Phase 3 — scope inheritance + Chat read-through Two related cuts: 3a. Sub-agent runs no longer override the parent's scope. Agent.do_async / astream push agent_usage_id and task_usage_id ONLY when the contextvars are not already set. Nested runs (memory summarisation, reliability validator / editor, tool- orchestration agents-as-tools) inherit the active ids. This is what makes the agreed default — "memory / reliability writes to the active id, no separate scope" — actually true at the ledger level. The finally branches now only reset the tokens they own (push-er pops, inherit-er leaves alone). 3b. Chat's public usage properties cut over to the registry. ``chat.input_tokens`` / ``output_tokens`` / ``total_tokens`` / ``total_cost`` / ``total_requests`` / ``total_tool_calls`` and ``chat.get_usage()`` now read from ``UsageRegistry.by_chat(chat.chat_usage_id)`` by default. The legacy SessionManager-backed path is reachable via ``UPSONIC_LEGACY_USAGE=1`` for the rollout window so users can diff the two views before the legacy field deletion in Phase 5. Why this matters for the user-visible total: the legacy session.usage path missed every spend a sub-agent (memory summary, reliability) made because those agents had no way to write into the parent session's RunUsage. The registry path picks them up automatically — the same contextvar inheritance that 3a guarantees also routes the entry to the parent chat's bill. Agent.usage / Task.usage were intentionally NOT touched here — they have many internal mutator sites (incr / start_timer / stop_timer / _finalize_agent_usage) that Phase 5 will rewrite alongside the deletion. For now they remain the legacy mutables; the registry is the source for chat-level totals. Tests (4 new + reused parity suite, 73 total in usage_registry): - ``test_scope_wiring.test_nested_agent_run_inherits_parent_scope``: outer.scope() + inner.do_async → inner's emission carries OUTER's agent_usage_id and task_usage_id. - ``test_read_through.py``: * chat.total_tokens / total_cost reflect a registry-only entry that legacy session.usage cannot see. * UPSONIC_LEGACY_USAGE=1 flips back to SessionManager reads. * chat.get_usage() returns an AggregatedUsage with the same shape callers used to get from RunUsage. 420/423 touched-area unit tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): Phase 4 — persistence (Storage layer + Chat resume) Lands the persistence half of the registry. The in-memory ledger built in Phase 0 now has a storage-backed twin: every ``record()`` writes through to the active backend's ``usage_entries`` table, and a Chat re-opening with the same chat_usage_id + storage rehydrates its history on construction so ``chat.total_cost`` survives process restarts and cross-worker resumes. Storage contracts (Phase 4a): - ``Storage`` and ``AsyncStorage`` gain three opt-in methods each: ``upsert_usage_entry`` / ``query_usage_entries`` / ``delete_usage_entries`` (with ``aupsert_`` / ``aquery_`` / ``adelete_`` async siblings). Default implementations raise NotImplementedError so a caller that depends on persistence gets a clear error instead of silent breakage on a backend that hasn't been ported. - ``usage_entry_table`` constructor kwarg + ``usage_entry_table_name`` attribute, defaulting to ``upsonic_usage_entries`` — the agreed separate table layout, not a session-JSON embedding. Backends ported (Phase 4b): - InMemoryStorage: dict keyed by entry_id, scope-tag AND filtering, delete-by-scope. - JSONStorage: ``upsonic_usage_entries.json`` next to the other per-table files; idempotent entry_id replace on upsert. - SqliteStorage: new ``usage_entries`` table schema with every scope tag indexed for cheap roll-up queries; SQLite-flavoured INSERT … ON CONFLICT DO UPDATE for idempotency. - Postgres / Mongo / Redis intentionally left unported in this commit — the base default raises a clear NotImplementedError so call sites that have a real persistence need fail loudly. Followup Phase-4b commits will fill them in. Registry → storage wiring (Phase 4c): - ``UsageRegistry(storage=...)`` constructor + ``attach_storage`` / ``detach_storage`` / ``flush_to_storage`` / ``load_from_storage``. - ``record()`` and ``record_many()`` write through to the attached storage when present, swallowing NotImplementedError so the in-memory mode keeps working on unported backends. - ``record_many`` now materialises its iterable once so a generator isn't exhausted by the in-memory loop before the storage loop sees it. - Chat constructor: after the storage / memory resolution settles, attach the resolved storage to the default registry and call ``load_from_storage(chat_usage_id=self.chat_usage_id)`` so the chat picks up any prior spend recorded against this scope. Failure is swallowed — registry wiring never breaks chat construction. Tests (13 new): - Round-trip suite (``_BackendRoundTripMixin``): upsert/query, idempotent entry_id replace, delete-by-scope. Run against InMemory and JSON; SQLite skipped when sqlalchemy isn't installed. - ``TestRegistryStorageAttach``: record() write-through; load_from_storage rehydrates a fresh registry; flush_to_storage back-fills storage from an in-memory ledger. - ``TestChatPersistsAndResumes``: invoke under a chat_usage_id, clear registry to simulate restart, open a fresh Chat with the same id + storage → chat.input_tokens / output_tokens / total_requests reflect the prior spend. 430/436 touched-area unit tests pass (6 skipped: pre-existing + the 3 SQLite cases gated on sqlalchemy). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): Phase 4b cont. — Postgres / Mongo / Redis backends Fills in the three storage backends Phase 4 left as NotImplementedError stubs. Every backend now exposes the same ``upsert_usage_entry`` / ``query_usage_entries`` / ``delete_usage_entries`` trio with idempotent entry_id semantics and every scope tag indexed. Postgres - ``USAGE_ENTRY_TABLE_SCHEMA`` added to postgres/schemas.py with one JSONB column for ``extra`` and individual indexes on every scope tag. - ``_get_usage_entry_table`` table cache field on the storage; tables list extended. - ``upsert_usage_entry`` uses PostgreSQL's INSERT … ON CONFLICT DO UPDATE on entry_id (matches the SQLite implementation Phase 4 already shipped). - query / delete share the same scope-filter loop pattern. Mongo - ``USAGE_ENTRY_COLLECTION_INDEXES`` added to mongo/schemas.py: entry_id unique + one index per scope tag + ``timestamp`` (desc) for recent-first queries. - ``_get_collection("usage_entries", ...)`` branch wired through the existing collection factory. - ``upsert_usage_entry`` uses ``replace_one({"entry_id": eid}, doc, upsert=True)`` for idempotency; query/delete use the AND-of-non-None filter the other backends share, with MongoDB's ``_id`` stripped on read so the docs roundtrip cleanly to ``UsageEntry.from_dict``. Redis - Layout: one ``upsonic:usage_entries:<entry_id>`` key per row, plus a Set per scope-tag value (``upsonic:usage_entries:idx:<field>:<value>``) to make AND-filtered queries an SINTER. Upserts diff old vs new tag values so a scope flip doesn't leave a phantom in the old Set. - ``_get_table_name`` learned the ``"usage_entries"`` branch so the helper plumbing already in this file (``_get_record`` / ``_get_all_records``) works for the new type. - Delete enforces "must specify at least one filter" — bare ``delete_usage_entries()`` is a no-op, matching the other backends. Tests - ``TestRedisBackend(_BackendRoundTripMixin)`` runs the same upsert / idempotency / delete-by-scope round-trip suite against a ``fakeredis.FakeRedis(decode_responses=True)`` client when fakeredis is installed. Postgres + Mongo are deferred — the smoke-test tier already exercises live instances and a mock-backed Mongo would need the optional ``mongomock`` dep we don't carry. 86/89 usage_registry unit tests pass (3 SQLite skipped — sqlalchemy not installed in the dev env). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry)!: Phase 5 — drop TECH-1588 baseline machinery The baseline / snapshot / subtract delta arithmetic introduced in TECH-1588 only existed to make the LEGACY ``TaskUsage.incr`` chain retry-safe and resume-safe: the same TaskUsage instance could be accumulated once by the retry exhaustion hook, then a second time by ``_finalize_agent_usage`` after a HITL resume, so a baseline snapshot let the second accumulation subtract the already-counted portion. With the Phase-2 usage registry that whole problem moves out of the incr chain. The ledger is keyed on ``entry_id`` (uuid per model call) and writes are upserts — recording the same call twice is a no-op, not a double-count. So: - ``TaskUsage.snapshot()`` and ``TaskUsage.subtract()`` — DELETED (77 lines). No callers remain. - ``AgentRunOutput._agent_usage_baseline`` field, its to_dict block, its from_dict round-trip and the ``_agent_usage_baseline=`` keyword in ``from_dict`` — DELETED (~20 lines). - ``Agent._on_retries_exhausted`` hook — DELETED (~65 lines). Its job was "capture the last failed attempt's usage so the user discarding the run still sees it in agent.usage". That capture now happens automatically: every model.request response has already been written to the registry under the active agent_usage_id by the Phase-2 emission hook, and ``agent.usage`` will become a registry view in a follow-up Phase-5 commit. The hook's secondary job (re-persist output state so the baseline survives cross-process resume) is also moot — the baseline is gone, and the registry's own ``upsert_usage_entry`` already persists every row idempotently. - ``Agent._finalize_agent_usage`` simplifies from "consider baseline, compute delta, refresh baseline" to a plain "accumulate usage into agent.usage" call. The retry block in ``do_async`` (lines ~3853) that pre-captures the failed attempt's usage into ``agent.usage`` BEFORE ``reset_run_state`` discards it stays put — that's still the only path that keeps the legacy ``agent.usage`` totals correct across in-process retries until ``agent.usage`` becomes a registry property (next Phase-5 commit). 433/439 touched-area unit tests pass (6 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): Phase 5/2 — agent.usage and task.usage read-through Cuts the public ``agent.usage`` and ``task.usage`` surfaces over to the usage registry, mirroring what Phase 3 did for Chat. The legacy mutable chain keeps running in parallel for one rollout window so a caller that needs the old shape can flip ``UPSONIC_LEGACY_USAGE=1`` and get the original ``AgentUsage`` / ``TaskUsage`` instance back. Agent: - ``self.usage`` (mutable field) renamed to ``self._usage_legacy``. - New ``@property usage`` returns ``get_default_registry().by_agent(self.agent_usage_id)`` — an :class:`AggregatedUsage` view, API-compatible with AgentUsage on every field we read internally (input_tokens, output_tokens, requests, cost, ...). - ``@usage.setter`` writes to ``_usage_legacy`` so the existing ``_accumulate_run_usage`` site (``self.usage = AgentUsage()``) doesn't crash. ``self.usage.incr(...)`` was rewritten to ``self._usage_legacy.incr(...)`` so the legacy chain still accumulates correctly. - Internal reads at ``Agent.cost`` and ``_create_promptlayer_workflow`` already used ``getattr`` so they work on both the legacy AgentUsage and the registry's AggregatedUsage. Task: - ``@property usage`` rewritten to query ``get_default_registry().by_task(self.task_usage_id_)``. Falls back to the live ``_usage`` mutable when (a) ``UPSONIC_LEGACY_USAGE=1`` is set or (b) the task has no task_usage_id_ yet (very early construction-time edge). - Internal mutators (``task._usage.incr(...)`` / ``start_timer`` / etc.) untouched — they're explicit and the property only governs the public surface. Key behavioural win: ``agent.usage`` and ``task.usage`` now pick up spend from memory / reliability sub-agents automatically. Those sub-agents inherit the parent's agent_usage_id + task_usage_id via Phase 1c+3a contextvars, write to the registry via Phase 2, and the registry roll-up surfaces them in the parent's totals — without the legacy ``incr`` chain knowing anything about them. Tests (6 new): - ``test_agent_task_read_through.py``: * agent.usage / task.usage reflect Phase-2 registry entries * external registry writes under agent_usage_id (memory / reliability sub-agent simulation) show up in agent.usage * task-per-task isolation * UPSONIC_LEGACY_USAGE=1 returns AgentUsage / TaskUsage instance 439/445 touched-area unit tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry)!: Phase 5/3 — delete legacy chain + UPSONIC_LEGACY_USAGE flag The Phase-3 and Phase-5/2 read-through wrappers kept a parallel legacy path alive behind ``UPSONIC_LEGACY_USAGE=1`` so callers could diff the two views during the rollout window. That window is over. Per explicit instruction, every "legacy" branch is gone — the registry is the only source of truth for ``agent.usage`` / ``task.usage`` / ``chat.total_*``. Removed ------- - ``Agent._usage_legacy`` field and the ``@usage.setter`` writeback — the agent has no mutable usage state any more. - ``Agent._accumulate_run_usage`` (the AgentUsage incr fold). - ``Agent._finalize_agent_usage`` and both its call sites in ``do_async`` / ``astream`` finally blocks. The pretty-print path (``print_agent_metrics`` for non-paused main tasks) is inlined into the same finally so user-visible behaviour is identical. - The "capture failed-attempt usage before reset" block in ``do_async``'s retry path — every model.request response was already written to the registry under the active agent_usage_id at emission time, so the legacy capture-then-fold was redundant. - ``UPSONIC_LEGACY_USAGE`` env-flag fallback on: * ``Agent.usage`` (was: return AgentUsage instance) * ``Task.usage`` (was: return live TaskUsage instance) * Every ``chat.input_tokens / output_tokens / total_tokens / total_cost / total_requests / total_tool_calls / get_usage`` — plus the ``Chat._use_legacy_usage`` helper. - The two test cases that asserted the env flag still flipped reads (``test_legacy_env_flag_returns_*_instance`` / ``test_legacy_env_flag_falls_back_to_session_manager``) — they have no behaviour left to test. - Docstring / comment references to ``_finalize_agent_usage`` in ``Task.reset_run_state`` and ``pipeline/manager.py``. Behaviour kept -------------- - ``Agent.usage`` / ``Task.usage`` always return an :class:`AggregatedUsage` (possibly empty for a fresh agent / task that has not run yet — was ``None`` previously). Code that did ``if agent.usage is None: return None`` would now return a zero dict, which is the cleaner shape going forward. - One smoke-test in ``test_retry_metrics.test_hitl_pause_resume_no_baseline_double_count`` retitled — its old assertion that ``agent.usage is None`` mid-pause was a property of the legacy chain (``_finalize_agent_usage`` skipped on pause). The new semantics: registry has the entry the moment the model.request fires, so the assertion was a behavioural artifact, not a contract. - ``task._usage`` field remains as the in-pipeline timer / scratchpad (``start_timer`` / ``add_model_execution_time``). Internal code that reads it stays unchanged; only the public ``task.usage`` surface is registry-derived. 436/442 unit tests pass in the touched areas. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry)!: Phase 5/4 — delete output.usage rollup patches Every sub-pipeline LLM call (culture, reliability validator/editor, reflection, agent / user / tool policy, cache LLM lookup, orchestrator sub-agents, agent-as-tool calls) is now recorded directly into the usage registry under the inherited scope tags via the Phase-1c contextvars + Phase-2 emission hook. The manual ``incr`` rollups onto ``_agent_run_output.usage`` (or the local ``context._ensure_usage()``) were parallel writes into the legacy mutable that nothing reads any more — every public surface (``agent.usage``, ``task.usage``, ``chat.total_*``) reads from the registry. Deleted rollup patches ---------------------- - ``agent.py``: culture-drain at ``_handle_model_response``; tool-policy usage drains in both ``_execute_tool_calls`` paths (sync + parallel); user-policy usage drain in the user-policy step. - ``agent.py::_drain_agent_tool_usage``: collapsed to a no-op call-site stub (the AgentTool's private ``_accumulated_usage`` staging is left alone; nothing consumes it). - ``context_managers/system_prompt_manager.py::aprepare``: culture-drain rollup onto agent's run output. - ``tools/orchestration.py::_propagate_sub_agent_usage``: collapsed to a no-op stub. - ``pipeline/steps.py``: * cache-LLM rollup in CacheStep (still clears the staging field). * reflection sub-agent rollup. * reliability sub-agent rollup (still clears ``task._reliability_sub_agent_usage``). * agent-policy rollup in the policy-feedback loop. Tests updated (4 cases, ``test_usage_tracking.py``) - ``test_propagate_sub_agent_usage_increments_parent_usage`` → ``test_propagate_sub_agent_usage_is_noop``: asserts parent usage stays untouched after a sub-agent rollup is attempted. - ``test_drain_agent_tool_usage_agent_tool_drains_into_run_output`` → ``test_drain_agent_tool_usage_is_noop``: same contract flip. - ``test_aprepare_drains_culture_usage_into_agent_run_output`` → ``test_aprepare_does_not_drain_culture_usage_onto_run_output``: ``drain_accumulated_usage`` is now not invoked at all. - ``test_aprepare_no_drain_when_culture_usage_none``: redundant after the contract flip; folded into the rewritten test above. What still uses the local ``output.usage`` field ------------------------------------------------ ``AgentRunOutput.usage`` remains a per-run snapshot populated by the 9 ``_ensure_usage().incr(response.usage)`` sites for the agent's own direct model calls. Sub-agent / sub-pipeline spend appears in the registry roll-up but NOT in this per-run snapshot — by design, since ``output.usage`` documents only this run's direct emissions. A future Phase will replace ``output.usage`` with a registry query keyed by ``run_id`` once run scope is plumbed through contextvars. 434/440 unit tests pass in the touched areas. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry)!: Phase 6 — drop dead accumulator infrastructure After Phase 5/4 deleted the manual ``output.usage.incr(...)`` rollup patches every sub-pipeline had upstream-side staging that no longer fed anything. This commit removes those write-only ``_accumulated_usage`` / ``_last_llm_usage`` fields, their drain methods, and the call sites that previously bridged them into the legacy chain. Sub-pipeline LLM calls keep landing in the usage registry under the inherited scope tags (Phase 1c + Phase 2) — that is now the only roll-up path. Removed ------- - ``Agent._drain_agent_tool_usage`` method and its two call sites in ``_execute_tool_calls``. - ``Orchestrator._propagate_sub_agent_usage`` method and its three call sites (analysis / revision / synthesis sub-agent runs). - ``AgentTool._accumulated_usage`` field, the staging ``.incr`` after ``do_async``, and ``drain_accumulated_usage`` method. - ``UpsonicLLMProvider._accumulated_usage`` field + the staging ``.incr`` it received from ``_accumulate_usage_from_output`` + ``drain_accumulated_usage``. The helper itself is kept as a no-op stub so the 10 sub-pipeline call sites stay valid. - ``CultureManager._last_llm_usage`` field, ``.incr`` after culture extraction, ``drain_accumulated_usage``. - ``CacheManager._last_llm_usage`` field + the ``.incr`` after the cache-comparison sub-agent + the now-redundant clear in the cache pipeline step. - ``PolicyManager.drain_accumulated_usage`` and ``ToolPolicyManager.drain_accumulated_usage`` methods — both walked every policy's ``base_llm`` / ``text_finder_llm`` / ``feedback_llm`` to harvest accumulators that no longer exist. - Memory layer drains in ``storage/memory/session/agent.py`` (summary generator) and ``storage/memory/user/user.py`` (schema + trait extractor): the ``_last_llm_usage`` aggregator + the ``.incr`` at every ``do_async`` site + the rollup onto ``output._ensure_usage()``. Tests ----- Deleted 4 test classes (~285 lines) that asserted behaviour of the now-deleted features: ``TestCultureManagerUsageTracking``, ``TestOrchestratorUsagePropagation``, ``TestAgentToolUsageTracking``, ``TestAgentDrainAgentToolUsage``, plus ``TestSystemPromptManagerCultureUsageDrain``. The remaining classes in ``test_usage_tracking.py`` cover the data classes themselves (``TestTaskUsageRequestUsage``, ``TestAgentUsageAggregation``, ``TestAgentRunOutputUsage``, ``TestTaskUsageAggregation``) and keep passing. 421/427 unit tests pass in the touched areas (6 pre-existing skipped for sqlalchemy / RAG infra). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): Phase 7 — run_id scope + safety_engine stub removal Two small follow-ups that close out the dead-state cleanup. run_id contextvar ----------------- ``Agent.do_async`` and ``Agent.astream`` now push the run's ``run_id`` onto the ``usage_registry.scope._run_id`` contextvar (next to the agent / task pushes) and reset it in the finally. Every UsageEntry recorded during the run therefore carries ``run_id`` automatically, which is what a future per-run roll-up (``AgentRunOutput.usage`` as a registry view) will key on. Inherit-don't-override is intentionally NOT applied to run_id — each agent.do_async invocation IS a distinct run, so we always push our own and reset on exit. safety_engine UpsonicLLMProvider -------------------------------- The Phase-6 no-op stub ``_accumulate_usage_from_output`` plus its 10 call sites in ``safety_engine/llm/upsonic_llm.py`` are gone. Each sub-agent's LLM call is recorded by the Phase-2 emission hook under the inherited scope tags; nothing needs to stage anything locally. 421/427 unit tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(smoke): rewrite retry_metrics suite for registry semantics The original suite was authored around TECH-1588's baseline / snapshot / subtract machinery and the ``_on_retries_exhausted`` hook — all of which are gone after the usage-registry refactor. Every test is restated in terms of what the registry guarantees: - one UsageEntry per model.request, idempotent on entry_id - ``agent.usage`` is a registry.by_agent(...) view, so retry / HITL / cross-process resume can never double-count (structural, not arithmetic) Removed assertions on ``_agent_usage_baseline``, the exhaustion-hook contract, and the ``final.usage - baseline`` delta. Replaced with the simpler "agent.usage covers every emission tagged with this agent's scope" invariant. Doc-comment header rewritten to match the new model. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: fix CI failures from registry-backed task.usage + missing api key Two CI breakages surfaced in PR #602: 1. ``test_scope_ids.py`` constructed agents with the literal ``Agent("openai/gpt-4o")`` string. After ``infer_model`` got eager-init semantics for the OpenAI provider, that path now opens an ``AsyncOpenAI`` client at construction time — which raises ``OpenAIError`` when ``OPENAI_API_KEY`` is unset in CI. Switched every agent build in this file to a ``MagicMock(model_name="mock-model")`` so the suite needs no key. 2. ``test_task_usage.py`` had four assertions that pre-dated the Phase-5/2 ``task.usage`` cut-over to a registry view: - ``assert task.usage is None`` (initial state) - ``assert task.usage is task._usage`` (identity) - ``assert task.usage.input_tokens == 80`` after mutating ``task._usage`` directly - ``isinstance(task.usage, TaskUsage)`` after from_dict Each is rewritten to assert the new contract: - ``task.usage`` is always a non-None ``AggregatedUsage`` (empty when nothing has been recorded). - ``task._usage`` is the in-pipeline timer scratchpad and is distinct from ``task.usage`` (which is a registry query). - Mutating ``_usage`` directly does NOT show up in ``task.usage``; callers must go through the Phase-2 ``record_request_usage`` emission hook (which the real pipeline does at every model.request). - ``from_dict`` round-trips ``_usage`` (the scratchpad with timer state) but does NOT auto-rehydrate registry entries — the storage layer does that explicitly via ``UsageRegistry.load_from_storage`` at session reopen. 494/500 unit tests pass with ``OPENAI_API_KEY`` unset (matches CI env). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: use spec=Model MagicMock so infer_model short-circuits Last commit switched ``test_scope_ids.py`` to ``MagicMock(model_name=...)`` agents, but ``MagicMock`` without ``spec=Model`` fails the ``isinstance(model, Model)`` short-circuit in ``infer_model`` — so construction still fell through to ``provider_factory("openai")`` → ``AsyncOpenAI(api_key=None)`` → ``OpenAIError`` in CI (no key). Caught locally only because ``tests/conftest.py`` auto-loads ``.env`` via ``python-dotenv``; with ``OPENAI_API_KEY`` set the path never exercised the no-key branch. Verified the new fix by temporarily moving ``.env`` aside: 17/17 pass with ``-u OPENAI_API_KEY``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry)!: Phase 8/1 — unify usage surface to .usage.X Removes the parallel ``chat.input_tokens`` / ``chat.total_cost`` / ``chat.total_requests`` / ``chat.total_tool_calls`` / ``chat.total_tokens`` / ``chat.run_duration`` / ``chat.time_to_first_token`` / ``chat.get_usage()`` / ``chat.get_session_metrics()`` / ``chat.get_session_summary()`` shortcuts AND ``agent.cost`` dict. Every entity now exposes one canonical surface — ``.usage`` returning an :class:`AggregatedUsage` — so the API is "look up ``something.usage.input_tokens`` / ``.cost`` / ``.requests`` / ``.duration``" no matter whether ``something`` is an Agent, a Task, or a Chat. Removed from ``Chat`` - properties: ``input_tokens``, ``output_tokens``, ``total_tokens``, ``total_cost``, ``total_requests``, ``total_tool_calls``, ``run_duration``, ``time_to_first_token`` - methods: ``get_usage()``, ``get_session_metrics()``, ``get_session_summary()`` - new property: ``chat.usage`` → ``registry.by_chat(chat_usage_id)`` (same body the deleted ``get_usage()`` had). - ``__repr__`` now reads ``self.usage.cost`` directly. Removed from ``Agent`` - ``agent.cost`` dict property (returned ``{input_tokens, output_tokens, total_tokens, estimated_cost, requests, tool_calls, cache_read_tokens, cache_write_tokens, reasoning_tokens}``). Read the equivalents off ``agent.usage`` — same names, no ``estimated_`` prefix on ``cost``. Removed from public exports - ``SessionMetrics`` dropped from ``upsonic.chat.__init__`` exports (the dataclass survives as a session_manager-internal helper). - ``tests/smoke_tests/agent/test_agent_cost_property.py`` deleted — the whole file tested the removed ``agent.cost`` property. Migrated call sites (sed-style ``chat.X → chat.usage.X``) - 13 doc-example chat scripts under ``tests/doc_examples/chat/`` - ``tests/smoke_tests/chat/test_chat.py`` (also drops the ``SessionMetrics`` / ``get_session_metrics`` / ``get_session_summary`` test block — those contracts are gone) - ``tests/unit_tests/chat/test_chat.py`` - ``tests/unit_tests/usage_registry/test_read_through.py``, ``test_parity.py``, ``test_persistence.py`` Notes - ``chat.total_cost`` previously returned ``0.0`` when the registry had no priced entries; the new ``chat.usage.cost`` returns ``None`` in that case. The sed migration writes ``(chat.usage.cost or 0.0)`` at every site that compared the value to a float, so observable behaviour stays the same. - ``chat.run_duration`` and ``chat.time_to_first_token`` used to come from session storage, not the registry. After the unification they come off ``chat.usage.duration`` and ``chat.usage.time_to_first_token`` — same data, registry-keyed. 494 / 500 unit tests pass with ``OPENAI_API_KEY`` unset (6 unrelated skipped: pre-existing dev failures + sqlalchemy-gated SQLite cases). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): Phase 8/2 — add Team.usage canonical surface ``Team`` had a ``team_usage_id`` since Phase 1a but no public ``.usage`` reader, so users had to drop down to ``registry.by_team(...)`` to see what their team had spent — out of step with Agent / Task / Chat / AgentRunOutput which all expose a ``.usage`` property. Added the missing property: ``team.usage`` → ``get_default_registry().by_team(self.team_usage_id)``. Same canonical :class:`AggregatedUsage` shape as the other surfaces, so the unified ``something.usage.X`` rule now holds across every aggregate-able entity the framework exposes. Test (1 new in ``test_scope_ids.TestTeamIds``): - ``test_team_usage_property_returns_aggregated_view``: empty registry → zero-valued AggregatedUsage; record one entry under the team scope → ``team.usage`` reflects it (input/output/requests). Note: Phase 8/3 was attempted (convert ``AgentRunOutput.usage`` from a mutable ``TaskUsage`` field to a registry-view property keyed by ``run_id``) but reverted — it broke 54 tests across pipeline / streaming / parity because internal pipeline code mutates ``output.usage`` directly and many tests assert the mutable contract. Per-run snapshots are a different concept from scope-level roll-ups and a unified property would conflate them; leaving the per-run mutable in place for now. The shape is already AggregatedUsage- compatible (``input_tokens`` / ``output_tokens`` / ``cost`` / ``duration`` / ``requests`` / ``tool_calls``) so callers reading ``output.usage.X`` see the same fields as everywhere else. 495 / 501 unit tests pass with ``OPENAI_API_KEY`` unset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry)!: Phase 8/4 — drop SessionMetrics / session summary ``SessionMetrics`` was the last "second way to read usage": a dataclass that mirrored Chat counters (total_input_tokens / total_output_tokens / total_cost / total_requests / total_tool_calls) plus a few session- runtime fields (duration, message_count, average_response_time, messages_per_minute). Phase 8/1 removed its Chat-side wrappers and its public export; this commit deletes the underlying machinery too. Removed - ``upsonic.chat.session_manager.SessionMetrics`` dataclass. - ``SessionManager.get_session_metrics`` / ``aget_session_metrics`` / ``get_session_summary`` methods. Doc examples migrated (4 files) - ``chat_metrics_3.py``: prints fields off ``chat.usage`` directly + ``chat.duration`` and ``len(chat.all_messages)`` for the runtime-only metrics. - ``chat_metrics_4.py``: hand-rolls the summary string from ``chat.usage`` instead of the deleted helper. - ``chat_examples_complex_chat_1.py``: same — reads from ``chat.usage`` + ``chat.duration``. - ``chat_examples_complex_chat_2.py``: same. Note: ``average_response_time`` and ``messages_per_minute`` (derived metrics) had no callers outside SessionMetrics itself, so they go with the dataclass. If anyone needs them they're trivial to compute from ``chat.usage.duration`` + ``len(chat.all_messages)``. 495 / 501 unit tests pass with ``OPENAI_API_KEY`` unset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry): plumb model_execution_time through emission hook Until now the registry recorded tokens / cost / scope tags for every model.request but not the per-call elapsed time, so ``task.usage.model_execution_time`` always read zero — even though the same number was sitting in ``task._usage.model_execution_time`` on the in-pipeline scratchpad. Reading the same metric through two surfaces gave two different answers, which violated the unified "every ``.usage.X`` reads from the registry" promise. This commit - adds ``model_execution_time`` and ``time_to_first_token`` kwargs to ``record_request_usage``; both flow into the resulting ``UsageEntry`` (model_execution_time also seeds ``duration`` so AggregatedUsage rolls up correctly). - passes the per-call elapsed time at 8 of the 9 emission sites (direct.py + steps.py model_call / policy_feedback / model_call_stream + agent.py retry / final / follow_up / guardrail). Every site already had the value in a local ``_*_model_elapsed`` variable; just adds it to the kwargs. - drops the summarization site's ``record_request_usage`` call — the context-management middleware's own model.request emits a UsageEntry under the inherited scope tags, so recording here was a silent double-count. Result: ``task.usage.model_execution_time``, ``task.model_execution_time``, and ``task._usage.model_execution_time`` all return the same number now. ``task.duration`` is the only distinct timing surface — it's the wall-clock from task_start to task_end (orchestration + tool + model time), genuinely different from the per-call sum. Verified live: ``Agent("openai/gpt-4o-mini").do(Task("Reply OK"))`` shows model_execution_time = 1.315s across all three reads, task.duration = 1.471s (wall-clock). 495 / 501 unit tests pass with ``OPENAI_API_KEY`` unset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(usage-registry)!: drop Task.duration / model_execution_time / tool_execution_time / upsonic_execution_time The previous commit made ``task.usage.model_execution_time`` / ``task.usage.duration`` carry the per-call timer data correctly, which meant the four top-level Task timer properties were the last duplicates of "read the same number two different ways". Per the unification rule, one canonical surface only: ``task.usage.X``. Removed from Task - ``task.duration`` (was timer.duration with start_time/end_time fallback) - ``task.model_execution_time`` (was ``_usage.model_execution_time``) - ``task.tool_execution_time`` (was ``_usage.tool_execution_time``) - ``task.upsonic_execution_time`` (was ``_usage.upsonic_execution_time``) All four were thin delegations to either ``task._usage`` (the in-pipeline timer scratchpad) or to ``end_time - start_time``. Now: - Read timing from ``task.usage.duration`` / ``.model_execution_time`` / ``.tool_execution_time`` / ``.upsonic_execution_time``. Registry-derived, same number, same shape as every other ``.usage.X``. - Wall-clock task length: derive directly from ``task.end_time - task.start_time`` (both still on Task as raw fields). The old ``task.duration`` was misleading anyway — it returned ``_usage.duration`` (model time, not wall-clock) when both were set, with a start_time/end_time fallback only when the timer hadn't run. Migrated 8 call sites in ``src/`` (graph.py, printing.py, pipeline/steps.py, context_managers/context_manager.py) — sed-pattern ``\\btask\\.duration\\b`` → ``task.usage.duration`` etc. Migrated 7 test files (smoke + doc_examples + unit). Deleted ``TestTaskPropertyDelegation`` test class in ``test_task_usage.py`` — its whole point was the delegation contract that's now gone. ``test_do.py::test_task_duration_set_after_start_and_end`` rewritten to assert directly on ``task.end_time - task.start_time`` instead. Live verification (``Agent("openai/gpt-4o-mini").do(Task("Reply OK"))``): - ``task.usage.duration`` = 1.635s - ``task.usage.model_execution_time`` = 1.635s - ``task.usage.tool_execution_time`` = 0.0 - ``task.usage.upsonic_execution_time`` = 0.0 - ``hasattr(task, 'duration')`` = False - ``hasattr(task, 'model_execution_time')`` = False - ``hasattr(task, 'tool_execution_time')`` = False - ``hasattr(task, 'upsonic_execution_time')`` = False 487 / 493 unit tests pass with OPENAI_API_KEY unset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(usage-registry): collapse emission + scope-push boilerplate Two helper extractions, no behavioural change — just less code that all says the same thing. #1: ``record_response_usage`` helper (in ``usage_registry/recorder.py``) - Every fresh model.request emission site was repeating the same 6-8 lines: snapshot.incr(response.usage), compute cost via calculate_cost_from_usage, set_usage_cost, then forward to record_request_usage with the same kwargs. Wrapped into a single function so each call site is now five lines instead of fifteen, with the per-step pipeline_step + model_execution_time as the only per-site arguments. - Migrated 8 sites (4 in agent.py: retry / final / follow_up / guardrail; 3 in pipeline/steps.py: model_call / policy_feedback / model_call_stream; the summarization site stays — special, no cost compute). direct.py left alone (only 5 lines, simpler). #2: ``push_scope_tags`` + ``reset_scope_tags`` helpers (in ``usage_registry/scope.py``) - The "push contextvar tokens at entry, reset them in finally" pattern was repeated six times (Agent.do_async / Agent.astream / Chat._invoke_blocking_async / Chat._invoke_streaming / Chat._invoke_streaming_events / Team.do_async). Each call site had ~18 lines of manual token tracking + try/except. - The new helper accepts every scope tag as kwargs, returns a ``[(var, token), …]`` list, and ``reset_scope_tags`` unwinds them in reverse with per-token error swallowing. - ``inherit=True`` flag preserves the Phase-3a "sub-agent inherits parent's scope" behaviour: a tag that's already active in the current context is left untouched, so nested ``Agent.do_async`` doesn't overwrite a parent's ids. - Multiple ``push_scope_tags`` calls can append to one tokens list — used in ``Agent.do_async`` / ``Agent.astream`` where ``agent`` / ``task`` push at entry (inherit) and ``run_id`` pushes later (always, no inherit). Net effect - ~155 lines deleted, ~95 added (-60 net). Every emission / scope-push pattern reads the same way now; no more line-by-line duplication across the eight sites. - Live verification with ``gpt-4o-mini``: ``chat.usage == agent.usage`` (82 in / 1 out / $0.0000129), models = ['gpt-4o-mini'], every assertion the unification probe ran in the previous round still passes. 487 / 493 unit tests pass with OPENAI_API_KEY unset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> | 1 个月前 | |
feat: add Examples repository as a git submodule | 4 个月前 | |
chore: New Version 0.76.2 (#572) Bump pyproject.toml + uv.lock, fast-forward the Docs submodule pointer to the v0.76.2 docs push, and start ignoring internal RELEASE_NOTES_v*.md drafts. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> | 2 个月前 | |
feat: rename AppliedScientist research_paper to research_source (#570) Accept any reference describing the new method — local file (PDF, Markdown, HTML, .ipynb, text), web URL, git repo, or Kaggle notebook / dataset page — not just a PDF. The agent detects the source kind at Phase 0 and materializes it into the experiment folder before reading it in Phase 2. Also add the Upsonic/Docs repository as a submodule at Docs/ so the rename can be kept in lockstep with the public docs. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> | 2 个月前 | |
fix of unit tests commit | 7 个月前 | |
Turning into Upsonic (#245) * New gca * chore: Update run_uv_commands.yml to run servers in the background * feat: Add environment variable setup for OpenAI API key in GitHub Actions workflow * chore: Update run_uv_commands.yml to run tools server and main server concurrently before pre-commit checks * fix: Increase timeout for tool requests to improve reliability * fix: Increase HTTP client timeout from 60s to 600s in function_client.py and mcp_client.py to enhance request reliability * fix: Ensure proper closure of HTTP client session in mcp_tools.py to prevent resource leaks * fix: Improve session management in mcp_tools.py by adding error handling and cleanup for client sessions * fix: Refactor session management in mcp_tools.py to improve error handling and ensure proper cleanup on session termination * refactor: Enhance session management in mcp_tools.py by implementing async context manager for better resource handling and error management * chore: Change GitHub Actions runner from ubuntu-latest to macos-latest for improved compatibility * refactor: Remove timeout decorators from list_tools and call_tool functions in mcp_tools.py to streamline request handling * Enhance timeout handling in mcp_tools.py by adding 30-second timeout decorators to list_tools and call_tool functions, and update GitHub Actions workflow to include a sleep command before running pre-commit checks for improved reliability. * refactor: Remove redundant test cases from test_mcp_client.py to streamline testing process and improve maintainability * test: Normalize string comparisons in test_call.py and add sleep in test_mcp_client.py for improved reliability * Enhance logging and error handling in call_tool function of mcp_tools.py by adding detailed print statements for session initialization and request cancellation. This improves traceability and debugging capabilities during tool calls. * Enhance logging in timeout decorator of api.py by adding print statements to track execution start, success, and timeout events. This improves debugging and traceability for asynchronous function calls. * fix: Update test_mcp_client.py to use localhost URL for tool testing, enhancing test reliability by avoiding external dependencies. * feat: Add /status endpoint to api.py and update test_mcp_client.py to check server status. This enhances the API functionality and improves test reliability by ensuring the correct endpoint is used for tool testing. * test: Update test_call_tool to use localhost URL and assert server status response. This change improves test reliability by ensuring it checks the correct server response instead of an external URL. * feat: Add dill dependency and refactor UpsonicClient to UpsonicServer. Update call method to support enhanced response formats and improve type handling. Remove outdated GitHub Actions workflow and clean up related tests. * fix: Update GitHub Actions workflow to ensure server test commands end with a trailing slash. This change improves consistency in test execution for both server and client tests. * fix: Update GitHub Actions workflow to use pytest for running server and client tests, improving test execution consistency and reliability. * fix: Update test_call_with_env_variable to include environment variable in mcp_servers configuration, enhancing test accuracy and ensuring proper tool execution. * feat: Introduce ToolList response format in test_call.py and enhance tool validation in tests. This update improves the structure of tool responses and ensures accurate assertions for tool presence in the results. * chore: Update pyproject.toml to include TestPyPI configuration and modify GitHub Actions workflow to use a new publishing token. This enhances the package publishing process and allows for testing on TestPyPI. * refactor: Remove UV sync step from GitHub Actions workflow to streamline the publishing process. This change simplifies the workflow by eliminating unnecessary steps, focusing on the build process. * chore: Add PyPI index configuration to pyproject.toml for improved package publishing. This update includes the addition of the main PyPI repository alongside the existing TestPyPI configuration, streamlining the publishing process. * refactor: Rename package from "upsonicai" to "upsonic" and update version to 0.35.0. This change includes modifications across multiple files, including the main server and client scripts, configuration files, and test cases, ensuring consistency in naming and improving overall project structure. * refactor: Simplify server startup by introducing run_server and run_tools_server functions in upsonic.server module. This change replaces the previous main function calls in run_main_server.py and run_tools_server.py, enhancing code clarity and maintainability. Additionally, update uv.lock to ensure consistent package source URLs by appending a trailing slash. * chore: Update dependencies in pyproject.toml and uv.lock to include new packages and versions. Added 'poetry' to the development dependencies and introduced several new packages such as 'build', 'cachecontrol', 'cffi', 'cleo', 'crashtest', 'cryptography', 'dulwich', 'fastjsonschema', 'installer', 'jaraco-classes', 'jaraco-context', 'requests-toolbelt', 'secretstorage', 'tomlkit', 'trove-classifiers', and 'xattr' with their respective versions. This enhances the project's dependency management and ensures compatibility with the latest libraries. * chore: Enhance GitHub Actions workflow for Test Publish process. Updated job names for clarity, added steps for generating a randomized alpha version using Poetry, and streamlined the installation and publishing steps. This improves the overall structure and functionality of the CI/CD pipeline. * chore: Update GitHub Actions workflow to improve development installation step. Changed the build step to use 'uv pip install' with requirements from pyproject.toml, enhancing clarity and ensuring proper dependency installation for development. This aligns with the overall goal of streamlining the CI/CD process. * chore: Update GitHub Actions workflow to include virtual environment setup in development installation step. Changed the installation command to create a virtual environment before installing dependencies from pyproject.toml, improving the development setup process. * chore: Remove 'poetry' from development dependencies in pyproject.toml and update GitHub Actions workflow to install dependencies using 'pip' instead. This change streamlines the development setup process and ensures compatibility with the latest package management practices. * chore: Add optional dependencies section in pyproject.toml for server setup. This update introduces a new section for optional dependencies, including 'fastapi' and 'mcp', enhancing the project's configuration for server-related functionalities. * Update pyproject.toml * Refactor uv.lock to reorganize dependencies. Moved 'server' dependencies to an optional section and restructured 'dev' dependencies for clarity. Added new optional dependencies for server functionality, enhancing project configuration. * Refactor server functionality by renaming run_server to run_main_server for clarity. Introduce a new Storage class for managing configuration values, and implement endpoints for getting and setting configuration in the storage server. This enhances the server's configuration management capabilities and improves code organization. * Add /status endpoint to API for server health check This commit introduces a new endpoint to the FastAPI application that allows users to check the server's status. The endpoint makes an asynchronous HTTP request to a local server and returns a JSON response indicating whether the server is running. If the server is unreachable, it raises an HTTPException with the appropriate status code. This enhancement improves the monitoring capabilities of the server. * Update dependencies and configuration files - Added 'sentry-sdk[opentelemetry]' as a new dependency in pyproject.toml to enhance error tracking and monitoring capabilities. - Introduced optional dependencies for 'sentry-sdk' in the project configuration. - Updated uv.lock to include new packages: 'deprecated', 'importlib-metadata', 'opentelemetry-api', 'opentelemetry-distro', 'opentelemetry-instrumentation', 'opentelemetry-sdk', and 'opentelemetry-semantic-conventions' with their respective versions. - Modified .gitignore to include 'notes.txt' for better file management. * Enhance project configuration and functionality - Added 'toml' and 'pickledb' as dependencies in pyproject.toml to support configuration management. - Introduced new configuration management system using 'pickledb' for storing and retrieving configuration values. - Implemented version retrieval from pyproject.toml in a new get_version.py module. - Added system ID generation and retrieval functionality in system_id.py. - Integrated Sentry SDK for error tracking and monitoring in client and server calls. - Refactored call methods to include Sentry transaction and span for better observability. - Updated imports for consistency across modules. - Removed obsolete test file for cleaner codebase. These changes improve the project's configuration management, error tracking, and overall code organization. * Refactor API client and server configuration handling - Removed debug print statements from the API client in base.py to clean up the code. - Simplified the get_config function in server.py by removing the HTTPException for missing keys, allowing for a more graceful handling of non-existent configuration values. These changes enhance code clarity and improve the handling of configuration retrieval in the server. * Refactor Sentry SDK imports for improved error tracking - Moved Sentry SDK imports to function scope in call.py and storage.py to enhance modularity and reduce global namespace clutter. - This change ensures that Sentry is only imported when needed, improving code organization and potentially reducing startup time. These updates contribute to better observability and maintainability of the codebase. * Update project configuration and server settings - Added 'httpx' as a new dependency in pyproject.toml and updated uv.lock to include it, enhancing HTTP client capabilities. - Modified .gitignore to exclude 'test2.py' for better file management. - Updated server port configurations in __init__.py and test_call.py to improve server accessibility. - Changed localhost references in API client and tools to ensure consistent server communication. These changes improve dependency management, enhance server configuration, and streamline the development process. * Enhance unit test workflow by adding options for syncing all groups and extras - Updated the 'Run UV Sync' command in the unit_tests.yml workflow to include '--all-groups' and '--all-extras' flags, improving the synchronization process for unit tests. This change ensures a more comprehensive testing environment by including all relevant components during the sync operation. * Update GitHub Actions workflow to refine Docker build process - Changed the job name in the test_publisher.yml workflow from "Build and Publish for ARM and AMD" to "Build and Publish for AMD", reflecting a focus on AMD architecture only. This update streamlines the workflow and clarifies the build target. * Enhance unit test workflow by updating UV Sync command - Modified the 'Run UV Sync' command in the unit_tests.yml workflow to include '--all-groups' and '--all-extras' flags. This change improves the synchronization process for unit tests, ensuring a more comprehensive testing environment by including all relevant components during the sync operation. * fix * Update Docker build workflow to target AMD architecture only - Modified the GitHub Actions workflow in test_publisher.yml to focus exclusively on the linux/amd64 platform by removing support for linux/arm64. This change streamlines the build process and clarifies the intended architecture for the Docker image. * Refactor Docker build workflow to include parent directory context - Updated the GitHub Actions workflow in test_publisher.yml to change the working directory to the parent directory before executing the Docker build commands for both AMD and ARM64 platforms. This adjustment ensures that the build context is correctly set, allowing for successful image creation and pushing to the specified tags. * a * fix * Fix * Update Docker build workflow to use 'server_test' tag for both AMD and ARM64 platforms - Modified the GitHub Actions workflow in test_publisher.yml to change the Docker image tags from 'upsonic/server:$VERSION' and 'upsonic/server:$VERSION-ARM64' to 'upsonic/server_test:$VERSION' and 'upsonic/server_test:$VERSION-ARM64', respectively. This change clarifies the purpose of the images being built and ensures consistency in tagging for testing purposes. * Refactor UpsonicServer to UpsonicClient and update imports - Changed the class name from `UpsonicServer` to `UpsonicClient` in `base.py` to better reflect its functionality. - Updated the import statements in `__init__.py` and `test_call.py` to use `UpsonicClient`. - Removed unnecessary print statements in `call.py` to clean up the code. - Added `BaseModel` import to `__init__.py` for better data validation support. * Add TimeoutException and handle request timeout in UpsonicClient * Add LLM model support and exception handling in Upsonic client and server - Introduced NoAPIKeyException and UnsupportedLLMModelException in the client to handle specific error cases. - Updated the Call class to accept an llm_model parameter and pass it in the request data. - Enhanced the server's CallManager to support multiple LLM models (OpenAI and Anthropic) with appropriate API key checks. - Modified the GPT4ORequest model and run_sync_gpt4o function to include llm_model as an optional parameter for better flexibility in requests. * Enhance Upsonic client with task management and response handling - Introduced a new Task class to encapsulate task descriptions and response formats. - Added various response types (ObjectResponse, StrResponse, IntResponse, FloatResponse, BoolResponse, StrInListResponse) for better response handling. - Updated the Call class to accept a Task object instead of a prompt string, improving clarity and structure. - Refactored tests to utilize the new Task class for making calls, ensuring consistency in response handling. * Fix * Update .gitignore and enhance Upsonic client with Tools integration - Added test3.py to .gitignore to exclude it from version control. - Integrated Tools class into UpsonicClient for improved functionality. - Updated import statements in various modules to include Tools, enhancing the overall structure and capabilities of the Upsonic client. * Fix * Fix * Enhance Upsonic client and server with tool registration functionality - Added a decorator to the Tools class for registering functions as tools, allowing for dynamic tool management. - Introduced new API endpoints for adding tools and installing libraries in the server. - Updated the ToolManager to support adding tools via HTTP requests. - Refactored existing functions to improve clarity and maintainability, including changes to library installation commands. - Enhanced response handling in the Task class to return None when no response is available. * fix * Fix * Fix * fix * Fix * Fix * Fix * fix * fix * Fix * Fix * fix * fix * fix * fix * Fix * Fix * Fix * Fix * Fİx * Fix * Fix * Fix * Fix * Fix * Fix * Add context feature * Update call method to accept a list of tasks in addition to a single task * Fix * Fİx * Fix * Fix * Fix * Fix * Fix * Fix * fix * Fix * Fix * Added aws bedrock support * Added computer use tools * Fix * Fix * Fix * Fix * fix * Fix * fix * fix * Fix * Fİx * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * fix * Fix * Fix * Fİx * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Update README.md * Update README.md * Update README.md * Fix * fix * Update README.md * Update dependency versions in pyproject.toml and uv.lock; specify exact version for psutil and add uv package * Fix * Update README.md * fixes * Update README.md * Update README.md * Update README.md | 1 年前 | |
chore(master): release 0.77.3 | 1 个月前 | |
chore(master): release 0.77.3 | 1 个月前 | |
TECH-1588: agent retry + usage tracking refactor (#595) * TECH-1588: agent retry + usage tracking refactor Make AgentRunOutput the single source of truth for usage; rebuild the retry path so failed attempts no longer drop tokens from agent.usage. - Add Task.reset_run_state() (per-attempt state cleanup) and call it from task_start() and Agent.do_async's retry block. _run_id is deliberately not reset — do_async assigns it before InitStep. - Add TaskUsage.snapshot() / subtract() for delta-based accumulation. - Add _agent_usage_baseline on AgentRunOutput (with to_dict/from_dict round-trip) so cross-process resume after retry exhaustion does not double-count. - Flip _prepare_continuation_context to task._usage = output.usage (output is canonical) via output._ensure_usage(). - Make _finalize_agent_usage baseline-aware: delta = usage - baseline when a baseline is present. - Add @retryable exhaustion hook (sync + async); Agent implements _on_retries_exhausted to capture the last failed attempt's usage, snapshot the baseline, and idempotently upsert the output to storage (avoiding save_session_async's message-append side effect). - Capture each prior failed attempt's usage in the retry block before reset_run_state discards its TaskUsage. - Prefix injected test errors with 'INJECTED ERROR:' so test assertions distinguish injected vs real failures. - Delegate Chat retries entirely to Agent.retry: drop Chat(retry_attempts, retry_delay), _execute_with_retry, _is_retryable_error and the stream-retry wrappers. - New smoke tests: tests/smoke_tests/agent/test_retry_metrics.py (7 scenarios) and tests/smoke_tests/chat/test_chat_retry.py (3 scenarios). Stale unit tests for Chat's retry validation / _is_retryable_error removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(chat): use agent-first storage/memory resolution - Chat reuses agent.memory (and its storage) when present; storage= and memory-config kwargs apply only when the agent has no memory. - Realign Chat.session_id / user_id with agent.memory and emit UserWarning on mismatch. SessionManager now keys against the aligned ids. - AutonomousAgent.autonomous_memory / autonomous_storage become live properties tracking self.memory (no more stale snapshot after a Memory swap). - Add 12 smoke regressions across chat and autonomous_agent suites covering the resolution order and id alignment. * fix(tools,messages): tolerate gemma stream artifacts in HQ chat - shell_toolkit.run_command / run_python: @tool(timeout=None, max_retries=0) so the generic tool-layer asyncio.wait_for stops duplicating the subprocess's own timeout and leaking child processes on cancel (was triggering a 6x retry loop on legitimate run_command calls with no observable progress). - messages.ToolCallPart.args_as_dict: extend the existing trailing-junk fallback to also handle truncated / unterminated args (stream cut mid-string) by returning {} instead of tearing down the whole stream. - Trim verbose comment blocks in agent.py, tasks.py and run/agent/output.py to reflect the 'concise comments' preference. * docs(claude): track pending explanation/ doc syncs for TECH-1588 Three documents/ai/explanation/ files still describe pre-TECH-1588 contracts: - chat.md: agent-first storage/memory wiring and session_id / user_id alignment with UserWarning. - messages.md: ToolCallPart.args_as_dict() now falls back to {} on truncated/unterminated args instead of asserting. - tools.md: ShellToolKit overrides ToolConfig defaults with @tool(timeout=None, max_retries=0). Surface them in CLAUDE.md so a future session reconciles all three in one 'docs: sync explanation/' pass before relying on the old text. * docs: sync explanation/ docs with TECH-1588 contracts - chat.md: rewrite the storage/memory wiring section to describe the new agent-first policy (Chat reuses agent.memory and its storage when present; storage= and memory-config kwargs only apply when agent.memory is None) and the new session_id / user_id alignment + UserWarning. Update the dependency tree and integration notes that still said 'Chat replaces agent.memory'. - messages.md: ToolCallPart.args_as_dict() no longer asserts dict; document the trailing-junk + truncated-args fallback to {}. - tools.md: ToolConfig defaults table now flags the ShellToolKit override (timeout=None, max_retries=0) alongside the existing MCPTool override. - CLAUDE.md: drop the 'Outstanding documentation updates' TODO now that the three docs are in sync. * docs(claude): require documents/ai/explanation/ sync on contract changes Add a permanent rule under 'AI Operational Guides' so future sessions know: when a code change alters an observable contract (public API, default, side effect, error shape, new emission, conditional invariant), the matching documents/ai/explanation/<subsystem>.md must be resynced in the same commit/PR or an immediate follow-up 'docs: sync explanation/...' commit. Lists the common triggers and a 4-step how-to (grep, edit surgically, bundle with the change, surface what you found at the start of the reply). * refactor(pipeline): move error-injection scaffolding out of production The error-injection helpers used to live in src/upsonic/agent/pipeline/ step.py as a global _ERROR_INJECTIONS dict + inject/clear/check funcs + a try/except wrapper in Step.run. That's test-only code on the hot path; ship it from production and move it to tests/. - step.py: drop _ERROR_INJECTIONS, inject_error_into_step, clear_error_injection, check_error_injection, and the try/except around the check call in Step.run. Net -75 lines from runtime. - tests/_pipeline_injection.py: new shared helper with the same surface API (inject_error_into_step / clear_error_injection). Monkey-patches Step.run on first use and restores it once the registry is empty. Crucially, mirrors the old behaviour of finalizing an ERROR StepResult on the context before raising so get_problematic_step() and continue_run_async can still find a resume point. - tests/conftest.py: add project root to sys.path so suites can do 'from tests._pipeline_injection import …'. - test_retry_metrics.py, test_chat_retry.py, test_comprehensive_hitl.py, usage_durable_execution.py: change the import path only; test bodies untouched. (test_comprehensive_hitl also splits the StepResult/StepStatus import which stays under upsonic.agent.pipeline.) - documents/ai/explanation/agent/agent.md: resync the step.run description per the CLAUDE.md doc-sync rule — no more check_error_injection in production. Verified: 15 retry/chat-retry/task-metric tests + 34 HITL tests (comprehensive + usage_metrics + delay + storage_verification) all pass against the new helper. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> | 1 个月前 | |
docs: fix test command in CONTRIBUTING.md to use uv run (#520) | 4 个月前 | |
Turning into Upsonic (#245) * New gca * chore: Update run_uv_commands.yml to run servers in the background * feat: Add environment variable setup for OpenAI API key in GitHub Actions workflow * chore: Update run_uv_commands.yml to run tools server and main server concurrently before pre-commit checks * fix: Increase timeout for tool requests to improve reliability * fix: Increase HTTP client timeout from 60s to 600s in function_client.py and mcp_client.py to enhance request reliability * fix: Ensure proper closure of HTTP client session in mcp_tools.py to prevent resource leaks * fix: Improve session management in mcp_tools.py by adding error handling and cleanup for client sessions * fix: Refactor session management in mcp_tools.py to improve error handling and ensure proper cleanup on session termination * refactor: Enhance session management in mcp_tools.py by implementing async context manager for better resource handling and error management * chore: Change GitHub Actions runner from ubuntu-latest to macos-latest for improved compatibility * refactor: Remove timeout decorators from list_tools and call_tool functions in mcp_tools.py to streamline request handling * Enhance timeout handling in mcp_tools.py by adding 30-second timeout decorators to list_tools and call_tool functions, and update GitHub Actions workflow to include a sleep command before running pre-commit checks for improved reliability. * refactor: Remove redundant test cases from test_mcp_client.py to streamline testing process and improve maintainability * test: Normalize string comparisons in test_call.py and add sleep in test_mcp_client.py for improved reliability * Enhance logging and error handling in call_tool function of mcp_tools.py by adding detailed print statements for session initialization and request cancellation. This improves traceability and debugging capabilities during tool calls. * Enhance logging in timeout decorator of api.py by adding print statements to track execution start, success, and timeout events. This improves debugging and traceability for asynchronous function calls. * fix: Update test_mcp_client.py to use localhost URL for tool testing, enhancing test reliability by avoiding external dependencies. * feat: Add /status endpoint to api.py and update test_mcp_client.py to check server status. This enhances the API functionality and improves test reliability by ensuring the correct endpoint is used for tool testing. * test: Update test_call_tool to use localhost URL and assert server status response. This change improves test reliability by ensuring it checks the correct server response instead of an external URL. * feat: Add dill dependency and refactor UpsonicClient to UpsonicServer. Update call method to support enhanced response formats and improve type handling. Remove outdated GitHub Actions workflow and clean up related tests. * fix: Update GitHub Actions workflow to ensure server test commands end with a trailing slash. This change improves consistency in test execution for both server and client tests. * fix: Update GitHub Actions workflow to use pytest for running server and client tests, improving test execution consistency and reliability. * fix: Update test_call_with_env_variable to include environment variable in mcp_servers configuration, enhancing test accuracy and ensuring proper tool execution. * feat: Introduce ToolList response format in test_call.py and enhance tool validation in tests. This update improves the structure of tool responses and ensures accurate assertions for tool presence in the results. * chore: Update pyproject.toml to include TestPyPI configuration and modify GitHub Actions workflow to use a new publishing token. This enhances the package publishing process and allows for testing on TestPyPI. * refactor: Remove UV sync step from GitHub Actions workflow to streamline the publishing process. This change simplifies the workflow by eliminating unnecessary steps, focusing on the build process. * chore: Add PyPI index configuration to pyproject.toml for improved package publishing. This update includes the addition of the main PyPI repository alongside the existing TestPyPI configuration, streamlining the publishing process. * refactor: Rename package from "upsonicai" to "upsonic" and update version to 0.35.0. This change includes modifications across multiple files, including the main server and client scripts, configuration files, and test cases, ensuring consistency in naming and improving overall project structure. * refactor: Simplify server startup by introducing run_server and run_tools_server functions in upsonic.server module. This change replaces the previous main function calls in run_main_server.py and run_tools_server.py, enhancing code clarity and maintainability. Additionally, update uv.lock to ensure consistent package source URLs by appending a trailing slash. * chore: Update dependencies in pyproject.toml and uv.lock to include new packages and versions. Added 'poetry' to the development dependencies and introduced several new packages such as 'build', 'cachecontrol', 'cffi', 'cleo', 'crashtest', 'cryptography', 'dulwich', 'fastjsonschema', 'installer', 'jaraco-classes', 'jaraco-context', 'requests-toolbelt', 'secretstorage', 'tomlkit', 'trove-classifiers', and 'xattr' with their respective versions. This enhances the project's dependency management and ensures compatibility with the latest libraries. * chore: Enhance GitHub Actions workflow for Test Publish process. Updated job names for clarity, added steps for generating a randomized alpha version using Poetry, and streamlined the installation and publishing steps. This improves the overall structure and functionality of the CI/CD pipeline. * chore: Update GitHub Actions workflow to improve development installation step. Changed the build step to use 'uv pip install' with requirements from pyproject.toml, enhancing clarity and ensuring proper dependency installation for development. This aligns with the overall goal of streamlining the CI/CD process. * chore: Update GitHub Actions workflow to include virtual environment setup in development installation step. Changed the installation command to create a virtual environment before installing dependencies from pyproject.toml, improving the development setup process. * chore: Remove 'poetry' from development dependencies in pyproject.toml and update GitHub Actions workflow to install dependencies using 'pip' instead. This change streamlines the development setup process and ensures compatibility with the latest package management practices. * chore: Add optional dependencies section in pyproject.toml for server setup. This update introduces a new section for optional dependencies, including 'fastapi' and 'mcp', enhancing the project's configuration for server-related functionalities. * Update pyproject.toml * Refactor uv.lock to reorganize dependencies. Moved 'server' dependencies to an optional section and restructured 'dev' dependencies for clarity. Added new optional dependencies for server functionality, enhancing project configuration. * Refactor server functionality by renaming run_server to run_main_server for clarity. Introduce a new Storage class for managing configuration values, and implement endpoints for getting and setting configuration in the storage server. This enhances the server's configuration management capabilities and improves code organization. * Add /status endpoint to API for server health check This commit introduces a new endpoint to the FastAPI application that allows users to check the server's status. The endpoint makes an asynchronous HTTP request to a local server and returns a JSON response indicating whether the server is running. If the server is unreachable, it raises an HTTPException with the appropriate status code. This enhancement improves the monitoring capabilities of the server. * Update dependencies and configuration files - Added 'sentry-sdk[opentelemetry]' as a new dependency in pyproject.toml to enhance error tracking and monitoring capabilities. - Introduced optional dependencies for 'sentry-sdk' in the project configuration. - Updated uv.lock to include new packages: 'deprecated', 'importlib-metadata', 'opentelemetry-api', 'opentelemetry-distro', 'opentelemetry-instrumentation', 'opentelemetry-sdk', and 'opentelemetry-semantic-conventions' with their respective versions. - Modified .gitignore to include 'notes.txt' for better file management. * Enhance project configuration and functionality - Added 'toml' and 'pickledb' as dependencies in pyproject.toml to support configuration management. - Introduced new configuration management system using 'pickledb' for storing and retrieving configuration values. - Implemented version retrieval from pyproject.toml in a new get_version.py module. - Added system ID generation and retrieval functionality in system_id.py. - Integrated Sentry SDK for error tracking and monitoring in client and server calls. - Refactored call methods to include Sentry transaction and span for better observability. - Updated imports for consistency across modules. - Removed obsolete test file for cleaner codebase. These changes improve the project's configuration management, error tracking, and overall code organization. * Refactor API client and server configuration handling - Removed debug print statements from the API client in base.py to clean up the code. - Simplified the get_config function in server.py by removing the HTTPException for missing keys, allowing for a more graceful handling of non-existent configuration values. These changes enhance code clarity and improve the handling of configuration retrieval in the server. * Refactor Sentry SDK imports for improved error tracking - Moved Sentry SDK imports to function scope in call.py and storage.py to enhance modularity and reduce global namespace clutter. - This change ensures that Sentry is only imported when needed, improving code organization and potentially reducing startup time. These updates contribute to better observability and maintainability of the codebase. * Update project configuration and server settings - Added 'httpx' as a new dependency in pyproject.toml and updated uv.lock to include it, enhancing HTTP client capabilities. - Modified .gitignore to exclude 'test2.py' for better file management. - Updated server port configurations in __init__.py and test_call.py to improve server accessibility. - Changed localhost references in API client and tools to ensure consistent server communication. These changes improve dependency management, enhance server configuration, and streamline the development process. * Enhance unit test workflow by adding options for syncing all groups and extras - Updated the 'Run UV Sync' command in the unit_tests.yml workflow to include '--all-groups' and '--all-extras' flags, improving the synchronization process for unit tests. This change ensures a more comprehensive testing environment by including all relevant components during the sync operation. * Update GitHub Actions workflow to refine Docker build process - Changed the job name in the test_publisher.yml workflow from "Build and Publish for ARM and AMD" to "Build and Publish for AMD", reflecting a focus on AMD architecture only. This update streamlines the workflow and clarifies the build target. * Enhance unit test workflow by updating UV Sync command - Modified the 'Run UV Sync' command in the unit_tests.yml workflow to include '--all-groups' and '--all-extras' flags. This change improves the synchronization process for unit tests, ensuring a more comprehensive testing environment by including all relevant components during the sync operation. * fix * Update Docker build workflow to target AMD architecture only - Modified the GitHub Actions workflow in test_publisher.yml to focus exclusively on the linux/amd64 platform by removing support for linux/arm64. This change streamlines the build process and clarifies the intended architecture for the Docker image. * Refactor Docker build workflow to include parent directory context - Updated the GitHub Actions workflow in test_publisher.yml to change the working directory to the parent directory before executing the Docker build commands for both AMD and ARM64 platforms. This adjustment ensures that the build context is correctly set, allowing for successful image creation and pushing to the specified tags. * a * fix * Fix * Update Docker build workflow to use 'server_test' tag for both AMD and ARM64 platforms - Modified the GitHub Actions workflow in test_publisher.yml to change the Docker image tags from 'upsonic/server:$VERSION' and 'upsonic/server:$VERSION-ARM64' to 'upsonic/server_test:$VERSION' and 'upsonic/server_test:$VERSION-ARM64', respectively. This change clarifies the purpose of the images being built and ensures consistency in tagging for testing purposes. * Refactor UpsonicServer to UpsonicClient and update imports - Changed the class name from `UpsonicServer` to `UpsonicClient` in `base.py` to better reflect its functionality. - Updated the import statements in `__init__.py` and `test_call.py` to use `UpsonicClient`. - Removed unnecessary print statements in `call.py` to clean up the code. - Added `BaseModel` import to `__init__.py` for better data validation support. * Add TimeoutException and handle request timeout in UpsonicClient * Add LLM model support and exception handling in Upsonic client and server - Introduced NoAPIKeyException and UnsupportedLLMModelException in the client to handle specific error cases. - Updated the Call class to accept an llm_model parameter and pass it in the request data. - Enhanced the server's CallManager to support multiple LLM models (OpenAI and Anthropic) with appropriate API key checks. - Modified the GPT4ORequest model and run_sync_gpt4o function to include llm_model as an optional parameter for better flexibility in requests. * Enhance Upsonic client with task management and response handling - Introduced a new Task class to encapsulate task descriptions and response formats. - Added various response types (ObjectResponse, StrResponse, IntResponse, FloatResponse, BoolResponse, StrInListResponse) for better response handling. - Updated the Call class to accept a Task object instead of a prompt string, improving clarity and structure. - Refactored tests to utilize the new Task class for making calls, ensuring consistency in response handling. * Fix * Update .gitignore and enhance Upsonic client with Tools integration - Added test3.py to .gitignore to exclude it from version control. - Integrated Tools class into UpsonicClient for improved functionality. - Updated import statements in various modules to include Tools, enhancing the overall structure and capabilities of the Upsonic client. * Fix * Fix * Enhance Upsonic client and server with tool registration functionality - Added a decorator to the Tools class for registering functions as tools, allowing for dynamic tool management. - Introduced new API endpoints for adding tools and installing libraries in the server. - Updated the ToolManager to support adding tools via HTTP requests. - Refactored existing functions to improve clarity and maintainability, including changes to library installation commands. - Enhanced response handling in the Task class to return None when no response is available. * fix * Fix * Fix * fix * Fix * Fix * Fix * fix * fix * Fix * Fix * fix * fix * fix * fix * Fix * Fix * Fix * Fix * Fİx * Fix * Fix * Fix * Fix * Fix * Fix * Add context feature * Update call method to accept a list of tasks in addition to a single task * Fix * Fİx * Fix * Fix * Fix * Fix * Fix * Fix * fix * Fix * Fix * Added aws bedrock support * Added computer use tools * Fix * Fix * Fix * Fix * fix * Fix * fix * fix * Fix * Fİx * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * fix * Fix * Fix * Fİx * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Update README.md * Update README.md * Update README.md * Fix * fix * Update README.md * Update dependency versions in pyproject.toml and uv.lock; specify exact version for psutil and add uv package * Fix * Update README.md * fixes * Update README.md * Update README.md * Update README.md | 1 年前 | |
fix: fix smoke tests by using new printing usage | 4 个月前 | |
standardize layout under src/upsonic/prebuilt (#580) * refactor(prebuilt): standardize layout under src/upsonic/prebuilt Move the base class into the prebuilt package and rename it to PrebuiltAutonomousAgentBase. Replace the monolithic upsonic_prebuilt_agents.py with per-agent folders (agent.py + template/), starting with applied_scientist. * docs: add new_prebuilt_agent_adding guide under documents/ Update references in CLAUDE.md and src/upsonic/prebuilt/__init__.py to point at the new location. | 2 个月前 | |
docs: add SECURITY.md with responsible disclosure policy | 24 天前 | |
chore(master): release 0.77.3 | 1 个月前 | |
fix: fix pytest.ini | 4 个月前 | |
fix(ci): refresh uv.lock at release and update release-please config | 1 个月前 | |
chore: refresh uv.lock for release PR [skip ci] | 1 个月前 |
概述
Upsonic 是一个 Python 框架,用于构建诸如 OpenClaw 和 Claude Cowork 之类的自主智能体,以及更传统的智能体系统。
快速开始
安装
uv pip install upsonic
# pip install upsonic
IDE 集成
在您的编码工具中添加 Upsonic 文档作为来源:
Cursor: 设置 → 索引与文档 → 添加 https://docs.upsonic.ai/llms-full.txt
同样适用于 VSCode、Windsurf 及类似工具。
创建自主代理
构建您自己的代理
from upsonic import AutonomousAgent, Task
agent = AutonomousAgent(
model="anthropic/claude-sonnet-4-5",
workspace="/path/to/logs"
)
task = Task("Analyze server logs and detect anomaly patterns")
agent.print_do(task)
所有文件和 shell 操作均限制在 workspace 目录下。路径遍历和危险命令已被屏蔽。
使用我们的预构建代理
预构建自主代理是由 Upsonic 社区构建的即运行代理,每个代理都打包了一项技能、系统提示和初始消息,让您从安装到运行只需几秒钟。该集合欢迎贡献,您可以提交您的代理并发起 PR。
了解更多:预构建自主代理
后续步骤: 连接 沙箱提供商 (E2B) 以获取隔离的云执行环境。
创建传统代理
from upsonic import Agent, Task
agent = Agent(model="anthropic/claude-sonnet-4-5", name="Stock Analyst Agent")
task = Task(description="Analyze the current market trends")
agent.print_do(task)
添加自定义工具
from upsonic import Agent, Task
from upsonic.tools import tool
@tool
def sum_tool(a: float, b: float) -> float:
"""
Add two numbers together.
Args:
a: First number
b: Second number
Returns:
The sum of a and b
"""
return a + b
task = Task(
description="Calculate 15 + 27",
tools=[sum_tool]
)
agent = Agent(model="anthropic/claude-sonnet-4-5", name="Calculator Agent")
result = agent.print_do(task)
后续步骤: 集成 MCP Tools,将你的智能体连接到数千个外部数据源和服务。
OCR 与文档处理
Upsonic 提供统一的 OCR 接口及分层处理流程:第 0 层负责文档准备(PDF 转图像、预处理),第 1 层运行 OCR 引擎。
uv pip install "upsonic[ocr]"
from upsonic.ocr import OCR
from upsonic.ocr.layer_1.engines import EasyOCREngine
engine = EasyOCREngine(languages=["en"])
ocr = OCR(layer_1_ocr_engine=engine)
text = ocr.get_text("invoice.pdf")
print(text)
支持的引擎:EasyOCR、RapidOCR、Tesseract、PaddleOCR、DeepSeek OCR、通过Ollama使用的DeepSeek。
了解更多:OCR 文档
查看我们的视频
|
|
文档和资源
社区与支持
💬 加入我们的 Discord 社区! — 提问、分享您正在构建的项目、获取团队帮助,并与其他使用 Upsonic 的开发者建立联系。
许可证
Upsonic 根据 MIT 许可证发布。详情请参见 LICENCE。
贡献
我们欢迎社区的贡献!在提交拉取请求之前,请阅读我们的 贡献指南 和行为准则。
项目介绍
适用于Windows、macOS及Ubuntu的GPT-4o【此简介由AI生成】