localagentbrowser 1.47__tar.gz → 1.52__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 (22) hide show
  1. {localagentbrowser-1.47 → localagentbrowser-1.52}/CHANGELOG.md +91 -0
  2. {localagentbrowser-1.47/localagentbrowser.egg-info → localagentbrowser-1.52}/PKG-INFO +15 -13
  3. {localagentbrowser-1.47 → localagentbrowser-1.52}/README.md +14 -12
  4. {localagentbrowser-1.47 → localagentbrowser-1.52}/agent_browser.py +81 -15
  5. {localagentbrowser-1.47 → localagentbrowser-1.52/localagentbrowser.egg-info}/PKG-INFO +15 -13
  6. {localagentbrowser-1.47 → localagentbrowser-1.52}/pyproject.toml +1 -1
  7. {localagentbrowser-1.47 → localagentbrowser-1.52}/tests/test_decisions.py +36 -15
  8. {localagentbrowser-1.47 → localagentbrowser-1.52}/tests/test_journal.py +11 -1
  9. {localagentbrowser-1.47 → localagentbrowser-1.52}/tests/test_model_turns.py +13 -0
  10. {localagentbrowser-1.47 → localagentbrowser-1.52}/LICENSE +0 -0
  11. {localagentbrowser-1.47 → localagentbrowser-1.52}/MANIFEST.in +0 -0
  12. {localagentbrowser-1.47 → localagentbrowser-1.52}/localagentbrowser.egg-info/SOURCES.txt +0 -0
  13. {localagentbrowser-1.47 → localagentbrowser-1.52}/localagentbrowser.egg-info/dependency_links.txt +0 -0
  14. {localagentbrowser-1.47 → localagentbrowser-1.52}/localagentbrowser.egg-info/entry_points.txt +0 -0
  15. {localagentbrowser-1.47 → localagentbrowser-1.52}/localagentbrowser.egg-info/requires.txt +0 -0
  16. {localagentbrowser-1.47 → localagentbrowser-1.52}/localagentbrowser.egg-info/top_level.txt +0 -0
  17. {localagentbrowser-1.47 → localagentbrowser-1.52}/setup.cfg +0 -0
  18. {localagentbrowser-1.47 → localagentbrowser-1.52}/tests/conftest.py +0 -0
  19. {localagentbrowser-1.47 → localagentbrowser-1.52}/tests/fakes.py +0 -0
  20. {localagentbrowser-1.47 → localagentbrowser-1.52}/tests/test_duplicates.py +0 -0
  21. {localagentbrowser-1.47 → localagentbrowser-1.52}/tests/test_main_loop.py +0 -0
  22. {localagentbrowser-1.47 → localagentbrowser-1.52}/tests/test_page_text.py +0 -0
@@ -4,6 +4,97 @@ Every version of `agent_browser.py`, newest first. Each version is also a tagged
4
4
  Timings are from live runs on the development machine (Intel i5-9500, 6 cores, no GPU, Qwen2.5-7B-Instruct
5
5
  Q4_K_M in LM Studio), where reading the prompt takes about 90% of every model call.
6
6
 
