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.
- {comodor-0.7.0 → comodor-0.8.0}/PKG-INFO +128 -1
- {comodor-0.7.0 → comodor-0.8.0}/README.md +127 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/_version.py +2 -2
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/agent/loop.py +24 -6
- comodor-0.8.0/src/comodor/agent/spawn.py +64 -0
- comodor-0.8.0/src/comodor/browser/__init__.py +7 -0
- comodor-0.8.0/src/comodor/browser/cdp.py +309 -0
- comodor-0.8.0/src/comodor/browser/launch.py +190 -0
- comodor-0.8.0/src/comodor/browser/page.py +356 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/cli.py +11 -1
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/config.py +65 -5
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/mcp/commands.py +50 -4
- comodor-0.8.0/src/comodor/mcp/http.py +203 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/mcp/manager.py +14 -4
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/providers/anthropic.py +14 -1
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/providers/openai_compat.py +15 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/skills/commands.py +35 -3
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/skills/loader.py +27 -2
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/skills/registry.py +38 -15
- comodor-0.8.0/src/comodor/tools/browse.py +248 -0
- comodor-0.8.0/src/comodor/tools/delegate.py +275 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/fs.py +49 -10
- comodor-0.8.0/src/comodor/tools/overflow.py +177 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/registry.py +40 -5
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/shell.py +15 -11
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/app.py +4 -0
- comodor-0.8.0/src/comodor/web/__init__.py +6 -0
- comodor-0.8.0/src/comodor/web/commands.py +85 -0
- comodor-0.8.0/src/comodor/web/index.html +330 -0
- comodor-0.8.0/src/comodor/web/server.py +252 -0
- comodor-0.8.0/src/comodor/web/session.py +243 -0
- comodor-0.8.0/tests/test_delegate.py +269 -0
- comodor-0.8.0/tests/test_mcp_http.py +354 -0
- comodor-0.8.0/tests/test_overflow.py +233 -0
- comodor-0.8.0/tests/test_real_browser.py +472 -0
- comodor-0.8.0/tests/test_skill_budget.py +223 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_skills.py +79 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_tools.py +37 -5
- comodor-0.8.0/tests/test_web.py +375 -0
- {comodor-0.7.0 → comodor-0.8.0}/.gitignore +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/LICENSE +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/pyproject.toml +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/__init__.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/__main__.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/agent/__init__.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/agent/context.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/agent/prompts.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/agent/tokens.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/catalogue.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/doctor.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/events.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/__init__.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/associations.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/bm25.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/hotindex.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/memory.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/progress.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/reflect.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/rules.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/signals.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/store.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/learning/writer.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/mcp/__init__.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/mcp/catalogue.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/mcp/protocol.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/net/__init__.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/net/http.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/net/sse.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/paths.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/providers/__init__.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/providers/base.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/providers/caching.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/providers/fake.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/providers/gateway.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/providers/registry.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/safety/__init__.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/safety/checkpoints.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/safety/permissions.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/safety/redact.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/session/__init__.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/session/search.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/session/store.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/setup.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/skills/__init__.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/skills/catalogue.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/skills/examples.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/skills/propose.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/__init__.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/base.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/browser.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/history.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/mcp.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/search.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/skills.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/todo.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/tools/web.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/__init__.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/bidi.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/chooser.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/console.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/input/__init__.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/input/keys.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/input/reader.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/layout.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/markdown.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/screen.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/theme.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/__init__.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/buttons.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/chat.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/history.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/overlay.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/panel.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/progress.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/prompt.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/statusbar.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/ui/widgets/toast.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/uninstall.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/update.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/src/comodor/workspace.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/conftest.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/support/fake_mcp_server.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_agent_loop.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_app.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_associations.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_bidi.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_browser.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_caching.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_chooser.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_doctor.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_history.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_input.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_layout.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_learning.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_markdown.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_mcp.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_performance.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_prefix_stability.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_progress.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_propose.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_providers.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_reflex.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_run_loop.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_setup.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_skill_catalogue.py +0 -0
- {comodor-0.7.0 → comodor-0.8.0}/tests/test_uninstall.py +0 -0
- {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.
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (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
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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"]
|