mimir-agent 0.1.0__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.
- mimir_agent-0.1.0/.gitignore +72 -0
- mimir_agent-0.1.0/LICENSE +26 -0
- mimir_agent-0.1.0/PKG-INFO +41 -0
- mimir_agent-0.1.0/mimir/__init__.py +3 -0
- mimir_agent-0.1.0/mimir/_context.py +206 -0
- mimir_agent-0.1.0/mimir/_jsonl_tail.py +134 -0
- mimir_agent-0.1.0/mimir/_langchain_claude_code_patches.py +500 -0
- mimir_agent-0.1.0/mimir/_paths.py +113 -0
- mimir_agent-0.1.0/mimir/_streaming_dispatch.py +424 -0
- mimir_agent-0.1.0/mimir/_tool_helpers.py +122 -0
- mimir_agent-0.1.0/mimir/agent.py +2155 -0
- mimir_agent-0.1.0/mimir/billing.py +689 -0
- mimir_agent-0.1.0/mimir/bridges/__init__.py +0 -0
- mimir_agent-0.1.0/mimir/bridges/_attachments.py +173 -0
- mimir_agent-0.1.0/mimir/bridges/_directives.py +222 -0
- mimir_agent-0.1.0/mimir/bridges/_emoji.py +106 -0
- mimir_agent-0.1.0/mimir/bridges/_history.py +36 -0
- mimir_agent-0.1.0/mimir/bridges/base.py +118 -0
- mimir_agent-0.1.0/mimir/bridges/bench.py +106 -0
- mimir_agent-0.1.0/mimir/bridges/discord.py +931 -0
- mimir_agent-0.1.0/mimir/bridges/slack.py +898 -0
- mimir_agent-0.1.0/mimir/bridges/web_chat.py +221 -0
- mimir_agent-0.1.0/mimir/budget.py +470 -0
- mimir_agent-0.1.0/mimir/channel_registry.py +102 -0
- mimir_agent-0.1.0/mimir/cli.py +2521 -0
- mimir_agent-0.1.0/mimir/codex_auth.py +143 -0
- mimir_agent-0.1.0/mimir/commitments/__init__.py +36 -0
- mimir_agent-0.1.0/mimir/commitments/cli.py +389 -0
- mimir_agent-0.1.0/mimir/commitments/extractor.py +381 -0
- mimir_agent-0.1.0/mimir/commitments/models.py +277 -0
- mimir_agent-0.1.0/mimir/commitments/poller.py +255 -0
- mimir_agent-0.1.0/mimir/commitments/render.py +156 -0
- mimir_agent-0.1.0/mimir/commitments/store.py +598 -0
- mimir_agent-0.1.0/mimir/config.py +854 -0
- mimir_agent-0.1.0/mimir/core_blocks.py +166 -0
- mimir_agent-0.1.0/mimir/cred_rotate.py +531 -0
- mimir_agent-0.1.0/mimir/cred_verify.py +477 -0
- mimir_agent-0.1.0/mimir/dispatcher.py +200 -0
- mimir_agent-0.1.0/mimir/event_logger.py +217 -0
- mimir_agent-0.1.0/mimir/feedback.py +2049 -0
- mimir_agent-0.1.0/mimir/git_bootstrap.py +822 -0
- mimir_agent-0.1.0/mimir/git_tracking.py +537 -0
- mimir_agent-0.1.0/mimir/health.py +137 -0
- mimir_agent-0.1.0/mimir/health_probe.py +589 -0
- mimir_agent-0.1.0/mimir/history.py +598 -0
- mimir_agent-0.1.0/mimir/identities.py +424 -0
- mimir_agent-0.1.0/mimir/identities_populator.py +639 -0
- mimir_agent-0.1.0/mimir/index.py +301 -0
- mimir_agent-0.1.0/mimir/index_integrity.py +385 -0
- mimir_agent-0.1.0/mimir/jsonl_snapshot.py +166 -0
- mimir_agent-0.1.0/mimir/loop_detector.py +95 -0
- mimir_agent-0.1.0/mimir/loop_inventory.py +133 -0
- mimir_agent-0.1.0/mimir/loops_cmd.py +244 -0
- mimir_agent-0.1.0/mimir/mcp_client.py +503 -0
- mimir_agent-0.1.0/mimir/minimax_usage_poller.py +340 -0
- mimir_agent-0.1.0/mimir/model_registry.py +326 -0
- mimir_agent-0.1.0/mimir/models.py +244 -0
- mimir_agent-0.1.0/mimir/ntfy.py +222 -0
- mimir_agent-0.1.0/mimir/oauth_usage_poller.py +1174 -0
- mimir_agent-0.1.0/mimir/ops_dashboard.py +1285 -0
- mimir_agent-0.1.0/mimir/pollers.py +878 -0
- mimir_agent-0.1.0/mimir/prompt_templates/__init__.py +73 -0
- mimir_agent-0.1.0/mimir/prompts.py +461 -0
- mimir_agent-0.1.0/mimir/quota_pause.py +295 -0
- mimir_agent-0.1.0/mimir/rate_limits.py +589 -0
- mimir_agent-0.1.0/mimir/reactions.py +131 -0
- mimir_agent-0.1.0/mimir/readonly_backend.py +463 -0
- mimir_agent-0.1.0/mimir/reflection/__init__.py +0 -0
- mimir_agent-0.1.0/mimir/reflection/applied_audit.py +639 -0
- mimir_agent-0.1.0/mimir/reflection/introspection_report.py +656 -0
- mimir_agent-0.1.0/mimir/reflection/most_retrieved.py +87 -0
- mimir_agent-0.1.0/mimir/reflection/proposed_changes_health.py +197 -0
- mimir_agent-0.1.0/mimir/reindex.py +486 -0
- mimir_agent-0.1.0/mimir/saga/__init__.py +178 -0
- mimir_agent-0.1.0/mimir/saga/_config_io.py +903 -0
- mimir_agent-0.1.0/mimir/saga/_llm.py +521 -0
- mimir_agent-0.1.0/mimir/saga/activation.py +349 -0
- mimir_agent-0.1.0/mimir/saga/async_pool.py +72 -0
- mimir_agent-0.1.0/mimir/saga/calibration.py +156 -0
- mimir_agent-0.1.0/mimir/saga/client.py +1956 -0
- mimir_agent-0.1.0/mimir/saga/cluster.py +151 -0
- mimir_agent-0.1.0/mimir/saga/config.py +210 -0
- mimir_agent-0.1.0/mimir/saga/consolidate.py +287 -0
- mimir_agent-0.1.0/mimir/saga/contributions.py +210 -0
- mimir_agent-0.1.0/mimir/saga/dedup.py +708 -0
- mimir_agent-0.1.0/mimir/saga/embeddings.py +467 -0
- mimir_agent-0.1.0/mimir/saga/forget.py +229 -0
- mimir_agent-0.1.0/mimir/saga/fts.py +240 -0
- mimir_agent-0.1.0/mimir/saga/mark_access.py +154 -0
- mimir_agent-0.1.0/mimir/saga/migrate.py +584 -0
- mimir_agent-0.1.0/mimir/saga/observations.py +311 -0
- mimir_agent-0.1.0/mimir/saga/query_rewrite.py +182 -0
- mimir_agent-0.1.0/mimir/saga/recall.py +614 -0
- mimir_agent-0.1.0/mimir/saga/reflect.py +369 -0
- mimir_agent-0.1.0/mimir/saga/retrieval_fusion.py +67 -0
- mimir_agent-0.1.0/mimir/saga/store.py +298 -0
- mimir_agent-0.1.0/mimir/saga/synthesize.py +701 -0
- mimir_agent-0.1.0/mimir/saga/triples.py +674 -0
- mimir_agent-0.1.0/mimir/saga/vector_index.py +304 -0
- mimir_agent-0.1.0/mimir/saga_client.py +832 -0
- mimir_agent-0.1.0/mimir/sagatools.py +177 -0
- mimir_agent-0.1.0/mimir/scaffold_docker.py +758 -0
- mimir_agent-0.1.0/mimir/scheduler.py +1822 -0
- mimir_agent-0.1.0/mimir/search.py +873 -0
- mimir_agent-0.1.0/mimir/server.py +1017 -0
- mimir_agent-0.1.0/mimir/session_boundary_log.py +330 -0
- mimir_agent-0.1.0/mimir/session_manager.py +333 -0
- mimir_agent-0.1.0/mimir/shell_jobs.py +504 -0
- mimir_agent-0.1.0/mimir/skill_catalog.py +234 -0
- mimir_agent-0.1.0/mimir/skill_defs.py +240 -0
- mimir_agent-0.1.0/mimir/skill_install.py +402 -0
- mimir_agent-0.1.0/mimir/skill_md.py +160 -0
- mimir_agent-0.1.0/mimir/skill_outcomes.py +718 -0
- mimir_agent-0.1.0/mimir/skill_resolver.py +155 -0
- mimir_agent-0.1.0/mimir/skills/__init__.py +0 -0
- mimir_agent-0.1.0/mimir/skills/alert/SKILL.md +70 -0
- mimir_agent-0.1.0/mimir/skills/async-tasks/SKILL.md +219 -0
- mimir_agent-0.1.0/mimir/skills/chainlink/SKILL.md +429 -0
- mimir_agent-0.1.0/mimir/skills/circuit-breaker/SKILL.md +133 -0
- mimir_agent-0.1.0/mimir/skills/commitments/SKILL.md +166 -0
- mimir_agent-0.1.0/mimir/skills/fallback-chains/SKILL.md +138 -0
- mimir_agent-0.1.0/mimir/skills/find-skills/SKILL.md +102 -0
- mimir_agent-0.1.0/mimir/skills/five-whys/CHAINLINK_SETUP.md +117 -0
- mimir_agent-0.1.0/mimir/skills/five-whys/CHAINLINK_USAGE.md +167 -0
- mimir_agent-0.1.0/mimir/skills/five-whys/SKILL.md +247 -0
- mimir_agent-0.1.0/mimir/skills/github/SKILL.md +101 -0
- mimir_agent-0.1.0/mimir/skills/identity-lookup/SKILL.md +210 -0
- mimir_agent-0.1.0/mimir/skills/introspection/SKILL.md +313 -0
- mimir_agent-0.1.0/mimir/skills/introspection/debugging-communication.md +222 -0
- mimir_agent-0.1.0/mimir/skills/introspection/debugging-drift.md +170 -0
- mimir_agent-0.1.0/mimir/skills/introspection/debugging-jobs.md +145 -0
- mimir_agent-0.1.0/mimir/skills/long-running-jobs/SKILL.md +281 -0
- mimir_agent-0.1.0/mimir/skills/memory/SKILL.md +281 -0
- mimir_agent-0.1.0/mimir/skills/memory/maintenance.md +46 -0
- mimir_agent-0.1.0/mimir/skills/mermaid-diagrams/SKILL.md +248 -0
- mimir_agent-0.1.0/mimir/skills/mermaid-diagrams/references/advanced-features.md +556 -0
- mimir_agent-0.1.0/mimir/skills/mermaid-diagrams/references/c4-diagrams.md +410 -0
- mimir_agent-0.1.0/mimir/skills/mermaid-diagrams/references/class-diagrams.md +361 -0
- mimir_agent-0.1.0/mimir/skills/mermaid-diagrams/references/erd-diagrams.md +510 -0
- mimir_agent-0.1.0/mimir/skills/mermaid-diagrams/references/flowcharts.md +450 -0
- mimir_agent-0.1.0/mimir/skills/mermaid-diagrams/references/sequence-diagrams.md +394 -0
- mimir_agent-0.1.0/mimir/skills/ntfy/SKILL.md +177 -0
- mimir_agent-0.1.0/mimir/skills/onboarding/SKILL.md +164 -0
- mimir_agent-0.1.0/mimir/skills/onboarding/establishing-goals.md +109 -0
- mimir_agent-0.1.0/mimir/skills/onboarding/establishing-identity.md +106 -0
- mimir_agent-0.1.0/mimir/skills/onboarding/establishing-schedules.md +262 -0
- mimir_agent-0.1.0/mimir/skills/onboarding/establishing-skills.md +76 -0
- mimir_agent-0.1.0/mimir/skills/pollers/SKILL.md +268 -0
- mimir_agent-0.1.0/mimir/skills/pollers/design-patterns.md +335 -0
- mimir_agent-0.1.0/mimir/skills/pollers/security.md +132 -0
- mimir_agent-0.1.0/mimir/skills/predictions/SKILL.md +175 -0
- mimir_agent-0.1.0/mimir/skills/predictions/script.py +611 -0
- mimir_agent-0.1.0/mimir/skills/review/SKILL.md +186 -0
- mimir_agent-0.1.0/mimir/skills/skill-acquisition/SKILL.md +234 -0
- mimir_agent-0.1.0/mimir/skills/skill-acquisition/clawhub-reference.md +198 -0
- mimir_agent-0.1.0/mimir/skills/skill-acquisition/skillflag-reference.md +217 -0
- mimir_agent-0.1.0/mimir/skills/skill-creator/SKILL.md +126 -0
- mimir_agent-0.1.0/mimir/skills/tmux/SKILL.md +85 -0
- mimir_agent-0.1.0/mimir/skills/try-harder/SKILL.md +212 -0
- mimir_agent-0.1.0/mimir/skills/view-attachment/SKILL.md +104 -0
- mimir_agent-0.1.0/mimir/skills/weather/SKILL.md +73 -0
- mimir_agent-0.1.0/mimir/skills/weather/get_weather.py +95 -0
- mimir_agent-0.1.0/mimir/skills/wiki/SKILL.md +260 -0
- mimir_agent-0.1.0/mimir/skills/world-scanning/SKILL.md +206 -0
- mimir_agent-0.1.0/mimir/stats_block.py +177 -0
- mimir_agent-0.1.0/mimir/subagent_defs.py +234 -0
- mimir_agent-0.1.0/mimir/subagent_inbox.py +106 -0
- mimir_agent-0.1.0/mimir/subagent_stats.py +264 -0
- mimir_agent-0.1.0/mimir/templates/git/gitignore +68 -0
- mimir_agent-0.1.0/mimir/templates/git/pre-commit +107 -0
- mimir_agent-0.1.0/mimir/templates.py +453 -0
- mimir_agent-0.1.0/mimir/token_usage_history.py +127 -0
- mimir_agent-0.1.0/mimir/tools/__init__.py +126 -0
- mimir_agent-0.1.0/mimir/tools/budget_gate.py +229 -0
- mimir_agent-0.1.0/mimir/tools/extra.py +329 -0
- mimir_agent-0.1.0/mimir/tools/mcp.py +38 -0
- mimir_agent-0.1.0/mimir/tools/memory.py +58 -0
- mimir_agent-0.1.0/mimir/tools/prohibited_action_guard.py +126 -0
- mimir_agent-0.1.0/mimir/tools/registry.py +1050 -0
- mimir_agent-0.1.0/mimir/tools/saga_ops.py +238 -0
- mimir_agent-0.1.0/mimir/tools/shell_async.py +220 -0
- mimir_agent-0.1.0/mimir/tools/store.py +83 -0
- mimir_agent-0.1.0/mimir/tools/web.py +518 -0
- mimir_agent-0.1.0/mimir/turn_hooks.py +309 -0
- mimir_agent-0.1.0/mimir/turn_logger.py +531 -0
- mimir_agent-0.1.0/mimir/turn_viewer.html +678 -0
- mimir_agent-0.1.0/mimir/upcoming.py +206 -0
- mimir_agent-0.1.0/mimir/update_on_start.py +454 -0
- mimir_agent-0.1.0/mimir/usage_history.py +249 -0
- mimir_agent-0.1.0/mimir/usage_stats.py +638 -0
- mimir_agent-0.1.0/mimir/version_check.py +306 -0
- mimir_agent-0.1.0/mimir/viability_metrics.py +623 -0
- mimir_agent-0.1.0/mimir/web_ui.py +173 -0
- mimir_agent-0.1.0/mimir/wiki_backlinks.py +490 -0
- mimir_agent-0.1.0/pyproject.toml +206 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
*.egg-info/
|
|
8
|
+
*.egg
|
|
9
|
+
build/
|
|
10
|
+
dist/
|
|
11
|
+
.eggs/
|
|
12
|
+
|
|
13
|
+
# Virtual envs
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
env/
|
|
17
|
+
|
|
18
|
+
# Tools
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.mypy_cache/
|
|
21
|
+
.ruff_cache/
|
|
22
|
+
.tool-versions
|
|
23
|
+
|
|
24
|
+
# IDE
|
|
25
|
+
.vscode/
|
|
26
|
+
.idea/
|
|
27
|
+
*.iml
|
|
28
|
+
*.swp
|
|
29
|
+
*.swo
|
|
30
|
+
|
|
31
|
+
# Claude Code per-project settings (operator/host-specific permissions,
|
|
32
|
+
# allowlists, and hooks — not part of the package contract).
|
|
33
|
+
.claude/
|
|
34
|
+
|
|
35
|
+
# OS
|
|
36
|
+
.DS_Store
|
|
37
|
+
Thumbs.db
|
|
38
|
+
|
|
39
|
+
# Mimir runtime artifacts (only meaningful inside an agent home, never in the
|
|
40
|
+
# package source — but the smoke-test harness occasionally drops them here).
|
|
41
|
+
home/
|
|
42
|
+
*.db
|
|
43
|
+
*.db-shm
|
|
44
|
+
*.db-wal
|
|
45
|
+
*.tar.gz
|
|
46
|
+
|
|
47
|
+
# v0.5 §3: saga's bench data + LongMemEval source are symlinks/dirs
|
|
48
|
+
# pointing at large datasets that don't belong in git (msam2's .gitignore
|
|
49
|
+
# had the same exclusions; preserved post-merge).
|
|
50
|
+
/saga/data
|
|
51
|
+
# saga/external is now vendored (the LongMemEval eval harness lives under
|
|
52
|
+
# saga/external/longmemeval/ — see its PROVENANCE.md). Specific subdirs
|
|
53
|
+
# that are too big or transient stay ignored.
|
|
54
|
+
/saga/external/hindsight
|
|
55
|
+
/saga/results
|
|
56
|
+
# Integration bench output (per-run hypothesis JSONLs + scratch mimir homes).
|
|
57
|
+
/results/
|
|
58
|
+
/benchmarks/longmemeval_via_mimir/results/
|
|
59
|
+
|
|
60
|
+
# Agent home dotenv (in case a smoke run spits one out at workspace root).
|
|
61
|
+
.env
|
|
62
|
+
|
|
63
|
+
# Local scratch / spike / backtest artifacts. Keep WIP scripts and
|
|
64
|
+
# one-shot data dumps out of the repo (e.g. commitments-extraction
|
|
65
|
+
# backtest results, throwaway profiling output).
|
|
66
|
+
/scratch/
|
|
67
|
+
|
|
68
|
+
# Runtime agent state. ``state/`` is per-deployment and accumulates
|
|
69
|
+
# identity bindings, spec results, proposed-change drafts, wiki topics,
|
|
70
|
+
# etc. — none of which belongs in source control. A fresh agent home
|
|
71
|
+
# generates its own ``state/`` on first run.
|
|
72
|
+
/state/
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jason Carreira
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
----
|
|
24
|
+
|
|
25
|
+
The saga/ subdirectory is independently MIT-licensed (combined copyright
|
|
26
|
+
Jaden Schwab + Jason Carreira). See saga/LICENSE.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mimir-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Memory-centric agent harness on deepagents / LangGraph
|
|
5
|
+
Author: Jason Carreira
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: aiohttp>=3.12
|
|
10
|
+
Requires-Dist: apscheduler>=3.11
|
|
11
|
+
Requires-Dist: deepagents>=0.6.1
|
|
12
|
+
Requires-Dist: fastembed>=0.4
|
|
13
|
+
Requires-Dist: langchain>=1.3
|
|
14
|
+
Requires-Dist: python-dotenv>=1.0
|
|
15
|
+
Requires-Dist: pyyaml>=6.0
|
|
16
|
+
Requires-Dist: requests>=2.33.1
|
|
17
|
+
Provides-Extra: anthropic
|
|
18
|
+
Requires-Dist: langchain-anthropic>=1.4; extra == 'anthropic'
|
|
19
|
+
Provides-Extra: bench
|
|
20
|
+
Requires-Dist: saga; extra == 'bench'
|
|
21
|
+
Provides-Extra: codex-plus
|
|
22
|
+
Requires-Dist: langchain-codex-plus<0.1,>=0.0.1; extra == 'codex-plus'
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: discord-py>=2.6; extra == 'dev'
|
|
25
|
+
Requires-Dist: faiss-cpu>=1.13; extra == 'dev'
|
|
26
|
+
Requires-Dist: langchain-anthropic>=1.4; extra == 'dev'
|
|
27
|
+
Requires-Dist: langchain-codex-plus<0.1,>=0.0.1; extra == 'dev'
|
|
28
|
+
Requires-Dist: langchain-openai>=1.2.1; extra == 'dev'
|
|
29
|
+
Requires-Dist: mcp>=1.27; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-aiohttp>=1.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=9.0.3; extra == 'dev'
|
|
33
|
+
Requires-Dist: slack-bolt>=1.18; extra == 'dev'
|
|
34
|
+
Provides-Extra: discord
|
|
35
|
+
Requires-Dist: discord-py>=2.6; extra == 'discord'
|
|
36
|
+
Provides-Extra: mcp
|
|
37
|
+
Requires-Dist: mcp>=1.27; extra == 'mcp'
|
|
38
|
+
Provides-Extra: openai
|
|
39
|
+
Requires-Dist: langchain-openai>=1.2.1; extra == 'openai'
|
|
40
|
+
Provides-Extra: slack
|
|
41
|
+
Requires-Dist: slack-bolt>=1.18; extra == 'slack'
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Per-turn ``TurnContext`` propagation via ``contextvars`` (SPEC §4.6, §9.3).
|
|
2
|
+
|
|
3
|
+
The SAGA ``saga_query`` tool needs to auto-append returned ``atom_id``s to the
|
|
4
|
+
parent's ``TurnContext.saga_atom_ids`` so the post-message hook can credit
|
|
5
|
+
mid-turn retrievals without the agent having to remember (SPEC §9.3 "mid-turn
|
|
6
|
+
``saga_query`` tracking"). Tools registered with the SDK are plain functions —
|
|
7
|
+
they need a way to find the active turn.
|
|
8
|
+
|
|
9
|
+
``contextvars`` are the right primitive for in-task lookups: each ``query()``
|
|
10
|
+
call runs in its own asyncio task, and we set the ContextVar before invoking
|
|
11
|
+
``query()``. Subagent calls run in distinct tasks with distinct contexts, so a
|
|
12
|
+
subagent's ``saga_query`` does NOT mutate the parent's ``saga_atom_ids`` —
|
|
13
|
+
matching SPEC §9.3 "Subagents do not inherit the parent's ``saga_atom_ids``".
|
|
14
|
+
|
|
15
|
+
Hook callbacks (PreToolUse / PostToolUse) are dispatched on a different
|
|
16
|
+
task — the SDK's control-protocol task, forked at first ``client.connect()``.
|
|
17
|
+
That task captured the contextvar value at fork time (``None``) and never
|
|
18
|
+
sees subsequent ``set()`` calls in ``run_turn``, so contextvar lookups from
|
|
19
|
+
hooks return stale data. The ``_active_turns`` map fixes this: ``run_turn``
|
|
20
|
+
registers the turn under its ``turn_id``, hooks pass their incoming
|
|
21
|
+
``session_id`` (which is ``ctx.turn_id`` since stage 2 of the ClaudeSDKClient
|
|
22
|
+
migration) to ``get_turn_by_session_id`` for a reliable lookup that doesn't
|
|
23
|
+
depend on task-fork inheritance.
|
|
24
|
+
|
|
25
|
+
**MCP tool dispatch hits the same pattern (chainlink #23).** Every MCP
|
|
26
|
+
``tools/call`` control request lands in
|
|
27
|
+
``Query._spawn_control_request_handler`` (SDK internals,
|
|
28
|
+
``claude_agent_sdk/_internal/query.py:232``), which calls
|
|
29
|
+
``spawn_detached`` to run the handler on a fresh asyncio task. That task
|
|
30
|
+
captures contextvars from the SDK's read-loop task — forked at connect
|
|
31
|
+
time, where ``_current_turn`` was ``None``. So the same staleness affects
|
|
32
|
+
MCP-dispatched tools (``saga_query``, ``saga_store``, ``saga_feedback``,
|
|
33
|
+
``saga_end_session``) as PreToolUse / PostToolUse hooks.
|
|
34
|
+
|
|
35
|
+
The hook fix uses ``input_data["session_id"]`` which the SDK forwards on
|
|
36
|
+
every hook callback. The MCP path is **asymmetric** — the SDK only
|
|
37
|
+
forwards ``(server_name, mcp_message)`` to the MCP handler; per-call
|
|
38
|
+
session_id is dropped at the boundary. So MCP tools can't use the same
|
|
39
|
+
fix shape as hooks.
|
|
40
|
+
|
|
41
|
+
The two helpers below cover the lookups available to MCP tool handlers:
|
|
42
|
+
|
|
43
|
+
- ``get_turn_by_saga_session_id(saga_session_id)`` — for tools whose args
|
|
44
|
+
carry the saga_session_id (currently just ``saga_end_session``). Iterates
|
|
45
|
+
``_active_turns`` matching ``ctx.saga_session_id``.
|
|
46
|
+
- ``get_only_active_turn()`` — best-effort heuristic for tools whose args
|
|
47
|
+
don't carry any per-turn key. Returns the single active turn if exactly
|
|
48
|
+
one is registered, else ``None``. Works in single-channel deployments;
|
|
49
|
+
multi-active cases must be surfaced via observability events rather than
|
|
50
|
+
silently picking one.
|
|
51
|
+
|
|
52
|
+
See ``state/spec/chainlink-23-saga-mcp-context-resolution.md`` for the
|
|
53
|
+
full design and the per-tool migration sequence.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
from contextvars import ContextVar, Token
|
|
59
|
+
from typing import TYPE_CHECKING, Any
|
|
60
|
+
|
|
61
|
+
if TYPE_CHECKING:
|
|
62
|
+
from .models import TurnContext
|
|
63
|
+
|
|
64
|
+
_current_turn: ContextVar["TurnContext | None"] = ContextVar(
|
|
65
|
+
"mimir_current_turn", default=None
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Registry of active turns keyed by turn_id. Populated by ``run_turn``,
|
|
69
|
+
# read by hook callbacks that can't rely on contextvar inheritance.
|
|
70
|
+
_active_turns: dict[str, "TurnContext"] = {}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class _TurnCell:
|
|
74
|
+
"""Per-client mutable holder for the currently-acquired turn id.
|
|
75
|
+
|
|
76
|
+
The SDK's hook control task is forked at first ``client.connect()``
|
|
77
|
+
and captures the surrounding ``ContextVar`` values at that instant.
|
|
78
|
+
A plain ``_current_turn`` value frozen at fork time is useless —
|
|
79
|
+
later turns can't update what the hook task sees.
|
|
80
|
+
|
|
81
|
+
A ``_TurnCell`` flips that property: the contextvar holds a *cell
|
|
82
|
+
reference*, which the hook task captures. The cell's ``turn_id``
|
|
83
|
+
attribute is mutable. Mimir stamps it on ``acquire`` and clears it
|
|
84
|
+
on ``release``; the hook reads ``cell.turn_id`` lazily and gets the
|
|
85
|
+
live value.
|
|
86
|
+
|
|
87
|
+
One cell per pooled client (created when the client is constructed),
|
|
88
|
+
so multi-channel concurrent turns each have their own cell — the
|
|
89
|
+
hook task on client A only sees writes from acquires of client A.
|
|
90
|
+
Cell reads/writes are bare attribute accesses (atomic under the GIL);
|
|
91
|
+
no lock needed because each cell has at most one acquire-stamping
|
|
92
|
+
task at a time (the pool serializes acquire of a given entry)."""
|
|
93
|
+
|
|
94
|
+
__slots__ = ("turn_id",)
|
|
95
|
+
|
|
96
|
+
def __init__(self) -> None:
|
|
97
|
+
self.turn_id: str | None = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ContextVar set by the pool before each new client's ``connect()`` so
|
|
101
|
+
# the SDK's forked hook task captures *this client's* cell. The cell
|
|
102
|
+
# stays None outside of a per-client connect.
|
|
103
|
+
_current_client_cell: ContextVar["_TurnCell | None"] = ContextVar(
|
|
104
|
+
"mimir_current_client_cell", default=None
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def set_current_turn(ctx: "TurnContext") -> Token:
|
|
109
|
+
_active_turns[ctx.turn_id] = ctx
|
|
110
|
+
return _current_turn.set(ctx)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def reset_current_turn(token: Token) -> None:
|
|
114
|
+
ctx = _current_turn.get()
|
|
115
|
+
if ctx is not None:
|
|
116
|
+
_active_turns.pop(ctx.turn_id, None)
|
|
117
|
+
_current_turn.reset(token)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_current_turn() -> "TurnContext | None":
|
|
121
|
+
return _current_turn.get()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_turn_by_session_id(session_id: str | None) -> "TurnContext | None":
|
|
125
|
+
"""Look up an active turn by its ``turn_id``. Used by hook callbacks
|
|
126
|
+
where contextvar inheritance is unreliable (the hook task forked at
|
|
127
|
+
first connect, captured contextvar=None, and never sees later sets).
|
|
128
|
+
Returns ``None`` if the session is unknown — caller should treat
|
|
129
|
+
that as "no active turn" and skip per-turn enforcement."""
|
|
130
|
+
if not session_id:
|
|
131
|
+
return None
|
|
132
|
+
return _active_turns.get(session_id)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_turn_by_saga_session_id(saga_session_id: str | None) -> "TurnContext | None":
|
|
136
|
+
"""Look up an active turn by its ``saga_session_id``. Used by MCP
|
|
137
|
+
tool handlers whose args carry a ``saga_session_id`` (currently
|
|
138
|
+
``saga_end_session``) where the SDK's task-fork dispatch breaks
|
|
139
|
+
contextvar inheritance (chainlink #23).
|
|
140
|
+
|
|
141
|
+
Iterates ``_active_turns.values()`` rather than maintaining a parallel
|
|
142
|
+
registry — active_turns is bounded by the dispatcher's per-channel
|
|
143
|
+
queue size (typically 1-3 in production), so the linear scan is cheap.
|
|
144
|
+
|
|
145
|
+
Returns ``None`` when ``saga_session_id`` is empty / None, or when no
|
|
146
|
+
active turn matches. Caller should fall back to ``get_current_turn``
|
|
147
|
+
(which works for direct-handler-call paths, e.g. unit tests) or
|
|
148
|
+
treat as "no active turn" and skip per-turn bookkeeping."""
|
|
149
|
+
if not saga_session_id:
|
|
150
|
+
return None
|
|
151
|
+
for ctx in _active_turns.values():
|
|
152
|
+
if ctx.saga_session_id == saga_session_id:
|
|
153
|
+
return ctx
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_only_active_turn() -> "TurnContext | None":
|
|
158
|
+
"""Return the unique active turn if exactly one is registered, else
|
|
159
|
+
``None``. Best-effort heuristic for MCP tool handlers whose args
|
|
160
|
+
don't carry any per-turn lookup key (``saga_query``, ``saga_store``,
|
|
161
|
+
``saga_feedback``) — works in single-channel deployments where
|
|
162
|
+
concurrent turns are serialized by the dispatcher.
|
|
163
|
+
|
|
164
|
+
Multi-active cases (multiple channels with concurrent in-flight
|
|
165
|
+
turns) return ``None`` rather than guessing — callers should emit a
|
|
166
|
+
``resolution_path`` observability event so the rate at which the
|
|
167
|
+
heuristic punts is visible. See chainlink #23 design doc."""
|
|
168
|
+
if len(_active_turns) == 1:
|
|
169
|
+
return next(iter(_active_turns.values()))
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def resolve_active_ctx(args: dict[str, Any]) -> tuple["TurnContext | None", str]:
|
|
174
|
+
"""Standard three-level lookup chain for MCP tool handlers running
|
|
175
|
+
on a forked task that can't see ``_current_turn``.
|
|
176
|
+
|
|
177
|
+
Tries:
|
|
178
|
+
|
|
179
|
+
1. ``args["session_id"]`` (model-passed via Option P) → match against
|
|
180
|
+
``ctx.saga_session_id`` in ``_active_turns``. Multi-channel safe.
|
|
181
|
+
2. ``get_only_active_turn()`` heuristic — the unique active turn if
|
|
182
|
+
exactly one is registered. Works in single-channel deployments;
|
|
183
|
+
returns None when 0 or >1 turns are active.
|
|
184
|
+
3. ``get_current_turn()`` contextvar — works for the direct-handler-
|
|
185
|
+
call test path. Won't fire under SDK dispatch.
|
|
186
|
+
|
|
187
|
+
Returns ``(ctx, resolution_path)`` where resolution_path is one of
|
|
188
|
+
``"saga_session_id" | "single_active" | "contextvar" | "missing"``.
|
|
189
|
+
The path is logged via per-tool ``<tool>_ctx_resolution`` events so
|
|
190
|
+
the rate of each path is visible in events.jsonl.
|
|
191
|
+
|
|
192
|
+
Mirrors the chainlink #23 sagatools resolution chain; lifted here
|
|
193
|
+
so any new MCP-dispatched tool (currently the bash_async family)
|
|
194
|
+
can use the same shape without duplicating the logic.
|
|
195
|
+
"""
|
|
196
|
+
sid = args.get("session_id") if args else None
|
|
197
|
+
ctx = get_turn_by_saga_session_id(sid) if sid else None
|
|
198
|
+
if ctx is not None:
|
|
199
|
+
return ctx, "saga_session_id"
|
|
200
|
+
ctx = get_only_active_turn()
|
|
201
|
+
if ctx is not None:
|
|
202
|
+
return ctx, "single_active"
|
|
203
|
+
ctx = get_current_turn()
|
|
204
|
+
if ctx is not None:
|
|
205
|
+
return ctx, "contextvar"
|
|
206
|
+
return None, "missing"
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Streaming tail-reader for JSONL files.
|
|
2
|
+
|
|
3
|
+
Used by ``feedback.py`` (recent feedback signals from events.jsonl /
|
|
4
|
+
turns.jsonl) and ``session_boundary_log.py`` (local mirror tail). Both
|
|
5
|
+
read newest-first up to a small bound — typically ≤ 20 records — but
|
|
6
|
+
the underlying file can grow unbounded (events.jsonl is the firehose).
|
|
7
|
+
Loading the whole file into memory per turn is an O(file_size) memory
|
|
8
|
+
spike on every prompt assembly; this module reads from the tail in
|
|
9
|
+
chunks so memory use stays O(chunk_size) regardless of file length.
|
|
10
|
+
|
|
11
|
+
Usage::
|
|
12
|
+
|
|
13
|
+
for rec in tail_jsonl_records(path):
|
|
14
|
+
if too_old(rec):
|
|
15
|
+
break
|
|
16
|
+
consume(rec)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Iterator
|
|
25
|
+
|
|
26
|
+
log = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# 8 KiB per chunk. Typical JSONL records are 100 B–4 KB, so each chunk
|
|
29
|
+
# yields multiple records; a small bound (≤ ~20 records) almost always
|
|
30
|
+
# resolves in one chunk read. Bigger chunks waste memory on small files;
|
|
31
|
+
# smaller chunks waste seeks.
|
|
32
|
+
_CHUNK_BYTES = 8192
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def tail_jsonl_records(path: Path) -> Iterator[dict]:
|
|
36
|
+
"""Yield JSON-decoded records from ``path`` newest-first.
|
|
37
|
+
|
|
38
|
+
Streams chunks from the end of the file rather than reading the
|
|
39
|
+
whole file into memory. Skips lines that fail to JSON-decode (the
|
|
40
|
+
firehose may have torn lines from a crash). Yields nothing when the
|
|
41
|
+
file is missing or unreadable.
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
for line in _tail_lines(path):
|
|
45
|
+
line = line.strip()
|
|
46
|
+
if not line:
|
|
47
|
+
continue
|
|
48
|
+
try:
|
|
49
|
+
yield json.loads(line)
|
|
50
|
+
except json.JSONDecodeError:
|
|
51
|
+
continue
|
|
52
|
+
except OSError:
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def count_lines_chunked(path: Path, *, chunk_bytes: int = 65536) -> int:
|
|
57
|
+
"""Count newline-terminated lines in ``path`` without reading the
|
|
58
|
+
whole file into memory.
|
|
59
|
+
|
|
60
|
+
Streams the file in 64 KiB chunks and counts ``\\n`` bytes. Used
|
|
61
|
+
by ``EventLogger`` and ``TurnLogger`` at startup to learn how many
|
|
62
|
+
records the existing log holds (so the trim hysteresis knows when
|
|
63
|
+
to fire) without a multi-hundred-MB memory spike on a hot log.
|
|
64
|
+
|
|
65
|
+
A trailing line without a final newline is counted as one more
|
|
66
|
+
record — that's the shape of a torn write that left the file
|
|
67
|
+
without a final ``\\n``. ``wc -l`` would *miss* this one; we don't.
|
|
68
|
+
|
|
69
|
+
**Behavior note (vs. previous splitlines+strip implementation).**
|
|
70
|
+
The old code dropped blank lines (``if line.strip()``); this
|
|
71
|
+
counter does not — every ``\\n`` is one record. On a clean
|
|
72
|
+
firehose those produce identical counts, but a torn write that
|
|
73
|
+
leaves a bare ``\\n`` in the middle, or an external process
|
|
74
|
+
appending an empty line, will over-count by one here. The trim
|
|
75
|
+
hysteresis absorbs over-counts (a one-line drift just makes the
|
|
76
|
+
next trim fire a hair sooner — not load-bearing).
|
|
77
|
+
|
|
78
|
+
Returns 0 for missing or unreadable files.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
with path.open("rb") as f:
|
|
82
|
+
count = 0
|
|
83
|
+
had_data = False
|
|
84
|
+
last_byte: bytes = b""
|
|
85
|
+
while True:
|
|
86
|
+
chunk = f.read(chunk_bytes)
|
|
87
|
+
if not chunk:
|
|
88
|
+
break
|
|
89
|
+
had_data = True
|
|
90
|
+
count += chunk.count(b"\n")
|
|
91
|
+
last_byte = chunk[-1:]
|
|
92
|
+
# File ended with content but no trailing newline → that's
|
|
93
|
+
# one more record (the pattern most JSONL appenders never
|
|
94
|
+
# produce, but a torn write can leave one behind).
|
|
95
|
+
if had_data and last_byte != b"\n":
|
|
96
|
+
count += 1
|
|
97
|
+
return count
|
|
98
|
+
except OSError:
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _tail_lines(path: Path) -> Iterator[str]:
|
|
103
|
+
"""Yield lines from ``path`` newest-first, reading in fixed-size
|
|
104
|
+
chunks from the end. Lines that span chunk boundaries are stitched
|
|
105
|
+
correctly via a leading-fragment buffer.
|
|
106
|
+
|
|
107
|
+
OSError is propagated to the caller (``tail_jsonl_records`` swallows
|
|
108
|
+
it); we don't paper over a deleted file mid-iteration here.
|
|
109
|
+
"""
|
|
110
|
+
with path.open("rb") as f:
|
|
111
|
+
f.seek(0, 2) # SEEK_END
|
|
112
|
+
pos = f.tell()
|
|
113
|
+
leading_fragment = b""
|
|
114
|
+
while pos > 0:
|
|
115
|
+
read_size = min(_CHUNK_BYTES, pos)
|
|
116
|
+
pos -= read_size
|
|
117
|
+
f.seek(pos)
|
|
118
|
+
chunk = f.read(read_size) + leading_fragment
|
|
119
|
+
lines = chunk.split(b"\n")
|
|
120
|
+
# If we haven't reached BOF, the first split is a partial
|
|
121
|
+
# line whose head lives in an earlier chunk — defer it.
|
|
122
|
+
if pos > 0:
|
|
123
|
+
leading_fragment = lines[0]
|
|
124
|
+
lines = lines[1:]
|
|
125
|
+
else:
|
|
126
|
+
leading_fragment = b""
|
|
127
|
+
# Yield in reverse so the caller sees newest-first.
|
|
128
|
+
for raw in reversed(lines):
|
|
129
|
+
if raw:
|
|
130
|
+
yield raw.decode("utf-8", errors="replace")
|
|
131
|
+
# Once we've reached BOF, the leading fragment is the very first
|
|
132
|
+
# line of the file — yield it last (it's the oldest record).
|
|
133
|
+
if leading_fragment:
|
|
134
|
+
yield leading_fragment.decode("utf-8", errors="replace")
|