7
+ ## [1.52] - 2026-10-16
8
+ ### Changed
9
+ - The note after each DISCOVER also says to work on parts of the objective that are not covered yet before
10
+ adding detail to parts that are. On the explanation task (how photosynthesis works), 1.51 recorded the
11
+ Calvin cycle enzyme by enzyme while the light reactions and the inputs and outputs were still missing.
12
+
13
+ Rerun: after two Calvin cycle discoveries the agent left for a page on the light reactions and recorded where
14
+ each stage happens and what the light reactions do. It still did not finish within 65 minutes: it lost about
15
+ 20 minutes reading the history and evolution sections of a long encyclopedia article twice, answering "none"
16
+ to each read-on check, and never recorded the inputs and outputs that article's first part gives.
17
+
18
+ Regression runs on 1.51 for the other kinds of task: the list task (three free, open-source alternatives to
19
+ Microsoft Office) finished in 7 turns and 15.5 minutes of model time (49.3 on 1.46), naming LibreOffice,
20
+ OnlyOffice and Apache OpenOffice, all open source; its final result wrongly says FreeOffice was "not
21
+ mentioned" in the discoveries while leaving it out.
22
+
23
+ ## [1.51] - 2026-10-16
24
+ ### Fixed
25
+ - On the repotting how-to, 1.50 sent back the model's invented steps once (part 2 still only said "follow
26
+ these 6 easy steps"), the model sent nearly the same steps again, and they went through as a confirmed
27
+ repeat. A DISCOVER far below anything real is now refused outright, and repeating it does not help: below
28
+ 40% of its word pairs on the page text shown, or with a sentence below 25%. Real discoveries in the
29
+ earlier runs scored at least 50% overall and 40% per sentence; these invented steps scored 32%, one of
30
+ them 9%. Between those levels and 70% / 35%, the one-time confirmation stays.
31
+
32
+ Rerun: the invented steps were refused, the agent read on and recorded the guide's real materials and steps,
33
+ and FINISH came on turn 7 after 28.5 minutes of model time (67.5 on 1.44). The final result has every step,
34
+ including putting the plant in the new pot, and says that no care instructions beyond the first watering
35
+ were found. Regression on the comparison task with 1.50: 8 turns and 24.9 minutes of model time (38.2 on
36
+ 1.42), all parts covered and cited, with a recommendation.
37
+
38
+ ## [1.50] - 2026-10-16
39
+ Three runs of the strict documentation objective (Playwright test agents, "do not FINISH until ... fully
40
+ validated", including "parameter schemas" the documentation does not have).
41
+ ### Changed
42
+ - `READ_MORE` with the number of the part already shown reads the next part. The model did this in most
43
+ long-page runs, meaning the next part, and refusing it cost a retry each time. On the last part it is still
44
+ refused.
45
+ - The read-on check also says to leave a page that is not about the objective: shown an unrelated "agent"
46
+ project's command reference, the model read on part after part.
47
+ ### Fixed
48
+ - After being told twice that a search was exhausted and that it could finish with the gap named, the model
49
+ tried FINISH, the finish check sent it to work on the uncovered part, and it repeated the search again (five
50
+ times in one run). Once a search is exhausted, FINISH goes through without the finish and gap checks.
51
+
52
+ Third run: FINISH on turn 10 (25.2 minutes of model time), the first time this objective has finished; earlier
53
+ versions searched for over an hour. The result has the official `npx playwright init-agents --loop=vscode`
54
+ command and the file layout from the official docs, but it is thin: the finish check was answered with
55
+ "covered by [1] covered by [2] covered by [3]" without naming the parts, one discovery was off topic, and the
56
+ result does not say that no parameter schemas were found.
57
+
58
+ ## [1.49] - 2026-10-16
59
+ A documentation task on 1.48 (create a Python virtual environment with venv, activate it on Windows and on
60
+ macOS or Linux, and install packages) garbled commands in two places.
61
+ ### Fixed
62
+ - Recording the Windows command `python -m venv C:\path\to\new\virtual\environment`, the model wrote single
63
+ backslashes in its JSON reply: `\t`, `\n` and `\r` became a tab, a newline and a carriage return, and since
64
+ the response format does not allow `\v`, the model turned `\virtual\environment` into `\real\name`. The
65
+ system prompt now asks for every backslash to be written twice, and a tab, carriage return, backspace or
66
+ form feed right before a letter is turned back into its backslash sequence when the text also has a
67
+ backslash or a drive letter (page tables keep their real tabs).
68
+ - The final result turned the recorded `.\venv\Scripts\activate` and `source venv/bin/activate` into
69
+ `.venv\Scripts\activate` and `source .venv/bin/activate`, which do not match the `python -m venv venv` it
70
+ gave first. Rule 1 of the final result now says to copy commands, code, file paths and names exactly.
71
+ Replaying that step three times on 1.48 changed the commands twice; with the rule, all three were exact.
72
+
73
+ Rerun with the backslash changes: FINISH on turn 6 (15.1 minutes of model time) with the create, activate
74
+ and install commands, the Windows path intact. It used a tutorial site rather than the official
75
+ documentation the objective named; checking conditions like that is still a weakness.
76
+
77
+ ## [1.48] - 2026-10-16
78
+ An opinion task on 1.47 (how at least three cultures or religions have viewed owls, then an opinion on why
79
+ they attract such different meanings) showed three problems.
80
+ ### Fixed
81
+ - A discovery copied two sentences from the page and added a third the page never said ("...depicted
82
+ alongside other wise figures like the prophet Samuel"); copied text lifted the whole discovery to 75%, past
83
+ the page-match check. Each sentence of 8 or more word pairs is now also scored, and one below 35% gets the
84
+ same cheap retry. Measured on the earlier runs' real discoveries, sentences scored 40-100%; invented ones
85
+ 0-41%.
86
+ - With three cultures recorded where three were asked for, the read-on check pushed the agent to keep
87
+ collecting. It now also says to choose FINISH if the discoveries already cover every part of the objective.
88
+ - The final result listed many traditions but gave no opinion, although the objective asked for one. When the
89
+ objective asks for an opinion, view or recommendation and the result has no "My view:" paragraph, a short
90
+ second call writes only that paragraph from the result.
91
+ - The report's number check treated citation ranges like "[1-4]" as numbers.
92
+
93
+ Rerun with the sentence and finish changes: FINISH on turn 11 (37.9 minutes of model time) with the views of
94
+ Greek, English and many Native American peoples from a detailed source, all discoveries matching the page. Its
95
+ three page-match checks were condensed summaries of real sentences; the model repeated each and it went
96
+ through, at 37-61 s per check. Replaying the final-result step on that run added a "My view:" paragraph.
97
+
7
98
  ## [1.47] - 2026-10-16
8
99
  A list task on 1.46 (three free, open-source alternatives to Microsoft Office and what each is best at)
9
100
  finished on its own in 18 turns and 49 minutes of model time. LibreOffice and OnlyOffice were right, but the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: localagentbrowser
3
- Version: 1.47
3
+ Version: 1.52
4
4
  Summary: An autonomous web agent driven by a local LLM (LM Studio) and Playwright. Runs entirely on your own machine.
5
5
  Author: SnowFX
6
6
  License-Expression: MIT
@@ -56,7 +56,7 @@ and tuned to work well with a **small 7B model on a CPU, no GPU required**.
56
56
  - ⚡ **Built for slow hardware.** It reuses LM Studio's prompt cache, so most turns process only what changed.
57
57
  - 📝 **A readable report every time.** `research_report.md` holds the final result and every finding with its
58
58
  source, written however the run ends; `research_log.txt` keeps every action and reason.
59
- - ✅ **Tested.** 107 tests on Linux and Windows, Python 3.10–3.13, run in about a second with no model or browser,
59
+ - ✅ **Tested.** 112 tests on Linux and Windows, Python 3.10–3.13, run in about a second with no model or browser,
60
60
  plus live runs on seven different kinds of task (see [results](#results-on-different-kinds-of-tasks)).
61
61
 
62
62
  📖 **The story behind it:** [Teaching a 7B model on a CPU to finish what it starts](https://dev.to/snowfxxx/teaching-a-7b-model-on-a-cpu-to-finish-what-it-starts-1amc),
@@ -184,14 +184,15 @@ does not generalise; the [changelog](https://github.com/SnowFXXX/LocalAgentBrows
184
184
  | Kind of task | Objective (abridged) | Version | Outcome |
185
185
  |---|---|---|---|
186
186
  | Documentation research | Setup commands and file layout for Playwright's test agents | 1.27 | Finished on its own in 13 turns (~36 min) with commands, roles and file layout |
187
- | Documentation research, strict | The same, but "do not FINISH until fully validated", including "parameter schemas" | 1.38–1.40 | Its most detailed findings (per-client setup, seed test, agent definition files), but it keeps searching for schemas the docs do not contain and has not finished within an hour |
187
+ | Documentation research, strict | The same, but "do not FINISH until fully validated", including "parameter schemas" | 1.38–1.40, 1.50 | 1.38–1.40: its most detailed findings (per-client setup, seed test, agent definition files), but it kept searching for schemas the docs do not contain and did not finish within an hour. 1.50: finished in 10 turns (25 min) with the official init command and file layout, but with thin findings and without saying the schemas were not found |
188
188
  | Opinion from several sources | How spiritual traditions view cats, then form an opinion | 1.30 | Six traditions from two sources and a stated opinion, in 18 turns (37 min of model time) |
189
+ | Opinion from several sources | How at least three cultures or religions have viewed owls, then an opinion on why | 1.47–1.48 | Greek, English and many Native American views from one detailed source in 11 turns (38 min); 1.48 adds the opinion the result had left out and catches an invented sentence in a discovery |
189
190
  | Factual question | Tallest mountain in Africa, height in meters and feet | 1.35, 1.40, 1.46 | Correct, cited answer in 4 turns (about 12 min of model time) in all three versions; before 1.35 it kept reading after finding it |
190
- | Comparison and recommendation | Python vs Rust for command-line tools; recommend one for a beginner. Later: SQLite vs PostgreSQL for a small web app, then a recommendation for a hobby project | 1.36, 1.42 | Python vs Rust: cited comparison recommending Python in 16 turns (33 min), Rust's libraries not covered. SQLite vs PostgreSQL: all three parts covered and cited, with a recommendation, in 13 turns (38 min) |
191
+ | Comparison and recommendation | Python vs Rust for command-line tools; recommend one for a beginner. Later: SQLite vs PostgreSQL for a small web app, then a recommendation for a hobby project | 1.36, 1.42, 1.50 | Python vs Rust: cited comparison recommending Python in 16 turns (33 min), Rust's libraries not covered. SQLite vs PostgreSQL: all three parts covered and cited, with a recommendation, in 13 turns (38 min); on 1.50 in 8 turns (25 min) |
191
192
  | List of items | Three free, open-source Photoshop alternatives and the strengths of each | 1.37–1.38 | GIMP, Krita and Darktable with their strengths, all cited, in 9 turns (24 min); 1.37 listed a fourth and added an unrequested opinion, which 1.38 fixes |
192
- | List of items | Three free, open-source alternatives to Microsoft Office and what each is best at | 1.46 | LibreOffice and OnlyOffice with their strengths, cited, in 18 turns (49 min); the third, FreeOffice, is free but not open source, and LibreOffice was recorded four times in different words |
193
+ | List of items | Three free, open-source alternatives to Microsoft Office and what each is best at | 1.46, 1.51 | LibreOffice and OnlyOffice with their strengths, cited, in 18 turns (49 min); the third, FreeOffice, is free but not open source, and LibreOffice was recorded four times in different words; on 1.51: LibreOffice, OnlyOffice and Apache OpenOffice, all open source, in 7 turns (16 min) |
193
194
  | Step-by-step how-to | Make a sourdough starter, with quantities and what to do each day | 1.36–1.37 | A clear plan in 7 turns (19 min), but it invented the first water amount; 1.37 stops guessing and flags unsupported numbers, and the agent then went looking for the missing details |
194
- | Step-by-step how-to | Repot a houseplant: what you need, the steps, and care for the first two weeks | 1.43–1.44 | 1.43 recorded six steps it wrote itself before reading the part that had them; 1.44 checks findings against the page and recorded the guide's real materials, steps and aftercare, finishing in 24 turns (68 min), though the final result left out one step |
195
+ | Step-by-step how-to | Repot a houseplant: what you need, the steps, and care for the first two weeks | 1.43–1.44, 1.51 | 1.43 recorded six steps it wrote itself before reading the part that had them; 1.44 recorded the guide's real steps but took 68 min and its result left one out. 1.51: every step from the page, in 7 turns (29 min), and it says that no care instructions beyond the first watering were found |
195
196
  | Explanation of a topic | How photosynthesis works: inputs and outputs, both stages and where each takes place | 1.41–1.42 | A complete, cited explanation in 18 turns (45 min); 1.41 read four parts of a relevant page without recording them and had not finished after 49 min |
196
197
 
197
198
  ## How it works
@@ -229,8 +230,9 @@ flowchart LR
229
230
  check: record something, or repeat the action to leave. Moving on to the next part of a page works the same
230
231
  way, and the model must first list what the part it is on says about the objective (or "none").
231
232
  - **Findings must match the page.** A finding that shares too few word pairs with the page text shown so far
232
- is sent back once, asking for only what the page says in its own words: a small model will otherwise
233
- "complete" a list from its own knowledge when a page says "follow these 6 steps" and the steps come later.
233
+ (or has a sentence that does) is sent back once, asking for only what the page says in its own words, and
234
+ one far from anything on the page is refused outright. A small model will otherwise "complete" a list from
235
+ its own knowledge when a page says "follow these 6 steps" and the steps come later.
234
236
  - **Numbered findings.** Findings are numbered in the prompt and listed with every check, and the model names
235
237
  the finding that covers each part of the objective ("covered by [2]") instead of just saying "covered".
236
238
  - **Knowing when it's done.** After every new finding the model is asked whether the objective is now fully
@@ -325,10 +327,10 @@ turn that brings in a new page takes a few minutes. See [Performance](#performan
325
327
  - **A small model's judgement is the ceiling.** It sometimes drifts to loosely related pages, prefers
326
328
  secondary sources or stays with one source too long, and results vary with the first pages a search turns
327
329
  up; every finding is in `research_report.md` regardless.
328
- - **Very strict objectives can keep it searching.** An objective that forbids finishing until everything is
329
- "fully validated" can keep the agent looking for something the web may not contain. It is told to finish
330
- and name what it could not find after looking in several places, but a 7B model does not always judge
331
- when that point has come; the turn limit (100) and Ctrl+C still apply, and the report is always current.
330
+ - **Very strict objectives.** An objective that forbids finishing until everything is "fully validated" used
331
+ to keep the agent looking for something the web may not contain. On 1.50 it finished such an objective in 25
332
+ minutes, but with thin findings and without naming what it could not find, so check the result against the
333
+ objective.
332
334
  - **Check the citations.** The final result is told to use only recorded findings, but a 7B model sometimes
333
335
  puts the wrong number on a citation, adds a line no finding supports or leaves out a step, so verify a
334
336
  claim against the listed findings before relying on it.
@@ -345,7 +347,7 @@ turn that brings in a new page takes a few minutes. See [Performance](#performan
345
347
  - [x] Write the final result in a separate, rule-bound step and flag unsupported numbers (1.32–1.38)
346
348
  - [x] Finish as soon as the objective is covered (1.35)
347
349
  - [x] Get the model to weigh each part of a long page before reading on (1.42)
348
- - [ ] Let very strict objectives end with the gaps named instead of searching indefinitely
350
+ - [ ] Let very strict objectives end with the gaps named instead of searching indefinitely (they finish since 1.50; naming the gaps is still open)
349
351
  - [ ] Try more kinds of objectives (seven so far) and adjust the rules that don't generalise
350
352
  - [ ] Make fresh prompts (the slowest kind of turn) cheaper still (31% cheaper since 1.34)
351
353
  - [ ] Interacting with pages (clicking, forms) for tasks beyond reading
@@ -24,7 +24,7 @@ and tuned to work well with a **small 7B model on a CPU, no GPU required**.
24
24
  - ⚡ **Built for slow hardware.** It reuses LM Studio's prompt cache, so most turns process only what changed.
25
25
  - 📝 **A readable report every time.** `research_report.md` holds the final result and every finding with its
26
26
  source, written however the run ends; `research_log.txt` keeps every action and reason.
27
- - ✅ **Tested.** 107 tests on Linux and Windows, Python 3.10–3.13, run in about a second with no model or browser,
27
+ - ✅ **Tested.** 112 tests on Linux and Windows, Python 3.10–3.13, run in about a second with no model or browser,
28
28
  plus live runs on seven different kinds of task (see [results](#results-on-different-kinds-of-tasks)).
29
29
 
30
30
  📖 **The story behind it:** [Teaching a 7B model on a CPU to finish what it starts](https://dev.to/snowfxxx/teaching-a-7b-model-on-a-cpu-to-finish-what-it-starts-1amc),
@@ -152,14 +152,15 @@ does not generalise; the [changelog](https://github.com/SnowFXXX/LocalAgentBrows
152
152
  | Kind of task | Objective (abridged) | Version | Outcome |
153
153
  |---|---|---|---|
154
154
  | Documentation research | Setup commands and file layout for Playwright's test agents | 1.27 | Finished on its own in 13 turns (~36 min) with commands, roles and file layout |
155
- | Documentation research, strict | The same, but "do not FINISH until fully validated", including "parameter schemas" | 1.38–1.40 | Its most detailed findings (per-client setup, seed test, agent definition files), but it keeps searching for schemas the docs do not contain and has not finished within an hour |
155
+ | Documentation research, strict | The same, but "do not FINISH until fully validated", including "parameter schemas" | 1.38–1.40, 1.50 | 1.38–1.40: its most detailed findings (per-client setup, seed test, agent definition files), but it kept searching for schemas the docs do not contain and did not finish within an hour. 1.50: finished in 10 turns (25 min) with the official init command and file layout, but with thin findings and without saying the schemas were not found |
156
156
  | Opinion from several sources | How spiritual traditions view cats, then form an opinion | 1.30 | Six traditions from two sources and a stated opinion, in 18 turns (37 min of model time) |
157
+ | Opinion from several sources | How at least three cultures or religions have viewed owls, then an opinion on why | 1.47–1.48 | Greek, English and many Native American views from one detailed source in 11 turns (38 min); 1.48 adds the opinion the result had left out and catches an invented sentence in a discovery |
157
158
  | Factual question | Tallest mountain in Africa, height in meters and feet | 1.35, 1.40, 1.46 | Correct, cited answer in 4 turns (about 12 min of model time) in all three versions; before 1.35 it kept reading after finding it |
158
- | Comparison and recommendation | Python vs Rust for command-line tools; recommend one for a beginner. Later: SQLite vs PostgreSQL for a small web app, then a recommendation for a hobby project | 1.36, 1.42 | Python vs Rust: cited comparison recommending Python in 16 turns (33 min), Rust's libraries not covered. SQLite vs PostgreSQL: all three parts covered and cited, with a recommendation, in 13 turns (38 min) |
159
+ | Comparison and recommendation | Python vs Rust for command-line tools; recommend one for a beginner. Later: SQLite vs PostgreSQL for a small web app, then a recommendation for a hobby project | 1.36, 1.42, 1.50 | Python vs Rust: cited comparison recommending Python in 16 turns (33 min), Rust's libraries not covered. SQLite vs PostgreSQL: all three parts covered and cited, with a recommendation, in 13 turns (38 min); on 1.50 in 8 turns (25 min) |
159
160
  | List of items | Three free, open-source Photoshop alternatives and the strengths of each | 1.37–1.38 | GIMP, Krita and Darktable with their strengths, all cited, in 9 turns (24 min); 1.37 listed a fourth and added an unrequested opinion, which 1.38 fixes |
160
- | List of items | Three free, open-source alternatives to Microsoft Office and what each is best at | 1.46 | LibreOffice and OnlyOffice with their strengths, cited, in 18 turns (49 min); the third, FreeOffice, is free but not open source, and LibreOffice was recorded four times in different words |
161
+ | List of items | Three free, open-source alternatives to Microsoft Office and what each is best at | 1.46, 1.51 | LibreOffice and OnlyOffice with their strengths, cited, in 18 turns (49 min); the third, FreeOffice, is free but not open source, and LibreOffice was recorded four times in different words; on 1.51: LibreOffice, OnlyOffice and Apache OpenOffice, all open source, in 7 turns (16 min) |
161
162
  | Step-by-step how-to | Make a sourdough starter, with quantities and what to do each day | 1.36–1.37 | A clear plan in 7 turns (19 min), but it invented the first water amount; 1.37 stops guessing and flags unsupported numbers, and the agent then went looking for the missing details |
162
- | Step-by-step how-to | Repot a houseplant: what you need, the steps, and care for the first two weeks | 1.43–1.44 | 1.43 recorded six steps it wrote itself before reading the part that had them; 1.44 checks findings against the page and recorded the guide's real materials, steps and aftercare, finishing in 24 turns (68 min), though the final result left out one step |
163
+ | Step-by-step how-to | Repot a houseplant: what you need, the steps, and care for the first two weeks | 1.43–1.44, 1.51 | 1.43 recorded six steps it wrote itself before reading the part that had them; 1.44 recorded the guide's real steps but took 68 min and its result left one out. 1.51: every step from the page, in 7 turns (29 min), and it says that no care instructions beyond the first watering were found |
163
164
  | Explanation of a topic | How photosynthesis works: inputs and outputs, both stages and where each takes place | 1.41–1.42 | A complete, cited explanation in 18 turns (45 min); 1.41 read four parts of a relevant page without recording them and had not finished after 49 min |
164
165
 
165
166
  ## How it works
@@ -197,8 +198,9 @@ flowchart LR
197
198
  check: record something, or repeat the action to leave. Moving on to the next part of a page works the same
198
199
  way, and the model must first list what the part it is on says about the objective (or "none").
199
200
  - **Findings must match the page.** A finding that shares too few word pairs with the page text shown so far
200
- is sent back once, asking for only what the page says in its own words: a small model will otherwise
201
- "complete" a list from its own knowledge when a page says "follow these 6 steps" and the steps come later.
201
+ (or has a sentence that does) is sent back once, asking for only what the page says in its own words, and
202
+ one far from anything on the page is refused outright. A small model will otherwise "complete" a list from
203
+ its own knowledge when a page says "follow these 6 steps" and the steps come later.
202
204
  - **Numbered findings.** Findings are numbered in the prompt and listed with every check, and the model names
203
205
  the finding that covers each part of the objective ("covered by [2]") instead of just saying "covered".
204
206
  - **Knowing when it's done.** After every new finding the model is asked whether the objective is now fully
@@ -293,10 +295,10 @@ turn that brings in a new page takes a few minutes. See [Performance](#performan
293
295
  - **A small model's judgement is the ceiling.** It sometimes drifts to loosely related pages, prefers
294
296
  secondary sources or stays with one source too long, and results vary with the first pages a search turns
295
297
  up; every finding is in `research_report.md` regardless.
296
- - **Very strict objectives can keep it searching.** An objective that forbids finishing until everything is
297
- "fully validated" can keep the agent looking for something the web may not contain. It is told to finish
298
- and name what it could not find after looking in several places, but a 7B model does not always judge
299
- when that point has come; the turn limit (100) and Ctrl+C still apply, and the report is always current.
298
+ - **Very strict objectives.** An objective that forbids finishing until everything is "fully validated" used
299
+ to keep the agent looking for something the web may not contain. On 1.50 it finished such an objective in 25
300
+ minutes, but with thin findings and without naming what it could not find, so check the result against the
301
+ objective.
300
302
  - **Check the citations.** The final result is told to use only recorded findings, but a 7B model sometimes
301
303
  puts the wrong number on a citation, adds a line no finding supports or leaves out a step, so verify a
302
304
  claim against the listed findings before relying on it.
@@ -313,7 +315,7 @@ turn that brings in a new page takes a few minutes. See [Performance](#performan
313
315
  - [x] Write the final result in a separate, rule-bound step and flag unsupported numbers (1.32–1.38)
314
316
  - [x] Finish as soon as the objective is covered (1.35)
315
317
  - [x] Get the model to weigh each part of a long page before reading on (1.42)
316
- - [ ] Let very strict objectives end with the gaps named instead of searching indefinitely
318
+ - [ ] Let very strict objectives end with the gaps named instead of searching indefinitely (they finish since 1.50; naming the gaps is still open)
317
319
  - [ ] Try more kinds of objectives (seven so far) and adjust the rules that don't generalise
318
320
  - [ ] Make fresh prompts (the slowest kind of turn) cheaper still (31% cheaper since 1.34)
319
321
  - [ ] Interacting with pages (clicking, forms) for tasks beyond reading
@@ -49,9 +49,15 @@ MAX_DISCOVERY_PROMPT_CHARS = 250 # Discoveries are shortened to this in fresh p
49
49
  MAX_CHECK_DISCOVERY_CHARS = 150 # Shorter still in the numbered list that comes with the finish and progress checks
50
50
  GROUNDED_PAIR_SHARE = 0.7 # A DISCOVER sharing fewer of its word pairs with the page text shown is confirmed first
51
51
  MIN_GROUNDING_PAIRS = 20 # Shorter discoveries are not checked: a few connecting words sink their score
52
+ GROUNDED_SENTENCE_SHARE = 0.35 # Any one sentence matching the page text shown less than this is confirmed too
53
+ MIN_SENTENCE_PAIRS = 8 # Shorter sentences are not scored on their own
54
+ UNGROUNDED_PAIR_SHARE = 0.4 # Below this (whole) or the next (any sentence) a DISCOVER is refused outright
55
+ UNGROUNDED_SENTENCE_SHARE = 0.25
52
56
  TURN_PAUSE_SECONDS = 1
53
57
  STALL_TURNS = 4 # Turns without a new discovery before the model is asked to check its progress
54
58
  COMPOSE_MAX_TOKENS = 900 # Length limit for the separately written final result
59
+ VIEW_MAX_TOKENS = 250 # Length limit for a 'My view:' paragraph added when the result left it out
60
+ OPINION_REQUEST = re.compile(r"\b(opinion|your (own )?view|recommend\w*|what do you think)\b", re.I)
55
61
  OBJECTIVE_CHECK_EVERY = 8 # Turns between scheduled objective checks, which happen even while discoveries continue
56
62
  READ_FIRST = ("First, if the CURRENT ENVIRONMENT (or an unread part of this page, via READ_MORE) contains information "
57
63
  "relevant to the objective that is not yet in DISCOVERIES SO FAR, DISCOVER it now.")
@@ -88,7 +94,7 @@ Strictly follow these rules:
88
94
  2. Never repeat a search query, and never NAVIGATE to the page you are already on. You may return to a previously visited URL when it still has information you need, at most 3 visits per URL. These limits are enforced automatically.
89
95
  3. Prefer primary, authoritative sources over copies and summaries of them.
90
96
  4. Gather only what the objective asks for. As soon as your discoveries cover every part of it, FINISH; do not keep reading for extra or merely interesting details.
91
- 5. Output ONLY raw JSON. No markdown wrappers, no backticks, no conversational filler."""
97
+ 5. Output ONLY raw JSON. No markdown wrappers, no backticks, no conversational filler. Inside JSON strings, write every backslash twice (a Windows path C:\\Users\\me is written "C:\\\\Users\\\\me")."""
92
98
 
93
99
  RESPONSE_FORMAT = {
94
100
  "type": "json_schema",
@@ -416,13 +422,25 @@ def grounding_check(parameter, page_state, current_url, objective=""):
416
422
  parts = page_state.get("parts") or []
417
423
  if not (current_url and page_state.get("url") == normalize_url(current_url) and parts):
418
424
  return None
419
- if len(word_pairs(parameter)) < MIN_GROUNDING_PAIRS:
420
- return None
421
425
  # The objective's wording counts as well: "Mount Kilimanjaro is the tallest mountain in Africa..." echoed the
422
426
  # question's "tallest" where the page says "highest", and was sent back although it was accurate
423
427
  shown = [parts[i] for i in sorted(page_state.get("seen_parts", {0})) if i < len(parts)] + [objective]
424
- if grounded_share(parameter, shown) >= GROUNDED_PAIR_SHARE:
428
+ long_enough = len(word_pairs(parameter)) >= MIN_GROUNDING_PAIRS
429
+ whole = grounded_share(parameter, shown) if long_enough else 1.0
430
+ # An invented sentence can hide behind copied ones: an opinion task's discovery was 75% copied, but its last
431
+ # sentence ("...depicted alongside other wise figures like the prophet Samuel") was not on the page (31%).
432
+ # Real sentences scored 40-100% against their page, invented ones 0-41%.
433
+ sentences = [s for s in re.split(r"(?<=[.!?])\s+|\n+", parameter) if len(word_pairs(s)) >= MIN_SENTENCE_PAIRS]
434
+ lowest = min((grounded_share(s, shown) for s in sentences), default=1.0)
435
+ if whole >= GROUNDED_PAIR_SHARE and lowest >= GROUNDED_SENTENCE_SHARE:
425
436
  return None
437
+ # Far below anything real (whole discoveries 50%+, sentences 40%+), repeating does not confirm it: asked once,
438
+ # the model sent its invented repotting steps again (32% overall, one step 9%) and they went through
439
+ if whole < UNGROUNDED_PAIR_SHARE or lowest < UNGROUNDED_SENTENCE_SHARE:
440
+ return "BLOCKED_DISCOVER", ("That DISCOVER is mostly not in the page text you have been shown, so it comes from "
441
+ "your own knowledge. DISCOVER only what the parts shown actually say, in the page's own "
442
+ "wording. If what you need is not there, READ_MORE (a later part may have it) or look "
443
+ "elsewhere.")
426
444
  # Only repeating the same DISCOVER confirms it: asked again, the model once sent a different text that was
427
445
  # still invented ("water sparingly", which the page never said), and a per-part pass let it through
428
446
  asked = page_state.setdefault("grounding_asked", [])
@@ -431,7 +449,7 @@ def grounding_check(parameter, page_state, current_url, objective=""):
431
449
  return None
432
450
  asked.append(mine)
433
451
  # Pointed to READ_MORE as well, the model left a part holding the real materials list and steps unrecorded
434
- return "CONFIRM_GROUNDED", (f"Much of that DISCOVER does not match the page text you have been shown, so part of it "
452
+ return "CONFIRM_GROUNDED", (f"Some of that DISCOVER does not match the page text you have been shown, so part of it "
435
453
  "seems to come from your own knowledge. DISCOVER again with only what the page actually "
436
454
  f"says in part {page_state['index'] + 1} (shown now) or the parts before it, using the "
437
455
  "page's own wording, for example its lists and steps. Do not add steps, amounts or "
@@ -470,7 +488,12 @@ def is_known_discovery(parameter, journal):
470
488
  def read_more_target(parameter, page_state):
471
489
  # The model sometimes writes "2 of 2" or "part 3"; the first number is the part it means
472
490
  number = re.search(r'\d+', parameter)
473
- return int(number.group()) - 1 if number else page_state["index"] + 1
491
+ target = int(number.group()) - 1 if number else page_state["index"] + 1
492
+ # Asking for the part already shown nearly always meant the next one ("READ_MORE 9" while on part 9 came up
493
+ # in most long-page runs, and refusing it cost a retry each time), so it is read as that, except on the last part
494
+ if target == page_state["index"] and target < len(page_state["parts"]) - 1:
495
+ target += 1
496
+ return target
474
497
 
475
498
  def leave_check(action, page_state, current_url, journal=()):
476
499
  # The model often left content pages without recording what it needed from them. The first attempt to
@@ -505,7 +528,9 @@ def read_on_check(page_state, current_url, target):
505
528
  # asking it to DISCOVER relevant information first: on an explanation task it read four parts of a page that
506
529
  # had the inputs, outputs and cell locations it needed and recorded them only when a progress check fired.
507
530
  # Moving on from a part nothing was recorded from gets one cheap retry that makes the model name what the
508
- # part says about the objective; repeating the READ_MORE then goes through.
531
+ # part says about the objective; repeating the READ_MORE then goes through. It also offers FINISH: with three
532
+ # cultures recorded where three were asked for, the check pushed an opinion task to keep collecting more, and
533
+ # it offers leaving: shown an unrelated "agent" project page, the model read on part after part.
509
534
  index = page_state["index"]
510
535
  # Only moving forward counts: going back to an earlier part is covered by the re-read check
511
536
  if target <= index or index in page_state.get("recorded_parts", set()):
@@ -517,13 +542,16 @@ def read_on_check(page_state, current_url, target):
517
542
  return "CONFIRM_READ_ON", (f"Before reading on: you have recorded nothing from part {index + 1}, which is shown now. "
518
543
  "In thought_process, list each fact in it that serves the OBJECTIVE, or write 'none'. If any "
519
544
  "of them are not in DISCOVERIES SO FAR, DISCOVER them now. If there are none, repeat your "
520
- "READ_MORE.")
545
+ "READ_MORE. But if DISCOVERIES SO FAR already cover every part of the OBJECTIVE, choose "
546
+ "FINISH instead of reading on, and if this page is not about the OBJECTIVE at all, leave "
547
+ "it with SEARCH or NAVIGATE.")
521
548
 
522
549
  def gap_check(thought, tried):
523
550
  # The model is asked to write 'not covered' for missing parts; on a comparison it wrote "Available Libraries:
524
551
  # Not covered" and finished anyway. A FINISH whose own reasoning says that is sent back to work on the gap,
525
552
  # at most GAP_REFUSAL_LIMIT times per run so a part that cannot be found does not block finishing for good
526
- if "not covered" not in str(thought).lower() or tried.get("gap_refusals", 0) >= GAP_REFUSAL_LIMIT:
553
+ if ("not covered" not in str(thought).lower() or tried.get("gap_refusals", 0) >= GAP_REFUSAL_LIMIT
554
+ or tried.get("search_exhausted")):
527
555
  return None
528
556
  tried["gap_refusals"] = tried.get("gap_refusals", 0) + 1
529
557
  return ("BLOCKED_FINISH", "Your own check marks a part of the objective as 'not covered'. Do not finish yet: work on "
@@ -543,6 +571,10 @@ def check_decision(action, parameter, journal, tried, page_state, current_url, t
543
571
  repeats = tried.setdefault("repeated_searches", {})
544
572
  repeats[normalize_query(query)] = repeats.get(normalize_query(query), 0) + 1
545
573
  if repeats[normalize_query(query)] >= 2:
574
+ # Told to finish with the gap, the model tried FINISH, was asked by the finish check to work on the
575
+ # uncovered part, and repeated the search again (five times on a strict objective); from here a
576
+ # FINISH goes straight through
577
+ tried["search_exhausted"] = True
546
578
  return "BLOCKED_SEARCH", (f"You have now tried to repeat the search '{query}' "
547
579
  f"{repeats[normalize_query(query)]} times. It was already done and did not find "
548
580
  "what you need. Either SEARCH with clearly different words, or accept that it "
@@ -616,7 +648,7 @@ def finish_check(tried, journal, page_state=None, current_url=None):
616
648
  # Asked to judge whether its discoveries "satisfy" the objective, a small model says yes too easily: on an
617
649
  # opinion task it finished after one source and gave a summary instead of the opinion asked for. The first
618
650
  # FINISH of each turn gets a cheap retry asking for a part-by-part check; repeating FINISH then goes through.
619
- if tried.get("finish_confirmed"):
651
+ if tried.get("finish_confirmed") or tried.get("search_exhausted"):
620
652
  return None
621
653
  tried["finish_confirmed"] = True # Reset at the start of every turn
622
654
  return "CONFIRM_FINISH", (f"{source_note(journal)}{unread_note(page_state or {}, current_url)}"
@@ -817,6 +849,19 @@ Active URL: {current_url if current_url else "None (Loop Initializing)"}
817
849
  {"role": "user", "content": user_payload}
818
850
  ]
819
851
 
852
+ PATH_ESCAPES = {"\t": "\\t", "\r": "\\r", "\b": "\\b", "\f": "\\f"}
853
+
854
+ def restore_path_escapes(text):
855
+ """Undoes JSON escapes the model did not mean: recording the Windows command "python -m venv
856
+ C:\\path\\to\\new\\virtual\\environment", it wrote single backslashes, and its \\t, \\n and \\r became a tab, a
857
+ newline and a carriage return. A tab, carriage return, backspace or form feed right before a letter is
858
+ treated as such a path; a newline is only restored where it sits between word characters (as in "to\\new").
859
+ Only text that also has a backslash or a drive letter is touched: page tables are read with real tabs."""
860
+ if not re.search(r"[\t\r\b\f][A-Za-z]", text) or not re.search(r"\\|\b[A-Za-z]:[\\\"\t\r\b\f\n]", text):
861
+ return text
862
+ text = re.sub(r"(?<=\w)\n(?=[a-z])", r"\\n", text)
863
+ return re.sub(r"[\t\r\b\f](?=[A-Za-z])", lambda m: PATH_ESCAPES[m.group()], text)
864
+
820
865
  async def request_decision(client, messages, journal, tried, page_state, current_url):
821
866
  # Cheap retries: a reply that is invalid or would be refused is answered with a short correction
822
867
  # appended to the same conversation. Everything before the correction is identical to what LM Studio
@@ -842,6 +887,9 @@ async def request_decision(client, messages, journal, tried, page_state, current
842
887
  action_decision = json.loads(raw_content)
843
888
  if not isinstance(action_decision, dict):
844
889
  raise ValueError("the reply was JSON but not an object")
890
+ for key in ("parameter", "thought_process"):
891
+ if isinstance(action_decision.get(key), str):
892
+ action_decision[key] = restore_path_escapes(action_decision[key])
845
893
  except (json.JSONDecodeError, ValueError) as parse_err:
846
894
  json_failures += 1
847
895
  print(f"⚠️ Model returned invalid JSON (attempt {json_failures}/{MAX_JSON_RETRIES}): {raw_content[:200]}")
@@ -883,7 +931,7 @@ def unverified_numbers(text, discoveries):
883
931
  A small model writing a how-to filled a gap with an invented quantity ("45 grams of lukewarm water (about
884
932
  1/4 cup)"); numbers are where such errors do the most harm and the easiest place to check them.
885
933
  """
886
- text = re.sub(r"\[\d+(?:\s*[,;]\s*\d+)*\]", " ", str(text)) # Citation markers like [2] or [4, 5]
934
+ text = re.sub(r"\[\d+(?:\s*[-–,;]\s*\d+)*\]", " ", str(text)) # Citation markers: [2], [4, 5], [1-4]
887
935
  text = re.sub(r"(?m)^\s*\d+[.)]\s", " ", text) # Numbered list items
888
936
  known = {n.replace(",", "") for e in discoveries for n in NUMBER_PATTERN.findall(e["extracted_data"])}
889
937
  unverified = []
@@ -937,7 +985,8 @@ async def compose_final_result(client, objective, journal):
937
985
  "the question, write the summary or the plan, or give the opinion). Rules:\n"
938
986
  "1. Use only the information in the DISCOVERIES. Do not fill gaps from your own knowledge or with "
939
987
  "guesses: no amounts, numbers, steps or advice that the discoveries do not contain. Where a detail the "
940
- "objective asks for is missing, say that it was not found.\n"
988
+ "objective asks for is missing, say that it was not found. Copy commands, code, file paths and "
989
+ "names exactly as the discoveries give them.\n"
941
990
  "2. A discovery about one source's own situation (for example a company's workload in a case study) "
942
991
  "describes that situation, not the one in the OBJECTIVE: say whose situation it is, and do not assume "
943
992
  "details of the OBJECTIVE's situation that it does not state.\n"
@@ -959,7 +1008,21 @@ async def compose_final_result(client, objective, journal):
959
1008
  text = str(reply.get("parameter") or reply.get("thought_process") or text)
960
1009
  except (json.JSONDecodeError, AttributeError):
961
1010
  pass
962
- return text.strip()
1011
+ text = text.strip()
1012
+ # Asked to "give your own opinion" about owls, the result listed the traditions and ended without one,
1013
+ # although rule 6 asks for a 'My view:' paragraph whenever the objective wants a view. A short second call
1014
+ # writes only that paragraph, from the result just written
1015
+ if OPINION_REQUEST.search(objective) and "my view:" not in text.lower():
1016
+ view = await client.chat.completions.create(model=MODEL, temperature=0.3, max_tokens=VIEW_MAX_TOKENS, messages=[
1017
+ {"role": "system", "content": COMPOSE_SYSTEM},
1018
+ {"role": "user", "content": f"### OBJECTIVE\n{objective}\n\n### RESULT\n{text}\n\nThe OBJECTIVE asks for your "
1019
+ "own view, opinion or recommendation, and the RESULT does not give it yet. Write "
1020
+ "only that, as one short paragraph that starts with 'My view:', based on the "
1021
+ "RESULT. Plain text."}])
1022
+ paragraph = extract_message_content(view).strip()
1023
+ if paragraph:
1024
+ text += "\n\n" + (paragraph if paragraph.lower().startswith("my view:") else f"My view: {paragraph}")
1025
+ return text
963
1026
 
964
1027
  async def load_page(page, url=None):
965
1028
  """Loads a URL (or reloads the page) and returns Playwright's response, which may be None."""
@@ -1130,13 +1193,16 @@ async def main():
1130
1193
  page_state.setdefault("recorded_parts", set()).add(page_state["index"])
1131
1194
  # A discovery is when the objective can become complete; on a one-answer question the model
1132
1195
  # recorded the full answer, said "the objective has been met", then kept reading for
1133
- # "interesting" details, so completion is checked right here
1196
+ # "interesting" details, so completion is checked right here. On an explanation task it recorded
1197
+ # the Calvin cycle step by step while the light reactions were still missing, so the note also
1198
+ # points to the parts not yet covered
1134
1199
  number = sum(1 for e in journal if e.get("action") == "DISCOVER")
1135
1200
  set_note(conversation, f"RESULT: Your DISCOVER was recorded as discovery [{number}] in DISCOVERIES SO FAR. "
1136
1201
  f"{where_you_are(page_state, current_url)} The page is unchanged. "
1137
1202
  "Do not record the same information again. If DISCOVERIES SO FAR now cover "
1138
1203
  "every part of the OBJECTIVE, choose FINISH now; do not read further for extra "
1139
- "or merely interesting details.", current_url)
1204
+ "or merely interesting details. If some parts are still not covered, work on "
1205
+ "those next instead of adding more detail to parts already covered.", current_url)
1140
1206
 
1141
1207
  # A single refusal gets a short warning; repeated refusals get an explicit list of other options
1142
1208
  last_entry = journal[-1]["action"] if journal else ""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: localagentbrowser
3
- Version: 1.47
3
+ Version: 1.52
4
4
  Summary: An autonomous web agent driven by a local LLM (LM Studio) and Playwright. Runs entirely on your own machine.
5
5
  Author: SnowFX
6
6
  License-Expression: MIT
@@ -56,7 +56,7 @@ and tuned to work well with a **small 7B model on a CPU, no GPU required**.
56
56
  - ⚡ **Built for slow hardware.** It reuses LM Studio's prompt cache, so most turns process only what changed.
57
57
  - 📝 **A readable report every time.** `research_report.md` holds the final result and every finding with its
58
58
  source, written however the run ends; `research_log.txt` keeps every action and reason.
59
- - ✅ **Tested.** 107 tests on Linux and Windows, Python 3.10–3.13, run in about a second with no model or browser,
59
+ - ✅ **Tested.** 112 tests on Linux and Windows, Python 3.10–3.13, run in about a second with no model or browser,
60
60
  plus live runs on seven different kinds of task (see [results](#results-on-different-kinds-of-tasks)).
61
61
 
62
62
  📖 **The story behind it:** [Teaching a 7B model on a CPU to finish what it starts](https://dev.to/snowfxxx/teaching-a-7b-model-on-a-cpu-to-finish-what-it-starts-1amc),
@@ -184,14 +184,15 @@ does not generalise; the [changelog](https://github.com/SnowFXXX/LocalAgentBrows
184
184
  | Kind of task | Objective (abridged) | Version | Outcome |
185
185
  |---|---|---|---|
186
186
  | Documentation research | Setup commands and file layout for Playwright's test agents | 1.27 | Finished on its own in 13 turns (~36 min) with commands, roles and file layout |
187
- | Documentation research, strict | The same, but "do not FINISH until fully validated", including "parameter schemas" | 1.38–1.40 | Its most detailed findings (per-client setup, seed test, agent definition files), but it keeps searching for schemas the docs do not contain and has not finished within an hour |
187
+ | Documentation research, strict | The same, but "do not FINISH until fully validated", including "parameter schemas" | 1.38–1.40, 1.50 | 1.38–1.40: its most detailed findings (per-client setup, seed test, agent definition files), but it kept searching for schemas the docs do not contain and did not finish within an hour. 1.50: finished in 10 turns (25 min) with the official init command and file layout, but with thin findings and without saying the schemas were not found |
188
188
  | Opinion from several sources | How spiritual traditions view cats, then form an opinion | 1.30 | Six traditions from two sources and a stated opinion, in 18 turns (37 min of model time) |
189
+ | Opinion from several sources | How at least three cultures or religions have viewed owls, then an opinion on why | 1.47–1.48 | Greek, English and many Native American views from one detailed source in 11 turns (38 min); 1.48 adds the opinion the result had left out and catches an invented sentence in a discovery |
189
190
  | Factual question | Tallest mountain in Africa, height in meters and feet | 1.35, 1.40, 1.46 | Correct, cited answer in 4 turns (about 12 min of model time) in all three versions; before 1.35 it kept reading after finding it |
190
- | Comparison and recommendation | Python vs Rust for command-line tools; recommend one for a beginner. Later: SQLite vs PostgreSQL for a small web app, then a recommendation for a hobby project | 1.36, 1.42 | Python vs Rust: cited comparison recommending Python in 16 turns (33 min), Rust's libraries not covered. SQLite vs PostgreSQL: all three parts covered and cited, with a recommendation, in 13 turns (38 min) |
191
+ | Comparison and recommendation | Python vs Rust for command-line tools; recommend one for a beginner. Later: SQLite vs PostgreSQL for a small web app, then a recommendation for a hobby project | 1.36, 1.42, 1.50 | Python vs Rust: cited comparison recommending Python in 16 turns (33 min), Rust's libraries not covered. SQLite vs PostgreSQL: all three parts covered and cited, with a recommendation, in 13 turns (38 min); on 1.50 in 8 turns (25 min) |
191
192
  | List of items | Three free, open-source Photoshop alternatives and the strengths of each | 1.37–1.38 | GIMP, Krita and Darktable with their strengths, all cited, in 9 turns (24 min); 1.37 listed a fourth and added an unrequested opinion, which 1.38 fixes |
192
- | List of items | Three free, open-source alternatives to Microsoft Office and what each is best at | 1.46 | LibreOffice and OnlyOffice with their strengths, cited, in 18 turns (49 min); the third, FreeOffice, is free but not open source, and LibreOffice was recorded four times in different words |
193
+ | List of items | Three free, open-source alternatives to Microsoft Office and what each is best at | 1.46, 1.51 | LibreOffice and OnlyOffice with their strengths, cited, in 18 turns (49 min); the third, FreeOffice, is free but not open source, and LibreOffice was recorded four times in different words; on 1.51: LibreOffice, OnlyOffice and Apache OpenOffice, all open source, in 7 turns (16 min) |
193
194
  | Step-by-step how-to | Make a sourdough starter, with quantities and what to do each day | 1.36–1.37 | A clear plan in 7 turns (19 min), but it invented the first water amount; 1.37 stops guessing and flags unsupported numbers, and the agent then went looking for the missing details |
194
- | Step-by-step how-to | Repot a houseplant: what you need, the steps, and care for the first two weeks | 1.43–1.44 | 1.43 recorded six steps it wrote itself before reading the part that had them; 1.44 checks findings against the page and recorded the guide's real materials, steps and aftercare, finishing in 24 turns (68 min), though the final result left out one step |
195
+ | Step-by-step how-to | Repot a houseplant: what you need, the steps, and care for the first two weeks | 1.43–1.44, 1.51 | 1.43 recorded six steps it wrote itself before reading the part that had them; 1.44 recorded the guide's real steps but took 68 min and its result left one out. 1.51: every step from the page, in 7 turns (29 min), and it says that no care instructions beyond the first watering were found |
195
196
  | Explanation of a topic | How photosynthesis works: inputs and outputs, both stages and where each takes place | 1.41–1.42 | A complete, cited explanation in 18 turns (45 min); 1.41 read four parts of a relevant page without recording them and had not finished after 49 min |
196
197
 
197
198
  ## How it works
@@ -229,8 +230,9 @@ flowchart LR
229
230
  check: record something, or repeat the action to leave. Moving on to the next part of a page works the same
230
231
  way, and the model must first list what the part it is on says about the objective (or "none").
231
232
  - **Findings must match the page.** A finding that shares too few word pairs with the page text shown so far
232
- is sent back once, asking for only what the page says in its own words: a small model will otherwise
233
- "complete" a list from its own knowledge when a page says "follow these 6 steps" and the steps come later.
233
+ (or has a sentence that does) is sent back once, asking for only what the page says in its own words, and
234
+ one far from anything on the page is refused outright. A small model will otherwise "complete" a list from
235
+ its own knowledge when a page says "follow these 6 steps" and the steps come later.
234
236
  - **Numbered findings.** Findings are numbered in the prompt and listed with every check, and the model names
235
237
  the finding that covers each part of the objective ("covered by [2]") instead of just saying "covered".
236
238
  - **Knowing when it's done.** After every new finding the model is asked whether the objective is now fully
@@ -325,10 +327,10 @@ turn that brings in a new page takes a few minutes. See [Performance](#performan
325
327
  - **A small model's judgement is the ceiling.** It sometimes drifts to loosely related pages, prefers
326
328
  secondary sources or stays with one source too long, and results vary with the first pages a search turns
327
329
  up; every finding is in `research_report.md` regardless.
328
- - **Very strict objectives can keep it searching.** An objective that forbids finishing until everything is
329
- "fully validated" can keep the agent looking for something the web may not contain. It is told to finish
330
- and name what it could not find after looking in several places, but a 7B model does not always judge
331
- when that point has come; the turn limit (100) and Ctrl+C still apply, and the report is always current.
330
+ - **Very strict objectives.** An objective that forbids finishing until everything is "fully validated" used
331
+ to keep the agent looking for something the web may not contain. On 1.50 it finished such an objective in 25
332
+ minutes, but with thin findings and without naming what it could not find, so check the result against the
333
+ objective.
332
334
  - **Check the citations.** The final result is told to use only recorded findings, but a 7B model sometimes
333
335
  puts the wrong number on a citation, adds a line no finding supports or leaves out a step, so verify a
334
336
  claim against the listed findings before relying on it.
@@ -345,7 +347,7 @@ turn that brings in a new page takes a few minutes. See [Performance](#performan
345
347
  - [x] Write the final result in a separate, rule-bound step and flag unsupported numbers (1.32–1.38)
346
348
  - [x] Finish as soon as the objective is covered (1.35)
347
349
  - [x] Get the model to weigh each part of a long page before reading on (1.42)
348
- - [ ] Let very strict objectives end with the gaps named instead of searching indefinitely
350
+ - [ ] Let very strict objectives end with the gaps named instead of searching indefinitely (they finish since 1.50; naming the gaps is still open)
349
351
  - [ ] Try more kinds of objectives (seven so far) and adjust the rules that don't generalise
350
352
  - [ ] Make fresh prompts (the slowest kind of turn) cheaper still (31% cheaper since 1.34)
351
353
  - [ ] Interacting with pages (clicking, forms) for tasks beyond reading
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "localagentbrowser"
7
- version = "1.47"
7
+ version = "1.52"
8
8
  description = "An autonomous web agent driven by a local LLM (LM Studio) and Playwright. Runs entirely on your own machine."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -132,7 +132,8 @@ class TestReadOnCheck:
132
132
  page = unrecorded_page(DOCS)
133
133
  first = decide("READ_MORE", "", page, DOCS)
134
134
  assert first[0] == "CONFIRM_READ_ON" and "recorded nothing from part 1" in first[1]
135
- assert "write 'none'" in first[1]
135
+ assert "write 'none'" in first[1] and "choose FINISH instead of reading on" in first[1]
136
+ assert "not about the OBJECTIVE at all, leave it" in first[1]
136
137
  assert decide("READ_MORE", "", page, DOCS) is None, "repeating it goes through"
137
138
 
138
139
  def test_no_check_after_recording_from_the_shown_part(self):
@@ -156,10 +157,11 @@ class TestReadOnCheck:
156
157
  page.update(index=4, seen_parts={0, 1, 2, 3, 4}, reread_confirmed={2})
157
158
  assert decide("READ_MORE", "3", page, DOCS) is None
158
159
 
159
- def test_asking_for_the_part_already_shown_is_refused(self):
160
- result = decide("READ_MORE", "1", unrecorded_page(DOCS), DOCS)
161
- assert result[0] == "BLOCKED_READ_MORE" and "already shown" in result[1] and "For part 2" in result[1]
160
+ def test_asking_for_the_part_already_shown_means_the_next_one(self):
161
+ assert ab.read_more_target("1", unrecorded_page(DOCS)) == 1
162
+ assert decide("READ_MORE", "1", recorded_page(DOCS, parts=("a", "b")), DOCS) is None
162
163
  last = recorded_page(DOCS, parts=("a", "b"), index=1)
164
+ assert ab.read_more_target("2", last) == 1, "on the last part it stays a request for that part"
163
165
  assert "last part" in decide("READ_MORE", "2", last, DOCS)[1]
164
166
 
165
167
 
@@ -248,14 +250,13 @@ class TestGroundingCheck:
248
250
  page = unrecorded_page(GUIDE, parts=(self.PAGE, "Step 1: Water the plant the day before."))
249
251
  return page
250
252
 
251
- def test_a_discover_the_page_does_not_say_is_confirmed_once(self):
253
+ def test_a_discover_the_page_does_not_say_is_refused_even_when_repeated(self):
252
254
  page = self.page()
253
255
  invented = ("1. Gently remove the plant from its pot. 2. Loosen any tangled roots and trim dead ones. "
254
256
  "3. Place fresh potting mix in the bottom of the new pot.")
255
- first = decide("DISCOVER", invented, page, GUIDE)
256
- assert first[0] == "CONFIRM_GROUNDED" and "does not match the page text" in first[1]
257
- assert "DISCOVER again with only what the page actually says in part 1" in first[1]
258
- assert decide("DISCOVER", invented, page, GUIDE) is None, "repeating it goes through"
257
+ for _ in range(2):
258
+ result = decide("DISCOVER", invented, page, GUIDE)
259
+ assert result[0] == "BLOCKED_DISCOVER" and "comes from your own knowledge" in result[1]
259
260
 
260
261
  def test_a_discover_copied_from_the_page_passes(self):
261
262
  copied = "Choose a pot one to two inches wider than the current one, with drainage holes."
@@ -264,10 +265,10 @@ class TestGroundingCheck:
264
265
  def test_short_discoveries_are_not_checked(self):
265
266
  assert decide("DISCOVER", "Use a bigger pot.", self.page(), GUIDE) is None
266
267
 
267
- def test_real_advice_mixed_with_invented_steps_is_confirmed(self):
268
+ def test_real_advice_mixed_with_invented_steps_is_refused(self):
268
269
  mixed = ("Choose a pot one to two inches wider than the current one, with drainage holes. The steps are: "
269
270
  "loosen and inspect the roots, prune any dead roots, then fill with fresh potting mix.")
270
- assert decide("DISCOVER", mixed, self.page(), GUIDE)[0] == "CONFIRM_GROUNDED"
271
+ assert decide("DISCOVER", mixed, self.page(), GUIDE)[0] == "BLOCKED_DISCOVER"
271
272
 
272
273
 
273
274
  def test_recorded_parts_are_remembered_across_visits(monkeypatch):
@@ -290,15 +291,14 @@ def test_recorded_parts_are_remembered_across_visits(monkeypatch):
290
291
  assert decide("READ_MORE", "", state, GUIDE) is None, "part 1 was recorded on the earlier visit"
291
292
 
292
293
 
293
- def test_a_different_ungrounded_discover_is_checked_again():
294
+ def test_a_different_ungrounded_discover_is_refused_again():
294
295
  page = TestGroundingCheck().page()
295
296
  first = ("1. Gently remove the plant from its pot. 2. Loosen any tangled roots and trim dead ones before you "
296
297
  "start. 3. Place fresh potting mix in the bottom of the new pot.")
297
298
  second = ("During the first two weeks water your plant sparingly and avoid fertilizing it until it settles in. "
298
299
  "Keep it out of direct sun and check the soil every few days.")
299
- assert decide("DISCOVER", first, page, GUIDE)[0] == "CONFIRM_GROUNDED"
300
- assert decide("DISCOVER", second, page, GUIDE)[0] == "CONFIRM_GROUNDED"
301
- assert decide("DISCOVER", second, page, GUIDE) is None, "repeating the same one goes through"
300
+ assert decide("DISCOVER", first, page, GUIDE)[0] == "BLOCKED_DISCOVER"
301
+ assert decide("DISCOVER", second, page, GUIDE)[0] == "BLOCKED_DISCOVER"
302
302
 
303
303
 
304
304
  def test_the_objectives_wording_counts_as_grounded():
@@ -311,3 +311,24 @@ def test_the_objectives_wording_counts_as_grounded():
311
311
  page["grounding_asked"] = []
312
312
  tried["objective"] = "What is the tallest mountain in Africa, and how tall is it in meters and in feet?"
313
313
  assert ab.check_decision("DISCOVER", finding, [], tried, page, DOCS) is None
314
+
315
+
316
+ def test_an_invented_sentence_behind_copied_ones_is_confirmed():
317
+ page = unrecorded_page(GUIDE, parts=(
318
+ "Owls have played a crucial role in Native American cultures since time immemorial, with sightings often "
319
+ "associated with shapeshifters, spiritual messages and supernatural signs. In Christianity, owls symbolize "
320
+ "wisdom and appear in several places.",))
321
+ finding = ("Owls have played a crucial role in Native American cultures since time immemorial, with sightings often "
322
+ "associated with shapeshifters, spiritual messages and supernatural signs. In Christianity, owls symbolize "
323
+ "wisdom and are often depicted alongside other wise figures like the prophet Samuel.")
324
+ first = decide("DISCOVER", finding, page, GUIDE)
325
+ assert first[0] == "CONFIRM_GROUNDED" and "DISCOVER again with only what the page actually says in part 1" in first[1]
326
+ assert decide("DISCOVER", finding, page, GUIDE) is None, "repeating a borderline one goes through"
327
+
328
+
329
+ def test_finish_goes_through_once_a_search_is_exhausted():
330
+ tried = {"queries": ["schemas"], "urls": []}
331
+ page = recorded_page(DOCS)
332
+ for _ in range(2):
333
+ assert ab.check_decision("SEARCH", "schemas", [], tried, page, DOCS)[0] == "BLOCKED_SEARCH"
334
+ assert ab.check_decision("FINISH", "result", [], tried, page, DOCS, "Schemas: not covered") is None
@@ -55,4 +55,14 @@ def test_old_entries_are_dropped_in_batches():
55
55
 
56
56
  def test_unverified_numbers_ignores_grouped_citations():
57
57
  found = [{"extracted_data": "LibreOffice offers good compatibility."}]
58
- assert ab.unverified_numbers("LibreOffice has good compatibility [4, 5] and more [2;3].", found) == []
58
+ assert ab.unverified_numbers("LibreOffice has good compatibility [4, 5] and more [2;3] [1-4].", found) == []
59
+
60
+
61
+ def test_restore_path_escapes():
62
+ # What json.loads makes of "C:\path\to\new\real" written with single backslashes
63
+ mangled = "python -m venv C:\\path" + "\t" + "o" + "\n" + "ew" + "\r" + "eal"
64
+ assert ab.restore_path_escapes(mangled) == r"python -m venv C:\path\to\new\real"
65
+ assert ab.restore_path_escapes("First line.\nSecond line.") == "First line.\nSecond line.", "real newlines stay"
66
+ row = "Medium plants\t4-5 inches\tChoose a pot just 1-2 inches wider"
67
+ assert ab.restore_path_escapes(row) == row, "tabs from a page table stay"
68
+ assert ab.restore_path_escapes("C:" + "\t" + "emp") == r"C:\temp"
@@ -93,12 +93,25 @@ class TestComposeFinalResult:
93
93
  assert "unless the objective explicitly asks for your view" in user["content"] and "Do not fill gaps" in user["content"]
94
94
  assert "give exactly that many" in user["content"]
95
95
  assert "describes that situation, not the one in the OBJECTIVE" in user["content"]
96
+ assert "Copy commands, code, file paths and names exactly" in user["content"]
96
97
  assert "DRAFT" not in user["content"], "the agent's unverified draft is not shown"
97
98
 
98
99
  def test_unwraps_a_json_reply(self):
99
100
  client, _ = fake_client([reply("FINISH", "My view: supported by [1].")])
100
101
  assert asyncio.run(ab.compose_final_result(client, "objective", discoveries("x"))) == "My view: supported by [1]."
101
102
 
103
+ def test_adds_a_missing_view_when_the_objective_asks_for_one(self):
104
+ client, model = fake_client(["Owls mean wisdom in Greece [1].", "Their night habits invite mystery."])
105
+ text = asyncio.run(ab.compose_final_result(client, "Find views on owls, then give your own opinion.",
106
+ discoveries("Greece: wisdom")))
107
+ assert text == "Owls mean wisdom in Greece [1].\n\nMy view: Their night habits invite mystery."
108
+ assert "does not give it yet" in model.calls[1][-1]["content"]
109
+
110
+ def test_no_extra_call_when_no_view_is_asked_for(self):
111
+ client, model = fake_client(["Three items [1]."])
112
+ asyncio.run(ab.compose_final_result(client, "Name three items.", discoveries("x")))
113
+ assert len(model.calls) == 1
114
+
102
115
 
103
116
  class TestConversation:
104
117
  def test_turns_continue_one_conversation(self, scrapes):