Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
192 commits
Select commit Hold shift + click to select a range
74fbef0
[eric] browser: auto-attach OOPIF child frames and route CDP by sessi…
ciregenz Jun 2, 2026
21360cd
[eric] browser: rank/cap/goal-boost the element list and click into c…
ciregenz Jun 2, 2026
82dcb6b
[eric] browser: per-domain hints, deterministic stagnation, and aux-L…
ciregenz Jun 2, 2026
43ba7d2
[eric] browser: unit and end-to-end loop tests for the new browser be…
ciregenz Jun 2, 2026
f574696
[eric] browser: one-shot crash-safe trial switch for webview on Windo…
ciregenz Jun 2, 2026
6e006dd
[eric] browser: detect a page's declared WebMCP tools (tier 1)
ciregenz Jun 2, 2026
e0359e0
[eric] browser: passively capture redacted shadow-API routes while br…
ciregenz Jun 2, 2026
15781d2
[eric] browser: list captured routes and replay safe GETs to skip the…
ciregenz Jun 2, 2026
ea82395
[eric] browser: loop test for WebMCP-detect and route list/replay tools
ciregenz Jun 2, 2026
a896483
[eric] browser: record per-tool latency, tokens, and per-task complet…
ciregenz Jun 2, 2026
e2102af
[eric] browser: metrics analyzer with ghost-success detection
ciregenz Jun 2, 2026
54249bc
[eric] browser: retry cold screenshot capture to dodge the turn-0 viz…
ciregenz Jun 2, 2026
8a0cb4a
[eric] browser: human-performance benchmark harness for rudimentary t…
ciregenz Jun 2, 2026
69c4546
[eric] browser: refine ghost detector so read tasks need returned con…
ciregenz Jun 2, 2026
e911898
[eric] browser: add robust click_by_name and share enumerate/click he…
ciregenz Jun 2, 2026
d668219
[eric] browser: in-memory skill cache that distills tasks into replay…
ciregenz Jun 2, 2026
d1c1b44
[eric] browser: replay learned skills with zero LLM calls, fall back …
ciregenz Jun 2, 2026
0144144
[eric] browser: decompose BrowserBatch into discrete replayable skill…
ciregenz Jun 2, 2026
a015036
[eric] browser: prompt-cache the fixed prefix and push batching to cu…
ciregenz Jun 2, 2026
3aa9cf6
[eric] browser: front-load page perception into turn 1 to cut orienta…
ciregenz Jun 2, 2026
e30d70c
[eric] browser: persist skills across sessions with secret-redaction …
ciregenz Jun 2, 2026
e2a77b5
[eric] browser: parameterize skills so one skill serves a task with d…
ciregenz Jun 2, 2026
5bb0b46
[eric] browser: give the agent tools to list and deprecate its own le…
ciregenz Jun 2, 2026
294e96d
[eric] browser: version skills behind a trust gate, compose on proven…
ciregenz Jun 3, 2026
2b2d590
[eric] browser: instrument skill lifecycle, measure replay speedup, f…
ciregenz Jun 3, 2026
6801746
[eric] browser: add a deterministic completion-honesty gate to catch …
ciregenz Jun 3, 2026
34faca1
[eric] browser: resolve the live card host for replay and report did-…
ciregenz Jun 3, 2026
bcfbb8d
[eric] browser: fail fast with an honest message when the card's webv…
ciregenz Jun 3, 2026
54d7e3c
[eric] browser: key skills on the user's stable request, not the orch…
ciregenz Jun 3, 2026
78f35aa
[eric] browser: re-check for a replayable skill after the first nav l…
ciregenz Jun 3, 2026
1208a2e
[eric] browser: don't record a skill when the deliverable was gathere…
ciregenz Jun 3, 2026
fe082c4
[eric] browser: durable reflective per-site playbook (tier-2 memory, …
ciregenz Jun 3, 2026
f187ec7
[eric] browser: surface learned playbook in-chat + REST list/forget s…
ciregenz Jun 3, 2026
e6a1a5f
[eric] browser: measure whether the playbook cuts exploration turns, …
ciregenz Jun 3, 2026
3a601ba
[eric] browser: calm zero-click signals so the user feels the agent r…
ciregenz Jun 3, 2026
4c0749d
[eric] browser: subtle Remembered/Learned chip on the agent card so m…
ciregenz Jun 3, 2026
7572462
[eric] browser: intra-run batch replay (BrowserRepeatFlow) with verif…
ciregenz Jun 3, 2026
186c6cf
[eric] browser: smart-wait returns when the page's network settles in…
ciregenz Jun 3, 2026
21b225d
[eric] browser: nudge the agent toward the fast captured-API tier onc…
ciregenz Jun 3, 2026
7175f4b
[eric] browser: sample captured-route count on read, not navigate, so…
ciregenz Jun 3, 2026
50230c8
[eric] browser: also sample route count on evaluate (the agent's real…
ciregenz Jun 3, 2026
61826d4
[eric] browser: BrowserRepeatFlow hands back capped per-item read dat…
ciregenz Jun 3, 2026
f121fa4
[eric] browser: surface a redacted concrete route example so the agen…
ciregenz Jun 3, 2026
9080f25
[eric] browser: log front-loaded perception reads so a read answered …
ciregenz Jun 3, 2026
4f48010
[eric] browser: route same-flow batch tasks to one sub-agent so Brows…
ciregenz Jun 3, 2026
792767b
[eric] browser: bound each smart-wait probe so a hung tab can't inher…
ciregenz Jun 3, 2026
bb422c0
[eric] browser: treat a hung tab (repeated timeouts) as card-unavaila…
ciregenz Jun 3, 2026
3d0dcca
[eric] browser: per-action command timeouts (reads 12s, nav 20s) so a…
ciregenz Jun 3, 2026
33f6e45
[eric] web: subscription-OAuth WebSearch 401s on token rotation, so k…
ciregenz Jun 3, 2026
928df4f
[eric] websearch: gate built-in tool by model route so subscription C…
ciregenz Jun 4, 2026
cf8e1f2
[eric] websearch: fast-first bounded search/fetch cascade for human-s…
ciregenz Jun 4, 2026
a562a5c
[eric] browser: prune stale screenshots + capture downscaled JPEG to …
ciregenz Jun 4, 2026
b77b294
[eric] browser: cut per-action waste (skip discarded loop-hash, cache…
ciregenz Jun 4, 2026
dc248b6
[eric] browser: prototype the shadow-API replay overlay (unwired refe…
ciregenz Jun 4, 2026
4688ca4
[eric] onboarding: step 3 says chat bubble, the new-agent + became a …
ciregenz Jun 4, 2026
609c4f6
[eric] overlays: extract PlanPickerModal from MessageBubble for reuse
ciregenz Jun 4, 2026
21d8c6c
[eric] settings: fold OpenSwarm Pro into the subscriptions list, pric…
ciregenz Jun 4, 2026
27686e2
[eric] browser: settle-aware in-batch wait and a terminal read sub-ac…
ciregenz Jun 4, 2026
1324d47
[eric] browser: skip dashboard capture while an agent is driving a we…
ciregenz Jun 4, 2026
9fd8936
[eric] browser: reset metrics-dir cache per test so suite isolation h…
ciregenz Jun 4, 2026
f59ab9d
[eric] auth: drop the mandatory first-launch sign-in gate, app works …
ciregenz Jun 4, 2026
e5ff670
[eric] settings: sign-in becomes an optional dialog opened from the a…
ciregenz Jun 4, 2026
3cf1c51
[eric] dashboard: empty-state copy says chat bubble, not the old plus
ciregenz Jun 4, 2026
085cade
[eric] settings: version-free model descriptions, monthly model churn…
ciregenz Jun 4, 2026
0446e9b
[eric] dashboard: boot straight into the latest dashboard with the si…
ciregenz Jun 4, 2026
6b737f2
[eric] dashboard: toolbar tabs pop in staggered, chat button bounces …
ciregenz Jun 4, 2026
ad75228
[eric] browser: guard capture-page on churning webviews to dodge the …
ciregenz Jun 4, 2026
aaed2c5
[eric] dashboard: empty state shows the literal chat bubble glyph, ic…
ciregenz Jun 4, 2026
3c3dd6c
[eric] browser: prompt the agent to plan once then run terse batched …
ciregenz Jun 4, 2026
2768d73
[eric] browser: log per-turn output tokens to measure terse-execution…
ciregenz Jun 4, 2026
5dcdc88
[eric] browser: abort a batch on the first failed sub-action, not jus…
ciregenz Jun 4, 2026
f3ff27a
[eric] browser: on deep stall, nudge a strategic re-plan, not just a …
ciregenz Jun 4, 2026
19e7816
[eric] browser: fix stale prompt lines for batch-abort-on-error and s…
ciregenz Jun 4, 2026
a32f53d
[eric] browser: de-dupe the system prompt and sharpen the terse-outpu…
ciregenz Jun 4, 2026
a4b3475
[eric] browser: flag a wall at 2 repeats and make stuck-nudges diagno…
ciregenz Jun 4, 2026
2640930
[eric] browser: add BrowserGetConsole so a stuck agent can read the p…
ciregenz Jun 4, 2026
ed3d4a3
[eric] browser: drop the infinite glow animations on agent-driven car…
ciregenz Jun 4, 2026
f014469
[eric] browser: end the wait on the target or DOM-settle, not network…
ciregenz Jun 4, 2026
9b6e45f
[eric] browser: act-and-confirm, declare the expected change and veri…
ciregenz Jun 4, 2026
f03dde7
[eric] browser: focus text boxes by node and insert text directly, re…
ciregenz Jun 4, 2026
f791728
[eric] browser: block autoplay in agent webviews and reload a hung re…
ciregenz Jun 5, 2026
f7ac556
[eric] browser: bound AX perception and cap the tracker-iframe walk s…
ciregenz Jun 5, 2026
4ba14c5
[eric] browser: remove the last card-sized glow pulse, the inset acce…
ciregenz Jun 5, 2026
53ffe92
[eric] browser: return navigate on dom-ready so heavy SPAs stop hangi…
ciregenz Jun 5, 2026
3f38852
[eric] browser: keep spawned cards alive for parent reuse, clean on p…
ciregenz Jun 5, 2026
b02aecb
[eric] browser: hit-test clicks before dispatch and verify typed text…
ciregenz Jun 5, 2026
98075fc
[eric] delete deprecated docs
ciregenz Jun 5, 2026
0249b66
[eric] browser: sub-agent prompt jumps to deep URLs, requires OUTCOME…
ciregenz Jun 5, 2026
05fa871
[eric] browser: occlusion filter + containment dedup + new-element ma…
ciregenz Jun 5, 2026
01e3e2b
[eric] browser: re-broadcast ws commands lost in dead-socket gaps, re…
ciregenz Jun 5, 2026
9cc245b
[eric] frontend: sidestep @types/react allowpopups boolean clash, kee…
ciregenz Jun 5, 2026
c0ff256
[eric] browser: ctx-tagged twin rows + stable indices across looks in…
ciregenz Jun 5, 2026
7949241
[eric] electron: disable renderer backgrounding, hidden-window agent …
ciregenz Jun 5, 2026
262d48f
[eric] browser: refuse zero-LLM replay of send/submit skills, the liv…
ciregenz Jun 5, 2026
88088a9
[eric] browser: BrowserExtract, aux-model schema extract + prompt doc…
ciregenz Jun 5, 2026
ed3ffd2
[eric] tests: reset metrics-dir cache in conftest, analyzer tests wro…
ciregenz Jun 5, 2026
65427f3
[eric] browser: reuse the parent's same-host card on re-dispatch, sta…
ciregenz Jun 5, 2026
ef30f9f
[eric] browser: widen secret redaction (2fa codes, credential fields)…
ciregenz Jun 5, 2026
17eac64
[eric] browser: derive the reuse host from the task text when the dis…
ciregenz Jun 5, 2026
20e1558
[eric] browser: auto-settle mutating actions and attach fresh page st…
ciregenz Jun 5, 2026
7b5b7ca
[eric] browser: prune superseded page-state attachments and heavy rea…
ciregenz Jun 5, 2026
059185d
[eric] browser: kill the double-fill, textbox rows show live value, t…
ciregenz Jun 5, 2026
4c5962d
[eric] browser: backgroundThrottling=no on agent webviews, occluded w…
ciregenz Jun 5, 2026
0992e25
[eric] browser: numbered index overlays on screenshots, vision and cl…
ciregenz Jun 5, 2026
99bd15f
[eric] browser: attached page state shrinks to changed rows, unchange…
ciregenz Jun 5, 2026
4f73693
[eric] browser: batch-by-default prompt, irreversible steps stay solo…
ciregenz Jun 5, 2026
6c83e1c
[eric] browser: orchestrator trusts the OUTCOME line, no verify re-di…
ciregenz Jun 5, 2026
de68d5e
[eric] browser: drop the top border-shimmer strip, static border and …
ciregenz Jun 5, 2026
4ebe080
[eric] chat: continue-chat banner and select-element button stop puls…
ciregenz Jun 5, 2026
1d4542b
[eric] electron: race CDP sendCommand with 10s timeout, recycle debug…
ciregenz Jun 5, 2026
8ee209a
[eric] browser: fast path skips orchestrator for browser-only first m…
ciregenz Jun 5, 2026
8a714b5
[eric] chat: 60fps reveal via imperative tail appends, markdown re-pa…
ciregenz Jun 5, 2026
fe63ae6
[eric] browser: fast path sharpens tasks via aux brief and retries on…
ciregenz Jun 5, 2026
792ff7e
[eric] browser: batch escape hatch narrowed, solo only when next targ…
ciregenz Jun 5, 2026
e37ba6c
[eric] browser: skill recording judges the task ask, OUTCOME boilerpl…
ciregenz Jun 5, 2026
84eeb29
[eric] browser: three-way fast path, READ answers public pages via lo…
ciregenz Jun 5, 2026
aaa0d30
[eric] browser: fast path failure detection fail-closed, card-gone su…
ciregenz Jun 5, 2026
6ac76d3
[eric] backend: aux-generated names get squeezed short, refusals fall…
ciregenz Jun 5, 2026
dde8186
[eric] dashboard: header name ellipsizes at 320px as a backstop
ciregenz Jun 5, 2026
2c55d17
[eric] browser: dead-dashboard dispatches fail in 1s instead of burni…
ciregenz Jun 5, 2026
99ba826
[eric] backend: INFO logging visible in dev plus ms-level traces acro…
ciregenz Jun 5, 2026
88ba59f
[eric] browser: send-skill prefix replay hands off at the composer, u…
ciregenz Jun 5, 2026
b3541d5
[eric] browser: possessive apostrophes no longer break skill sig matc…
ciregenz Jun 5, 2026
7a30cc9
[eric] browser: prefix replay also fires at the deferred host re-chec…
ciregenz Jun 5, 2026
249f425
[eric] backend: stat-validated caches for settings.json and tools dir…
ciregenz Jun 5, 2026
8c556dd
[eric] dashboard: memoize the browser-agent session lookup, drags sto…
ciregenz Jun 5, 2026
f2632d5
[eric] browser: long click names are card blobs not send controls, pr…
ciregenz Jun 5, 2026
e010a40
[eric] browser: prefix cut expectation pinned to the Message composer…
ciregenz Jun 5, 2026
1480fdd
[eric] browser: replay clicks scroll-and-retry when off-screen, long …
ciregenz Jun 6, 2026
2fe8e0f
[eric] browser: schema-forced batching, solo mutators hidden, ClickIn…
ciregenz Jun 6, 2026
69f15ce
[eric] browser: batched click_index distills via per-sub identities, …
ciregenz Jun 6, 2026
2a3e531
[eric] browser: park send-skill prefix replay behind a flag until set…
ciregenz Jun 6, 2026
8c060da
[eric] browser: nudge redundant solo re-reads, the biggest turn sink …
ciregenz Jun 6, 2026
1a86ee2
[eric] browser: redundant-read flag survives intervening reads, catch…
ciregenz Jun 6, 2026
dcba6d4
Revert "[eric] browser: redundant-read flag survives intervening read…
ciregenz Jun 6, 2026
065c006
Revert "[eric] browser: nudge redundant solo re-reads, the biggest tu…
ciregenz Jun 6, 2026
e7099cc
Revert "[eric] browser: schema-forced batching, solo mutators hidden,…
ciregenz Jun 6, 2026
9af3375
[eric] browser: re-land schema-forced batching + redundant-read nudge…
ciregenz Jun 6, 2026
339d756
[eric] browser: hard send-guard on live batches, irreversible steps p…
ciregenz Jun 6, 2026
042e7db
[eric] browser: pruning-safe incremental cache marker, transcript fin…
ciregenz Jun 6, 2026
fcc02ce
[eric] browser: evaluation_previous optional, output tokens drive tur…
ciregenz Jun 6, 2026
6fd1047
[eric] browser: mechanical verify-first gate on send-class recovery, …
ciregenz Jun 6, 2026
92df6ea
[eric] browser: send-probe gate catches focus-type payloads, batched …
ciregenz Jun 6, 2026
be35065
[eric] browser: similar-skill route hints with adoption telemetry, pl…
ciregenz Jun 6, 2026
62758cc
[eric] browser: fast-path brief ENTRY url flows into dispatch, new ca…
ciregenz Jun 6, 2026
56eea88
[eric] browser: wire route hints, entry pre-nav, auto candidate scan …
ciregenz Jun 6, 2026
852968d
[eric] browser: candidate scan also covers the pre-nav landing, share…
ciregenz Jun 6, 2026
1b45a26
[eric] browser: playbook distill also captures perception traps, not …
ciregenz Jun 6, 2026
6dc00e4
[eric] browser: think-shorter, no redundant prose beside action tools…
ciregenz Jun 6, 2026
cdbdec7
[eric] browser: merge-verify, a confirmed send is the proof, no separ…
ciregenz Jun 6, 2026
4e32003
[eric] browser: prune abandoned-navigate detours at distill so record…
ciregenz Jun 6, 2026
d4e4327
[eric] browser: settle on the click target before a replay step so a …
ciregenz Jun 6, 2026
581af5b
[eric] browser: auto-dismiss junk interstitials (cookie/upsell/coachm…
ciregenz Jun 6, 2026
01ec866
[eric] browser: OSW_BROWSER_NO_LEVERS bench flag to A/B the speed lev…
ciregenz Jun 6, 2026
9972171
[eric] browser: cheap-model laps behind OSW_BROWSER_CHEAP_LAPS, escal…
ciregenz Jun 6, 2026
5741e8f
[eric] browser: endgame helper locates Send after a composer fill (ne…
ciregenz Jun 6, 2026
07ea7d0
[eric] browser: compose helper settles and re-lists once when Send re…
ciregenz Jun 6, 2026
13baf94
[eric] browser: compose helper polls for the late-rendering Send butt…
ciregenz Jun 6, 2026
d542ef2
[eric] browser: compose helper also fires when the fill is a batched …
ciregenz Jun 6, 2026
0cc6188
[eric] browser: generalizable send-timing prior (Send renders late, s…
ciregenz Jun 7, 2026
6380480
[eric] browser: on a recoverable error (stale index/occlusion) attach…
ciregenz Jun 7, 2026
4664065
[eric] browser: converge speed-lever A/B, ship the winners, cut the d…
ciregenz Jun 7, 2026
59658dd
[eric] browser: strip orphan surrogates from webview text so emoji pa…
ciregenz Jun 7, 2026
f186e0f
[eric] browser: ship seed playbooks so a fresh install starts warm on…
ciregenz Jun 7, 2026
7d2d8ea
[eric] browser: pin why send-skill prefix replay stays parked (opener…
ciregenz Jun 7, 2026
0d66e31
[eric] browser: only the Send ends a skill's replay prefix, not the c…
ciregenz Jun 7, 2026
40ca175
[eric] browser: send into an already-open thread instead of re-verify…
ciregenz Jun 7, 2026
39f35a1
[eric] browser: after typing, click the Send index you already saw in…
ciregenz Jun 7, 2026
a588645
[eric] browser: ride out a momentary dashboard WS drop instead of fai…
ciregenz Jun 8, 2026
a54b4c0
[eric] browser: run the action and remind on a missing ReportProgress…
ciregenz Jun 8, 2026
889c770
[eric] browser: a confirmed send ends the run, no more stalling to re…
ciregenz Jun 8, 2026
0f63697
[eric] browser: confirm waits for the target to actually render so a …
ciregenz Jun 8, 2026
e19def1
[eric] browser: a clean Send-class click ends the run; bound the look…
ciregenz Jun 8, 2026
1204e73
[eric] browser: stall backstop only fires after a real action, never …
ciregenz Jun 8, 2026
7ec0561
[eric] browser: hand the Send button to the model after a composer fi…
ciregenz Jun 8, 2026
6ef7b33
[eric] browser: cross-site meta-playbook (tier 3) + self-audit of the…
ciregenz Jun 8, 2026
2409e2d
[eric] browser: fire the learning self-audit every 25 finished tasks …
ciregenz Jun 8, 2026
d5fc146
[eric] browser: never leave a tool_use unanswered (backfill mid-turn …
ciregenz Jun 8, 2026
725578b
[eric] browser: orchestrator translates the OUTCOME line into a plain…
ciregenz Jun 8, 2026
ce8e74b
[eric] browser: skill-replay returns a normal OUTCOME line so the par…
ciregenz Jun 8, 2026
f8c8123
[eric] browser: agent writes a plain human final message (no UI jargo…
ciregenz Jun 8, 2026
2d59a0f
[eric] browser: stall-path completion text is a plain confirmation, n…
ciregenz Jun 8, 2026
cdba825
[eric] browser: final message is ONLY the OUTCOME line in plain human…
ciregenz Jun 8, 2026
bf5f955
[eric] browser: wait up to 6s (not 2.5s) for the lazy-rendering Send …
ciregenz Jun 8, 2026
8d9c758
[eric] browser: restore the Send-button handoff (points the model at …
ciregenz Jun 8, 2026
b634638
[eric] browser: poll the interactives list for the real Send button a…
ciregenz Jun 8, 2026
7238f86
[eric] electron: silence the per-CDP-node ipc.handle log spam (hundre…
ciregenz Jun 8, 2026
110dc1c
[eric] browser: fast-path emits a synthetic BrowserAgent tool-call/re…
ciregenz Jun 8, 2026
030844d
[eric] onboarding: Skip fully dismisses the tour (hidden) until Setti…
ciregenz Jun 8, 2026
1d57ee6
[eric] settings: new agent chats spawn expanded by default (toggle of…
ciregenz Jun 8, 2026
dd0f70d
[eric] onboarding: delete the now-unreachable docked resume tab, migr…
ciregenz Jun 8, 2026
4e78925
[eric] release: bump version to 1.2.73
ciregenz Jun 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ permissions:
jobs:
# Release-readiness gate (mirrors release-windows): the dogfood loop must have validated 12 consecutive clean runs per platform OR the v* tag halts before any DMG is built.
release-gate:
if: false # bypassed through v1.1.72: dogfood loop not yet at 12 clean runs, shipping Mac now (mirrors release-windows); remove this line to re-arm the gate
if: false # bypassed through v1.2.73: dogfood loop not yet at 12 clean runs, shipping Mac now (mirrors release-windows); remove this line to re-arm the gate
runs-on: ubuntu-latest
permissions: { contents: read, actions: read }
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ jobs:
$dir = 'electron/dist/squirrel-windows'
pwsh -NoProfile -File scripts\gen-squirrel-latest-yml.ps1 -SetupPath "$dir/OpenSwarm-Setup-x64.exe" -Version $version -OutPath "$dir/latest.yml"
# Experimental builds: electron-updater (allowPrerelease) fetches a channel
# yml named after the first semver-suffix id (1.1.72-rc.1 -> rc.yml). Same
# yml named after the first semver-suffix id (1.2.73-rc.1 -> rc.yml). Same
# content as latest.yml; copy it so the experimental channel resolves.
if ($version -match '-([0-9A-Za-z]+)') {
Copy-Item "$dir/latest.yml" "$dir/$($matches[1]).yml" -Force
Expand Down
229 changes: 184 additions & 45 deletions backend/apps/agents/agent_manager.py

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions backend/apps/agents/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,32 @@ async def get_browser_agent_children(session_id: str):
children = agent_manager.get_browser_agent_children(session_id)
return {"sessions": children}

@agents.router.get("/browser-memory")
async def list_browser_memory():
"""Everything the browser agent has learned, per site, so the user can see it
and clear it: tier-1 skills (replayable shortcuts) + tier-2 playbook (strategy
text). Read-only; pure introspection."""
from backend.apps.agents.browser import browser_playbook, browser_skills
sites: dict[str, dict] = {}
for entry in browser_playbook.list_hosts():
sites.setdefault(entry["host"], {"host": entry["host"], "skills": [], "strategy": []})
sites[entry["host"]]["strategy"] = entry["bullets"]
sites[entry["host"]]["updated_at"] = entry.get("updated_at", 0)
for host in list(sites.keys()):
sites[host]["skills"] = browser_skills.list_skills(host)
return {"sites": sorted(sites.values(), key=lambda s: -s.get("updated_at", 0))}


@agents.router.delete("/browser-memory/{host}")
async def forget_browser_memory(host: str):
"""Clear what the browser agent learned about one site (strategy + skills); it
re-learns on the next successful run."""
from backend.apps.agents.browser import browser_playbook, browser_skills
forgot_strategy = browser_playbook.forget(host)
forgot_skills = browser_skills.forget_host(host)
return {"ok": True, "host": host, "forgot_strategy": forgot_strategy, "forgot_skills": forgot_skills}


@agents.router.post("/sessions/{session_id}/resume")
async def resume_session(session_id: str):
try:
Expand Down
1,603 changes: 1,502 additions & 101 deletions backend/apps/agents/browser/browser_agent.py

Large diffs are not rendered by default.

293 changes: 293 additions & 0 deletions backend/apps/agents/browser/browser_batch_replay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
"""
Intra-run batch replay: do a mechanical sub-flow ONCE, then replay it for many
inputs without re-screenshotting/re-analyzing each time.

The case (the user's): the agent searches LinkedIn and reads person A's profile,
realizes it must do the same for B, C, D... Instead of the full
screenshot->analyze->decide loop per person, it hands us the step template (with
{{value}} where the input varies) + the list of values, and we replay it per
value at machine speed: zero screenshots, zero LLM turns.

Held to extreme rigor because this is the HIGHEST ghost-risk feature, per-item
pages vary (profile A has "Message", B is connect-only, C hits a wall), so blind
replay would click the wrong thing and claim success it didn't earn:

1. VERIFY EVERY STEP, FALL BACK NEVER GHOST. Each step's result is checked; the
instant an item's page doesn't match the template (any step errors), that
item is abandoned and reported as needs-manual, the loop never pretends.
Honest tally always: "did N of M; these need you."

2. SENDS ARE GATED, READS ARE FREE. Read/search/navigate loops are safe and run
freely. Any irreversible step (click "Send"/"Submit"/"Connect"/"Pay"/..., or
typing into a message composer) makes the whole template unsafe to auto-replay,
we refuse and tell the agent to do those one at a time with confirmation. A
ghost in a send-loop isn't slow, it's wrong messages, so we don't allow it.

3. NETWORK-FIRST WHERE POSSIBLE. A step can be a `replay_route` (hit a captured
API endpoint with the value substituted) instead of clicking the UI, the fast,
reliable tier the audit found we use ~0% of the time. A read-loop over an
endpoint is dramatically faster than navigating N pages.

This module is the PURE, browser-free core (validate / gate / fill); the execute+
verify+report loop lives in browser_agent where the executor is.
"""

import re

PLACEHOLDER = "{{value}}"

# Agent-facing step action -> (tool_name, the param keys it carries).
_STEP_TOOLS: dict[str, tuple[str, tuple[str, ...]]] = {
"navigate": ("BrowserNavigate", ("url",)),
"get_text": ("BrowserGetText", ()),
"evaluate": ("BrowserEvaluate", ("expression",)),
"type": ("BrowserType", ("selector", "text")),
"click": ("BrowserClickByName", ("role", "name")),
"press_key": ("BrowserPressKey", ("key",)),
"scroll": ("BrowserScroll", ("direction", "amount")),
"replay_route": ("BrowserReplayRoute", ("url",)), # the fast network tier
}

# Reads/navigation don't mutate anything irreversible; safe to loop freely.
_READONLY_ACTIONS = {"navigate", "get_text", "evaluate", "scroll", "replay_route"}

# Irreversible / outward-facing words on a clicked control. Conservative on
# purpose: we'd rather refuse a borderline loop than auto-send 10 messages.
_SEND_NAME_RE = re.compile(
r"\b(send|submit|post|publish|connect|invite|follow|like|react|comment|reply|"
r"share|message|dm|pay|buy|order|checkout|purchase|place\s*order|book|"
r"confirm|apply|accept|decline|delete|remove|unsend|withdraw|endorse)\b",
re.I,
)
# A field that reads like a message/comment composer; typing here is part of a send.
_COMPOSE_SEL_RE = re.compile(r"message|compose|comment|msg|reply|editor|body|tweet|post", re.I)


def is_send_step(step: dict) -> bool:
"""True if this step is irreversible / outward-facing, so the whole loop must
be gated rather than auto-replayed."""
action = step.get("action")
if action == "click" and _SEND_NAME_RE.search(str(step.get("name") or "")):
return True
if action == "type" and _COMPOSE_SEL_RE.search(str(step.get("selector") or "")):
return True
return False


def validate_template(steps) -> tuple[bool, str]:
"""Structural check: non-empty, every step a known action with its required
fields present. Returns (ok, reason)."""
if not isinstance(steps, list) or not steps:
return False, "no steps provided"
for i, step in enumerate(steps):
if not isinstance(step, dict):
return False, f"step {i+1} is not an object"
action = step.get("action")
spec = _STEP_TOOLS.get(action)
if not spec:
return False, f"step {i+1}: unknown action {action!r} (allowed: {', '.join(_STEP_TOOLS)})"
_, required = spec
for key in required:
if step.get(key) in (None, ""):
return False, f"step {i+1} ({action}) is missing '{key}'"
return True, ""


def template_safety(steps) -> tuple[bool, str]:
"""True if the template is safe to auto-replay (no irreversible step). On a
send/submit step, returns (False, reason naming it) so the caller refuses and
routes those through normal per-item confirmation."""
for i, step in enumerate(steps):
if is_send_step(step):
what = step.get("name") or step.get("selector") or step.get("action")
return False, (f"step {i+1} looks irreversible/outward-facing ({what!r}); "
"do sends/submits one at a time with confirmation, not in a batch")
return True, ""


# Like _SEND_NAME_RE minus composer-openers ("Message"/"DM" buttons open a
# compose box, they don't send), so routine flows still batch freely.
_LIVE_IRREVERSIBLE_RE = re.compile(
r"\b(send|submit|post|publish|connect|invite|follow|like|react|comment|reply|"
r"share|pay|buy|order|checkout|purchase|place\s*order|book|"
r"confirm|apply|accept|decline|delete|remove|unsend|withdraw|endorse)\b",
re.I,
)


def is_replay_boundary(step: dict) -> bool:
"""The genuinely irreversible step where a learned skill's mechanical replay
must STOP and hand to the live agent. Same as is_send_step EXCEPT a composer
OPENER ('Message'/'DM' click) is reversible and NOT a boundary: the prefix can
mechanically open the composer, and only the real Send (and composer typing)
crosses to the live model. Uses the same opener-excluded wordlist the live
send-guard already trusts, so a recorded Send still stops the prefix."""
action = step.get("action")
if action == "click" and _LIVE_IRREVERSIBLE_RE.search(str(step.get("name") or "")):
return True
if action == "type" and _COMPOSE_SEL_RE.search(str(step.get("selector") or "")):
return True
return False


def live_batch_guard(actions, seen_lines, composer_pending: bool = False) -> str:
"""Reason string if a live BrowserBatch carries an irreversible step, else ''.

The solo-send rule was prompt-only until now; this makes it physical. A
click_index resolves to its element line from the last attached state (an
unresolvable index passes: it fails at execution anyway), and Enter after
typing into a composer counts as the send it is. composer_pending arms the
Enter check across turns: r47 typed solo then batched [Enter, wait], which
slid past the within-batch check."""
typed_composer = composer_pending
for i, a in enumerate(actions or []):
if not isinstance(a, dict):
continue
typ = a.get("type")
params = a.get("params") if isinstance(a.get("params"), dict) else {}
label = ""
if typ == "click_index":
prefix = f"[{params.get('index')}]"
label = next((l for l in (seen_lines or ()) if str(l).startswith(prefix)), "")
elif typ == "click":
label = str(params.get("selector") or "")
elif typ == "type":
if _COMPOSE_SEL_RE.search(str(params.get("selector") or "")):
typed_composer = True
continue
elif typ == "press_key":
if typed_composer and str(params.get("key") or "").strip().lower() in ("enter", "return"):
return (f"sub-action {i+1} presses Enter after typing into a message "
"composer, which sends it")
continue
else:
continue
# selectors hide words behind underscores/dashes (msg-form__send-button),
# which defeat \b; flatten separators so the word check still sees them
if label and _LIVE_IRREVERSIBLE_RE.search(re.sub(r"[_\-./#\[\]]+", " ", label)):
return (f"sub-action {i+1} ({typ}) targets {label.strip()!r}, "
"which is irreversible/outward-facing")
return ""


def send_payload_from_log(action_log, prompt: str = "") -> str:
"""The text a failed run typed into a composer-ish field, '' if it never
reached the send zone. Gates the recovery verify-first probe: r44's retry
SAID it would verify first then didn't, so the check must be code, not prose."""
typed: list[str] = []
for a in action_log or []:
if not isinstance(a, dict):
continue
tool = a.get("tool")
inp = a.get("input") if isinstance(a.get("input"), dict) else {}
text = str(inp.get("text") or "").strip()
if not text and tool != "BrowserBatch":
continue
if tool == "BrowserClickIndex":
name = str(a.get("clicked_name") or "")
role = str(a.get("clicked_role") or "")
summ = str(a.get("result_summary") or "")
# focus+type results carry no clicked fields (r47's live miss); the
# executor's own "typed the text" wording is the surviving signal
if _COMPOSE_SEL_RE.search(name) or (len(text) >= 20 and (
role == "textbox" or "typed the text" in summ.lower())):
typed.append(text)
elif tool == "BrowserType":
sel = str(inp.get("selector") or "")
if _COMPOSE_SEL_RE.search(sel) or (not sel and len(text) >= 20):
typed.append(text)
elif tool == "BrowserBatch":
for sub in (inp.get("actions") or []):
if not isinstance(sub, dict):
continue
p = sub.get("params") if isinstance(sub.get("params"), dict) else {}
sub_text = str(p.get("text") or "").strip()
sub_sel = str(p.get("selector") or "")
if sub.get("type") == "type" and sub_text and (
_COMPOSE_SEL_RE.search(sub_sel)
or (not sub_sel and len(sub_text) >= 20)):
typed.append(sub_text)
if not typed:
return ""
# the task usually quotes the message; a candidate echoed there beats a
# longer search query or a garbled retype
for t in reversed(typed):
if t in (prompt or ""):
return t
return typed[-1]


def _sub(val, value: str):
return value if val == PLACEHOLDER else (
val.replace(PLACEHOLDER, value) if isinstance(val, str) else val
)


def fill_step(step: dict, value: str) -> tuple[str, dict]:
"""Turn one template step + one value into (tool_name, params) ready for
execute_browser_tool. Substitutes {{value}} anywhere it appears."""
action = step["action"]
tool_name, keys = _STEP_TOOLS[action]
params = {}
for k in keys:
if k in step:
params[k] = _sub(step[k], value)
# carry an optional role default for clicks
if action == "click" and "role" not in params:
params["role"] = _sub(step.get("role", ""), value)
return tool_name, params


def fill_template(steps, value: str) -> list[tuple[str, dict]]:
return [fill_step(s, value) for s in steps]


def is_readonly_template(steps) -> bool:
"""True if every step is a pure read/navigation (no clicks/types at all), the
safest class of loop."""
return all(s.get("action") in _READONLY_ACTIONS for s in steps)


# A batch READ is useless if it doesn't hand the data back. We return each item's
# read output, capped so a 20-item batch stays cheap, and stay honest about
# failures (named, with the error) and truncation (named, never silently dropped).
_MAX_ITEM_CHARS = 500
_MAX_TOTAL_CHARS = 6000


def summarize_batch(records: list[dict], readonly: bool,
max_item_chars: int = _MAX_ITEM_CHARS,
max_total_chars: int = _MAX_TOTAL_CHARS) -> str:
"""Turn per-item batch results into the text the agent gets back.

`records`: [{value, ok, text}]. For a successful item `text` is its read
output (the data); for a failed one it's the error. Successes show their data
(capped); once the total budget is hit, remaining successes are listed by
value only (so nothing is silently lost); failures are always named with a
short reason so a partial batch never reads as 'all done'."""
done = [r for r in records if r.get("ok")]
failed = [r for r in records if not r.get("ok")]
verb = "Read" if readonly else "Completed"
lines, used, overflow = [], 0, []
for r in done:
body = " ".join(str(r.get("text") or "").split())[:max_item_chars]
line = f"- {r['value']}: {body}" if body else f"- {r['value']}: (done, no content)"
if used and used + len(line) > max_total_chars:
overflow.append(str(r["value"]))
continue
lines.append(line)
used += len(line)
out = f"{verb} {len(done)} of {len(records)}."
if lines:
out += "\n" + "\n".join(lines)
if overflow:
out += (f"\n(+{len(overflow)} more done but not shown to save space: "
f"{', '.join(overflow[:20])}; ask for specific ones if needed)")
if failed:
fails = ", ".join(
f"{r['value']} ({' '.join(str(r.get('text') or 'failed').split())[:60]})"
for r in failed[:20]
)
out += (f"\n{len(failed)} couldn't be done and need you to handle them "
f"individually: {fails}")
return out
Loading
Loading