browserrig 0.1.0__py3-none-any.whl
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.
- browserrig/__init__.py +158 -0
- browserrig/activity/__init__.py +5 -0
- browserrig/activity/models.py +93 -0
- browserrig/activity/plugin.py +247 -0
- browserrig/activity/state.py +16 -0
- browserrig/agent.py +55 -0
- browserrig/browser/__init__.py +33 -0
- browserrig/browser/base.py +70 -0
- browserrig/browser/chrome.py +242 -0
- browserrig/browser/config.py +119 -0
- browserrig/browser/downloads.py +128 -0
- browserrig/browser/favicon.py +119 -0
- browserrig/browser/frame.py +227 -0
- browserrig/browser/frames.py +116 -0
- browserrig/browser/images.py +23 -0
- browserrig/browser/input.py +147 -0
- browserrig/browser/models.py +8 -0
- browserrig/browser/plugin.py +44 -0
- browserrig/browser/process.py +144 -0
- browserrig/browser/setup.py +12 -0
- browserrig/browser/tab.py +311 -0
- browserrig/browser/tabs.py +366 -0
- browserrig/browser/worlds.py +64 -0
- browserrig/choreography/__init__.py +31 -0
- browserrig/choreography/events.py +133 -0
- browserrig/choreography/plugin.py +75 -0
- browserrig/choreography/scenes.py +167 -0
- browserrig/choreography/stage.py +53 -0
- browserrig/choreography/tabstrip.py +112 -0
- browserrig/completion.py +184 -0
- browserrig/composition.py +108 -0
- browserrig/core/__init__.py +61 -0
- browserrig/core/errors.py +18 -0
- browserrig/core/geometry.py +149 -0
- browserrig/core/ids.py +10 -0
- browserrig/core/kinds.py +180 -0
- browserrig/core/options.py +7 -0
- browserrig/core/tools.py +32 -0
- browserrig/events.py +665 -0
- browserrig/files/__init__.py +25 -0
- browserrig/files/config.py +13 -0
- browserrig/files/containers.py +200 -0
- browserrig/files/content.py +65 -0
- browserrig/files/data.py +80 -0
- browserrig/files/documents.py +248 -0
- browserrig/files/formats.py +84 -0
- browserrig/files/imaging.py +138 -0
- browserrig/files/kinds.py +78 -0
- browserrig/files/models.py +71 -0
- browserrig/files/reading.py +382 -0
- browserrig/files/spreadsheets.py +417 -0
- browserrig/files/system.py +204 -0
- browserrig/files/text.py +64 -0
- browserrig/files/xlsx.py +53 -0
- browserrig/handoff/__init__.py +4 -0
- browserrig/handoff/changes.py +48 -0
- browserrig/handoff/models.py +31 -0
- browserrig/handoff/plugin.py +308 -0
- browserrig/handoff/state.py +21 -0
- browserrig/input/__init__.py +35 -0
- browserrig/input/controller.py +69 -0
- browserrig/input/keyboard.py +267 -0
- browserrig/input/motor.py +118 -0
- browserrig/input/mouse.py +346 -0
- browserrig/input/pointer.py +84 -0
- browserrig/input/presenter.py +49 -0
- browserrig/input/target.py +67 -0
- browserrig/input/wheel.py +40 -0
- browserrig/kinds.py +33 -0
- browserrig/nudges/__init__.py +3 -0
- browserrig/nudges/plugin.py +295 -0
- browserrig/nudges/state.py +23 -0
- browserrig/observation/__init__.py +53 -0
- browserrig/observation/capture.py +374 -0
- browserrig/observation/changes.py +63 -0
- browserrig/observation/config.py +12 -0
- browserrig/observation/content.py +176 -0
- browserrig/observation/delta.py +69 -0
- browserrig/observation/dom.py +97 -0
- browserrig/observation/extraction/__init__.py +335 -0
- browserrig/observation/extraction/accessibility.py +62 -0
- browserrig/observation/extraction/document.py +471 -0
- browserrig/observation/extraction/elements.py +676 -0
- browserrig/observation/extraction/semantics.py +351 -0
- browserrig/observation/extraction/styles.py +30 -0
- browserrig/observation/extraction/visibility.py +108 -0
- browserrig/observation/find.py +152 -0
- browserrig/observation/handle.py +170 -0
- browserrig/observation/images.py +243 -0
- browserrig/observation/marks.py +157 -0
- browserrig/observation/models.py +230 -0
- browserrig/observation/plugin.py +413 -0
- browserrig/observation/redirects.py +40 -0
- browserrig/observation/references.py +47 -0
- browserrig/observation/render.py +291 -0
- browserrig/observation/renderer.py +76 -0
- browserrig/observation/state.py +37 -0
- browserrig/pacing/__init__.py +5 -0
- browserrig/pacing/plugin.py +22 -0
- browserrig/py.typed +0 -0
- browserrig/readiness/__init__.py +25 -0
- browserrig/readiness/config.py +12 -0
- browserrig/readiness/dom.py +83 -0
- browserrig/readiness/downloads.py +30 -0
- browserrig/readiness/hooks.py +27 -0
- browserrig/readiness/models.py +13 -0
- browserrig/readiness/plugin.py +170 -0
- browserrig/readiness/policies.py +43 -0
- browserrig/readiness/state.py +20 -0
- browserrig/readiness/strategy.py +30 -0
- browserrig/readiness/wait.py +238 -0
- browserrig/scripts/README.md +39 -0
- browserrig/scripts/__init__.py +186 -0
- browserrig/scripts/content.js +526 -0
- browserrig/scripts/cursor.js +57 -0
- browserrig/scripts/element.js +82 -0
- browserrig/scripts/find.js +377 -0
- browserrig/scripts/focused.js +15 -0
- browserrig/scripts/lib/composed.js +21 -0
- browserrig/scripts/lib/elements.js +9 -0
- browserrig/scripts/lib/geometry.js +13 -0
- browserrig/scripts/lib/hit.js +84 -0
- browserrig/scripts/lib/markdown.js +32 -0
- browserrig/scripts/lib/scroll.js +226 -0
- browserrig/scripts/lib/secret.js +74 -0
- browserrig/scripts/lib/visibility.js +42 -0
- browserrig/scripts/models.py +153 -0
- browserrig/scripts/owner_hits.js +4 -0
- browserrig/scripts/owner_scroll.js +7 -0
- browserrig/scripts/owner_size.js +11 -0
- browserrig/scripts/page_facts.js +9 -0
- browserrig/scripts/reset.js +4 -0
- browserrig/scripts/scroll.js +5 -0
- browserrig/scripts/secret_field.js +7 -0
- browserrig/scripts/secret_focus.js +31 -0
- browserrig/scripts/seed.js +13 -0
- browserrig/scripts/stability.js +140 -0
- browserrig/scripts/tab_strip.js +27 -0
- browserrig/scripts/viewport.js +3 -0
- browserrig/secrets/__init__.py +5 -0
- browserrig/secrets/models.py +90 -0
- browserrig/secrets/origin.py +95 -0
- browserrig/secrets/plugin.py +397 -0
- browserrig/secrets/redaction.py +107 -0
- browserrig/secrets/state.py +6 -0
- browserrig/secrets/totp.py +62 -0
- browserrig/terminal.py +120 -0
- browserrig/tools/__init__.py +51 -0
- browserrig/tools/chains.py +55 -0
- browserrig/tools/dialogs.py +23 -0
- browserrig/tools/effects.py +8 -0
- browserrig/tools/execution.py +333 -0
- browserrig/tools/files.py +411 -0
- browserrig/tools/find.py +125 -0
- browserrig/tools/forms.py +76 -0
- browserrig/tools/intents.py +82 -0
- browserrig/tools/javascript.py +37 -0
- browserrig/tools/keyboard.py +126 -0
- browserrig/tools/mouse.py +224 -0
- browserrig/tools/navigation.py +189 -0
- browserrig/tools/scrolling.py +39 -0
- browserrig/tools/tabs.py +81 -0
- browserrig/tools/targeting.py +59 -0
- browserrig-0.1.0.dist-info/METADATA +174 -0
- browserrig-0.1.0.dist-info/RECORD +167 -0
- browserrig-0.1.0.dist-info/WHEEL +4 -0
- browserrig-0.1.0.dist-info/licenses/LICENSE +21 -0
browserrig/__init__.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from .activity import Activity
|
|
2
|
+
from .agent import Agent # smelt: ignore[SMT106] -- public facade
|
|
3
|
+
from .browser import (
|
|
4
|
+
Browser,
|
|
5
|
+
Chrome,
|
|
6
|
+
ChromeBrowser,
|
|
7
|
+
Connect,
|
|
8
|
+
Launch,
|
|
9
|
+
SessionSetup,
|
|
10
|
+
Viewport,
|
|
11
|
+
)
|
|
12
|
+
from .choreography import InputVisualization, TabStrip
|
|
13
|
+
from .completion import Done
|
|
14
|
+
from .composition import ( # smelt: ignore[SMT106] -- public facade
|
|
15
|
+
ContextCompaction,
|
|
16
|
+
WindowRatio,
|
|
17
|
+
browser_kit,
|
|
18
|
+
browser_plugins,
|
|
19
|
+
)
|
|
20
|
+
from .core import Point
|
|
21
|
+
from .files import FileSystem, FileSystemOptions
|
|
22
|
+
from .handoff import HandoffCallback, HandoffReceipt, HumanHandoff, HumanResponse
|
|
23
|
+
from .kinds import MouseEventKind
|
|
24
|
+
from .nudges import Nudges
|
|
25
|
+
from .observation import (
|
|
26
|
+
CaptureContent,
|
|
27
|
+
CaptureStrategy,
|
|
28
|
+
DomCapture,
|
|
29
|
+
DomElementHandle,
|
|
30
|
+
DomReferences,
|
|
31
|
+
DomRenderer,
|
|
32
|
+
ElementHandle,
|
|
33
|
+
ElementInfo,
|
|
34
|
+
ElementRef,
|
|
35
|
+
ImageContext,
|
|
36
|
+
ImageRetention,
|
|
37
|
+
Observe,
|
|
38
|
+
PageContent,
|
|
39
|
+
References,
|
|
40
|
+
Renderer,
|
|
41
|
+
Screenshots,
|
|
42
|
+
ScreenshotTrigger,
|
|
43
|
+
ScrollableElement,
|
|
44
|
+
ScrollStep,
|
|
45
|
+
TrimInBatches,
|
|
46
|
+
UntilCompaction,
|
|
47
|
+
)
|
|
48
|
+
from .pacing import InputPacing
|
|
49
|
+
from .readiness import (
|
|
50
|
+
CheckReadiness,
|
|
51
|
+
DomReady,
|
|
52
|
+
DownloadReadiness,
|
|
53
|
+
Readiness,
|
|
54
|
+
ReadinessCheck,
|
|
55
|
+
ReadinessContext,
|
|
56
|
+
ReadinessOptions,
|
|
57
|
+
ReadinessPolicy,
|
|
58
|
+
ReadinessStrategy,
|
|
59
|
+
SelectReadiness,
|
|
60
|
+
wait_for_page,
|
|
61
|
+
)
|
|
62
|
+
from .secrets import Secret, Secrets, SecretValue, Totp, env
|
|
63
|
+
from .terminal import TerminalEvents
|
|
64
|
+
from .tools import (
|
|
65
|
+
Actions,
|
|
66
|
+
DialogTools,
|
|
67
|
+
DomActionChains,
|
|
68
|
+
FileTools,
|
|
69
|
+
FindTools,
|
|
70
|
+
FormTools,
|
|
71
|
+
JavaScriptTools,
|
|
72
|
+
KeyboardTools,
|
|
73
|
+
NavigationTools,
|
|
74
|
+
ObserveAfterInput,
|
|
75
|
+
PointerTools,
|
|
76
|
+
TabTools,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
__version__ = "0.1.0"
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
"Agent",
|
|
83
|
+
"ContextCompaction",
|
|
84
|
+
"CheckReadiness",
|
|
85
|
+
"DownloadReadiness",
|
|
86
|
+
"ReadinessCheck",
|
|
87
|
+
"SelectReadiness",
|
|
88
|
+
"Done",
|
|
89
|
+
"HandoffCallback",
|
|
90
|
+
"HandoffReceipt",
|
|
91
|
+
"HumanHandoff",
|
|
92
|
+
"HumanResponse",
|
|
93
|
+
"CaptureContent",
|
|
94
|
+
"CaptureStrategy",
|
|
95
|
+
"DomCapture",
|
|
96
|
+
"DomElementHandle",
|
|
97
|
+
"DomReferences",
|
|
98
|
+
"ElementHandle",
|
|
99
|
+
"ElementInfo",
|
|
100
|
+
"PageContent",
|
|
101
|
+
"ElementRef",
|
|
102
|
+
"References",
|
|
103
|
+
"ScrollableElement",
|
|
104
|
+
"ScrollStep",
|
|
105
|
+
"DomRenderer",
|
|
106
|
+
"ImageContext",
|
|
107
|
+
"ImageRetention",
|
|
108
|
+
"TrimInBatches",
|
|
109
|
+
"UntilCompaction",
|
|
110
|
+
"Renderer",
|
|
111
|
+
"Screenshots",
|
|
112
|
+
"ScreenshotTrigger",
|
|
113
|
+
"DomReady",
|
|
114
|
+
"ReadinessContext",
|
|
115
|
+
"ReadinessPolicy",
|
|
116
|
+
"ReadinessStrategy",
|
|
117
|
+
"FindTools",
|
|
118
|
+
"DialogTools",
|
|
119
|
+
"FileTools",
|
|
120
|
+
"JavaScriptTools",
|
|
121
|
+
"InputVisualization",
|
|
122
|
+
"TabStrip",
|
|
123
|
+
"FormTools",
|
|
124
|
+
"PointerTools",
|
|
125
|
+
"KeyboardTools",
|
|
126
|
+
"NavigationTools",
|
|
127
|
+
"TabTools",
|
|
128
|
+
"ObserveAfterInput",
|
|
129
|
+
"DomActionChains",
|
|
130
|
+
"Activity",
|
|
131
|
+
"Actions",
|
|
132
|
+
"Point",
|
|
133
|
+
"MouseEventKind",
|
|
134
|
+
"Browser",
|
|
135
|
+
"Chrome",
|
|
136
|
+
"ChromeBrowser",
|
|
137
|
+
"Connect",
|
|
138
|
+
"FileSystem",
|
|
139
|
+
"FileSystemOptions",
|
|
140
|
+
"Launch",
|
|
141
|
+
"SessionSetup",
|
|
142
|
+
"Nudges",
|
|
143
|
+
"Observe",
|
|
144
|
+
"InputPacing",
|
|
145
|
+
"Readiness",
|
|
146
|
+
"ReadinessOptions",
|
|
147
|
+
"Secret",
|
|
148
|
+
"SecretValue",
|
|
149
|
+
"Secrets",
|
|
150
|
+
"Totp",
|
|
151
|
+
"env",
|
|
152
|
+
"Viewport",
|
|
153
|
+
"WindowRatio",
|
|
154
|
+
"TerminalEvents",
|
|
155
|
+
"browser_plugins",
|
|
156
|
+
"browser_kit",
|
|
157
|
+
"wait_for_page",
|
|
158
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from time import monotonic
|
|
4
|
+
from typing import assert_never
|
|
5
|
+
|
|
6
|
+
from harnessed.events import SessionEvent, StepOutcome, TurnOutcome
|
|
7
|
+
|
|
8
|
+
from browserrig.core import ActivityKind, ActivityOutcome
|
|
9
|
+
from browserrig.events import ActivityFinishedEvent, ActivityStartedEvent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class ActivityKey:
|
|
14
|
+
"""Correlated request/tool/readiness slots and a singleton pause slot."""
|
|
15
|
+
|
|
16
|
+
kind: ActivityKind
|
|
17
|
+
correlation_id: str = ""
|
|
18
|
+
|
|
19
|
+
def __post_init__(self) -> None:
|
|
20
|
+
object.__setattr__(self, "kind", ActivityKind(self.kind))
|
|
21
|
+
if not isinstance(self.correlation_id, str):
|
|
22
|
+
raise TypeError("Activity correlation IDs must be strings")
|
|
23
|
+
correlated = self.kind != ActivityKind.PAUSED
|
|
24
|
+
if bool(self.correlation_id) != correlated:
|
|
25
|
+
raise ValueError(
|
|
26
|
+
"Model, tool, readiness and compaction activities require a correlation ID"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def step_outcome(reason: StepOutcome) -> ActivityOutcome:
|
|
31
|
+
match reason:
|
|
32
|
+
case StepOutcome.COMPLETED:
|
|
33
|
+
return ActivityOutcome.COMPLETED
|
|
34
|
+
case StepOutcome.INTERRUPTED:
|
|
35
|
+
return ActivityOutcome.INTERRUPTED
|
|
36
|
+
case StepOutcome.ABORTED:
|
|
37
|
+
return ActivityOutcome.ABORTED
|
|
38
|
+
case StepOutcome.CONTEXT_LIMIT:
|
|
39
|
+
return ActivityOutcome.CONTEXT_LIMIT
|
|
40
|
+
case _:
|
|
41
|
+
assert_never(reason)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def turn_outcome(reason: TurnOutcome) -> ActivityOutcome:
|
|
45
|
+
match reason:
|
|
46
|
+
case TurnOutcome.COMPLETED:
|
|
47
|
+
outcome = ActivityOutcome.COMPLETED
|
|
48
|
+
case TurnOutcome.INTERRUPTED:
|
|
49
|
+
outcome = ActivityOutcome.INTERRUPTED
|
|
50
|
+
case TurnOutcome.STOPPED:
|
|
51
|
+
outcome = ActivityOutcome.STOPPED
|
|
52
|
+
case TurnOutcome.MAX_STEPS:
|
|
53
|
+
outcome = ActivityOutcome.MAX_STEPS
|
|
54
|
+
case TurnOutcome.TOKEN_BUDGET:
|
|
55
|
+
outcome = ActivityOutcome.TOKEN_BUDGET
|
|
56
|
+
case TurnOutcome.REJECTED:
|
|
57
|
+
outcome = ActivityOutcome.REJECTED
|
|
58
|
+
case TurnOutcome.ABORTED:
|
|
59
|
+
outcome = ActivityOutcome.ABORTED
|
|
60
|
+
case TurnOutcome.DISCARDED:
|
|
61
|
+
outcome = ActivityOutcome.DISCARDED
|
|
62
|
+
case _:
|
|
63
|
+
assert_never(reason)
|
|
64
|
+
return outcome
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def project_intervals(
|
|
68
|
+
records: Iterable[SessionEvent], *, now: float | None = None
|
|
69
|
+
) -> tuple[ActivityFinishedEvent, ...]:
|
|
70
|
+
"""Read live or restored facts without depending on a mounted Activity instance."""
|
|
71
|
+
completed: list[ActivityFinishedEvent] = []
|
|
72
|
+
opened: dict[str, ActivityStartedEvent] = {}
|
|
73
|
+
for record in records:
|
|
74
|
+
match record:
|
|
75
|
+
case ActivityStartedEvent(interval_id=interval_id):
|
|
76
|
+
opened[interval_id] = record
|
|
77
|
+
case ActivityFinishedEvent(interval_id=interval_id):
|
|
78
|
+
completed.append(record)
|
|
79
|
+
opened.pop(interval_id, None)
|
|
80
|
+
timestamp = monotonic() if now is None else now
|
|
81
|
+
completed.extend(
|
|
82
|
+
ActivityFinishedEvent(
|
|
83
|
+
interval_id=event.interval_id,
|
|
84
|
+
kind=event.kind,
|
|
85
|
+
cause_id=event.cause_id,
|
|
86
|
+
started_at=event.at,
|
|
87
|
+
at=timestamp,
|
|
88
|
+
outcome=ActivityOutcome.RUNNING,
|
|
89
|
+
epoch_id=event.epoch_id,
|
|
90
|
+
)
|
|
91
|
+
for event in opened.values()
|
|
92
|
+
)
|
|
93
|
+
return tuple(completed)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
from typing import assert_never
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
|
|
4
|
+
from harnessed import Context, Plugin, PluginState, SessionLog, ToolStatus, hook
|
|
5
|
+
from harnessed.events import (
|
|
6
|
+
AgentControlChangedEvent,
|
|
7
|
+
AssistantMessageEvent,
|
|
8
|
+
CompactionFinishedEvent,
|
|
9
|
+
CompactionOutcome,
|
|
10
|
+
CompactionStartedEvent,
|
|
11
|
+
ControlState,
|
|
12
|
+
ModelRequestedEvent,
|
|
13
|
+
ModelRequestFailedEvent,
|
|
14
|
+
ModelRequestOutcome,
|
|
15
|
+
StepEndedEvent,
|
|
16
|
+
ToolExecutionFinishedEvent,
|
|
17
|
+
ToolExecutionStartedEvent,
|
|
18
|
+
TurnEndedEvent,
|
|
19
|
+
)
|
|
20
|
+
from harnessed.kernel import dependency
|
|
21
|
+
|
|
22
|
+
from browserrig.core import ActivityKind, ActivityOutcome
|
|
23
|
+
from browserrig.events import (
|
|
24
|
+
ACTIVITY_EVENTS,
|
|
25
|
+
ActivityFinishedEvent,
|
|
26
|
+
ActivityStartedEvent,
|
|
27
|
+
HandoffFinishedEvent,
|
|
28
|
+
HandoffRequestedEvent,
|
|
29
|
+
ReadinessCancelledEvent,
|
|
30
|
+
ReadinessDegradedEvent,
|
|
31
|
+
ReadinessEndedEvent,
|
|
32
|
+
ReadinessFailedEvent,
|
|
33
|
+
ReadinessStartedEvent,
|
|
34
|
+
ReadyEvent,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
from .models import ActivityKey, project_intervals, step_outcome, turn_outcome
|
|
38
|
+
from .state import ActivityState
|
|
39
|
+
|
|
40
|
+
_PAUSE = ActivityKey(ActivityKind.PAUSED)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Activity(Plugin, slot="browserrig.activity", provides=True):
|
|
44
|
+
events = ACTIVITY_EVENTS
|
|
45
|
+
_state = PluginState(ActivityState)
|
|
46
|
+
_log = dependency(SessionLog)
|
|
47
|
+
|
|
48
|
+
async def setup(self, ctx: Context) -> None:
|
|
49
|
+
ctx.effect(self._close_all)
|
|
50
|
+
|
|
51
|
+
def intervals(
|
|
52
|
+
self, *, now: float | None = None
|
|
53
|
+
) -> tuple[ActivityFinishedEvent, ...]:
|
|
54
|
+
return project_intervals(self._state.records, now=now)
|
|
55
|
+
|
|
56
|
+
async def _start(self, key: ActivityKey, *, cause_id: str) -> None:
|
|
57
|
+
"""A new interval in the same slot supersedes only that slot's previous interval."""
|
|
58
|
+
await self._finish(key, ActivityOutcome.SUPERSEDED)
|
|
59
|
+
event = ActivityStartedEvent(
|
|
60
|
+
interval_id=uuid4().hex,
|
|
61
|
+
kind=key.kind,
|
|
62
|
+
cause_id=cause_id,
|
|
63
|
+
epoch_id=self._state.epoch,
|
|
64
|
+
)
|
|
65
|
+
self._state.open[key] = event
|
|
66
|
+
self._state.records.append(event)
|
|
67
|
+
await self._log.emit(event)
|
|
68
|
+
|
|
69
|
+
async def _finish(
|
|
70
|
+
self,
|
|
71
|
+
key: ActivityKey,
|
|
72
|
+
outcome: ActivityOutcome = ActivityOutcome.COMPLETED,
|
|
73
|
+
*,
|
|
74
|
+
reason: str = "",
|
|
75
|
+
) -> None:
|
|
76
|
+
event = self._state.open.pop(key, None)
|
|
77
|
+
if event is None:
|
|
78
|
+
return
|
|
79
|
+
finished = ActivityFinishedEvent(
|
|
80
|
+
interval_id=event.interval_id,
|
|
81
|
+
kind=event.kind,
|
|
82
|
+
cause_id=event.cause_id,
|
|
83
|
+
started_at=event.at,
|
|
84
|
+
outcome=outcome,
|
|
85
|
+
reason=reason,
|
|
86
|
+
epoch_id=event.epoch_id,
|
|
87
|
+
)
|
|
88
|
+
self._state.records.append(finished)
|
|
89
|
+
await self._log.emit(finished)
|
|
90
|
+
|
|
91
|
+
async def _close_all(self) -> None:
|
|
92
|
+
for key in tuple(self._state.open):
|
|
93
|
+
await self._finish(key, ActivityOutcome.CLOSED)
|
|
94
|
+
|
|
95
|
+
@hook(ModelRequestedEvent)
|
|
96
|
+
async def _on_model(self, event: ModelRequestedEvent) -> None:
|
|
97
|
+
await self._start(
|
|
98
|
+
ActivityKey(ActivityKind.LLM, event.request_id), cause_id=event.request_id
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
@hook(AssistantMessageEvent)
|
|
102
|
+
async def _on_assistant(self, event: AssistantMessageEvent) -> None:
|
|
103
|
+
await self._finish(
|
|
104
|
+
ActivityKey(ActivityKind.LLM, event.request_id),
|
|
105
|
+
ActivityOutcome.INTERRUPTED
|
|
106
|
+
if event.interrupted
|
|
107
|
+
else ActivityOutcome.COMPLETED,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@hook(ModelRequestFailedEvent)
|
|
111
|
+
async def _on_model_failed(self, event: ModelRequestFailedEvent) -> None:
|
|
112
|
+
match event.outcome:
|
|
113
|
+
case ModelRequestOutcome.FAILED:
|
|
114
|
+
outcome = ActivityOutcome.FAILED
|
|
115
|
+
case ModelRequestOutcome.CANCELLED:
|
|
116
|
+
outcome = ActivityOutcome.CANCELLED
|
|
117
|
+
case ModelRequestOutcome.CONTEXT_EXCEEDED:
|
|
118
|
+
outcome = ActivityOutcome.CONTEXT_LIMIT
|
|
119
|
+
case _:
|
|
120
|
+
assert_never(event.outcome)
|
|
121
|
+
await self._finish(
|
|
122
|
+
ActivityKey(ActivityKind.LLM, event.request_id),
|
|
123
|
+
outcome,
|
|
124
|
+
reason=event.outcome.value,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
@hook(CompactionStartedEvent)
|
|
128
|
+
async def _on_compaction(self, event: CompactionStartedEvent) -> None:
|
|
129
|
+
await self._start(
|
|
130
|
+
ActivityKey(ActivityKind.COMPACTION, event.compaction_id),
|
|
131
|
+
cause_id=event.compaction_id,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
@hook(CompactionFinishedEvent)
|
|
135
|
+
async def _on_compaction_finished(self, event: CompactionFinishedEvent) -> None:
|
|
136
|
+
outcome = {
|
|
137
|
+
CompactionOutcome.APPLIED: ActivityOutcome.COMPLETED,
|
|
138
|
+
CompactionOutcome.DISCARDED: ActivityOutcome.DISCARDED,
|
|
139
|
+
CompactionOutcome.FAILED: ActivityOutcome.FAILED,
|
|
140
|
+
CompactionOutcome.CANCELLED: ActivityOutcome.CANCELLED,
|
|
141
|
+
}[event.outcome]
|
|
142
|
+
await self._finish(
|
|
143
|
+
ActivityKey(ActivityKind.COMPACTION, event.compaction_id),
|
|
144
|
+
outcome,
|
|
145
|
+
reason=event.outcome.value,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
@hook(ToolExecutionStartedEvent)
|
|
149
|
+
async def _on_tool(self, event: ToolExecutionStartedEvent) -> None:
|
|
150
|
+
await self._start(
|
|
151
|
+
ActivityKey(ActivityKind.TOOL, event.call_id), cause_id=event.call_id
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
@hook(ToolExecutionFinishedEvent)
|
|
155
|
+
async def _on_tool_finished(self, event: ToolExecutionFinishedEvent) -> None:
|
|
156
|
+
reason = event.result.status.value if event.result.is_error else ""
|
|
157
|
+
if event.result.exit_code not in (None, 0):
|
|
158
|
+
reason = f"{event.result.status.value}; exit_code={event.result.exit_code}"
|
|
159
|
+
if event.result.status is ToolStatus.CANCELLED:
|
|
160
|
+
outcome = ActivityOutcome.CANCELLED
|
|
161
|
+
elif event.result.status is ToolStatus.TIMEOUT:
|
|
162
|
+
outcome = ActivityOutcome.TIMEOUT
|
|
163
|
+
else:
|
|
164
|
+
outcome = (
|
|
165
|
+
ActivityOutcome.FAILED
|
|
166
|
+
if event.result.is_error
|
|
167
|
+
else ActivityOutcome.COMPLETED
|
|
168
|
+
)
|
|
169
|
+
await self._finish(
|
|
170
|
+
ActivityKey(ActivityKind.TOOL, event.call_id),
|
|
171
|
+
outcome,
|
|
172
|
+
reason=reason,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
@hook(HandoffRequestedEvent)
|
|
176
|
+
async def _on_handoff(self, event: HandoffRequestedEvent) -> None:
|
|
177
|
+
if event.call_id:
|
|
178
|
+
await self._finish(ActivityKey(ActivityKind.TOOL, event.call_id))
|
|
179
|
+
await self._start(
|
|
180
|
+
ActivityKey(ActivityKind.HANDOFF, event.handoff_id),
|
|
181
|
+
cause_id=event.handoff_id,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
@hook(HandoffFinishedEvent)
|
|
185
|
+
async def _on_handoff_finished(self, event: HandoffFinishedEvent) -> None:
|
|
186
|
+
if event.call_id:
|
|
187
|
+
outcome = {
|
|
188
|
+
"done": ActivityOutcome.COMPLETED,
|
|
189
|
+
"declined": ActivityOutcome.REJECTED,
|
|
190
|
+
"timeout": ActivityOutcome.TIMEOUT,
|
|
191
|
+
"cancelled": ActivityOutcome.CANCELLED,
|
|
192
|
+
"failed": ActivityOutcome.FAILED,
|
|
193
|
+
"resumed": ActivityOutcome.COMPLETED,
|
|
194
|
+
}[event.status]
|
|
195
|
+
await self._finish(
|
|
196
|
+
ActivityKey(ActivityKind.HANDOFF, event.handoff_id),
|
|
197
|
+
outcome,
|
|
198
|
+
reason=event.status,
|
|
199
|
+
)
|
|
200
|
+
await self._start(
|
|
201
|
+
ActivityKey(ActivityKind.TOOL, event.call_id), cause_id=event.call_id
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
@hook(ReadinessStartedEvent)
|
|
205
|
+
async def _on_readiness(self, event: ReadinessStartedEvent) -> None:
|
|
206
|
+
await self._start(
|
|
207
|
+
ActivityKey(ActivityKind.READINESS, event.readiness_id),
|
|
208
|
+
cause_id=event.readiness_id,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
@hook(ReadinessEndedEvent)
|
|
212
|
+
async def _on_ready(self, event: ReadinessEndedEvent) -> None:
|
|
213
|
+
match event:
|
|
214
|
+
case ReadyEvent():
|
|
215
|
+
outcome = ActivityOutcome.COMPLETED
|
|
216
|
+
case ReadinessDegradedEvent():
|
|
217
|
+
outcome = ActivityOutcome.DEGRADED
|
|
218
|
+
case ReadinessFailedEvent():
|
|
219
|
+
outcome = ActivityOutcome.FAILED
|
|
220
|
+
case ReadinessCancelledEvent():
|
|
221
|
+
outcome = ActivityOutcome.CANCELLED
|
|
222
|
+
case _:
|
|
223
|
+
raise TypeError(f"Unsupported readiness event: {type(event).__name__}")
|
|
224
|
+
await self._finish(
|
|
225
|
+
ActivityKey(ActivityKind.READINESS, event.readiness_id),
|
|
226
|
+
outcome,
|
|
227
|
+
reason=event.reason,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
@hook(AgentControlChangedEvent)
|
|
231
|
+
async def _on_control(self, event: AgentControlChangedEvent) -> None:
|
|
232
|
+
if event.state == ControlState.PAUSED:
|
|
233
|
+
await self._start(_PAUSE, cause_id=self._state.epoch)
|
|
234
|
+
elif event.state == ControlState.ACTIVE:
|
|
235
|
+
await self._finish(_PAUSE)
|
|
236
|
+
|
|
237
|
+
@hook(StepEndedEvent)
|
|
238
|
+
async def _on_step(self, event: StepEndedEvent) -> None:
|
|
239
|
+
for key in tuple(self._state.open):
|
|
240
|
+
if key.kind is ActivityKind.LLM:
|
|
241
|
+
await self._finish(key, step_outcome(event.reason))
|
|
242
|
+
|
|
243
|
+
@hook(TurnEndedEvent)
|
|
244
|
+
async def _on_turn(self, event: TurnEndedEvent) -> None:
|
|
245
|
+
for key in tuple(self._state.open):
|
|
246
|
+
if key.kind != ActivityKind.PAUSED:
|
|
247
|
+
await self._finish(key, turn_outcome(event.reason))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
|
|
4
|
+
from browserrig.events import ActivityFinishedEvent, ActivityStartedEvent
|
|
5
|
+
|
|
6
|
+
from .models import ActivityKey
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ActivityState:
|
|
11
|
+
open: dict[ActivityKey, ActivityStartedEvent] = field(default_factory=dict)
|
|
12
|
+
# The session log only holds the open turn, so intervals keep their own record.
|
|
13
|
+
records: list[ActivityStartedEvent | ActivityFinishedEvent] = field(
|
|
14
|
+
default_factory=list
|
|
15
|
+
)
|
|
16
|
+
epoch: str = field(default_factory=lambda: uuid4().hex)
|
browserrig/agent.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
|
|
3
|
+
import harnessed
|
|
4
|
+
from harnessed import (
|
|
5
|
+
Kit,
|
|
6
|
+
Plugin,
|
|
7
|
+
PluginValidation,
|
|
8
|
+
SystemPrompt,
|
|
9
|
+
)
|
|
10
|
+
from harnessed.kernel import PluginLike
|
|
11
|
+
from harnessed.models import Model
|
|
12
|
+
from harnessed.sessions import SessionStore
|
|
13
|
+
|
|
14
|
+
from .browser import BrowserOptions
|
|
15
|
+
from .composition import browser_kit
|
|
16
|
+
|
|
17
|
+
DEFAULT_PROMPT = "Complete the requested browser task using the observed page state."
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Agent(harnessed.Agent):
|
|
21
|
+
"""A harnessed agent with browserrig's default browser plugins.
|
|
22
|
+
|
|
23
|
+
`plugins` replace the defaults they match and add the rest, and `without`
|
|
24
|
+
drops defaults by type; see `browser_kit`. Explicit provider clients
|
|
25
|
+
remain caller-owned. Conversations stay in memory by default; pass `store`
|
|
26
|
+
to reopen them by ID, or continue with an explicit `SessionState`. Long runs
|
|
27
|
+
summarize older steps once the request fills 80% of the model's context window,
|
|
28
|
+
or 100k tokens when the model reports none. Pass `without=(ContextCompaction,)`
|
|
29
|
+
to keep everything, or a configured `ContextCompaction`. Input is paced like a
|
|
30
|
+
person and streamed for people
|
|
31
|
+
watching; pass `without=(InputPacing, InputVisualization)` to work at full speed unseen.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
*,
|
|
37
|
+
model: Model | str | None = None,
|
|
38
|
+
browser: BrowserOptions | None = None,
|
|
39
|
+
plugins: Iterable[PluginLike | Kit | None] = (),
|
|
40
|
+
without: Iterable[type[Plugin]] = (),
|
|
41
|
+
instructions: str | None = DEFAULT_PROMPT,
|
|
42
|
+
prompt: SystemPrompt | None = None,
|
|
43
|
+
store: SessionStore | None = None,
|
|
44
|
+
plugin_validation: PluginValidation = PluginValidation.ERROR,
|
|
45
|
+
) -> None:
|
|
46
|
+
kit = browser_kit(browser=browser)
|
|
47
|
+
kit = kit.without(*without)
|
|
48
|
+
super().__init__(
|
|
49
|
+
model=model,
|
|
50
|
+
instructions=instructions,
|
|
51
|
+
prompt=prompt,
|
|
52
|
+
plugins=[kit, *plugins],
|
|
53
|
+
store=store,
|
|
54
|
+
plugin_validation=plugin_validation,
|
|
55
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from browserrig.files import Download, DownloadState
|
|
2
|
+
|
|
3
|
+
from .base import Browser, BrowserEvents
|
|
4
|
+
from .chrome import ChromeBrowser
|
|
5
|
+
from .config import BrowserOptions, Connect, Launch, Viewport
|
|
6
|
+
from .frame import Frame
|
|
7
|
+
from .images import screenshot
|
|
8
|
+
from .models import TabInfo
|
|
9
|
+
from .plugin import Chrome, TabChanged
|
|
10
|
+
from .setup import SessionSetup
|
|
11
|
+
from .tab import Tab, validated_url
|
|
12
|
+
from .tabs import Tabs
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Browser",
|
|
16
|
+
"BrowserEvents",
|
|
17
|
+
"BrowserOptions",
|
|
18
|
+
"Chrome",
|
|
19
|
+
"ChromeBrowser",
|
|
20
|
+
"Connect",
|
|
21
|
+
"Frame",
|
|
22
|
+
"Download",
|
|
23
|
+
"DownloadState",
|
|
24
|
+
"Launch",
|
|
25
|
+
"SessionSetup",
|
|
26
|
+
"Tab",
|
|
27
|
+
"TabChanged",
|
|
28
|
+
"TabInfo",
|
|
29
|
+
"Tabs",
|
|
30
|
+
"Viewport",
|
|
31
|
+
"screenshot",
|
|
32
|
+
"validated_url",
|
|
33
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
from typing import TYPE_CHECKING, Protocol
|
|
7
|
+
|
|
8
|
+
from browserrig.events import BrowserEvent, LiveEvent
|
|
9
|
+
from browserrig.files import Download, FileSystem
|
|
10
|
+
from browserrig.input import Input, Keyboard, Mouse
|
|
11
|
+
|
|
12
|
+
from .tab import Tab
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .tabs import Tabs
|
|
16
|
+
|
|
17
|
+
type EventSink = Callable[[BrowserEvent | LiveEvent], Awaitable[None]]
|
|
18
|
+
type EventFilter = Callable[[BrowserEvent | LiveEvent], BrowserEvent | LiveEvent]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BrowserEvents:
|
|
22
|
+
"""Publish events stamped with the browser that produced them.
|
|
23
|
+
|
|
24
|
+
Filters rewrite each event before it reaches the sink, e.g. to redact secrets.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, browser_id: str, sink: EventSink) -> None:
|
|
28
|
+
self.browser_id = browser_id
|
|
29
|
+
self._sink = sink
|
|
30
|
+
self._filters: list[EventFilter] = []
|
|
31
|
+
|
|
32
|
+
def add_filter(self, event_filter: EventFilter) -> Callable[[], None]:
|
|
33
|
+
"""Apply `event_filter` to later events; the returned callable removes it."""
|
|
34
|
+
self._filters.append(event_filter)
|
|
35
|
+
|
|
36
|
+
def remove() -> None:
|
|
37
|
+
if event_filter in self._filters:
|
|
38
|
+
self._filters.remove(event_filter)
|
|
39
|
+
|
|
40
|
+
return remove
|
|
41
|
+
|
|
42
|
+
async def emit(self, event: BrowserEvent | LiveEvent) -> None:
|
|
43
|
+
event = replace(event, browser_id=self.browser_id)
|
|
44
|
+
for event_filter in tuple(self._filters):
|
|
45
|
+
event = event_filter(event)
|
|
46
|
+
await self._sink(event)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Browser(Protocol):
|
|
50
|
+
# Shared by Observe and Actions; it cannot live in either without a dependency cycle.
|
|
51
|
+
action_lock: asyncio.Lock
|
|
52
|
+
files: FileSystem | None
|
|
53
|
+
# The browser's one mouse and keyboard; they act on the active tab.
|
|
54
|
+
input: Input
|
|
55
|
+
tabs: Tabs
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def active_tab(self) -> Tab: ...
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def mouse(self) -> Mouse: ...
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def keyboard(self) -> Keyboard: ...
|
|
65
|
+
|
|
66
|
+
async def downloads(self) -> tuple[Download, ...]: ...
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def _discard(_event: BrowserEvent | LiveEvent) -> None:
|
|
70
|
+
pass
|