docpull 5.0.0__tar.gz → 5.0.2__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.
Files changed (159) hide show
  1. {docpull-5.0.0/src/docpull.egg-info → docpull-5.0.2}/PKG-INFO +2 -2
  2. {docpull-5.0.0 → docpull-5.0.2}/README.md +1 -1
  3. {docpull-5.0.0 → docpull-5.0.2}/pyproject.toml +1 -1
  4. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/__init__.py +6 -1
  5. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/cli.py +5 -0
  6. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/mcp/server.py +281 -71
  7. docpull-5.0.2/src/docpull/share.py +547 -0
  8. {docpull-5.0.0 → docpull-5.0.2/src/docpull.egg-info}/PKG-INFO +2 -2
  9. {docpull-5.0.0 → docpull-5.0.2}/src/docpull.egg-info/SOURCES.txt +2 -0
  10. {docpull-5.0.0 → docpull-5.0.2}/tests/test_mcp_server.py +65 -0
  11. docpull-5.0.2/tests/test_share.py +101 -0
  12. {docpull-5.0.0 → docpull-5.0.2}/tests/test_surface_contract.py +5 -0
  13. {docpull-5.0.0 → docpull-5.0.2}/LICENSE +0 -0
  14. {docpull-5.0.0 → docpull-5.0.2}/setup.cfg +0 -0
  15. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/__main__.py +0 -0
  16. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/accounting.py +0 -0
  17. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/auth_cli.py +0 -0
  18. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/benchmark.py +0 -0
  19. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/cache/__init__.py +0 -0
  20. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/cache/frontier.py +0 -0
  21. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/cache/manager.py +0 -0
  22. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/cache/streaming_dedup.py +0 -0
  23. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/conversion/__init__.py +0 -0
  24. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/conversion/chunking.py +0 -0
  25. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/conversion/extractor.py +0 -0
  26. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/conversion/filings.py +0 -0
  27. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/conversion/markdown.py +0 -0
  28. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/conversion/protocols.py +0 -0
  29. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/conversion/special_cases.py +0 -0
  30. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/conversion/trafilatura_extractor.py +0 -0
  31. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/core/__init__.py +0 -0
  32. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/core/fetcher.py +0 -0
  33. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/discovery/__init__.py +0 -0
  34. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/discovery/_fetch.py +0 -0
  35. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/discovery/composite.py +0 -0
  36. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/discovery/contracts.py +0 -0
  37. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/discovery/crawler.py +0 -0
  38. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/discovery/filters.py +0 -0
  39. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/discovery/link_extractors/__init__.py +0 -0
  40. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/discovery/link_extractors/enhanced.py +0 -0
  41. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/discovery/link_extractors/protocols.py +0 -0
  42. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/discovery/link_extractors/static.py +0 -0
  43. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/discovery/protocols.py +0 -0
  44. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/discovery/sitemap.py +0 -0
  45. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/discovery_cli.py +0 -0
  46. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/doctor.py +0 -0
  47. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/evidence_pack.py +0 -0
  48. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/exports.py +0 -0
  49. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/fixtures/__init__.py +0 -0
  50. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/fixtures/parallel-search-extract.json +0 -0
  51. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/graph.py +0 -0
  52. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/http/__init__.py +0 -0
  53. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/http/client.py +0 -0
  54. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/http/protocols.py +0 -0
  55. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/http/rate_limiter.py +0 -0
  56. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/judge.py +0 -0
  57. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/local_workflows.py +0 -0
  58. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/mcp/__init__.py +0 -0
  59. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/mcp/sources.py +0 -0
  60. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/mcp/tools.py +0 -0
  61. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/metadata_extractor.py +0 -0
  62. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/models/__init__.py +0 -0
  63. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/models/config.py +0 -0
  64. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/models/document.py +0 -0
  65. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/models/events.py +0 -0
  66. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/models/profiles.py +0 -0
  67. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/models/run.py +0 -0
  68. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/monitor.py +0 -0
  69. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/pack_reader.py +0 -0
  70. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/pack_tools.py +0 -0
  71. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/parallel_workflows.py +0 -0
  72. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/parity.py +0 -0
  73. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/parity_cli.py +0 -0
  74. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/passk.py +0 -0
  75. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/pipeline/__init__.py +0 -0
  76. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/pipeline/base.py +0 -0
  77. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/pipeline/manifest.py +0 -0
  78. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/pipeline/steps/__init__.py +0 -0
  79. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/pipeline/steps/chunk.py +0 -0
  80. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/pipeline/steps/convert.py +0 -0
  81. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/pipeline/steps/dedup.py +0 -0
  82. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/pipeline/steps/fetch.py +0 -0
  83. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/pipeline/steps/metadata.py +0 -0
  84. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/pipeline/steps/render.py +0 -0
  85. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/pipeline/steps/save.py +0 -0
  86. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/pipeline/steps/save_json.py +0 -0
  87. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/pipeline/steps/save_ndjson.py +0 -0
  88. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/pipeline/steps/save_okf.py +0 -0
  89. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/pipeline/steps/save_sqlite.py +0 -0
  90. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/pipeline/steps/validate.py +0 -0
  91. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/policy.py +0 -0
  92. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/policy_cli.py +0 -0
  93. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/provider_adapters.py +0 -0
  94. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/provider_capabilities.py +0 -0
  95. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/provider_cli.py +0 -0
  96. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/provider_keys.py +0 -0
  97. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/provider_probes.py +0 -0
  98. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/py.typed +0 -0
  99. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/rendering.py +0 -0
  100. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/scraper.py +0 -0
  101. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/security/__init__.py +0 -0
  102. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/security/download_policy.py +0 -0
  103. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/security/robots.py +0 -0
  104. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/security/url_validator.py +0 -0
  105. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/server.py +0 -0
  106. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/skill_export.py +0 -0
  107. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/source_scoring.py +0 -0
  108. {docpull-5.0.0 → docpull-5.0.2}/src/docpull/time_utils.py +0 -0
  109. {docpull-5.0.0 → docpull-5.0.2}/src/docpull.egg-info/dependency_links.txt +0 -0
  110. {docpull-5.0.0 → docpull-5.0.2}/src/docpull.egg-info/entry_points.txt +0 -0
  111. {docpull-5.0.0 → docpull-5.0.2}/src/docpull.egg-info/requires.txt +0 -0
  112. {docpull-5.0.0 → docpull-5.0.2}/src/docpull.egg-info/top_level.txt +0 -0
  113. {docpull-5.0.0 → docpull-5.0.2}/tests/test_accounting.py +0 -0
  114. {docpull-5.0.0 → docpull-5.0.2}/tests/test_auth_cli.py +0 -0
  115. {docpull-5.0.0 → docpull-5.0.2}/tests/test_benchmark.py +0 -0
  116. {docpull-5.0.0 → docpull-5.0.2}/tests/test_cache_conditional_get.py +0 -0
  117. {docpull-5.0.0 → docpull-5.0.2}/tests/test_chunking.py +0 -0
  118. {docpull-5.0.0 → docpull-5.0.2}/tests/test_ci_policy.py +0 -0
  119. {docpull-5.0.0 → docpull-5.0.2}/tests/test_cli.py +0 -0
  120. {docpull-5.0.0 → docpull-5.0.2}/tests/test_conversion.py +0 -0
  121. {docpull-5.0.0 → docpull-5.0.2}/tests/test_convert_step_new.py +0 -0
  122. {docpull-5.0.0 → docpull-5.0.2}/tests/test_discovery.py +0 -0
  123. {docpull-5.0.0 → docpull-5.0.2}/tests/test_discovery_contracts.py +0 -0
  124. {docpull-5.0.0 → docpull-5.0.2}/tests/test_doctor.py +0 -0
  125. {docpull-5.0.0 → docpull-5.0.2}/tests/test_document_record.py +0 -0
  126. {docpull-5.0.0 → docpull-5.0.2}/tests/test_evidence_pack.py +0 -0
  127. {docpull-5.0.0 → docpull-5.0.2}/tests/test_exports.py +0 -0
  128. {docpull-5.0.0 → docpull-5.0.2}/tests/test_frontier_resume.py +0 -0
  129. {docpull-5.0.0 → docpull-5.0.2}/tests/test_graph.py +0 -0
  130. {docpull-5.0.0 → docpull-5.0.2}/tests/test_integration.py +0 -0
  131. {docpull-5.0.0 → docpull-5.0.2}/tests/test_judge.py +0 -0
  132. {docpull-5.0.0 → docpull-5.0.2}/tests/test_link_extractors.py +0 -0
  133. {docpull-5.0.0 → docpull-5.0.2}/tests/test_local_workflows.py +0 -0
  134. {docpull-5.0.0 → docpull-5.0.2}/tests/test_mcp_tools.py +0 -0
  135. {docpull-5.0.0 → docpull-5.0.2}/tests/test_metadata_extractor.py +0 -0
  136. {docpull-5.0.0 → docpull-5.0.2}/tests/test_monitor.py +0 -0
  137. {docpull-5.0.0 → docpull-5.0.2}/tests/test_naming.py +0 -0
  138. {docpull-5.0.0 → docpull-5.0.2}/tests/test_outputs_e2e.py +0 -0
  139. {docpull-5.0.0 → docpull-5.0.2}/tests/test_pack_server.py +0 -0
  140. {docpull-5.0.0 → docpull-5.0.2}/tests/test_pack_tools.py +0 -0
  141. {docpull-5.0.0 → docpull-5.0.2}/tests/test_parallel_workflows.py +0 -0
  142. {docpull-5.0.0 → docpull-5.0.2}/tests/test_parity_workflows.py +0 -0
  143. {docpull-5.0.0 → docpull-5.0.2}/tests/test_passk.py +0 -0
  144. {docpull-5.0.0 → docpull-5.0.2}/tests/test_pipeline.py +0 -0
  145. {docpull-5.0.0 → docpull-5.0.2}/tests/test_policy.py +0 -0
  146. {docpull-5.0.0 → docpull-5.0.2}/tests/test_policy_cli.py +0 -0
  147. {docpull-5.0.0 → docpull-5.0.2}/tests/test_provider_adapters.py +0 -0
  148. {docpull-5.0.0 → docpull-5.0.2}/tests/test_provider_cli.py +0 -0
  149. {docpull-5.0.0 → docpull-5.0.2}/tests/test_provider_keys.py +0 -0
  150. {docpull-5.0.0 → docpull-5.0.2}/tests/test_provider_probes.py +0 -0
  151. {docpull-5.0.0 → docpull-5.0.2}/tests/test_rate_limiter.py +0 -0
  152. {docpull-5.0.0 → docpull-5.0.2}/tests/test_real_site_regressions.py +0 -0
  153. {docpull-5.0.0 → docpull-5.0.2}/tests/test_rendering.py +0 -0
  154. {docpull-5.0.0 → docpull-5.0.2}/tests/test_save_ndjson.py +0 -0
  155. {docpull-5.0.0 → docpull-5.0.2}/tests/test_save_sqlite.py +0 -0
  156. {docpull-5.0.0 → docpull-5.0.2}/tests/test_security_hardening.py +0 -0
  157. {docpull-5.0.0 → docpull-5.0.2}/tests/test_special_cases.py +0 -0
  158. {docpull-5.0.0 → docpull-5.0.2}/tests/test_time_utils.py +0 -0
  159. {docpull-5.0.0 → docpull-5.0.2}/tests/test_trafilatura_extractor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docpull
