strata-agent-memory 0.2.8__tar.gz → 0.2.9__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.
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/PKG-INFO +4 -1
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/__init__.py +1 -1
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/cli.py +47 -1
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/compiler.py +22 -1
- strata_agent_memory-0.2.9/agent_memory/endpoints.py +258 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/evidence.py +47 -4
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/kit.py +29 -6
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/lane.py +21 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/ledger.py +1 -0
- strata_agent_memory-0.2.9/agent_memory/retrieval.py +878 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/mac/README.md +9 -1
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/mac/agent-session.sh +30 -3
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/mac/install-hooks-mac.sh +25 -1
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/pyproject.toml +12 -6
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/strata_agent_memory.egg-info/PKG-INFO +4 -1
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/strata_agent_memory.egg-info/SOURCES.txt +7 -1
- strata_agent_memory-0.2.9/strata_agent_memory.egg-info/requires.txt +7 -0
- strata_agent_memory-0.2.9/tests/test_compact_window_778d310f.py +403 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_deploy_mac.py +9 -5
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_install_lane_cb430a30.py +1 -1
- strata_agent_memory-0.2.9/tests/test_retrieval_llm_9e166c2f.py +907 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_retrieval_plugin_9e166c2f.py +1 -1
- strata_agent_memory-0.2.9/tests/test_retrieval_vectors_9e166c2f.py +648 -0
- strata_agent_memory-0.2.9/tests/test_taskset_66bac366.py +356 -0
- strata_agent_memory-0.2.9/tests/test_verify_branch_aff56f06.py +255 -0
- strata_agent_memory-0.2.8/agent_memory/retrieval.py +0 -205
- strata_agent_memory-0.2.8/strata_agent_memory.egg-info/requires.txt +0 -3
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/LICENSE +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/README.md +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/__main__.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/approvals.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/audit.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/compact.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/contracts.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/export.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/hooks.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/identity.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/instances.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/mcp_server.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/measure.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/memorizer.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/pathmap.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/recheck.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/records.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/relay/__init__.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/relay/adapters.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/relay/cli.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/relay/client.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/relay/core.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/reliability.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/remote.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/render.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/roster.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/serve.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/signing.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/stats.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/supersession.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/trust.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/agent_memory/wake.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/lib/served-ledger.sh +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/mac/agent-keys.sh +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/mac/backup-tokens.sh +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/mac/install-relay-driver.sh +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/mac/install-replica-driver.sh +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/mac/install-runner-driver.sh +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/mac/instances.example.json +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/mac/onboard-agent.sh +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/mac/relay-agent.sh +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/mac/seed-board.sh +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/mac/seed-mail-context.sh +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/mac/seed-topics.sh +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/mac/upgrade-driver.sh +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/plugins/.claude-plugin/marketplace.json +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/plugins/README.md +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/bin/agent-memory-approvals +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/bin/agent-memory-wake +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/hooks/hooks.json +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/monitors/monitors.json +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/rules/approvals.md +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/skills/approvals/SKILL.md +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/skills/approve/SKILL.md +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/skills/inbox/SKILL.md +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/plugins/claude-code/skills/reject/SKILL.md +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/plugins/codex/skills/agent-memory-answer/SKILL.md +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/plugins/codex/skills/agent-memory-answer/agents/openai.yaml +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/plugins/codex/skills/agent-memory-approvals/SKILL.md +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/plugins/codex/skills/agent-memory-approvals/agents/openai.yaml +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/plugins/codex/skills/agent-memory-approvals/rules/approvals.md +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/plugins/codex/skills/agent-memory-approvals/scripts/agent-memory-approvals +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/relay/README.md +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/relay/agent-memory-relay@.service +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/relay/install-relay-mac.sh +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/relay/install-relay.sh +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/relay/macos/com.strata.agent-memory.relay.plist.template +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/deploy/relay/relay-launch.sh +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/setup.cfg +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/strata_agent_memory.egg-info/dependency_links.txt +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/strata_agent_memory.egg-info/entry_points.txt +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/strata_agent_memory.egg-info/top_level.txt +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_assessment_67b26eae.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_audit.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_audit_lane_numbers_ea89ba26.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_audit_measurement_13190a20.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_backlog_check.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_binding_refusal_97e36882.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_build_recheck_image_e4a93fd6.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_changelog_fragments.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_changelog_guard_shallow_57a2f78e.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_ci_docs_only_353ce010.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_cli.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_compact.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_compiler.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_compiler_render.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_config.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_contract_amend_649e0b45.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_contracts.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_deploy_relay.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_deploy_replica.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_deploy_tokens.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_evidence.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_find_root_b4b5019b.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_hooks.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_identity.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_issue_bridge.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_lane_63b7cf8b.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_lane_derived_roots_817ab446.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_lane_nightly_84b22b63.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_lane_priority_779d51ba.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_lane_stop_63b7cf8b.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_ledger.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_ledger_index.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_live_wake_c217c336.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_local_lanes_sibling_1411a2b2.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_memorizer.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_memoryagentbench_60a8abfc.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_memsyco_7b16e8e4.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_outcome_commit_binding_b1de564d.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_outcome_needs_evidence_b1de564d.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_packaging.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_pathmap.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_phase2.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_plugins.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_proof_exclusions_e4a93fd6.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_receipt_reports_274e6199.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_recheck.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_recheck_away_207af145.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_recheck_remote_8185d2d3.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_recheck_sandbox_543e48a7.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_recheck_sandbox_nits_543e48a7.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_records.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_redaction.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_relay_adapters.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_relay_client.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_relay_codex.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_relay_core.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_relay_instance_2a2e3887.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_relay_release_526f5261.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_relay_release_gate_b3b3e487.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_reliability.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_remote.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_remote_signing.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_reply_cap_cfbd7681.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_review2_fixes.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_review3_fixes.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_review_fixes.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_rollback_78710ebf.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_sandbox_image_e4a93fd6.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_scale.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_serve.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_served_ledger_6e8e49f3.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_signing.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_sit_fleet_1b24b0b1.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_sit_fleet_ci_0d9b76a2.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_sit_var_bindings_e802ca84.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_stats.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_supersedes_61416280.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_tools_clone_4b72f0c1.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_trust.py +0 -0
- {strata_agent_memory-0.2.8 → strata_agent_memory-0.2.9}/tests/test_uv_lock_env_274e6199.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: strata-agent-memory
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: Portable, auditable, cross-vendor memory for LLM agents: evidence-bound claims, trust tiers, and compiled context views.
|
|
5
5
|
Author: Strata Intelligence
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -20,6 +20,9 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
License-File: LICENSE
|
|
21
21
|
Provides-Extra: sign
|
|
22
22
|
Requires-Dist: cryptography>=42; extra == "sign"
|
|
23
|
+
Provides-Extra: retrieval
|
|
24
|
+
Requires-Dist: sqlite-vec>=0.1.9; extra == "retrieval"
|
|
25
|
+
Requires-Dist: fastembed>=0.8; extra == "retrieval"
|
|
23
26
|
Dynamic: license-file
|
|
24
27
|
|
|
25
28
|
# agent-memory
|
|
@@ -987,6 +987,39 @@ def cmd_memorize(args) -> int:
|
|
|
987
987
|
return 0
|
|
988
988
|
|
|
989
989
|
|
|
990
|
+
def cmd_retrieval(args) -> int:
|
|
991
|
+
"""The retrieval plugin's two steps outside compile (ctr_9e166c2f291b829b): fetch-model,
|
|
992
|
+
the one that opens a socket, and index, which embeds the records (compile embeds only
|
|
993
|
+
the task, so its cost never grows with the ledger)."""
|
|
994
|
+
from . import retrieval as retrieval_mod
|
|
995
|
+
try:
|
|
996
|
+
if args.action == "fetch-model":
|
|
997
|
+
model = args.model
|
|
998
|
+
if model is None:
|
|
999
|
+
try:
|
|
1000
|
+
cfg = retrieval_mod.settings(Ledger.open().config)
|
|
1001
|
+
except FileNotFoundError:
|
|
1002
|
+
cfg = None # no ledger here: the default model
|
|
1003
|
+
embed = (cfg or {}).get("embed") or {}
|
|
1004
|
+
if embed.get("backend") == "endpoint":
|
|
1005
|
+
print(f"retrieval fetch-model: config.retrieval.embed names the endpoint {embed['endpoint']}, "
|
|
1006
|
+
f"which serves its model: there is nothing to fetch", file=sys.stderr)
|
|
1007
|
+
return 2
|
|
1008
|
+
model = embed.get("model", retrieval_mod.DEFAULT_MODEL)
|
|
1009
|
+
_print(retrieval_mod.fetch_model(model))
|
|
1010
|
+
return 0
|
|
1011
|
+
ledger = Ledger.open()
|
|
1012
|
+
cfg = retrieval_mod.settings(ledger.config)
|
|
1013
|
+
if cfg is None:
|
|
1014
|
+
print("config.retrieval names no plugin: there is no index to build", file=sys.stderr)
|
|
1015
|
+
return 2
|
|
1016
|
+
_print(retrieval_mod.index(ledger, cfg))
|
|
1017
|
+
return 0
|
|
1018
|
+
except retrieval_mod.RetrievalError as exc:
|
|
1019
|
+
print(f"retrieval {args.action}: {exc}", file=sys.stderr)
|
|
1020
|
+
return 1
|
|
1021
|
+
|
|
1022
|
+
|
|
990
1023
|
def cmd_export(args) -> int:
|
|
991
1024
|
ledger = Ledger.open()
|
|
992
1025
|
reader = _author(args, ledger) # another user's private receipts are not ours to export
|
|
@@ -1073,7 +1106,7 @@ def cmd_serve(args) -> int:
|
|
|
1073
1106
|
|
|
1074
1107
|
# the local ledger is the whole of what these do; there is nothing to send
|
|
1075
1108
|
LOCAL_ONLY = ("init", "trust", "untrust", "fsck", "redact", "keygen", "roster", "serve",
|
|
1076
|
-
"mcp", "compact", "memorize")
|
|
1109
|
+
"mcp", "compact", "memorize", "retrieval")
|
|
1077
1110
|
# answered over --remote, --instance or $AGENT_MEMORY_REMOTE, and never because a checkout's
|
|
1078
1111
|
# committed config names a replica: the replica's own clone carries that file and still
|
|
1079
1112
|
# re-checks locally, and so does the nightly CI lane in a checkout that names one
|
|
@@ -2527,6 +2560,19 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2527
2560
|
_add_identity(s)
|
|
2528
2561
|
s.set_defaults(fn=cmd_memorize)
|
|
2529
2562
|
|
|
2563
|
+
s = sub.add_parser("retrieval", help="the reader-side retrieval plugin's steps outside compile "
|
|
2564
|
+
"(config.retrieval, docs/PROTOCOL.md section 6)")
|
|
2565
|
+
rs = s.add_subparsers(dest="action", required=True)
|
|
2566
|
+
r = rs.add_parser("fetch-model", help="download the embedding model's weights into the cache: the one "
|
|
2567
|
+
"step that opens a socket; compile loads them offline")
|
|
2568
|
+
r.add_argument("--model", help="one of the models the plugin knows (default: config.retrieval.embed's "
|
|
2569
|
+
"model, else nomic-ai/nomic-embed-text-v1.5-Q)")
|
|
2570
|
+
rs.add_parser("index", help="bring this ledger's retrieval index up to the ledger and, with "
|
|
2571
|
+
"config.retrieval.restate, restate every record without a restatement, and "
|
|
2572
|
+
"with config.retrieval.embed, embed every record without a vector: compile "
|
|
2573
|
+
"does neither, so run this after records arrive")
|
|
2574
|
+
s.set_defaults(fn=cmd_retrieval)
|
|
2575
|
+
|
|
2530
2576
|
s = sub.add_parser("export", help="export the ledger in a standard format")
|
|
2531
2577
|
s.add_argument("--format", choices=list(export_mod.FORMATS), default="prov-json",
|
|
2532
2578
|
help="prov-json (W3C PROV for auditors), in-toto (one unsigned Statement per receipt), "
|
|
@@ -729,15 +729,36 @@ def _retrieval(ledger, task: Optional[str], eligible: Dict[str, "Item"], out: "C
|
|
|
729
729
|
if cfg is None:
|
|
730
730
|
return None, []
|
|
731
731
|
out.retrieval = {"plugin": cfg["plugin"], "share": cfg["share"], "ids": []}
|
|
732
|
+
for part in ("embed", "analyse", "restate"): # the configuration a view came from, whether it ran or not
|
|
733
|
+
if part in cfg:
|
|
734
|
+
out.retrieval[part] = dict(cfg[part])
|
|
732
735
|
if not task:
|
|
733
736
|
return cfg, [] # nothing to search by: a view with no task is compile's alone
|
|
737
|
+
report: dict = {}
|
|
738
|
+
|
|
739
|
+
def reported():
|
|
740
|
+
for part in ("analyse", "embed"): # what answered, what it cost, and the scope it chose
|
|
741
|
+
if part in report:
|
|
742
|
+
out.retrieval[part].update(report[part])
|
|
743
|
+
if "withheld" in report: # user-scoped records kept off an endpoint, and sent to one
|
|
744
|
+
out.retrieval["withheld"], out.retrieval["sent_private"] = report["withheld"], report["sent_private"]
|
|
745
|
+
out.retrieval["pending_for"] = report["pending_for"] # which part of the index is behind
|
|
746
|
+
if "error" in report.get("analyse", {}):
|
|
747
|
+
print(f"agent-memory: the retrieval plugin's query analysis did not run ({report['analyse']['error']}); "
|
|
748
|
+
f"searched by the task's own terms", file=sys.stderr)
|
|
734
749
|
try:
|
|
735
|
-
|
|
750
|
+
# the reader's policy filter goes into the query, so another reader's records never
|
|
751
|
+
# take this reader's places under the plugin's cuts (an index is never global)
|
|
752
|
+
found, pending = retrieval_mod.retrieve(ledger, task, cfg, assessment, allowed=eligible, report=report)
|
|
753
|
+
if pending is not None: # records BM25 alone could find: this view is not the configured C1's yet
|
|
754
|
+
out.retrieval["pending"] = pending
|
|
736
755
|
except retrieval_mod.RetrievalError as exc:
|
|
756
|
+
reported() # an analysis call made before the failure still cost what it cost
|
|
737
757
|
out.retrieval["error"] = str(exc)
|
|
738
758
|
print(f"agent-memory: the retrieval plugin {cfg['plugin']} did not run ({exc}); compiled without it",
|
|
739
759
|
file=sys.stderr)
|
|
740
760
|
return cfg, []
|
|
761
|
+
reported()
|
|
741
762
|
return cfg, [eligible[rid] for rid in found if rid in eligible]
|
|
742
763
|
|
|
743
764
|
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""The endpoint registry's config half (the operator's decision clm_f1fe183b55ed42e5).
|
|
2
|
+
|
|
3
|
+
The model endpoints this machine's components may call are named once, in
|
|
4
|
+
``config.endpoints``, each for the model it serves in short, and a component names the
|
|
5
|
+
one it uses:
|
|
6
|
+
|
|
7
|
+
"endpoints": {
|
|
8
|
+
"Qwen3.5-122B": {"url": "https://node-1.example/v1", "model": "Intel/Qwen3.5-122B-A10B-int4-AutoRound",
|
|
9
|
+
"key_env": "NODE_1_API_KEY", "send_private": true}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
``url`` is an OpenAI-compatible base (``/chat/completions`` and ``/embeddings`` are appended
|
|
13
|
+
to it) and ``model`` the id asked for; what model actually answered is read from each
|
|
14
|
+
response. The key is named, never held: ``key_env`` is an environment variable and
|
|
15
|
+
``key_keychain`` a login-keychain service (the endpoint's name is the item's account), read
|
|
16
|
+
when a call is made and never when the config is checked, so a key is never in a config, an
|
|
17
|
+
argv, a receipt or an error. ``plaintext_key`` lets a key go over ``http://``, as memorize's
|
|
18
|
+
``--llm-insecure`` does. ``send_private`` says the endpoint may receive user-scoped records,
|
|
19
|
+
which no endpoint does by default: it is the endpoint's property (the fleet's own machine,
|
|
20
|
+
yes; a vendor's, no), and each reader of the registry honours it. Over ``http://`` it also
|
|
21
|
+
needs ``plaintext_private``, whatever the key: a key authenticates, and only https hides the
|
|
22
|
+
records in flight; a receipt then says ``plaintext``. ``timeout`` is a call's
|
|
23
|
+
limit in seconds.
|
|
24
|
+
|
|
25
|
+
The retrieval plugin reads the registry first (ctr_9e166c2f291b829b); the memorizer, the
|
|
26
|
+
assessor and the benchmark runners adopt it in their own stories. ``endpoints`` is an
|
|
27
|
+
untrusted-policy key (ledger.UNTRUSTED_POLICY): a clone that named its own would be choosing
|
|
28
|
+
where this machine sends its records, and which secret goes with them.
|
|
29
|
+
|
|
30
|
+
The client opens a socket to the endpoint the config names and nowhere else: it takes no
|
|
31
|
+
proxy from the environment (``HTTP_PROXY`` and its kin), and it follows no redirect, since
|
|
32
|
+
urllib would carry the ``Authorization`` header to wherever a proxy or a redirect pointed.
|
|
33
|
+
It reads a reply only up to MAX_REPLY_BYTES.
|
|
34
|
+
"""
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import json
|
|
38
|
+
import math
|
|
39
|
+
import os
|
|
40
|
+
import re
|
|
41
|
+
import urllib.error
|
|
42
|
+
import urllib.parse
|
|
43
|
+
import urllib.request
|
|
44
|
+
from dataclasses import dataclass
|
|
45
|
+
from typing import Dict, List, Mapping, Optional, Tuple
|
|
46
|
+
|
|
47
|
+
ENTRY_KEYS = ("url", "model", "key_env", "key_keychain", "plaintext_key", "send_private", "plaintext_private",
|
|
48
|
+
"timeout")
|
|
49
|
+
# fields that would hold a secret itself: refused by name, so a key pasted into the config
|
|
50
|
+
# is an error rather than a key committed beside it
|
|
51
|
+
SECRET_FIELDS = ("key", "api_key", "apikey", "token", "bearer", "password", "secret")
|
|
52
|
+
NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$")
|
|
53
|
+
KEY_ENV = re.compile(r"^[A-Z_][A-Z0-9_]*$")
|
|
54
|
+
DEFAULT_TIMEOUT = 60.0
|
|
55
|
+
MAX_TIMEOUT = 600.0
|
|
56
|
+
MAX_REPLY_BYTES = 16 * 1024 * 1024 # a completion or a batch of embeddings is far smaller
|
|
57
|
+
DETAIL_CHARS = 300 # of an error reply, quoted in a degraded run's reason
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class EndpointError(ValueError):
|
|
61
|
+
"""An entry is malformed, or a call to it failed: the sentence says which, never the key."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class Endpoint:
|
|
66
|
+
name: str
|
|
67
|
+
url: str
|
|
68
|
+
model: str
|
|
69
|
+
key_env: Optional[str] = None
|
|
70
|
+
key_keychain: Optional[str] = None
|
|
71
|
+
plaintext_key: bool = False
|
|
72
|
+
send_private: bool = False
|
|
73
|
+
plaintext_private: bool = False
|
|
74
|
+
timeout: float = DEFAULT_TIMEOUT
|
|
75
|
+
|
|
76
|
+
def key(self, env: Optional[Mapping[str, str]] = None) -> Optional[str]:
|
|
77
|
+
"""The key this endpoint's entry names, read now; None when it names none. A named
|
|
78
|
+
key that is not there is an error: the config says this endpoint wants one."""
|
|
79
|
+
if self.key_env:
|
|
80
|
+
found = (os.environ if env is None else env).get(self.key_env)
|
|
81
|
+
if not found:
|
|
82
|
+
raise EndpointError(f"endpoint {self.name!r}: its key_env {self.key_env} is not set")
|
|
83
|
+
return found
|
|
84
|
+
if self.key_keychain:
|
|
85
|
+
from .remote import keychain_token # local: the keychain is read only for a call
|
|
86
|
+
found = keychain_token(self.key_keychain, self.name)
|
|
87
|
+
if not found:
|
|
88
|
+
raise EndpointError(f"endpoint {self.name!r}: no login-keychain item with service "
|
|
89
|
+
f"{self.key_keychain} and account {self.name}")
|
|
90
|
+
return found
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def describe(self) -> dict:
|
|
94
|
+
"""What a receipt says of the endpoint: never its key, nor where the key is kept; and
|
|
95
|
+
``plaintext`` when it is sent user-scoped records over http://."""
|
|
96
|
+
out = {"endpoint": self.name, "url": self.url, "model": self.model, "send_private": self.send_private}
|
|
97
|
+
if self.send_private and self.url.startswith("http://"):
|
|
98
|
+
out["plaintext"] = True
|
|
99
|
+
return out
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def check(name: str, entry) -> Endpoint:
|
|
103
|
+
"""The entry ``name`` as an Endpoint, or EndpointError saying what is wrong with it."""
|
|
104
|
+
if not isinstance(name, str) or not NAME.match(name):
|
|
105
|
+
raise EndpointError(f"an endpoint's name is letters, digits and _.:- (up to 64), not {name!r}")
|
|
106
|
+
if not isinstance(entry, dict):
|
|
107
|
+
raise EndpointError(f"endpoint {name!r} must be an object with url and model")
|
|
108
|
+
secrets = sorted(set(entry) & set(SECRET_FIELDS))
|
|
109
|
+
if secrets:
|
|
110
|
+
raise EndpointError(f"endpoint {name!r}: a key never goes in the config ({', '.join(secrets)}); name "
|
|
111
|
+
f"where it is kept with key_env or key_keychain")
|
|
112
|
+
unknown = sorted(set(entry) - set(ENTRY_KEYS))
|
|
113
|
+
if unknown:
|
|
114
|
+
raise EndpointError(f"endpoint {name!r} has keys the registry does not read: {unknown}")
|
|
115
|
+
url = entry.get("url")
|
|
116
|
+
parts = urllib.parse.urlsplit(url) if isinstance(url, str) else None
|
|
117
|
+
if parts is None or parts.scheme not in ("https", "http") or not parts.hostname:
|
|
118
|
+
raise EndpointError(f"endpoint {name!r}: url must be an http(s) URL, not {url!r}")
|
|
119
|
+
if parts.username is not None or parts.password is not None or parts.query or parts.fragment:
|
|
120
|
+
# a receipt names the url, so nothing that could be a secret may ride in it
|
|
121
|
+
raise EndpointError(f"endpoint {name!r}: url must carry no credentials, query or fragment")
|
|
122
|
+
model = entry.get("model")
|
|
123
|
+
if not isinstance(model, str) or not model.strip():
|
|
124
|
+
raise EndpointError(f"endpoint {name!r}: model is required (the id to ask for)")
|
|
125
|
+
key_env, key_keychain = entry.get("key_env"), entry.get("key_keychain")
|
|
126
|
+
if key_env is not None and key_keychain is not None:
|
|
127
|
+
raise EndpointError(f"endpoint {name!r}: name one key, key_env or key_keychain")
|
|
128
|
+
if key_env is not None and not (isinstance(key_env, str) and KEY_ENV.match(key_env)):
|
|
129
|
+
raise EndpointError(f"endpoint {name!r}: key_env must be an environment variable's name")
|
|
130
|
+
if key_keychain is not None and not (isinstance(key_keychain, str) and key_keychain.strip()):
|
|
131
|
+
raise EndpointError(f"endpoint {name!r}: key_keychain must be a keychain service's name")
|
|
132
|
+
flags = {}
|
|
133
|
+
for flag in ("plaintext_key", "send_private", "plaintext_private"):
|
|
134
|
+
value = entry.get(flag, False)
|
|
135
|
+
if not isinstance(value, bool):
|
|
136
|
+
raise EndpointError(f"endpoint {name!r}: {flag} must be true or false")
|
|
137
|
+
flags[flag] = value
|
|
138
|
+
if (key_env or key_keychain) and parts.scheme == "http" and not flags["plaintext_key"]:
|
|
139
|
+
raise EndpointError(f"endpoint {name!r}: refusing to send its key to a plaintext endpoint ({url}); use "
|
|
140
|
+
f"https, or set plaintext_key if you accept the risk")
|
|
141
|
+
if flags["send_private"] and parts.scheme == "http" and not flags["plaintext_private"]:
|
|
142
|
+
# whatever its key: a key authenticates, and only https hides the records in flight
|
|
143
|
+
raise EndpointError(f"endpoint {name!r}: refusing to send user-scoped records to a plaintext endpoint "
|
|
144
|
+
f"({url}); use https, or set plaintext_private if you accept the risk")
|
|
145
|
+
timeout = entry.get("timeout", DEFAULT_TIMEOUT)
|
|
146
|
+
if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or not 0 < timeout <= MAX_TIMEOUT:
|
|
147
|
+
raise EndpointError(f"endpoint {name!r}: timeout must be seconds in (0, {MAX_TIMEOUT:g}]")
|
|
148
|
+
return Endpoint(name=name, url=url.rstrip("/"), model=model, key_env=key_env, key_keychain=key_keychain,
|
|
149
|
+
timeout=float(timeout), **flags)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def endpoint(config: Optional[dict], name) -> Endpoint:
|
|
153
|
+
"""The endpoint ``name`` from ``config.endpoints``, or EndpointError."""
|
|
154
|
+
table = (config or {}).get("endpoints")
|
|
155
|
+
if table is None:
|
|
156
|
+
table = {}
|
|
157
|
+
if not isinstance(table, dict):
|
|
158
|
+
raise EndpointError("config.endpoints must be an object of endpoints by name")
|
|
159
|
+
if not isinstance(name, str) or name not in table:
|
|
160
|
+
raise EndpointError(f"no endpoint named {name!r} in config.endpoints")
|
|
161
|
+
return check(name, table[name])
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
165
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
166
|
+
return None # the 3xx is answered as an error: the key goes nowhere but the named url
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _opener() -> urllib.request.OpenerDirector:
|
|
170
|
+
"""No proxy, which urllib would otherwise take from HTTP_PROXY and its kin (an http one
|
|
171
|
+
reads the Authorization header), and no redirect: a socket to the named url and nowhere
|
|
172
|
+
else. Built for each call, as cheap as the call is not."""
|
|
173
|
+
return urllib.request.build_opener(urllib.request.ProxyHandler({}), _NoRedirect)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _scrub(text: str, key: Optional[str]) -> str:
|
|
177
|
+
return text.replace(key, "<key>") if key else text
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def call(ep: Endpoint, path: str, payload: dict, env: Optional[Mapping[str, str]] = None) -> dict:
|
|
181
|
+
"""POST ``payload`` to the endpoint's ``path`` and return the reply's JSON object."""
|
|
182
|
+
key = ep.key(env)
|
|
183
|
+
headers = {"Content-Type": "application/json"}
|
|
184
|
+
if key:
|
|
185
|
+
headers["Authorization"] = f"Bearer {key}"
|
|
186
|
+
req = urllib.request.Request(ep.url + path, data=json.dumps(payload).encode("utf-8"), headers=headers,
|
|
187
|
+
method="POST")
|
|
188
|
+
try:
|
|
189
|
+
with _opener().open(req, timeout=ep.timeout) as resp:
|
|
190
|
+
raw = resp.read(MAX_REPLY_BYTES + 1)
|
|
191
|
+
except urllib.error.HTTPError as exc:
|
|
192
|
+
try:
|
|
193
|
+
detail = exc.read(DETAIL_CHARS * 4).decode("utf-8", "replace")[:DETAIL_CHARS]
|
|
194
|
+
except OSError:
|
|
195
|
+
detail = ""
|
|
196
|
+
raise EndpointError(f"endpoint {ep.name!r} answered {exc.code}: {_scrub(detail, key)}") from None
|
|
197
|
+
except (urllib.error.URLError, OSError, ValueError) as exc: # refused, unreachable, timed out
|
|
198
|
+
raise EndpointError(f"endpoint {ep.name!r} could not be reached ({type(exc).__name__}: "
|
|
199
|
+
f"{_scrub(str(exc), key)[:DETAIL_CHARS]})") from None
|
|
200
|
+
if len(raw) > MAX_REPLY_BYTES:
|
|
201
|
+
raise EndpointError(f"endpoint {ep.name!r} replied with more than {MAX_REPLY_BYTES} bytes")
|
|
202
|
+
try:
|
|
203
|
+
body = json.loads(raw.decode("utf-8"))
|
|
204
|
+
except (UnicodeDecodeError, ValueError):
|
|
205
|
+
raise EndpointError(f"endpoint {ep.name!r} replied with something other than JSON") from None
|
|
206
|
+
if not isinstance(body, dict):
|
|
207
|
+
raise EndpointError(f"endpoint {ep.name!r} replied with JSON that is not an object")
|
|
208
|
+
return body
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def usage(body: dict) -> dict:
|
|
212
|
+
"""The reply's token counts, as numbers only: what the read cost is counted from."""
|
|
213
|
+
found = body.get("usage") if isinstance(body.get("usage"), dict) else {}
|
|
214
|
+
return {k: v for k, v in found.items() if k in ("prompt_tokens", "completion_tokens", "total_tokens")
|
|
215
|
+
and isinstance(v, int) and not isinstance(v, bool) and v >= 0}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def served(body: dict) -> Optional[str]:
|
|
219
|
+
model = body.get("model")
|
|
220
|
+
return model[:200] if isinstance(model, str) and model else None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def chat(ep: Endpoint, messages: List[dict], max_tokens: int, env: Optional[Mapping[str, str]] = None
|
|
224
|
+
) -> Tuple[str, Optional[str], dict]:
|
|
225
|
+
"""(the reply's text, the served model id, its usage) for one chat completion."""
|
|
226
|
+
body = call(ep, "/chat/completions", {"model": ep.model, "messages": messages, "temperature": 0.0,
|
|
227
|
+
"max_tokens": max_tokens, "stream": False}, env)
|
|
228
|
+
try:
|
|
229
|
+
text = body["choices"][0]["message"]["content"]
|
|
230
|
+
except (KeyError, IndexError, TypeError):
|
|
231
|
+
text = None
|
|
232
|
+
if not isinstance(text, str):
|
|
233
|
+
raise EndpointError(f"endpoint {ep.name!r} replied with no message content")
|
|
234
|
+
return text, served(body), usage(body)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def embeddings(ep: Endpoint, texts: List[str], env: Optional[Mapping[str, str]] = None
|
|
238
|
+
) -> Tuple[List[List[float]], Optional[str], dict]:
|
|
239
|
+
"""(one vector per text, in order; the served model id; the usage) from ``/embeddings``."""
|
|
240
|
+
body = call(ep, "/embeddings", {"model": ep.model, "input": list(texts)}, env)
|
|
241
|
+
data = body.get("data")
|
|
242
|
+
if not isinstance(data, list) or len(data) != len(texts):
|
|
243
|
+
raise EndpointError(f"endpoint {ep.name!r} replied with {len(data) if isinstance(data, list) else 'no'} "
|
|
244
|
+
f"embeddings for {len(texts)} texts")
|
|
245
|
+
by_index: Dict[int, List[float]] = {}
|
|
246
|
+
for position, item in enumerate(data):
|
|
247
|
+
index = item.get("index", position) if isinstance(item, dict) else None
|
|
248
|
+
vector = item.get("embedding") if isinstance(item, dict) else None
|
|
249
|
+
if (not isinstance(index, int) or isinstance(index, bool) or not 0 <= index < len(texts)
|
|
250
|
+
or index in by_index or not isinstance(vector, list) or not vector
|
|
251
|
+
or not all(isinstance(x, (int, float)) and not isinstance(x, bool) and math.isfinite(x)
|
|
252
|
+
for x in vector)):
|
|
253
|
+
raise EndpointError(f"endpoint {ep.name!r} replied with a malformed embedding")
|
|
254
|
+
by_index[index] = [float(x) for x in vector]
|
|
255
|
+
dims = {len(v) for v in by_index.values()}
|
|
256
|
+
if len(dims) != 1:
|
|
257
|
+
raise EndpointError(f"endpoint {ep.name!r} replied with embeddings of different lengths")
|
|
258
|
+
return [by_index[i] for i in range(len(texts))], served(body), usage(body)
|
|
@@ -1530,6 +1530,49 @@ def _git(args: Sequence[str], cwd: str) -> subprocess.CompletedProcess:
|
|
|
1530
1530
|
timeout=GIT_CHECK_SECONDS)
|
|
1531
1531
|
|
|
1532
1532
|
|
|
1533
|
+
def _branch_copies(branch: str, repo: str) -> List[str]:
|
|
1534
|
+
"""The names a binding's branch may go by in ``repo``, in the order they are asked: as
|
|
1535
|
+
given, then origin/<branch>, then <remote>/<branch> under the clone's other remotes. A
|
|
1536
|
+
clean clone holds a pull request's branch only as origin/<branch>, and a lane's local
|
|
1537
|
+
branches are the ones it was cloned with, never moved since (ctr_aff56f06786902ec)."""
|
|
1538
|
+
others = sorted(name for name in _git(["remote"], repo).stdout.split() if name != "origin")
|
|
1539
|
+
# a name that begins with a dash is no ref (check-ref-format refuses it) and would be an option
|
|
1540
|
+
given = [] if branch.startswith("-") else [branch]
|
|
1541
|
+
return [*given, *(f"{remote}/{branch}" for remote in ["origin", *others])]
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
def _on_branch(sha: str, branch: str, repo: str) -> Tuple[str, str]:
|
|
1545
|
+
"""Whether the commit is on the binding's branch, asked of every copy of the branch this
|
|
1546
|
+
clone holds: a match when one contains it, a mismatch only when every copy that resolves
|
|
1547
|
+
answers that it does not (git exits 1), and an error when none resolves or git cannot
|
|
1548
|
+
answer -- a branch this clone does not hold is a question it could not ask, never a
|
|
1549
|
+
refutation (ctr_aff56f06786902ec). A copy that still holds the commit matches though
|
|
1550
|
+
another was rewritten past it: a refutation taints what was built on the claim, so it
|
|
1551
|
+
needs every copy to say no."""
|
|
1552
|
+
tried: List[str] = []
|
|
1553
|
+
behind: List[str] = []
|
|
1554
|
+
failed = ""
|
|
1555
|
+
for name in _branch_copies(branch, repo):
|
|
1556
|
+
tried.append(name)
|
|
1557
|
+
ref = name if name == branch else f"refs/remotes/{name}"
|
|
1558
|
+
tip = _git(["rev-parse", "--verify", "--quiet", f"{ref}^{{commit}}"], repo).stdout.strip()
|
|
1559
|
+
if not tip:
|
|
1560
|
+
continue
|
|
1561
|
+
r = _git(["merge-base", "--is-ancestor", sha, tip, "--"], repo)
|
|
1562
|
+
if r.returncode == 0:
|
|
1563
|
+
return "match", f"commit {sha[:12]} present, on {branch}" + (f" as {name}" if name != branch else "")
|
|
1564
|
+
if r.returncode == 1:
|
|
1565
|
+
behind.append(f"{name} at {tip[:12]}")
|
|
1566
|
+
else:
|
|
1567
|
+
failed = failed or f"git merge-base on {name} exited {r.returncode}: {(r.stderr or '').strip()[:200]}"
|
|
1568
|
+
if failed:
|
|
1569
|
+
return "error", failed
|
|
1570
|
+
if behind:
|
|
1571
|
+
return "mismatch", f"commit {sha} is not an ancestor of {branch} ({', '.join(behind)})"
|
|
1572
|
+
return "error", (f"branch {branch} is not in this clone of {repo} (tried {', '.join(tried)}); "
|
|
1573
|
+
f"fetch it to re-check")
|
|
1574
|
+
|
|
1575
|
+
|
|
1533
1576
|
class BindingError(ValueError):
|
|
1534
1577
|
"""A binding that could not be built on this host: a commit its repository does not
|
|
1535
1578
|
hold, a file that is missing or not a regular one. The message is this host's detail
|
|
@@ -2349,8 +2392,8 @@ def _check_binding(body: dict, cwd: Optional[str] = None, strict: bool = False,
|
|
|
2349
2392
|
return "error", f"repo missing: {repo}"
|
|
2350
2393
|
try:
|
|
2351
2394
|
r = _git(["cat-file", "-e", "--", f"{body['sha']}^{{commit}}"], repo)
|
|
2352
|
-
|
|
2353
|
-
|
|
2395
|
+
on_branch = (_on_branch(body["sha"], body["branch"], repo)
|
|
2396
|
+
if r.returncode == 0 and body.get("branch") else None)
|
|
2354
2397
|
except subprocess.TimeoutExpired:
|
|
2355
2398
|
return "error", f"git did not answer within {GIT_CHECK_SECONDS}s in {repo}"
|
|
2356
2399
|
if r.returncode != 0:
|
|
@@ -2365,8 +2408,8 @@ def _check_binding(body: dict, cwd: Optional[str] = None, strict: bool = False,
|
|
|
2365
2408
|
# mismatch here would refute every merged outcome a lane on main re-checks.
|
|
2366
2409
|
# (No notes["missing"]: the repo was found, so no path map rule is to blame)
|
|
2367
2410
|
return "error", f"commit {body['sha']} is not in this clone of {repo}; fetch it to re-check"
|
|
2368
|
-
if
|
|
2369
|
-
return
|
|
2411
|
+
if on_branch is not None:
|
|
2412
|
+
return on_branch
|
|
2370
2413
|
return "match", f"commit {body['sha'][:12]} present"
|
|
2371
2414
|
if kind == "file":
|
|
2372
2415
|
p = Path(body["path"])
|
|
@@ -109,9 +109,17 @@ def _install_claude(plugins: Path, scope: str, dry_run: bool) -> int:
|
|
|
109
109
|
return 0
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
# Every SessionStart source Codex has: after it compacts a session, the hooks that match
|
|
113
|
+
# `compact` "run before the next model request" (https://learn.chatgpt.com/docs/hooks), so
|
|
114
|
+
# the approvals line comes back with the rest of the context.
|
|
115
|
+
CODEX_SESSION_SOURCES = "startup|resume|clear|compact"
|
|
116
|
+
|
|
117
|
+
|
|
112
118
|
def _merge_codex_hook(hooks_file: Path, command: str) -> dict:
|
|
113
119
|
"""~/.codex/hooks.json with the plugin's SessionStart entry in it once: an entry of ours
|
|
114
|
-
already there (a re-run) is replaced, everything else is left as it was.
|
|
120
|
+
already there (a re-run) is replaced, everything else is left as it was. One under
|
|
121
|
+
another matcher (an older install's startup|resume) moves to a group of its own with
|
|
122
|
+
CODEX_SESSION_SOURCES, and a group that held nothing else goes with it."""
|
|
115
123
|
doc = {}
|
|
116
124
|
if hooks_file.is_file():
|
|
117
125
|
doc = json.loads(hooks_file.read_text(encoding="utf-8") or "{}")
|
|
@@ -121,12 +129,27 @@ def _merge_codex_hook(hooks_file: Path, command: str) -> dict:
|
|
|
121
129
|
if not isinstance(groups, list):
|
|
122
130
|
raise ValueError(f"{hooks_file}: hooks.SessionStart is not a list; fix it by hand")
|
|
123
131
|
entry = {"type": "command", "command": command, "timeout": 25}
|
|
132
|
+
|
|
133
|
+
def mine(have) -> bool:
|
|
134
|
+
return isinstance(have, dict) and PLUGIN_HOOK_MARK in str(have.get("command", ""))
|
|
135
|
+
|
|
136
|
+
placed, kept = False, []
|
|
124
137
|
for group in groups:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
138
|
+
hooks = group.get("hooks") if isinstance(group, dict) else None
|
|
139
|
+
if not isinstance(hooks, list) or not any(mine(h) for h in hooks):
|
|
140
|
+
kept.append(group)
|
|
141
|
+
continue
|
|
142
|
+
if not placed and group.get("matcher") == CODEX_SESSION_SOURCES:
|
|
143
|
+
first = next(i for i, h in enumerate(hooks) if mine(h))
|
|
144
|
+
group["hooks"] = [entry if i == first else h for i, h in enumerate(hooks) if i == first or not mine(h)]
|
|
145
|
+
placed = True
|
|
146
|
+
else:
|
|
147
|
+
group["hooks"] = [h for h in hooks if not mine(h)]
|
|
148
|
+
if group["hooks"]:
|
|
149
|
+
kept.append(group)
|
|
150
|
+
if not placed:
|
|
151
|
+
kept.append({"matcher": CODEX_SESSION_SOURCES, "hooks": [entry]})
|
|
152
|
+
groups[:] = kept
|
|
130
153
|
return doc
|
|
131
154
|
|
|
132
155
|
|
|
@@ -233,6 +233,25 @@ def fetch_commit(clone: str, sha: str, log: Optional[Callable[[dict], None]] = N
|
|
|
233
233
|
return False
|
|
234
234
|
|
|
235
235
|
|
|
236
|
+
def fetch_branch(clone: str, branch: str, log: Optional[Callable[[dict], None]] = None) -> None:
|
|
237
|
+
"""Bring origin's ``branch`` to refs/remotes/origin/<branch>, so a binding's branch is
|
|
238
|
+
asked about as origin has it now: fetch_commit takes main and the pull requests' heads,
|
|
239
|
+
so a branch pushed after the clone was made is under no name in it at all
|
|
240
|
+
(ctr_aff56f06786902ec). A branch origin no longer has (merged and deleted) keeps the tip
|
|
241
|
+
this clone last saw, or stays absent, which evidence.check calls a question it could
|
|
242
|
+
not ask."""
|
|
243
|
+
# a name that is not a ref under refs/heads is never put in a refspec: a colon in it
|
|
244
|
+
# would name the destination
|
|
245
|
+
if not branch or _git(clone, "check-ref-format", f"refs/heads/{branch}").returncode != 0:
|
|
246
|
+
return
|
|
247
|
+
with clone_lock(clone, FETCH_SECONDS, log, "fetch branch"):
|
|
248
|
+
try:
|
|
249
|
+
_git(clone, "fetch", "--quiet", "--no-tags", "origin", "--",
|
|
250
|
+
f"+refs/heads/{branch}:refs/remotes/origin/{branch}", timeout=FETCH_SECONDS)
|
|
251
|
+
except subprocess.SubprocessError:
|
|
252
|
+
pass # the tip this clone last saw, if any, is what it can tell
|
|
253
|
+
|
|
254
|
+
|
|
236
255
|
def main_head(clone: str, log: Optional[Callable[[dict], None]] = None) -> Optional[str]:
|
|
237
256
|
with clone_lock(clone, FETCH_SECONDS, log, "fetch main"):
|
|
238
257
|
try:
|
|
@@ -799,6 +818,8 @@ class Lane:
|
|
|
799
818
|
sha = str(binding.get("sha") or "")
|
|
800
819
|
if not fetch_commit(self.clone, sha, log=self.log):
|
|
801
820
|
return None, "none", f"commit {sha[:12]} is not in the lane's clone or its origin"
|
|
821
|
+
if binding.get("branch"):
|
|
822
|
+
fetch_branch(self.clone, str(binding["branch"]), log=self.log)
|
|
802
823
|
with worktree(self.clone, sha, self.workdir, log=self.log) as wt:
|
|
803
824
|
base = self._base_map(wt)
|
|
804
825
|
# last, so it is the rule for its prefix: PathMap keeps the last rule a prefix is
|
|
@@ -131,6 +131,7 @@ UNTRUSTED_POLICY = {
|
|
|
131
131
|
"recheck_allow_shell": False,
|
|
132
132
|
"recheck_sandbox": None, # which image a re-check runs in, and whether: the machine's, never a clone's
|
|
133
133
|
"retrieval": None, # the plugin a compile runs (ctr_9e166c2f291b829b): the machine's, never a clone's
|
|
134
|
+
"endpoints": {}, # where this machine sends records and which secret goes with them (endpoints.py)
|
|
134
135
|
"redact": [], # an attacker-supplied regex can hang capture (catastrophic backtracking)
|
|
135
136
|
"path_map": [], # where a re-check runs
|
|
136
137
|
"org_path": None, # what it reads and writes outside the checkout
|