comodor 0.7.0__tar.gz → 0.8.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.
Files changed (147) hide show
  1. {comodor-0.7.0 → comodor-0.8.0}/PKG-INFO +128 -1
  2. {comodor-0.7.0 → comodor-0.8.0}/README.md +127 -0
  3. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/_version.py +2 -2
  4. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/agent/loop.py +24 -6
  5. comodor-0.8.0/src/comodor/agent/spawn.py +64 -0
  6. comodor-0.8.0/src/comodor/browser/__init__.py +7 -0
  7. comodor-0.8.0/src/comodor/browser/cdp.py +309 -0
  8. comodor-0.8.0/src/comodor/browser/launch.py +190 -0
  9. comodor-0.8.0/src/comodor/browser/page.py +356 -0
  10. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/cli.py +11 -1
  11. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/config.py +65 -5
  12. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/mcp/commands.py +50 -4
  13. comodor-0.8.0/src/comodor/mcp/http.py +203 -0
  14. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/mcp/manager.py +14 -4
  15. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/providers/anthropic.py +14 -1
  16. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/providers/openai_compat.py +15 -0
  17. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/skills/commands.py +35 -3
  18. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/skills/loader.py +27 -2
  19. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/skills/registry.py +38 -15
  20. comodor-0.8.0/src/comodor/tools/browse.py +248 -0
  21. comodor-0.8.0/src/comodor/tools/delegate.py +275 -0
  22. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/fs.py +49 -10
  23. comodor-0.8.0/src/comodor/tools/overflow.py +177 -0
  24. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/registry.py +40 -5
  25. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/shell.py +15 -11
  26. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/app.py +4 -0
  27. comodor-0.8.0/src/comodor/web/__init__.py +6 -0
  28. comodor-0.8.0/src/comodor/web/commands.py +85 -0
  29. comodor-0.8.0/src/comodor/web/index.html +330 -0
  30. comodor-0.8.0/src/comodor/web/server.py +252 -0
  31. comodor-0.8.0/src/comodor/web/session.py +243 -0
  32. comodor-0.8.0/tests/test_delegate.py +269 -0
  33. comodor-0.8.0/tests/test_mcp_http.py +354 -0
  34. comodor-0.8.0/tests/test_overflow.py +233 -0
  35. comodor-0.8.0/tests/test_real_browser.py +472 -0
  36. comodor-0.8.0/tests/test_skill_budget.py +223 -0
  37. {comodor-0.7.0 → comodor-0.8.0}/tests/test_skills.py +79 -0
  38. {comodor-0.7.0 → comodor-0.8.0}/tests/test_tools.py +37 -5
  39. comodor-0.8.0/tests/test_web.py +375 -0
  40. {comodor-0.7.0 → comodor-0.8.0}/.gitignore +0 -0
  41. {comodor-0.7.0 → comodor-0.8.0}/LICENSE +0 -0
  42. {comodor-0.7.0 → comodor-0.8.0}/pyproject.toml +0 -0
  43. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/__init__.py +0 -0
  44. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/__main__.py +0 -0
  45. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/agent/__init__.py +0 -0
  46. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/agent/context.py +0 -0
  47. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/agent/prompts.py +0 -0
  48. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/agent/tokens.py +0 -0
  49. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/catalogue.py +0 -0
  50. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/doctor.py +0 -0
  51. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/events.py +0 -0
  52. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/__init__.py +0 -0
  53. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/associations.py +0 -0
  54. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/bm25.py +0 -0
  55. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/hotindex.py +0 -0
  56. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/memory.py +0 -0
  57. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/progress.py +0 -0
  58. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/reflect.py +0 -0
  59. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/rules.py +0 -0
  60. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/signals.py +0 -0
  61. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/store.py +0 -0
  62. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/writer.py +0 -0
  63. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/mcp/__init__.py +0 -0
  64. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/mcp/catalogue.py +0 -0
  65. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/mcp/protocol.py +0 -0
  66. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/net/__init__.py +0 -0
  67. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/net/http.py +0 -0
  68. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/net/sse.py +0 -0
  69. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/paths.py +0 -0
  70. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/providers/__init__.py +0 -0
  71. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/providers/base.py +0 -0
  72. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/providers/caching.py +0 -0
  73. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/providers/fake.py +0 -0
  74. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/providers/gateway.py +0 -0
  75. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/providers/registry.py +0 -0
  76. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/safety/__init__.py +0 -0
  77. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/safety/checkpoints.py +0 -0
  78. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/safety/permissions.py +0 -0
  79. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/safety/redact.py +0 -0
  80. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/session/__init__.py +0 -0
  81. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/session/search.py +0 -0
  82. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/session/store.py +0 -0
  83. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/setup.py +0 -0
  84. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/skills/__init__.py +0 -0
  85. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/skills/catalogue.py +0 -0
  86. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/skills/examples.py +0 -0
  87. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/skills/propose.py +0 -0
  88. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/__init__.py +0 -0
  89. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/base.py +0 -0
  90. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/browser.py +0 -0
  91. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/history.py +0 -0
  92. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/mcp.py +0 -0
  93. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/search.py +0 -0
  94. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/skills.py +0 -0
  95. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/todo.py +0 -0
  96. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/web.py +0 -0
  97. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/__init__.py +0 -0
  98. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/bidi.py +0 -0
  99. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/chooser.py +0 -0
  100. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/console.py +0 -0
  101. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/input/__init__.py +0 -0
  102. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/input/keys.py +0 -0
  103. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/input/reader.py +0 -0
  104. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/layout.py +0 -0
  105. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/markdown.py +0 -0
  106. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/screen.py +0 -0
  107. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/theme.py +0 -0
  108. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/__init__.py +0 -0
  109. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/buttons.py +0 -0
  110. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/chat.py +0 -0
  111. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/history.py +0 -0
  112. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/overlay.py +0 -0
  113. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/panel.py +0 -0
  114. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/progress.py +0 -0
  115. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/prompt.py +0 -0
  116. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/statusbar.py +0 -0
  117. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/toast.py +0 -0
  118. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/uninstall.py +0 -0
  119. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/update.py +0 -0
  120. {comodor-0.7.0 → comodor-0.8.0}/src/comodor/workspace.py +0 -0
  121. {comodor-0.7.0 → comodor-0.8.0}/tests/conftest.py +0 -0
  122. {comodor-0.7.0 → comodor-0.8.0}/tests/support/fake_mcp_server.py +0 -0
  123. {comodor-0.7.0 → comodor-0.8.0}/tests/test_agent_loop.py +0 -0
  124. {comodor-0.7.0 → comodor-0.8.0}/tests/test_app.py +0 -0
  125. {comodor-0.7.0 → comodor-0.8.0}/tests/test_associations.py +0 -0
  126. {comodor-0.7.0 → comodor-0.8.0}/tests/test_bidi.py +0 -0
  127. {comodor-0.7.0 → comodor-0.8.0}/tests/test_browser.py +0 -0
  128. {comodor-0.7.0 → comodor-0.8.0}/tests/test_caching.py +0 -0
  129. {comodor-0.7.0 → comodor-0.8.0}/tests/test_chooser.py +0 -0
  130. {comodor-0.7.0 → comodor-0.8.0}/tests/test_doctor.py +0 -0
  131. {comodor-0.7.0 → comodor-0.8.0}/tests/test_history.py +0 -0
  132. {comodor-0.7.0 → comodor-0.8.0}/tests/test_input.py +0 -0
  133. {comodor-0.7.0 → comodor-0.8.0}/tests/test_layout.py +0 -0
  134. {comodor-0.7.0 → comodor-0.8.0}/tests/test_learning.py +0 -0
  135. {comodor-0.7.0 → comodor-0.8.0}/tests/test_markdown.py +0 -0
  136. {comodor-0.7.0 → comodor-0.8.0}/tests/test_mcp.py +0 -0
  137. {comodor-0.7.0 → comodor-0.8.0}/tests/test_performance.py +0 -0
  138. {comodor-0.7.0 → comodor-0.8.0}/tests/test_prefix_stability.py +0 -0
  139. {comodor-0.7.0 → comodor-0.8.0}/tests/test_progress.py +0 -0
  140. {comodor-0.7.0 → comodor-0.8.0}/tests/test_propose.py +0 -0
  141. {comodor-0.7.0 → comodor-0.8.0}/tests/test_providers.py +0 -0
  142. {comodor-0.7.0 → comodor-0.8.0}/tests/test_reflex.py +0 -0
  143. {comodor-0.7.0 → comodor-0.8.0}/tests/test_run_loop.py +0 -0
  144. {comodor-0.7.0 → comodor-0.8.0}/tests/test_setup.py +0 -0
  145. {comodor-0.7.0 → comodor-0.8.0}/tests/test_skill_catalogue.py +0 -0
  146. {comodor-0.7.0 → comodor-0.8.0}/tests/test_uninstall.py +0 -0
  147. {comodor-0.7.0 → comodor-0.8.0}/tests/test_update.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: comodor
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Comodor — a self-improving terminal coding agent with a Rich TUI
5
5
  Project-URL: Homepage, https://comodor.ai