3
- Version: 5.0.0
3
+ Version: 5.0.2
4
4
  Summary: Convert public web pages into clean Markdown for AI agents and RAG
5
5
  Author-email: Zachary Roth <support@raintree.technology>
6
6
  Maintainer-email: Raintree Technology <support@raintree.technology>
@@ -501,7 +501,7 @@ part of the package release contract.
501
501
  rendering, authenticated-source checks, and cron-friendly monitors:
502
502
  `docpull policy`, `docpull discover`, `docpull refresh`,
503
503
  `docpull pack audit`, `docpull answer-pack`, `docpull export`,
504
- `docpull serve`, `docpull render`, `docpull auth check`, and
504
+ `docpull serve`, `docpull share`, `docpull render`, `docpull auth check`, and
505
505
  `docpull monitor`.
506
506
  - `docpull export` writes local files for OpenAI vector JSONL, LangChain,
507
507
  LlamaIndex, DSPy, Sheets CSV/TSV, n8n workflow JSON, Vercel AI SDK JSON,
@@ -389,7 +389,7 @@ part of the package release contract.
389
389
  rendering, authenticated-source checks, and cron-friendly monitors:
390
390
  `docpull policy`, `docpull discover`, `docpull refresh`,
391
391
  `docpull pack audit`, `docpull answer-pack`, `docpull export`,
