venice-cli 0.83.44__tar.gz → 0.83.45__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {venice_cli-0.83.44 → venice_cli-0.83.45}/PKG-INFO +25 -6
- {venice_cli-0.83.44 → venice_cli-0.83.45}/README.md +24 -5
- venice_cli-0.83.45/src/venice/__init__.py +1 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_agent.py +8 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_compact.py +60 -11
- venice_cli-0.83.45/src/venice/commands/_context_archive.py +283 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_repl.py +124 -13
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_session.py +35 -5
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/chat.py +16 -2
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/code.py +28 -7
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/userconfig.py +8 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_chat.py +1 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_code_command.py +1 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_compact.py +70 -1
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_config.py +20 -4
- venice_cli-0.83.45/tests/test_context_archive.py +88 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_drive_cli.py +15 -3
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_planner.py +1 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_repl.py +83 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_review.py +1 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_sessions.py +46 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_spawn.py +2 -1
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_web_search.py +1 -0
- venice_cli-0.83.44/src/venice/__init__.py +0 -1
- {venice_cli-0.83.44 → venice_cli-0.83.45}/.dockerignore +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/.gitignore +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/CONTRIBUTING.md +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/Dockerfile +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/LICENSE +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/Makefile +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/SECURITY.md +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/bin/venice +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/contracts/openapi/implementation.json +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/contracts/openapi/venice.lock.json +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/deploy/kubernetes/remote-mcp.yaml +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/install.sh +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/pyproject.toml +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/scripts/openapi-requirements.txt +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/scripts/openapi_contract.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/scripts/verify_release.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/__main__.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/_egress.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/_numeric.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/audio_player.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/audio_post.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/auth.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/billing.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/cli.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/client.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/__init__.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_audio.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_browser.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_browser_proxy.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_code.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_exec.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_index.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_mailbox.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_mcp.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_mcp_client.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_memory.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_models.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_openai.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_persona.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_queue.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_review.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_shared.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_steer.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/_video_jobs.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/balance.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/bg_remove.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/cache_probe.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/completion.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/config.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/contact_sheet.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/embed.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/image.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/image_edit.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/index.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/login.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/master.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/mcp_serve.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/memory.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/models.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/music.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/review.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/search.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/secret.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/sessions.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/sfx.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/tts.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/upscale.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/commands/video.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/config.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/image_montage.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/mcp_server.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/src/venice/remote_media.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/__init__.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/_drive.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/_hygiene.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/_mcp_fake_server.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/_venice_fake_server.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_agent.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_auth.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_balance.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_bg_remove.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_browser.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_browser_integration.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_cache_probe.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_cli.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_client.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_code.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_completion.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_contact_sheet.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_egress.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_embed.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_exec.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_fake_server.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_image.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_image_edit.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_index.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_install_scripts.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_login.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_mailbox.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_master.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_mcp_client.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_mcp_http.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_mcp_serve.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_mcp_tools.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_memory.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_models.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_music.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_openapi_contract.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_persona.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_profiles.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_release_gate.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_remote_media.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_search.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_secret.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_sfx.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_shared.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_shared_openai.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_source_hygiene.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_steer.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_tts.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_upscale.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_video.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/tests/test_video_jobs.py +0 -0
- {venice_cli-0.83.44 → venice_cli-0.83.45}/uninstall.sh +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: venice-cli
|
|
3
|
-
Version: 0.83.
|
|
3
|
+
Version: 0.83.45
|
|
4
4
|
Summary: A stdlib-only Python CLI for the Venice.ai API: audio, video, images, chat, and embeddings.
|
|
5
5
|
Project-URL: Homepage, https://github.com/gobha-me/venice-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/gobha-me/venice-cli
|
|
@@ -679,6 +679,8 @@ catalog), `/models` (list the available models, marking the current and the
|
|
|
679
679
|
default), `/auto` and `/manual` (toggle auto-accepting paid/side-effecting tool
|
|
680
680
|
calls for following turns), `/compact [N]` (summarize older history into one
|
|
681
681
|
message, keeping the last `N` turns verbatim),
|
|
682
|
+
`/context list [CURSOR]` (list archived evidence metadata) and
|
|
683
|
+
`/context read ID [OFFSET]` (read up to 32 KiB of an exact archived message),
|
|
682
684
|
`/cost` (this session's estimated spend so far, with the active model's
|
|
683
685
|
current-run cache hit rate on the same line; `--session-max-spend` adds a
|
|
684
686
|
cap), `/usage` (a token + cost breakdown for the session, keeping the
|
|
@@ -717,9 +719,19 @@ older prefix into one synthetic message once the prompt crosses
|
|
|
717
719
|
the last `--compact-keep-turns` turns (default 10) verbatim. The trigger uses
|
|
718
720
|
the server-reported `usage.prompt_tokens` when a turn provides it (falling
|
|
719
721
|
back to a chars-per-token estimate otherwise), so it fires on the real prompt
|
|
720
|
-
size rather than a guess.
|
|
721
|
-
|
|
722
|
-
|
|
722
|
+
size rather than a guess. `--compact-loss-policy aggressive|evidence` controls
|
|
723
|
+
what happens to the summarized prefix. Chat defaults to `aggressive`, preserving
|
|
724
|
+
the original behavior. Code defaults to `evidence`: every removed user message,
|
|
725
|
+
assistant tool call (including arguments), and tool result is copied exactly into
|
|
726
|
+
the private session envelope before the live history is replaced. The model gets
|
|
727
|
+
a bounded index plus the read-only `venice_context_archive` tool; operators can use
|
|
728
|
+
the `/context` commands above even in a plain REPL. The archive is capped at 512
|
|
729
|
+
entries and 8 MiB. If the next compaction would cross either cap, it is refused
|
|
730
|
+
before the summary API call and both history and archive remain unchanged. Reads
|
|
731
|
+
are paged at 32 KiB and list pages at 50 entries. Compaction is otherwise
|
|
732
|
+
best-effort — a failed or empty summarization leaves both stores untouched — and
|
|
733
|
+
never orphans a tool result from its assistant turn. It's off by default because
|
|
734
|
+
it costs a summarization call.
|
|
723
735
|
|
|
724
736
|
#### Sessions
|
|
725
737
|
|
|
@@ -728,7 +740,11 @@ each turn to `~/.config/venice/sessions/<id>.json` (mode 0600;
|
|
|
728
740
|
`$VENICE_SESSIONS_DIR` overrides the location). Unlike a bare `/save` transcript,
|
|
729
741
|
a session travels with its **settings** — model, system prompt, generation
|
|
730
742
|
parameters, `max-tool-calls`, the `venice code` sandbox root, and the running
|
|
731
|
-
token/cost usage —
|
|
743
|
+
token/cost usage — plus any bounded exact context archive — so resuming restores
|
|
744
|
+
the whole context, not just the messages. Session envelope v2 adds
|
|
745
|
+
`context_archive` plus the structural count of authoritative leading system
|
|
746
|
+
messages; v1 envelopes remain readable, conservatively preserve their complete
|
|
747
|
+
leading system prefix, and resume with an empty archive.
|
|
732
748
|
The API key is never written to a session.
|
|
733
749
|
|
|
734
750
|
`venice code` also assigns each session an opaque `prompt_cache_key`, sent as an
|
|
@@ -1598,6 +1614,7 @@ jq '.usage.billed_total' ~/.config/venice/sessions/<id>.json
|
|
|
1598
1614
|
| `venice_scout` | delegate a read-only investigation to a disposable subagent with a fresh context; returns a structured report so exploration doesn't pollute the main context — **opt-in with `--scout`** (see [Scout subagent](#scout-subagent---scout)) | no |
|
|
1599
1615
|
| `venice_web_search` | search the web to **discover** documentation you don't have a URL for; returns a short answer + cited URLs (billed, but bounded by the tool-call budget) — **opt-in with `--web-search`** (see [Web search](#web-search---web-search)) | no |
|
|
1600
1616
|
| `venice_review` | hand the current diff to a **cold-context reviewer** — a disposable subagent with a fresh context, read-only tools and (where the catalog allows) a different model — and get back defects with `file:line` + a repro. Findings only: it approves and certifies nothing — **opt-in with `--review`** (see [Reviewer rail](#reviewer-rail---review)) | no |
|
|
1617
|
+
| `venice_context_archive` | list bounded metadata or page through exact messages removed by evidence-preserving compaction; current session only, with no filesystem, network, or process access | no |
|
|
1601
1618
|
|
|
1602
1619
|
**Safety.** Every filesystem path is resolved and confined to the **writable roots** —
|
|
1603
1620
|
the startup root (default: cwd, or `--root` / `$VENICE_CODE_ROOT`) plus any added with
|
|
@@ -1659,6 +1676,7 @@ external diff helpers. Use the confirmed `run` tool for other Git forms.
|
|
|
1659
1676
|
| `--review-model MODEL` | model for `--review` (default: a function-calling model from a **different family** than the coding model; falls back to the coding model with a warning); config `defaults.code.review_model` |
|
|
1660
1677
|
| `--review-rounds N` | passes `venice_review` makes over the same diff (default **1**, max 3); config `defaults.code.review_rounds` |
|
|
1661
1678
|
| `--auto-compact` | summarize older history once the prompt crosses `--compact-threshold` tokens (default 100 000), keeping the last `--compact-keep-turns` turns (default 10); long runs stay in-context |
|
|
1679
|
+
| `--compact-loss-policy aggressive\|evidence` | discard summarized messages or retain their exact JSON in the bounded private session archive; default **evidence** for code and **aggressive** for chat; config `defaults.code.compact_loss_policy` / `defaults.chat.compact_loss_policy` |
|
|
1662
1680
|
| `--cache-guard off\|warn\|stop` | react when a cache-priced model explicitly reports zero cached tokens after the cold first API call and a 2,000-token prompt; default **warn**, config `defaults.code.cache_guard` |
|
|
1663
1681
|
| `-i`, `--json`, `--model`, `--system` | interactive REPL · JSON envelope · model · extra system instructions |
|
|
1664
1682
|
| `--persona NAME` | load `~/.config/venice/personas/NAME.md` as the system prompt at launch (`/persona` in the REPL) |
|
|
@@ -1677,7 +1695,8 @@ model and a sane `--max-tool-calls` when running unattended.
|
|
|
1677
1695
|
|
|
1678
1696
|
Per-flag config defaults live under `defaults.code.*` (e.g. `model`, `root`, `auto`,
|
|
1679
1697
|
`assets`, `scout`, `spawn`, `spawn_max_spend`, `subagent_max_tokens`, `planner`,
|
|
1680
|
-
`max_tool_calls`, `review`, `review_model`, `review_rounds`, `cache_guard
|
|
1698
|
+
`max_tool_calls`, `review`, `review_model`, `review_rounds`, `cache_guard`,
|
|
1699
|
+
`compact_loss_policy`).
|
|
1681
1700
|
|
|
1682
1701
|
#### Scout subagent (`--scout`)
|
|
1683
1702
|
|
|
@@ -638,6 +638,8 @@ catalog), `/models` (list the available models, marking the current and the
|
|
|
638
638
|
default), `/auto` and `/manual` (toggle auto-accepting paid/side-effecting tool
|
|
639
639
|
calls for following turns), `/compact [N]` (summarize older history into one
|
|
640
640
|
message, keeping the last `N` turns verbatim),
|
|
641
|
+
`/context list [CURSOR]` (list archived evidence metadata) and
|
|
642
|
+
`/context read ID [OFFSET]` (read up to 32 KiB of an exact archived message),
|
|
641
643
|
`/cost` (this session's estimated spend so far, with the active model's
|
|
642
644
|
current-run cache hit rate on the same line; `--session-max-spend` adds a
|
|
643
645
|
cap), `/usage` (a token + cost breakdown for the session, keeping the
|
|
@@ -676,9 +678,19 @@ older prefix into one synthetic message once the prompt crosses
|
|
|
676
678
|
the last `--compact-keep-turns` turns (default 10) verbatim. The trigger uses
|
|
677
679
|
the server-reported `usage.prompt_tokens` when a turn provides it (falling
|
|
678
680
|
back to a chars-per-token estimate otherwise), so it fires on the real prompt
|
|
679
|
-
size rather than a guess.
|
|
680
|
-
|
|
681
|
-
|
|
681
|
+
size rather than a guess. `--compact-loss-policy aggressive|evidence` controls
|
|
682
|
+
what happens to the summarized prefix. Chat defaults to `aggressive`, preserving
|
|
683
|
+
the original behavior. Code defaults to `evidence`: every removed user message,
|
|
684
|
+
assistant tool call (including arguments), and tool result is copied exactly into
|
|
685
|
+
the private session envelope before the live history is replaced. The model gets
|
|
686
|
+
a bounded index plus the read-only `venice_context_archive` tool; operators can use
|
|
687
|
+
the `/context` commands above even in a plain REPL. The archive is capped at 512
|
|
688
|
+
entries and 8 MiB. If the next compaction would cross either cap, it is refused
|
|
689
|
+
before the summary API call and both history and archive remain unchanged. Reads
|
|
690
|
+
are paged at 32 KiB and list pages at 50 entries. Compaction is otherwise
|
|
691
|
+
best-effort — a failed or empty summarization leaves both stores untouched — and
|
|
692
|
+
never orphans a tool result from its assistant turn. It's off by default because
|
|
693
|
+
it costs a summarization call.
|
|
682
694
|
|
|
683
695
|
#### Sessions
|
|
684
696
|
|
|
@@ -687,7 +699,11 @@ each turn to `~/.config/venice/sessions/<id>.json` (mode 0600;
|
|
|
687
699
|
`$VENICE_SESSIONS_DIR` overrides the location). Unlike a bare `/save` transcript,
|
|
688
700
|
a session travels with its **settings** — model, system prompt, generation
|
|
689
701
|
parameters, `max-tool-calls`, the `venice code` sandbox root, and the running
|
|
690
|
-
token/cost usage —
|
|
702
|
+
token/cost usage — plus any bounded exact context archive — so resuming restores
|
|
703
|
+
the whole context, not just the messages. Session envelope v2 adds
|
|
704
|
+
`context_archive` plus the structural count of authoritative leading system
|
|
705
|
+
messages; v1 envelopes remain readable, conservatively preserve their complete
|
|
706
|
+
leading system prefix, and resume with an empty archive.
|
|
691
707
|
The API key is never written to a session.
|
|
692
708
|
|
|
693
709
|
`venice code` also assigns each session an opaque `prompt_cache_key`, sent as an
|
|
@@ -1557,6 +1573,7 @@ jq '.usage.billed_total' ~/.config/venice/sessions/<id>.json
|
|
|
1557
1573
|
| `venice_scout` | delegate a read-only investigation to a disposable subagent with a fresh context; returns a structured report so exploration doesn't pollute the main context — **opt-in with `--scout`** (see [Scout subagent](#scout-subagent---scout)) | no |
|
|
1558
1574
|
| `venice_web_search` | search the web to **discover** documentation you don't have a URL for; returns a short answer + cited URLs (billed, but bounded by the tool-call budget) — **opt-in with `--web-search`** (see [Web search](#web-search---web-search)) | no |
|
|
1559
1575
|
| `venice_review` | hand the current diff to a **cold-context reviewer** — a disposable subagent with a fresh context, read-only tools and (where the catalog allows) a different model — and get back defects with `file:line` + a repro. Findings only: it approves and certifies nothing — **opt-in with `--review`** (see [Reviewer rail](#reviewer-rail---review)) | no |
|
|
1576
|
+
| `venice_context_archive` | list bounded metadata or page through exact messages removed by evidence-preserving compaction; current session only, with no filesystem, network, or process access | no |
|
|
1560
1577
|
|
|
1561
1578
|
**Safety.** Every filesystem path is resolved and confined to the **writable roots** —
|
|
1562
1579
|
the startup root (default: cwd, or `--root` / `$VENICE_CODE_ROOT`) plus any added with
|
|
@@ -1618,6 +1635,7 @@ external diff helpers. Use the confirmed `run` tool for other Git forms.
|
|
|
1618
1635
|
| `--review-model MODEL` | model for `--review` (default: a function-calling model from a **different family** than the coding model; falls back to the coding model with a warning); config `defaults.code.review_model` |
|
|
1619
1636
|
| `--review-rounds N` | passes `venice_review` makes over the same diff (default **1**, max 3); config `defaults.code.review_rounds` |
|
|
1620
1637
|
| `--auto-compact` | summarize older history once the prompt crosses `--compact-threshold` tokens (default 100 000), keeping the last `--compact-keep-turns` turns (default 10); long runs stay in-context |
|
|
1638
|
+
| `--compact-loss-policy aggressive\|evidence` | discard summarized messages or retain their exact JSON in the bounded private session archive; default **evidence** for code and **aggressive** for chat; config `defaults.code.compact_loss_policy` / `defaults.chat.compact_loss_policy` |
|
|
1621
1639
|
| `--cache-guard off\|warn\|stop` | react when a cache-priced model explicitly reports zero cached tokens after the cold first API call and a 2,000-token prompt; default **warn**, config `defaults.code.cache_guard` |
|
|
1622
1640
|
| `-i`, `--json`, `--model`, `--system` | interactive REPL · JSON envelope · model · extra system instructions |
|
|
1623
1641
|
| `--persona NAME` | load `~/.config/venice/personas/NAME.md` as the system prompt at launch (`/persona` in the REPL) |
|
|
@@ -1636,7 +1654,8 @@ model and a sane `--max-tool-calls` when running unattended.
|
|
|
1636
1654
|
|
|
1637
1655
|
Per-flag config defaults live under `defaults.code.*` (e.g. `model`, `root`, `auto`,
|
|
1638
1656
|
`assets`, `scout`, `spawn`, `spawn_max_spend`, `subagent_max_tokens`, `planner`,
|
|
1639
|
-
`max_tool_calls`, `review`, `review_model`, `review_rounds`, `cache_guard
|
|
1657
|
+
`max_tool_calls`, `review`, `review_model`, `review_rounds`, `cache_guard`,
|
|
1658
|
+
`compact_loss_policy`).
|
|
1640
1659
|
|
|
1641
1660
|
#### Scout subagent (`--scout`)
|
|
1642
1661
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.83.45"
|
|
@@ -3779,6 +3779,8 @@ def run_loop(
|
|
|
3779
3779
|
oai_tools = to_openai_tools(tools)
|
|
3780
3780
|
dispatch = dispatch_map(tools)
|
|
3781
3781
|
runtime = _ToolRuntime(model=model, models=models)
|
|
3782
|
+
if budget is not None and budget.protected_system_messages is None:
|
|
3783
|
+
budget.protected_system_messages = _compact.leading_system_count(messages)
|
|
3782
3784
|
calls_made = 0
|
|
3783
3785
|
gate = {"auto": bool(yes)} # mutable so an `a`/`all` confirm flips the run to auto
|
|
3784
3786
|
# #82: the per-tool timing sink, bound ONCE so both dispatch paths -- and every pool
|
|
@@ -3860,6 +3862,9 @@ def run_loop(
|
|
|
3860
3862
|
on_compact=lambda b, a: _progress(
|
|
3861
3863
|
f"(auto-compacted history: {b} -> {a} messages)", enabled=show,
|
|
3862
3864
|
),
|
|
3865
|
+
on_blocked=lambda reason: print(
|
|
3866
|
+
f"auto-compaction refused: {reason}", file=sys.stderr,
|
|
3867
|
+
),
|
|
3863
3868
|
ledger=ledger, # #99: log the event; #101: and bill the summary call
|
|
3864
3869
|
)
|
|
3865
3870
|
_t0 = time.monotonic()
|
|
@@ -3960,6 +3965,9 @@ def run_loop(
|
|
|
3960
3965
|
on_compact=lambda b, a: _progress(
|
|
3961
3966
|
f"(auto-compacted history: {b} -> {a} messages)", enabled=show,
|
|
3962
3967
|
),
|
|
3968
|
+
on_blocked=lambda reason: print(
|
|
3969
|
+
f"auto-compaction refused: {reason}", file=sys.stderr,
|
|
3970
|
+
),
|
|
3963
3971
|
ledger=ledger, # #99
|
|
3964
3972
|
)
|
|
3965
3973
|
return _force_final(
|
|
@@ -50,7 +50,7 @@ import time
|
|
|
50
50
|
from dataclasses import dataclass
|
|
51
51
|
from typing import List, Optional, Tuple
|
|
52
52
|
|
|
53
|
-
from . import _openai
|
|
53
|
+
from . import _context_archive, _openai
|
|
54
54
|
|
|
55
55
|
# Rough chars-per-token for English/code text. Deliberately conservative
|
|
56
56
|
# (overestimate tokens) so the fallback triggers compaction a little early
|
|
@@ -64,6 +64,7 @@ SUMMARY_MAX_TOKENS = 1024
|
|
|
64
64
|
|
|
65
65
|
DEFAULT_THRESHOLD_TOKENS = 100_000
|
|
66
66
|
DEFAULT_KEEP_TURNS = 10
|
|
67
|
+
LOSS_POLICY_CHOICES = ("aggressive", "evidence")
|
|
67
68
|
|
|
68
69
|
_SUMMARY_PREFIX = "[Summary of earlier conversation]"
|
|
69
70
|
_INSTRUCT = (
|
|
@@ -124,6 +125,9 @@ class Budget:
|
|
|
124
125
|
threshold_tokens: int = DEFAULT_THRESHOLD_TOKENS
|
|
125
126
|
keep_turns: int = DEFAULT_KEEP_TURNS
|
|
126
127
|
last_prompt_tokens: Optional[int] = None
|
|
128
|
+
loss_policy: str = "aggressive"
|
|
129
|
+
archive: Optional[_context_archive.ContextArchive] = None
|
|
130
|
+
protected_system_messages: Optional[int] = None
|
|
127
131
|
|
|
128
132
|
def observe(self, usage) -> None:
|
|
129
133
|
"""Record prompt tokens from a response's `usage` (dict or SDK obj)."""
|
|
@@ -150,7 +154,7 @@ class Budget:
|
|
|
150
154
|
return estimate_tokens(messages) >= self.threshold_tokens
|
|
151
155
|
|
|
152
156
|
|
|
153
|
-
def budget_from_args(args) -> Optional["Budget"]:
|
|
157
|
+
def budget_from_args(args, archive=None) -> Optional["Budget"]:
|
|
154
158
|
"""The auto-compact Budget for a parsed-args namespace, or None when it
|
|
155
159
|
isn't opted into (#48).
|
|
156
160
|
|
|
@@ -168,6 +172,8 @@ def budget_from_args(args) -> Optional["Budget"]:
|
|
|
168
172
|
keep_turns=(
|
|
169
173
|
getattr(args, "compact_keep_turns", None) or DEFAULT_KEEP_TURNS
|
|
170
174
|
),
|
|
175
|
+
loss_policy=getattr(args, "compact_loss_policy", None) or "aggressive",
|
|
176
|
+
archive=archive,
|
|
171
177
|
)
|
|
172
178
|
|
|
173
179
|
|
|
@@ -212,8 +218,16 @@ def _groups(messages: List[dict]) -> List[List[dict]]:
|
|
|
212
218
|
return groups
|
|
213
219
|
|
|
214
220
|
|
|
221
|
+
def leading_system_count(messages: List[dict]) -> int:
|
|
222
|
+
"""Count the contiguous authoritative/generated system prefix."""
|
|
223
|
+
count = 0
|
|
224
|
+
while count < len(messages) and messages[count].get("role") == "system":
|
|
225
|
+
count += 1
|
|
226
|
+
return count
|
|
227
|
+
|
|
228
|
+
|
|
215
229
|
def split_for_compaction(
|
|
216
|
-
messages: List[dict], keep_turns: int
|
|
230
|
+
messages: List[dict], keep_turns: int, protected_system_messages: Optional[int] = None
|
|
217
231
|
) -> Optional[Tuple[List[dict], List[dict]]]:
|
|
218
232
|
"""Split history into (prefix to summarize, tail to keep verbatim).
|
|
219
233
|
|
|
@@ -224,14 +238,21 @@ def split_for_compaction(
|
|
|
224
238
|
"""
|
|
225
239
|
if keep_turns < 1:
|
|
226
240
|
keep_turns = 1
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
241
|
+
leading_systems = leading_system_count(messages)
|
|
242
|
+
# None is the conservative compatibility path: every leading system
|
|
243
|
+
# message is authoritative. Session-aware callers persist the exact count,
|
|
244
|
+
# which lets later generated summary/index messages be replaced without
|
|
245
|
+
# classifying authority from forgeable content text.
|
|
246
|
+
if protected_system_messages is None:
|
|
247
|
+
sys_end = leading_systems
|
|
248
|
+
else:
|
|
249
|
+
sys_end = min(max(0, protected_system_messages), leading_systems)
|
|
250
|
+
generated_end = leading_systems
|
|
251
|
+
tail_groups = _groups(messages[generated_end:])
|
|
231
252
|
if len(tail_groups) <= keep_turns:
|
|
232
253
|
return None
|
|
233
254
|
cut = len(tail_groups) - keep_turns
|
|
234
|
-
prefix: List[dict] = []
|
|
255
|
+
prefix: List[dict] = list(messages[sys_end:generated_end])
|
|
235
256
|
for g in tail_groups[:cut]:
|
|
236
257
|
prefix.extend(g)
|
|
237
258
|
tail: List[dict] = []
|
|
@@ -283,6 +304,9 @@ def compact_messages(
|
|
|
283
304
|
ledger=None,
|
|
284
305
|
budget: Optional[Budget] = None,
|
|
285
306
|
trigger: str = "auto",
|
|
307
|
+
loss_policy: str = "aggressive",
|
|
308
|
+
archive: Optional[_context_archive.ContextArchive] = None,
|
|
309
|
+
protected_system_messages: Optional[int] = None,
|
|
286
310
|
) -> bool:
|
|
287
311
|
"""Summarize the older prefix in place; keep system + last `keep_turns`.
|
|
288
312
|
|
|
@@ -311,12 +335,28 @@ def compact_messages(
|
|
|
311
335
|
history it now describes -- i.e. re-firing compaction immediately, a sawtooth #99's
|
|
312
336
|
trace would show and nothing would prevent.
|
|
313
337
|
"""
|
|
314
|
-
split = split_for_compaction(
|
|
338
|
+
split = split_for_compaction(
|
|
339
|
+
messages, keep_turns, protected_system_messages=protected_system_messages
|
|
340
|
+
)
|
|
315
341
|
if split is None:
|
|
316
342
|
return False
|
|
317
343
|
prefix, tail = split
|
|
318
344
|
sys_msgs = messages[: len(messages) - len(prefix) - len(tail)]
|
|
319
345
|
|
|
346
|
+
# Evidence mode refuses before spending a summarization call if the exact
|
|
347
|
+
# removed messages cannot fit. Staging is side-effect-free; commit happens
|
|
348
|
+
# only after a usable summary exists, alongside the live-history rewrite.
|
|
349
|
+
staged = []
|
|
350
|
+
if loss_policy == "evidence":
|
|
351
|
+
if archive is None:
|
|
352
|
+
return False
|
|
353
|
+
archive.last_error = None
|
|
354
|
+
try:
|
|
355
|
+
staged = archive.stage(prefix)
|
|
356
|
+
except _context_archive.ArchiveError as e:
|
|
357
|
+
archive.last_error = str(e)
|
|
358
|
+
return False
|
|
359
|
+
|
|
320
360
|
# #128: compaction is a deliberately fresh summarization conversation. Reusing
|
|
321
361
|
# the parent session's routing identity would mix unrelated prefixes on one cache
|
|
322
362
|
# affinity key; preserve all other generation/Venice parameters while stripping it.
|
|
@@ -366,7 +406,11 @@ def compact_messages(
|
|
|
366
406
|
# lie in the artifact that exists to stop one.
|
|
367
407
|
est_before = estimate_tokens(messages)
|
|
368
408
|
msgs_before = len(messages)
|
|
369
|
-
|
|
409
|
+
replacement = sys_msgs + [synthetic_message(summary)]
|
|
410
|
+
if loss_policy == "evidence":
|
|
411
|
+
archive.commit(staged)
|
|
412
|
+
replacement.append(archive.live_index_message())
|
|
413
|
+
messages[:] = replacement + tail
|
|
370
414
|
if ledger is not None:
|
|
371
415
|
ledger.record_compaction({
|
|
372
416
|
"trigger": trigger,
|
|
@@ -411,7 +455,7 @@ def compact_messages(
|
|
|
411
455
|
|
|
412
456
|
def maybe_compact(oai, model: str, messages: List[dict],
|
|
413
457
|
budget: Optional[Budget], base_kwargs: Optional[dict] = None,
|
|
414
|
-
on_compact=None, ledger=None) -> bool:
|
|
458
|
+
on_compact=None, on_blocked=None, ledger=None) -> bool:
|
|
415
459
|
"""Compact `messages` in place when `budget` says they're over budget.
|
|
416
460
|
|
|
417
461
|
The shared gate for every compaction site (`run_loop`'s per-turn check, its
|
|
@@ -433,7 +477,12 @@ def maybe_compact(oai, model: str, messages: List[dict],
|
|
|
433
477
|
oai, model, messages,
|
|
434
478
|
keep_turns=budget.keep_turns, base_kwargs=base_kwargs,
|
|
435
479
|
ledger=ledger, budget=budget, trigger="auto",
|
|
480
|
+
loss_policy=budget.loss_policy, archive=budget.archive,
|
|
481
|
+
protected_system_messages=budget.protected_system_messages,
|
|
436
482
|
):
|
|
483
|
+
if (on_blocked is not None and budget.archive is not None
|
|
484
|
+
and budget.archive.last_error):
|
|
485
|
+
on_blocked(budget.archive.last_error)
|
|
437
486
|
return False
|
|
438
487
|
if on_compact is not None:
|
|
439
488
|
on_compact(before, len(messages))
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Bounded, lossless local archive for evidence-preserving compaction (#74)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import copy
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
MAX_ARCHIVE_BYTES = 8 * 1024 * 1024
|
|
12
|
+
MAX_ARCHIVE_ENTRIES = 512
|
|
13
|
+
MAX_LIST_ENTRIES = 50
|
|
14
|
+
MAX_READ_BYTES = 32 * 1024
|
|
15
|
+
LIVE_INDEX_ENTRIES = 32
|
|
16
|
+
EXCERPT_CHARS = 160
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ArchiveError(ValueError):
|
|
20
|
+
"""An archive envelope is malformed or a capacity boundary was crossed."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _canonical(message: dict) -> str:
|
|
24
|
+
try:
|
|
25
|
+
return json.dumps(
|
|
26
|
+
message, ensure_ascii=False, sort_keys=True, separators=(",", ":"),
|
|
27
|
+
allow_nan=False,
|
|
28
|
+
)
|
|
29
|
+
except (TypeError, ValueError) as e:
|
|
30
|
+
raise ArchiveError(f"archived message is not finite JSON: {e}") from None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _digest(text: str) -> str:
|
|
34
|
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _excerpt(text: str, *, tail: bool = False) -> str:
|
|
38
|
+
value = text[-EXCERPT_CHARS:] if tail else text[:EXCERPT_CHARS]
|
|
39
|
+
return value.replace("\n", "\\n")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _metadata(entry: dict) -> dict:
|
|
43
|
+
result = {
|
|
44
|
+
"id": entry["id"],
|
|
45
|
+
"role": entry["role"],
|
|
46
|
+
"chars": entry["chars"],
|
|
47
|
+
"sha256": entry["sha256"],
|
|
48
|
+
"head": entry["head"],
|
|
49
|
+
"tail": entry["tail"],
|
|
50
|
+
}
|
|
51
|
+
if entry.get("name"):
|
|
52
|
+
result["name"] = entry["name"]
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _bounded_utf8_slice(text: str, offset: int) -> str:
|
|
57
|
+
"""Return the longest char-aligned slice no larger than MAX_READ_BYTES."""
|
|
58
|
+
lo, hi = offset, len(text)
|
|
59
|
+
while lo < hi:
|
|
60
|
+
mid = (lo + hi + 1) // 2
|
|
61
|
+
if len(text[offset:mid].encode("utf-8")) <= MAX_READ_BYTES:
|
|
62
|
+
lo = mid
|
|
63
|
+
else:
|
|
64
|
+
hi = mid - 1
|
|
65
|
+
return text[offset:lo]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class ContextArchive:
|
|
70
|
+
"""A bounded collection of exact, canonicalized archived messages."""
|
|
71
|
+
|
|
72
|
+
entries: List[dict] = field(default_factory=list)
|
|
73
|
+
last_error: Optional[str] = None
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_envelope(cls, raw) -> "ContextArchive":
|
|
77
|
+
if raw is None:
|
|
78
|
+
return cls()
|
|
79
|
+
if not isinstance(raw, list):
|
|
80
|
+
raise ArchiveError("context_archive must be a JSON list")
|
|
81
|
+
if len(raw) > MAX_ARCHIVE_ENTRIES:
|
|
82
|
+
raise ArchiveError(
|
|
83
|
+
f"context_archive exceeds {MAX_ARCHIVE_ENTRIES} entries"
|
|
84
|
+
)
|
|
85
|
+
validated = []
|
|
86
|
+
seen = set()
|
|
87
|
+
for index, item in enumerate(raw, 1):
|
|
88
|
+
if not isinstance(item, dict) or not isinstance(item.get("message"), dict):
|
|
89
|
+
raise ArchiveError(f"context_archive entry {index} is malformed")
|
|
90
|
+
message = item["message"]
|
|
91
|
+
if not isinstance(message.get("role"), str):
|
|
92
|
+
raise ArchiveError(f"context_archive entry {index} has no role")
|
|
93
|
+
text = _canonical(message)
|
|
94
|
+
entry_id = item.get("id")
|
|
95
|
+
expected_id = f"ctx-{index:06d}"
|
|
96
|
+
if entry_id != expected_id:
|
|
97
|
+
raise ArchiveError(f"context_archive entry {index} has an invalid id")
|
|
98
|
+
if entry_id in seen:
|
|
99
|
+
raise ArchiveError(f"context_archive entry {index} repeats id {entry_id}")
|
|
100
|
+
seen.add(entry_id)
|
|
101
|
+
expected = cls._entry(entry_id, message, text)
|
|
102
|
+
for key in ("sha256", "chars", "role", "head", "tail"):
|
|
103
|
+
if item.get(key) != expected[key]:
|
|
104
|
+
raise ArchiveError(
|
|
105
|
+
f"context_archive entry {entry_id} has invalid {key}"
|
|
106
|
+
)
|
|
107
|
+
if item.get("name") != expected.get("name"):
|
|
108
|
+
raise ArchiveError(
|
|
109
|
+
f"context_archive entry {entry_id} has invalid name"
|
|
110
|
+
)
|
|
111
|
+
validated.append(expected)
|
|
112
|
+
total = len(_canonical(validated).encode("utf-8"))
|
|
113
|
+
if total > MAX_ARCHIVE_BYTES:
|
|
114
|
+
raise ArchiveError(f"context_archive exceeds {MAX_ARCHIVE_BYTES} bytes")
|
|
115
|
+
return cls(validated)
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _entry(entry_id: str, message: dict, text: Optional[str] = None) -> dict:
|
|
119
|
+
text = _canonical(message) if text is None else text
|
|
120
|
+
entry = {
|
|
121
|
+
"id": entry_id,
|
|
122
|
+
"role": message["role"],
|
|
123
|
+
"chars": len(text),
|
|
124
|
+
"sha256": _digest(text),
|
|
125
|
+
"head": _excerpt(text),
|
|
126
|
+
"tail": _excerpt(text, tail=True),
|
|
127
|
+
"message": copy.deepcopy(message),
|
|
128
|
+
}
|
|
129
|
+
name = message.get("name")
|
|
130
|
+
if isinstance(name, str) and name:
|
|
131
|
+
entry["name"] = name
|
|
132
|
+
elif message.get("role") == "assistant":
|
|
133
|
+
calls = message.get("tool_calls")
|
|
134
|
+
if isinstance(calls, list):
|
|
135
|
+
names = []
|
|
136
|
+
for call in calls:
|
|
137
|
+
fn = call.get("function") if isinstance(call, dict) else None
|
|
138
|
+
if isinstance(fn, dict) and isinstance(fn.get("name"), str):
|
|
139
|
+
names.append(fn["name"])
|
|
140
|
+
if names:
|
|
141
|
+
entry["name"] = ",".join(names)
|
|
142
|
+
return entry
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def bytes_used(self) -> int:
|
|
146
|
+
return len(_canonical(self.entries).encode("utf-8"))
|
|
147
|
+
|
|
148
|
+
def stage(self, messages: List[dict]) -> List[dict]:
|
|
149
|
+
"""Build entries and enforce both caps without changing the archive."""
|
|
150
|
+
staged = []
|
|
151
|
+
for message in messages:
|
|
152
|
+
if not isinstance(message, dict) or not isinstance(message.get("role"), str):
|
|
153
|
+
raise ArchiveError("message selected for archival is malformed")
|
|
154
|
+
text = _canonical(message)
|
|
155
|
+
entry_id = f"ctx-{len(self.entries) + len(staged) + 1:06d}"
|
|
156
|
+
staged.append(self._entry(entry_id, message, text))
|
|
157
|
+
count = len(self.entries) + len(staged)
|
|
158
|
+
if count > MAX_ARCHIVE_ENTRIES:
|
|
159
|
+
raise ArchiveError(
|
|
160
|
+
f"evidence archive full: compaction needs {count} entries "
|
|
161
|
+
f"(limit {MAX_ARCHIVE_ENTRIES})"
|
|
162
|
+
)
|
|
163
|
+
used = len(_canonical(self.entries + staged).encode("utf-8"))
|
|
164
|
+
if used > MAX_ARCHIVE_BYTES:
|
|
165
|
+
raise ArchiveError(
|
|
166
|
+
f"evidence archive full: compaction needs {used} bytes "
|
|
167
|
+
f"(limit {MAX_ARCHIVE_BYTES})"
|
|
168
|
+
)
|
|
169
|
+
return staged
|
|
170
|
+
|
|
171
|
+
def commit(self, staged: List[dict]) -> None:
|
|
172
|
+
self.entries.extend(staged)
|
|
173
|
+
self.last_error = None
|
|
174
|
+
|
|
175
|
+
def clear(self) -> None:
|
|
176
|
+
self.entries.clear()
|
|
177
|
+
self.last_error = None
|
|
178
|
+
|
|
179
|
+
def to_envelope(self) -> list:
|
|
180
|
+
return copy.deepcopy(self.entries)
|
|
181
|
+
|
|
182
|
+
def list_page(self, cursor: int = 0, limit: int = MAX_LIST_ENTRIES) -> dict:
|
|
183
|
+
if not isinstance(cursor, int) or isinstance(cursor, bool) or cursor < 0:
|
|
184
|
+
raise ArchiveError("cursor must be a non-negative integer")
|
|
185
|
+
if not isinstance(limit, int) or isinstance(limit, bool) or not 1 <= limit <= MAX_LIST_ENTRIES:
|
|
186
|
+
raise ArchiveError(f"limit must be between 1 and {MAX_LIST_ENTRIES}")
|
|
187
|
+
page = self.entries[cursor:cursor + limit]
|
|
188
|
+
end = cursor + len(page)
|
|
189
|
+
return {
|
|
190
|
+
"entries": [_metadata(e) for e in page],
|
|
191
|
+
"cursor": cursor,
|
|
192
|
+
"next_cursor": end if end < len(self.entries) else None,
|
|
193
|
+
"total": len(self.entries),
|
|
194
|
+
"bytes": self.bytes_used,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
def read(self, entry_id: str, offset: int = 0) -> dict:
|
|
198
|
+
if not isinstance(entry_id, str):
|
|
199
|
+
raise ArchiveError("entry_id must be a string")
|
|
200
|
+
if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
|
|
201
|
+
raise ArchiveError("offset must be a non-negative integer")
|
|
202
|
+
entry = next((e for e in self.entries if e["id"] == entry_id), None)
|
|
203
|
+
if entry is None:
|
|
204
|
+
raise ArchiveError(f"unknown archive entry {entry_id!r}")
|
|
205
|
+
text = _canonical(entry["message"])
|
|
206
|
+
if offset > len(text):
|
|
207
|
+
raise ArchiveError(f"offset {offset} exceeds entry length {len(text)}")
|
|
208
|
+
content = _bounded_utf8_slice(text, offset)
|
|
209
|
+
end = offset + len(content)
|
|
210
|
+
return {
|
|
211
|
+
"id": entry_id,
|
|
212
|
+
"sha256": entry["sha256"],
|
|
213
|
+
"offset": offset,
|
|
214
|
+
"content": content,
|
|
215
|
+
"next_offset": end if end < len(text) else None,
|
|
216
|
+
"complete": end >= len(text),
|
|
217
|
+
"chars": len(text),
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
def live_index_message(self) -> dict:
|
|
221
|
+
newest = self.entries[-LIVE_INDEX_ENTRIES:]
|
|
222
|
+
lines = [
|
|
223
|
+
"[Archived context evidence index]",
|
|
224
|
+
f"{len(self.entries)} exact message(s), {self.bytes_used} byte(s). "
|
|
225
|
+
"Use venice_context_archive to list metadata or read exact content.",
|
|
226
|
+
]
|
|
227
|
+
older = len(self.entries) - len(newest)
|
|
228
|
+
if older:
|
|
229
|
+
lines.append(f"{older} older entry/entries are discoverable with list pagination.")
|
|
230
|
+
for entry in newest:
|
|
231
|
+
label = entry["role"]
|
|
232
|
+
if entry.get("name"):
|
|
233
|
+
label += f"/{entry['name']}"
|
|
234
|
+
lines.append(
|
|
235
|
+
f"{entry['id']} {label} {entry['chars']} chars "
|
|
236
|
+
f"sha256={entry['sha256']} head={entry['head']!r} tail={entry['tail']!r}"
|
|
237
|
+
)
|
|
238
|
+
return {"role": "system", "content": "\n".join(lines)}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def archive_tool(archive: ContextArchive):
|
|
242
|
+
"""Build the current-session, read-only archive lookup tool."""
|
|
243
|
+
from . import _agent
|
|
244
|
+
|
|
245
|
+
def invoke(args, *, confirm=False):
|
|
246
|
+
del confirm
|
|
247
|
+
try:
|
|
248
|
+
action = args.get("action")
|
|
249
|
+
if action == "list":
|
|
250
|
+
return {"status": "ok", **archive.list_page(
|
|
251
|
+
args.get("cursor", 0), args.get("limit", MAX_LIST_ENTRIES)
|
|
252
|
+
)}
|
|
253
|
+
if action == "read":
|
|
254
|
+
return {"status": "ok", **archive.read(
|
|
255
|
+
args.get("entry_id"), args.get("offset", 0)
|
|
256
|
+
)}
|
|
257
|
+
return {"status": "error", "message": "action must be list or read"}
|
|
258
|
+
except ArchiveError as e:
|
|
259
|
+
return {"status": "error", "message": str(e)}
|
|
260
|
+
|
|
261
|
+
return _agent.Tool(
|
|
262
|
+
name="venice_context_archive",
|
|
263
|
+
description=(
|
|
264
|
+
"Read evidence archived from earlier context compactions in this session. "
|
|
265
|
+
"List bounded metadata pages, then read exact canonical message JSON by id."
|
|
266
|
+
),
|
|
267
|
+
parameters={
|
|
268
|
+
"type": "object",
|
|
269
|
+
"properties": {
|
|
270
|
+
"action": {"type": "string", "enum": ["list", "read"]},
|
|
271
|
+
"entry_id": {"type": "string"},
|
|
272
|
+
"cursor": {"type": "integer", "minimum": 0},
|
|
273
|
+
"offset": {"type": "integer", "minimum": 0},
|
|
274
|
+
"limit": {"type": "integer", "minimum": 1, "maximum": MAX_LIST_ENTRIES},
|
|
275
|
+
},
|
|
276
|
+
"required": ["action"],
|
|
277
|
+
"additionalProperties": False,
|
|
278
|
+
},
|
|
279
|
+
invoke=invoke,
|
|
280
|
+
paid=False,
|
|
281
|
+
category="context",
|
|
282
|
+
tags=("read",),
|
|
283
|
+
)
|