6
6
  Project-URL: Repository, https://github.com/ifekri/Comodor
@@ -310,8 +310,14 @@ comodor mcp catalogue # twelve servers, ready to go
310
310
  comodor mcp add filesystem --path ~/work
311
311
  comodor mcp add github --env GITHUB_PERSONAL_ACCESS_TOKEN=…
312
312
  comodor mcp custom my-server uvx my-package # anything else
313
+ comodor mcp remote team https://mcp.example.com/mcp --token … # a hosted one
313
314
  ```
314
315
 
316
+ A server can be a command to launch or a URL to reach. Hosted servers are
317
+ spoken to over Streamable HTTP, session header and all, and a plain `http://`
318
+ endpoint that is not on this machine is refused — the token and everything the
319
+ tools return would cross in the clear.
320
+
315
321
  | | |
316
322
  |---|---|
317
323
  | **Files and code** | Filesystem · Git · GitHub |
@@ -451,6 +457,127 @@ comodor --provider groq --model llama-3.3-70b-versatile
451
457
 
452
458
  ---
453
459
 
460
+ ## A browser, and the reason it is not screenshots
461
+
462
+ Comodor drives a real Chrome — one that runs JavaScript, keeps cookies and can
463
+ log in. It does not download one: it uses the Chrome, Chromium, Edge or Brave
464
+ already on the machine, in a profile of its own that is signed into nothing.
465
+ There is no Playwright, no Selenium, no node. The DevTools protocol is JSON-RPC
466
+ over a WebSocket, and a WebSocket is a handshake and a frame format.
467
+
468
+ The usual design is to send a screenshot every step and have the model click a
469
+ coordinate. That is wrong twice, and the second time is measurable.
470
+
471
+ It is wrong on precision, because a model judging pixels on a resized image
472
+ misses — by a little on an empty page and by a whole button on a dense one, with
473
+ nothing in its answer to say which control it meant.
474
+
475
+ And it is wrong on cost:
476
+
477
+ | | whole accessibility tree | **only what is on screen** | a screenshot |
478
+ |---|---|---|---|
479
+ | Hacker News | 8,760 | **933** | 1,365 |
480
+ | a GitHub repository | 18,681 | **778** | 1,365 |
481
+ | a Wikipedia article | 32,342 | **414** | 1,365 |
482
+
483
+ Note the first column: the page's own accessibility tree, sent whole, costs
484
+ *more* than a picture. The advantage comes from filtering, which is a thing you
485
+ can do to text and cannot do to pixels — half a screenshot answers nothing. So
486
+ what the agent gets is the controls a person could actually see and click: on
487
+ screen, visible, not disabled, named, with the same link in the header and the
488
+ footer counted once. Each one numbered, so the model answers with a number and
489
+ cannot miss.
490
+
491
+ Screenshots are still there, and still necessary — for the questions that are
492
+ genuinely about appearance. *Does this look right. Is the layout broken. What
493
+ does this chart say.* That is the `look` verb, taken when the question is
494
+ visual rather than on every step, which is the difference between a browser an
495
+ agent can afford and one it cannot.
496
+
497
+ ```
498
+ browse open news.ycombinator.com → title, text, 93 numbered controls
499
+ browse click 7 → clicks it, returns the new page
500
+ browse type 3 "search terms" submit → fills a field and presses Enter
501
+ browse look → a screenshot, when that is the question
502
+ browse script "…" → JavaScript, when nothing else will do
503
+ ```
504
+
505
+ Set `browser.headless = false` to watch it work, or `browser.port` to a Chrome
506
+ you started yourself — which is how you use one you are already logged into
507
+ without handing over your everyday profile.
508
+
509
+ ---
510
+
511
+ ## In a browser, if you want one
512
+
513
+ ```bash
514
+ comodor web
515
+ ```
516
+
517
+ It opens a page, and the page is the same agent: the terminal interface was
518
+ never the agent, it was one subscriber to an event bus, and a browser is a
519
+ second one. Streaming answers, tool calls as they run, permission prompts you
520
+ approve with a click, the mode switch and the running cost.
521
+
522
+ No new dependency, and the page loads nothing from the internet — no CDN, no
523
+ font host, no analytics — so it works on a machine that can reach the model and
524
+ nothing else.
525
+
526
+ **Read this part before you put it on a server.** Comodor runs shell commands
527
+ and edits files. A web interface to it is, literally, a remote code execution
528
+ endpoint: that *is* the feature. So
529
+
530
+ - it listens on `127.0.0.1` and nothing outside your machine can reach it;
531
+ - every request carries a token generated for that run and never written to
532
+ disk — it arrives once in the URL, then moves into an `HttpOnly`,
533
+ `SameSite=Strict` cookie so it leaves the address bar, your history and the
534
+ referrer of anything the page links to;
535
+ - writing requests need a header no cross-origin form can set, and the
536
+ preflight grants nobody anything.
537
+
538
+ There is no TLS. If you bind it anywhere else it says so in as many words and
539
+ tells you what to do instead:
540
+
541
+ ```bash
542
+ ssh -N -L 8765:127.0.0.1:8765 you@your-server # then open it locally
543
+ ```
544
+
545
+ Which is the right way to use it on a server: run it bound to loopback there,
546
+ and reach it through the tunnel.
547
+
548
+ ---
549
+
550
+ ## Reading that does not stay read
551
+
552
+ Two of the ways a session gets expensive have nothing to do with the model and
553
+ everything to do with what is kept.
554
+
555
+ **One tool result is paid for many times.** It is written into the conversation
556
+ once and resent with every request after it, so its price is its size times the
557
+ steps still to come. Reading one ordinary module here cost 23,082 tokens.
558
+ Anything past the budget is now *moved* rather than cut: the whole of it goes
559
+ to a file, and what comes back is the head, the tail and the path. Output that
560
+ was already a file on disk is not copied — the pointer names the original.
561
+ Nothing is lost, which truncation cannot say: the middle of a failing test run
562
+ is exactly where the failure was.
563
+
564
+ ```
565
+ one round of six ordinary calls 29,633 → 10,553 tokens
566
+ ```
567
+
568
+ **Some questions take a lot of reading and very little to answer.** *Which
569
+ module owns retries?* might mean opening nine files to produce one sentence,
570
+ and in the main conversation those nine files are permanent. `delegate` hands
571
+ that to a second agent with its own context and brings back only the sentence.
572
+
573
+ It cannot write unless told to, and it cannot delegate. Told to, and in a git
574
+ project, it works in a checkout of its own: a real worktree at the same commit,
575
+ and what comes back is a patch — applied here if it applies cleanly, kept aside
576
+ with its path if something moved underneath it. Your working tree is never what
577
+ it experiments in.
578
+
579
+ ---
580
+
454
581
  ## It pays for the same tokens once