392
- `docpull serve`, `docpull render`, `docpull auth check`, and
392
+ `docpull serve`, `docpull share`, `docpull render`, `docpull auth check`, and
393
393
  `docpull monitor`.
394
394
  - `docpull export` writes local files for OpenAI vector JSONL, LangChain,
395
395
  LlamaIndex, DSPy, Sheets CSV/TSV, n8n workflow JSON, Vercel AI SDK JSON,
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "docpull"
7
- version = "5.0.0"
7
+ version = "5.0.2"
8
8
  dynamic = []
9
9
  description = "Convert public web pages into clean Markdown for AI agents and RAG"
10
10
  readme = {file = "README.md", content-type = "text/markdown"}
@@ -14,7 +14,7 @@ Usage:
14
14
  print(event)
15
15
  """
16
16
 
17
- __version__ = "5.0.0"
17
+ __version__ = "5.0.2"
18
18
 
19
19
  from .cache import CacheManager, StreamingDeduplicator
20
20
  from .conversion.chunking import Chunk, TokenCounter, chunk_markdown
@@ -94,6 +94,7 @@ from .scraper import (
94
94
  scrape_site,
95
95
  )
96
96
  from .server import PackASGIApp, PackServerError, create_pack_app
97
+ from .share import ReportHTTPServer, ShareError, create_report_server, render_report_document
97
98
 
98
99
  __all__ = [
99
100
  "__version__",
@@ -134,6 +135,10 @@ __all__ = [
134
135
  "create_pack_app",
135
136
  "PackASGIApp",
136
137
  "PackServerError",
138
+ "create_report_server",
139
+ "render_report_document",
140
+ "ReportHTTPServer",
141
+ "ShareError",
137
142
  "ScrapeResult",
138
143
  "ScrapeRunResult",
139
144
  "Scraper",
@@ -206,6 +206,7 @@ Subcommands:
206
206
  answer-pack Answer from local pack evidence with citations
207
207
  export Export a pack for agent or RAG tools
208
208
  serve Serve a local pack over localhost JSON routes
209
+ share Serve a Markdown or HTML report at a local URL
209
210
  monitor Run cron-friendly local pack monitors
210
211
  parallel Build optional Parallel-backed context packs
211
212
  provider Check provider keys and run provider-backed context packs
@@ -1369,6 +1370,10 @@ def main(argv: list[str] | None = None) -> int:
1369
1370
  from .server import run_serve_cli
1370
1371
 
1371
1372
  return run_serve_cli(raw_argv[1:])
1373
+ if raw_argv and raw_argv[0] == "share":
1374
+ from .share import run_share_cli
1375
+
1376
+ return run_share_cli(raw_argv[1:])
1372
1377
  if raw_argv and raw_argv[0] == "export":
1373
1378
  from .exports import run_export_cli
1374
1379
 
@@ -48,6 +48,7 @@ from __future__ import annotations
48
48
 
49
49
  import argparse
50
50
  import asyncio
51
+ import json
51
52
  import logging
52
53
  import sys
53
54
  from collections.abc import Awaitable, Callable
@@ -55,9 +56,14 @@ from pathlib import Path
55
56
  from typing import Any, Literal, cast
56
57
 
57
58
  from ..accounting import (
59
+ ACCOUNTING_ARTIFACT,
60
+ RunAccounting,
61
+ blocked_action,
58
62
  budget_block_payload,
63
+ default_route_steps,
59
64
  effective_budget_limit,
60
- enforce_paid_budget,
65
+ maybe_write_run_accounting,
66
+ paid_action_blocked,
61
67
  )
62
68
  from ..discovery import (
63
69
  CandidateSourceRecord,
@@ -136,6 +142,42 @@ SERVER_INSTRUCTIONS = (
136
142
  # Tools that return free-form Markdown (fetch_url) intentionally omit a
137
143
  # schema; the rest expose structured payloads alongside the rendered text.
138
144
 
145
+ _ACCOUNTING_OUTPUT_SCHEMA = {
146
+ "type": "object",
147
+ "description": (
148
+ "Non-secret run accounting receipt. Mirrors run.accounting.json when an artifact was written."
149
+ ),
150
+ "properties": {
151
+ "schema_version": {"type": "integer"},
152
+ "generated_at": {"type": "string"},
153
+ "budget_limit_usd": {"type": ["number", "null"]},
154
+ "estimated_paid_cost_usd": {"type": "number"},
155
+ "actual_paid_cost_usd": {"type": ["number", "null"]},
156
+ "paid_request_count": {"type": "integer"},
157
+ "local_browser_seconds": {"type": "number"},
158
+ "http_request_count": {"type": "integer"},
159
+ "cache_hit_count": {"type": "integer"},
160
+ "blocked_actions": {"type": "array", "items": {"type": "object"}},
161
+ "route_steps": {"type": "array", "items": {"type": "object"}},
162
+ "command": {"type": "string"},
163
+ "metadata": {"type": "object"},
164
+ "artifact_path": {"type": "string"},
165
+ },
166
+ "required": [
167
+ "schema_version",
168
+ "generated_at",
169
+ "budget_limit_usd",
170
+ "estimated_paid_cost_usd",
171
+ "actual_paid_cost_usd",
172
+ "paid_request_count",
173
+ "local_browser_seconds",
174
+ "http_request_count",
175
+ "cache_hit_count",
176
+ "blocked_actions",
177
+ "route_steps",
178
+ ],
179
+ }
180
+
139
181
  _LIST_SOURCES_OUTPUT_SCHEMA = {
140
182
  "type": "object",
141
183
  "properties": {
@@ -274,6 +316,7 @@ _PARALLEL_PACK_OUTPUT_SCHEMA = {
274
316
  "output_dir": {"type": "string"},
275
317
  "dry_run": {"type": "boolean"},
276
318
  "estimated_cost_usd": {"type": "number"},
319
+ "accounting": _ACCOUNTING_OUTPUT_SCHEMA,
277
320
  },
278
321
  "required": ["workflow", "output_dir"],
279
322
  }
@@ -470,6 +513,7 @@ _ANSWER_PACK_OUTPUT_SCHEMA = {
470
513
  "search": {"type": "object"},
471
514
  "brief": {"type": "object"},
472
515
  "artifacts": {"type": "object"},
516
+ "accounting": _ACCOUNTING_OUTPUT_SCHEMA,
473
517
  },
474
518
  "required": ["question", "answer", "search", "artifacts"],
475
519
  }
@@ -494,8 +538,14 @@ _RENDER_URL_OUTPUT_SCHEMA = {
494
538
  "sidecar_path": {"type": "string"},
495
539
  "html_bytes": {"type": "integer"},
496
540
  "html_sha256": {"type": "string"},
541
+ "output_dir": {"type": "string"},
542
+ "dry_run": {"type": "boolean"},
543
+ "blocked_by_budget": {"type": "boolean"},
544
+ "budget_limit_usd": {"type": ["number", "null"]},
545
+ "blocked_action": {"type": "object"},
546
+ "accounting": _ACCOUNTING_OUTPUT_SCHEMA,
497
547
  },
498
- "required": ["url", "backend", "html_path", "sidecar_path", "html_bytes", "html_sha256"],
548
+ "required": ["url", "backend"],
499
549
  }
500
550
 
501
551
  _DISCOVER_SOURCES_OUTPUT_SCHEMA = {
@@ -615,6 +665,43 @@ def _policy_arg(arguments: dict[str, Any]) -> PolicyConfig:
615
665
  return PolicyConfig.from_file(policy_path) if policy_path else PolicyConfig()
616
666
 
617
667
 
668
+ def _read_accounting_payload(path: Path) -> dict[str, Any] | None:
669
+ try:
670
+ payload = json.loads(path.read_text(encoding="utf-8"))
671
+ except (OSError, json.JSONDecodeError):
672
+ return None
673
+ if not isinstance(payload, dict):
674
+ return None
675
+ payload["artifact_path"] = str(path)
676
+ return payload
677
+
678
+
679
+ def _mcp_accounting_payload(
680
+ accounting: RunAccounting,
681
+ *,
682
+ output_dir: Path | None = None,
683
+ budget_limit_usd: float | None,
684
+ paid_capable: bool,
685
+ ) -> dict[str, Any]:
686
+ path = None
687
+ if output_dir is not None:
688
+ path = maybe_write_run_accounting(
689
+ output_dir,
690
+ budget_limit_usd=budget_limit_usd,
691
+ paid_capable=paid_capable,
692
+ accounting=accounting,
693
+ )
694
+ if path is not None:
695
+ artifact_payload = _read_accounting_payload(path)
696
+ if artifact_payload is not None:
697
+ return artifact_payload
698
+ return accounting.to_dict()
699
+
700
+
701
+ def _pack_accounting_payload(pack_dir: Path) -> dict[str, Any] | None:
702
+ return _read_accounting_payload(pack_dir / ACCOUNTING_ARTIFACT)
703
+
704
+
618
705
  async def _dispatch_tool(
619
706
  name: str,
620
707
  arguments: dict[str, Any],
@@ -669,6 +756,26 @@ async def _dispatch_tool(
669
756
  cloud_agent_browser_binary = arguments.get("cloud_agent_browser_binary", "agent-browser")
670
757
  if not isinstance(cloud_agent_browser_binary, str):
671
758
  raise ValueError("'cloud_agent_browser_binary' must be a string")
759
+ output_dir = _path_arg(arguments, "output_dir", "rendered")
760
+ estimated = 0.0
761
+ paid_capable_render = backend in {"vercel-sandbox", "e2b-sandbox"}
762
+ accounting = RunAccounting(
763
+ budget_limit_usd=budget_limit,
764
+ estimated_paid_cost_usd=estimated,
765
+ route_steps=default_route_steps(
766
+ include_local_render=True,
767
+ include_cloud=paid_capable_render,
768
+ budget_limit_usd=budget_limit,
769
+ ),
770
+ command="mcp render_url",
771
+ metadata={
772
+ "mcp_tool": "render_url",
773
+ "url": url,
774
+ "runtime": runtime,
775
+ "backend": backend,
776
+ },
777
+ )
778
+ render_blocked = False
672
779
  if backend in {"vercel-sandbox", "e2b-sandbox"}:
673
780
  from ..models.config import RenderConfig
674
781
  from ..rendering import estimate_cloud_render_cost_usd
@@ -682,40 +789,77 @@ async def _dispatch_tool(
682
789
  timeout_seconds=float(timeout_arg),
683
790
  ),
684
791
  )
685
- enforce_paid_budget(
686
- f"render:{backend}",
792
+ accounting.estimated_paid_cost_usd = estimated
793
+ if paid_action_blocked(budget_limit, estimated_cost_usd=estimated):
794
+ render_blocked = True
795
+ accounting.blocked_actions.append(
796
+ blocked_action(
797
+ f"render:{backend}",
798
+ budget_limit_usd=budget_limit,
799
+ estimated_cost_usd=estimated,
800
+ provider=backend,
801
+ )
802
+ )
803
+ accounting_payload = _mcp_accounting_payload(
804
+ accounting,
805
+ output_dir=output_dir,
806
+ budget_limit_usd=budget_limit,
807
+ paid_capable=True,
808
+ )
809
+ result = ToolResult(
810
+ f"Render blocked by budget before {backend}.",
811
+ data={
812
+ "url": url,
813
+ "backend": backend,
814
+ "output_dir": str(output_dir),
815
+ "dry_run": False,
816
+ **budget_block_payload(
817
+ f"render:{backend}",
818
+ budget_limit_usd=budget_limit,
819
+ estimated_cost_usd=estimated,
820
+ provider=backend,
821
+ ),
822
+ "accounting": accounting_payload,
823
+ },
824
+ )
825
+ else:
826
+ accounting.paid_request_count = 1
827
+ if not render_blocked:
828
+ artifact = await render_url_to_directory(
829
+ url,
830
+ output_dir,
831
+ config={
832
+ "mode": "agent-browser",
833
+ "backend": backend,
834
+ "allowed_domains": _string_list_arg(arguments, "allowed_domains"),
835
+ "timeout_seconds": float(timeout_arg),
836
+ "wait_for": wait_for,
837
+ "cloud_agent_browser_install": cloud_agent_browser_install,
838
+ "cloud_result_transport": cloud_result_transport,
839
+ "cloud_max_estimated_cost_usd": cloud_max_estimated_cost,
840
+ "cloud_agent_browser_binary": cloud_agent_browser_binary,
841
+ "e2b_template": template,
842
+ },
843
+ )
844
+ accounting_payload = _mcp_accounting_payload(
845
+ accounting,
846
+ output_dir=output_dir,
687
847
  budget_limit_usd=budget_limit,
688
- estimated_cost_usd=estimated,
689
- provider=backend,
848
+ paid_capable=paid_capable_render,
849
+ )
850
+ payload = {
851
+ "url": artifact.page.url,
852
+ "backend": artifact.page.backend,
853
+ "html_path": str(artifact.html_path),
854
+ "sidecar_path": str(artifact.sidecar_path),
855
+ "html_bytes": artifact.page.html_bytes,
856
+ "html_sha256": artifact.page.html_sha256,
857
+ "accounting": accounting_payload,
858
+ }
859
+ result = ToolResult(
860
+ f"Rendered {artifact.page.html_bytes} bytes: {artifact.html_path}",
861
+ data=payload,
690
862
  )
691
- artifact = await render_url_to_directory(
692
- url,
693
- _path_arg(arguments, "output_dir", "rendered"),
694
- config={
695
- "mode": "agent-browser",
696
- "backend": backend,
697
- "allowed_domains": _string_list_arg(arguments, "allowed_domains"),
698
- "timeout_seconds": float(timeout_arg),
699
- "wait_for": wait_for,
700
- "cloud_agent_browser_install": cloud_agent_browser_install,
701
- "cloud_result_transport": cloud_result_transport,
702
- "cloud_max_estimated_cost_usd": cloud_max_estimated_cost,
703
- "cloud_agent_browser_binary": cloud_agent_browser_binary,
704
- "e2b_template": template,
705
- },
706
- )
707
- payload = {
708
- "url": artifact.page.url,
709
- "backend": artifact.page.backend,
710
- "html_path": str(artifact.html_path),
711
- "sidecar_path": str(artifact.sidecar_path),
712
- "html_bytes": artifact.page.html_bytes,
713
- "html_sha256": artifact.page.html_sha256,
714
- }
715
- result = ToolResult(
716
- f"Rendered {artifact.page.html_bytes} bytes: {artifact.html_path}",
717
- data=payload,
718
- )
719
863
 
720
864
  elif name == "ensure_docs":
721
865
  source = _require_str(arguments, "source")
@@ -794,6 +938,7 @@ async def _dispatch_tool(
794
938
  max_estimated_cost,
795
939
  float(budget_arg) if budget_arg is not None else None,
796
940
  )
941
+ output_dir = _path_arg(arguments, "output_dir", "packs/parallel-context-pack")
797
942
  request_options = _build_request_options(
798
943
  source_policy=source_policy,
799
944
  fetch_policy=fetch_policy,
@@ -808,6 +953,32 @@ async def _dispatch_tool(
808
953
  else None,
809
954
  full_content=True,
810
955
  )
956
+ blocked_by_budget = paid_action_blocked(budget_limit, estimated_cost_usd=estimated_cost)
957
+ accounting = RunAccounting(
958
+ budget_limit_usd=budget_limit,
959
+ estimated_paid_cost_usd=estimated_cost,
960
+ route_steps=default_route_steps(
961
+ include_provider=True,
962
+ budget_limit_usd=budget_limit,
963
+ ),
964
+ command="mcp parallel_context_pack",
965
+ metadata={
966
+ "mcp_tool": "parallel_context_pack",
967
+ "objective": objective,
968
+ "query_count": len(queries),
969
+ "extract_limit": extract_limit,
970
+ "max_search_results": max_search_results,
971
+ },
972
+ )
973
+ if blocked_by_budget:
974
+ accounting.blocked_actions.append(
975
+ blocked_action(
976
+ "parallel:context-pack",
977
+ budget_limit_usd=budget_limit,
978
+ estimated_cost_usd=estimated_cost,
979
+ provider="parallel",
980
+ )
981
+ )
811
982
  if bool(arguments.get("dry_run", False)):
812
983
  blocked = (
813
984
  budget_block_payload(
@@ -816,60 +987,95 @@ async def _dispatch_tool(
816
987
  estimated_cost_usd=estimated_cost,
817
988
  provider="parallel",
818
989
  )
819
- if budget_limit is not None and budget_limit <= 0
990
+ if blocked_by_budget
820
991
  else {}
821
992
  )
993
+ accounting_payload = _mcp_accounting_payload(
994
+ accounting,
995
+ output_dir=None,
996
+ budget_limit_usd=budget_limit,
997
+ paid_capable=True,
998
+ )
822
999
  result = ToolResult(
823
1000
  "Parallel context pack dry run.",
824
1001
  data={
825
1002
  "workflow": "context-pack",
826
- "output_dir": str(_path_arg(arguments, "output_dir", "packs/parallel-context-pack")),
1003
+ "output_dir": str(output_dir),
827
1004
  "dry_run": True,
828
1005
  "estimated_cost_usd": estimated_cost,
829
1006
  "budget_limit_usd": budget_limit,
830
1007
  "request_options": request_options,
831
1008
  **blocked,
1009
+ "accounting": accounting_payload,
832
1010
  },
833
1011
  )
834
1012
  else:
835
- enforce_paid_budget(
836
- "parallel:context-pack",
837
- budget_limit_usd=budget_limit,
838
- estimated_cost_usd=estimated_cost,
839
- provider="parallel",
840
- )
841
- if estimated_cost > max_estimated_cost:
1013
+ if blocked_by_budget:
1014
+ accounting_payload = _mcp_accounting_payload(
1015
+ accounting,
1016
+ output_dir=output_dir,
1017
+ budget_limit_usd=budget_limit,
1018
+ paid_capable=True,
1019
+ )
1020
+ result = ToolResult(
1021
+ "Parallel context pack blocked by budget before provider call.",
1022
+ data={
1023
+ "workflow": "context-pack",
1024
+ "output_dir": str(output_dir),
1025
+ "dry_run": False,
1026
+ "estimated_cost_usd": estimated_cost,
1027
+ "budget_limit_usd": budget_limit,
1028
+ "request_options": request_options,
1029
+ **budget_block_payload(
1030
+ "parallel:context-pack",
1031
+ budget_limit_usd=budget_limit,
1032
+ estimated_cost_usd=estimated_cost,
1033
+ provider="parallel",
1034
+ ),
1035
+ "accounting": accounting_payload,
1036
+ },
1037
+ )
1038
+ elif estimated_cost > max_estimated_cost:
842
1039
  raise ValueError(
843
1040
  f"Estimated Parallel cost ${estimated_cost:.3f} exceeds max_estimated_cost "
844
1041
  f"${max_estimated_cost:.3f}"
845
1042
  )
846
- output_dir = _path_arg(arguments, "output_dir", "packs/parallel-context-pack")
847
- mode_arg = arguments.get("mode")
848
- mode = mode_arg if isinstance(mode_arg, str) else "advanced"
849
- pack_path = await asyncio.to_thread(
850
- run_live_context_pack,
851
- objective=objective,
852
- queries=queries,
853
- output_dir=output_dir,
854
- mode=mode,
855
- extract_limit=extract_limit,
856
- max_tokens_per_file=DEFAULT_MAX_TOKENS,
857
- source_policy=source_policy,
858
- max_search_results=max_search_results,
859
- client_model=arguments.get("client_model")
860
- if isinstance(arguments.get("client_model"), str)
861
- else None,
862
- estimated_cost_usd=estimated_cost,
863
- )
864
- result = ToolResult(
865
- f"Wrote Parallel context pack: {pack_path}",
866
- data={
867
- "workflow": "context-pack",
868
- "output_dir": str(pack_path),
869
- "dry_run": False,
870
- "estimated_cost_usd": estimated_cost,
871
- },
872
- )
1043
+ else:
1044
+ mode_arg = arguments.get("mode")
1045
+ mode = mode_arg if isinstance(mode_arg, str) else "advanced"
1046
+ accounting.paid_request_count = 1
1047
+ pack_path = await asyncio.to_thread(
1048
+ run_live_context_pack,
1049
+ objective=objective,
1050
+ queries=queries,
1051
+ output_dir=output_dir,
1052
+ mode=mode,
1053
+ extract_limit=extract_limit,
1054
+ max_tokens_per_file=DEFAULT_MAX_TOKENS,
1055
+ source_policy=source_policy,
1056
+ max_search_results=max_search_results,
1057
+ client_model=arguments.get("client_model")
1058
+ if isinstance(arguments.get("client_model"), str)
1059
+ else None,
1060
+ estimated_cost_usd=estimated_cost,
1061
+ )
1062
+ accounting.metadata["output_dir"] = str(pack_path)
1063
+ accounting_payload = _mcp_accounting_payload(
1064
+ accounting,
1065
+ output_dir=pack_path,
1066
+ budget_limit_usd=budget_limit,
1067
+ paid_capable=True,
1068
+ )
1069
+ result = ToolResult(
1070
+ f"Wrote Parallel context pack: {pack_path}",
1071
+ data={
1072
+ "workflow": "context-pack",
1073
+ "output_dir": str(pack_path),
1074
+ "dry_run": False,
1075
+ "estimated_cost_usd": estimated_cost,
1076
+ "accounting": accounting_payload,
1077
+ },
1078
+ )
873
1079
 
874
1080
  elif name == "parallel_api_pack":
875
1081
  source = _require_str(arguments, "source")
@@ -1135,13 +1341,17 @@ async def _dispatch_tool(
1135
1341
  )
1136
1342
 
1137
1343
  elif name == "answer_pack":
1344
+ answer_pack_dir = _path_arg(arguments, "pack_dir")
1138
1345
  answer_payload: dict[str, Any] = await asyncio.to_thread(
1139
1346
  answer_pack,
1140
- _path_arg(arguments, "pack_dir"),
1347
+ answer_pack_dir,
1141
1348
  _require_str(arguments, "question"),
1142
1349
  required_domains=_string_list_arg(arguments, "required_domains"),
1143
1350
  limit=_coerce_int(arguments.get("limit"), name="limit", default=8),
1144
1351
  )
1352
+ existing_accounting = _pack_accounting_payload(answer_pack_dir)
1353
+ if existing_accounting is not None:
1354
+ answer_payload["accounting"] = existing_accounting
1145
1355
  answer_raw = answer_payload.get("answer")
1146
1356
  answer_data: dict[str, Any] = answer_raw if isinstance(answer_raw, dict) else {}
1147
1357
  result = ToolResult(