455
582
 
456
583
  An agent loop has a shape that is unusually wasteful, and it is not obvious
@@ -284,8 +284,14 @@ comodor mcp catalogue # twelve servers, ready to go
284
284
  comodor mcp add filesystem --path ~/work
285
285
  comodor mcp add github --env GITHUB_PERSONAL_ACCESS_TOKEN=…
286
286
  comodor mcp custom my-server uvx my-package # anything else
287
+ comodor mcp remote team https://mcp.example.com/mcp --token … # a hosted one
287
288
  ```
288
289
 
290
+ A server can be a command to launch or a URL to reach. Hosted servers are
291
+ spoken to over Streamable HTTP, session header and all, and a plain `http://`
292
+ endpoint that is not on this machine is refused — the token and everything the
293
+ tools return would cross in the clear.
294
+
289
295
  | | |
290
296
  |---|---|
291
297
  | **Files and code** | Filesystem · Git · GitHub |
@@ -425,6 +431,127 @@ comodor --provider groq --model llama-3.3-70b-versatile
425
431
 
426
432
  ---
427
433
 
434
+ ## A browser, and the reason it is not screenshots
435
+
436
+ Comodor drives a real Chrome — one that runs JavaScript, keeps cookies and can
437
+ log in. It does not download one: it uses the Chrome, Chromium, Edge or Brave
438
+ already on the machine, in a profile of its own that is signed into nothing.
439
+ There is no Playwright, no Selenium, no node. The DevTools protocol is JSON-RPC
440
+ over a WebSocket, and a WebSocket is a handshake and a frame format.
441
+
442
+ The usual design is to send a screenshot every step and have the model click a
443
+ coordinate. That is wrong twice, and the second time is measurable.
444
+
445
+ It is wrong on precision, because a model judging pixels on a resized image
446
+ misses — by a little on an empty page and by a whole button on a dense one, with
447
+ nothing in its answer to say which control it meant.
448
+
449
+ And it is wrong on cost:
450
+
451
+ | | whole accessibility tree | **only what is on screen** | a screenshot |
452
+ |---|---|---|---|
453
+ | Hacker News | 8,760 | **933** | 1,365 |
454
+ | a GitHub repository | 18,681 | **778** | 1,365 |
455
+ | a Wikipedia article | 32,342 | **414** | 1,365 |
456
+
457
+ Note the first column: the page's own accessibility tree, sent whole, costs
458
+ *more* than a picture. The advantage comes from filtering, which is a thing you
459
+ can do to text and cannot do to pixels — half a screenshot answers nothing. So
460
+ what the agent gets is the controls a person could actually see and click: on
461
+ screen, visible, not disabled, named, with the same link in the header and the
462
+ footer counted once. Each one numbered, so the model answers with a number and
463
+ cannot miss.
464
+
465
+ Screenshots are still there, and still necessary — for the questions that are
466
+ genuinely about appearance. *Does this look right. Is the layout broken. What
467
+ does this chart say.* That is the `look` verb, taken when the question is
468
+ visual rather than on every step, which is the difference between a browser an
469
+ agent can afford and one it cannot.
470
+
471
+ ```
472
+ browse open news.ycombinator.com → title, text, 93 numbered controls
473
+ browse click 7 → clicks it, returns the new page
474
+ browse type 3 "search terms" submit → fills a field and presses Enter
475
+ browse look → a screenshot, when that is the question
476
+ browse script "…" → JavaScript, when nothing else will do
477
+ ```
478
+
479
+ Set `browser.headless = false` to watch it work, or `browser.port` to a Chrome
480
+ you started yourself — which is how you use one you are already logged into
481
+ without handing over your everyday profile.
482
+
483
+ ---
484
+
485
+ ## In a browser, if you want one
486
+
487
+ ```bash
488
+ comodor web
489
+ ```
490
+
491
+ It opens a page, and the page is the same agent: the terminal interface was
492
+ never the agent, it was one subscriber to an event bus, and a browser is a
493
+ second one. Streaming answers, tool calls as they run, permission prompts you
494
+ approve with a click, the mode switch and the running cost.
495
+
496
+ No new dependency, and the page loads nothing from the internet — no CDN, no
497
+ font host, no analytics — so it works on a machine that can reach the model and
498
+ nothing else.
499
+
500
+ **Read this part before you put it on a server.** Comodor runs shell commands
501
+ and edits files. A web interface to it is, literally, a remote code execution
502
+ endpoint: that *is* the feature. So
503
+
504
+ - it listens on `127.0.0.1` and nothing outside your machine can reach it;
505
+ - every request carries a token generated for that run and never written to
506
+ disk — it arrives once in the URL, then moves into an `HttpOnly`,
507
+ `SameSite=Strict` cookie so it leaves the address bar, your history and the
508
+ referrer of anything the page links to;
509
+ - writing requests need a header no cross-origin form can set, and the
510
+ preflight grants nobody anything.
511
+
512
+ There is no TLS. If you bind it anywhere else it says so in as many words and
513
+ tells you what to do instead:
514
+
515
+ ```bash
516
+ ssh -N -L 8765:127.0.0.1:8765 you@your-server # then open it locally
517
+ ```
518
+
519
+ Which is the right way to use it on a server: run it bound to loopback there,
520
+ and reach it through the tunnel.
521
+
522
+ ---
523
+
524
+ ## Reading that does not stay read
525
+
526
+ Two of the ways a session gets expensive have nothing to do with the model and
527
+ everything to do with what is kept.
528
+
529
+ **One tool result is paid for many times.** It is written into the conversation
530
+ once and resent with every request after it, so its price is its size times the
531
+ steps still to come. Reading one ordinary module here cost 23,082 tokens.
532
+ Anything past the budget is now *moved* rather than cut: the whole of it goes
533
+ to a file, and what comes back is the head, the tail and the path. Output that
534
+ was already a file on disk is not copied — the pointer names the original.
535
+ Nothing is lost, which truncation cannot say: the middle of a failing test run
536
+ is exactly where the failure was.
537
+
538
+ ```
539
+ one round of six ordinary calls 29,633 → 10,553 tokens
540
+ ```
541
+
542
+ **Some questions take a lot of reading and very little to answer.** *Which
543
+ module owns retries?* might mean opening nine files to produce one sentence,
544
+ and in the main conversation those nine files are permanent. `delegate` hands
545
+ that to a second agent with its own context and brings back only the sentence.
546
+
547
+ It cannot write unless told to, and it cannot delegate. Told to, and in a git
548
+ project, it works in a checkout of its own: a real worktree at the same commit,
549
+ and what comes back is a patch — applied here if it applies cleanly, kept aside
550
+ with its path if something moved underneath it. Your working tree is never what
551
+ it experiments in.
552
+
553
+ ---
554
+
428
555
  ## It pays for the same tokens once
429
556
 
430
557
  An agent loop has a shape that is unusually wasteful, and it is not obvious
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.7.0'
22
- __version_tuple__ = version_tuple = (0, 7, 0)
21
+ __version__ = version = '0.8.0'
22
+ __version_tuple__ = version_tuple = (0, 8, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -257,10 +257,17 @@ class AgentLoop:
257
257
  results = [self._run_one(call, context) for call in calls]
258
258
 
259
259
  for call, result in zip(calls, results):
260
- self.conversation.add(Message.tool(
260
+ message = Message.tool(
261
261
  call_id=call.id, name=call.name,
262
262
  content=result.content, is_error=not result.ok,
263
- ))
263
+ )
264
+ # A tool that produced a picture — a screenshot of a page — sends
265
+ # it as one. Where the dialect allows an image beside a tool result
266
+ # it goes there; where it does not, the adapter moves it.
267
+ picture = result.meta.get("image")
268
+ if isinstance(picture, str) and picture:
269
+ message.images = [picture]
270
+ self.conversation.add(message)
264
271
 
265
272
  def _run_one(self, call: ToolCall, context: ToolContext) -> ToolResult:
266
273
  self.bus.emit(Kind.TOOL_START, id=call.id, name=call.name,
@@ -416,10 +423,21 @@ class AgentLoop:
416
423
  if not matched:
417
424
  return ""
418
425
 
419
- self._skills_used = matched
420
- self.bus.emit(Kind.MEMORY, action="skills",
421
- items=[skill.as_dict() for skill in matched])
422
- return self.skills.render(matched, max_tokens=settings.max_tokens)
426
+ kept, dropped = self.skills.fit(matched, settings.max_tokens)
427
+ for skill in dropped:
428
+ # Naming the setting, because the fix is one number and the user
429
+ # has no other way to discover that their skill never arrived.
430
+ self._note(f"{skill.name} matched but is too large for the "
431
+ f"{settings.max_tokens:,}-token skill budget — raise "
432
+ f"skills.max_tokens to use it.")
433
+
434
+ # Announced from what actually travelled. Announcing the match instead
435
+ # tells the user a skill is shaping the answer when it was discarded.
436
+ self._skills_used = kept
437
+ if kept:
438
+ self.bus.emit(Kind.MEMORY, action="skills",
439
+ items=[skill.as_dict() for skill in kept])
440
+ return self.skills.render(kept, max_tokens=settings.max_tokens)
423
441
 
424
442
  def _learn(self, user_text: str, result: TurnResult) -> None:
425
443
  """Credit the lessons that were in play, then reflect on the episode."""
@@ -0,0 +1,64 @@
1
+ """Building a delegate, which the tool layer must not know how to do.
2
+
3
+ The `delegate` tool needs a whole second agent — a gateway, a tool registry, a
4
+ permission engine, a conversation — and a tool has none of those and should not
5
+ learn. So it is handed a callable and knows only that calling it returns
6
+ something with a ``run``.
7
+
8
+ What a delegate does *not* inherit is as deliberate as what it does:
9
+
10
+ * **No brain.** It does not recall lessons and it does not reflect afterwards.
11
+ A delegate's episode is half a task seen out of context, and reflecting on it
12
+ teaches the brain about a fragment. The parent's own turn is the episode
13
+ worth learning from, and it still is one.
14
+ * **No delegating.** One level deep. A tree of agents is a way to spend an
15
+ afternoon's budget in ninety seconds, and nothing else in the design stops it.
16
+ * **No history search.** It cannot see this conversation; letting it search
17
+ every past one instead is a strange kind of amnesia.
18
+ * **The parent's cancellation.** Escape stops the whole thing, not the outer
19
+ loop while a delegate keeps working somewhere with a shell open.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import copy
25
+ from dataclasses import replace
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ from ..events import Cancellation, EventBus
30
+ from ..paths import Paths
31
+ from ..safety import PermissionEngine
32
+ from ..tools import ToolRegistry
33
+
34
+
35
+ def spawner(config: Any, gateway: Any, bus: EventBus, skills: Any = None,
36
+ mcp: Any = None) -> Any:
37
+ """Return the callable the `delegate` tool is constructed with."""
38
+
39
+ def spawn(cwd: Path, mode: str = "plan", max_steps: int = 8,
40
+ max_seconds: float = 300.0,
41
+ cancel: Cancellation | None = None) -> Any:
42
+ from .context import Conversation
43
+ from .loop import AgentLoop
44
+
45
+ settings = copy.deepcopy(config)
46
+ settings.paths = replace(settings.paths, project=Path(cwd)) \
47
+ if isinstance(settings.paths, Paths) else settings.paths
48
+ settings.agent.mode = mode
49
+ settings.agent.max_steps = max_steps
50
+ settings.agent.max_seconds = max_seconds
51
+ settings.agent.loop = True
52
+ # Its answer is prose for another agent to read, not a report for a
53
+ # person, so it does not need the parent's output allowance.
54
+ settings.agent.max_output_tokens = min(settings.agent.max_output_tokens, 4096)
55
+
56
+ tools = ToolRegistry(skills=skills, mcp=mcp)
57
+ loop = AgentLoop(settings, gateway, tools, bus,
58
+ PermissionEngine(settings, bus), Conversation(),
59
+ memory=None, skills=skills)
60
+ if cancel is not None:
61
+ loop.cancel = cancel
62
+ return loop
63
+
64
+ return spawn
@@ -0,0 +1,7 @@
1
+ """A real browser, driven with nothing but the standard library."""
2
+
3
+ from .cdp import BrowserError, Session
4
+ from .launch import Browser
5
+ from .page import Element, Page, Snapshot
6
+
7
+ __all__ = ["Browser", "BrowserError", "Element", "Page", "Session", "Snapshot"]