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.
Files changed (167) hide show
  1. browserrig/__init__.py +158 -0
  2. browserrig/activity/__init__.py +5 -0
  3. browserrig/activity/models.py +93 -0
  4. browserrig/activity/plugin.py +247 -0
  5. browserrig/activity/state.py +16 -0
  6. browserrig/agent.py +55 -0
  7. browserrig/browser/__init__.py +33 -0
  8. browserrig/browser/base.py +70 -0
  9. browserrig/browser/chrome.py +242 -0
  10. browserrig/browser/config.py +119 -0
  11. browserrig/browser/downloads.py +128 -0
  12. browserrig/browser/favicon.py +119 -0
  13. browserrig/browser/frame.py +227 -0
  14. browserrig/browser/frames.py +116 -0
  15. browserrig/browser/images.py +23 -0
  16. browserrig/browser/input.py +147 -0
  17. browserrig/browser/models.py +8 -0
  18. browserrig/browser/plugin.py +44 -0
  19. browserrig/browser/process.py +144 -0
  20. browserrig/browser/setup.py +12 -0
  21. browserrig/browser/tab.py +311 -0
  22. browserrig/browser/tabs.py +366 -0
  23. browserrig/browser/worlds.py +64 -0
  24. browserrig/choreography/__init__.py +31 -0
  25. browserrig/choreography/events.py +133 -0
  26. browserrig/choreography/plugin.py +75 -0
  27. browserrig/choreography/scenes.py +167 -0
  28. browserrig/choreography/stage.py +53 -0
  29. browserrig/choreography/tabstrip.py +112 -0
  30. browserrig/completion.py +184 -0
  31. browserrig/composition.py +108 -0
  32. browserrig/core/__init__.py +61 -0
  33. browserrig/core/errors.py +18 -0
  34. browserrig/core/geometry.py +149 -0
  35. browserrig/core/ids.py +10 -0
  36. browserrig/core/kinds.py +180 -0
  37. browserrig/core/options.py +7 -0
  38. browserrig/core/tools.py +32 -0
  39. browserrig/events.py +665 -0
  40. browserrig/files/__init__.py +25 -0
  41. browserrig/files/config.py +13 -0
  42. browserrig/files/containers.py +200 -0
  43. browserrig/files/content.py +65 -0
  44. browserrig/files/data.py +80 -0
  45. browserrig/files/documents.py +248 -0
  46. browserrig/files/formats.py +84 -0
  47. browserrig/files/imaging.py +138 -0
  48. browserrig/files/kinds.py +78 -0
  49. browserrig/files/models.py +71 -0
  50. browserrig/files/reading.py +382 -0
  51. browserrig/files/spreadsheets.py +417 -0
  52. browserrig/files/system.py +204 -0
  53. browserrig/files/text.py +64 -0
  54. browserrig/files/xlsx.py +53 -0
  55. browserrig/handoff/__init__.py +4 -0
  56. browserrig/handoff/changes.py +48 -0
  57. browserrig/handoff/models.py +31 -0
  58. browserrig/handoff/plugin.py +308 -0
  59. browserrig/handoff/state.py +21 -0
  60. browserrig/input/__init__.py +35 -0
  61. browserrig/input/controller.py +69 -0
  62. browserrig/input/keyboard.py +267 -0
  63. browserrig/input/motor.py +118 -0
  64. browserrig/input/mouse.py +346 -0
  65. browserrig/input/pointer.py +84 -0
  66. browserrig/input/presenter.py +49 -0
  67. browserrig/input/target.py +67 -0
  68. browserrig/input/wheel.py +40 -0
  69. browserrig/kinds.py +33 -0
  70. browserrig/nudges/__init__.py +3 -0
  71. browserrig/nudges/plugin.py +295 -0
  72. browserrig/nudges/state.py +23 -0
  73. browserrig/observation/__init__.py +53 -0
  74. browserrig/observation/capture.py +374 -0
  75. browserrig/observation/changes.py +63 -0
  76. browserrig/observation/config.py +12 -0
  77. browserrig/observation/content.py +176 -0
  78. browserrig/observation/delta.py +69 -0
  79. browserrig/observation/dom.py +97 -0
  80. browserrig/observation/extraction/__init__.py +335 -0
  81. browserrig/observation/extraction/accessibility.py +62 -0
  82. browserrig/observation/extraction/document.py +471 -0
  83. browserrig/observation/extraction/elements.py +676 -0
  84. browserrig/observation/extraction/semantics.py +351 -0
  85. browserrig/observation/extraction/styles.py +30 -0
  86. browserrig/observation/extraction/visibility.py +108 -0
  87. browserrig/observation/find.py +152 -0
  88. browserrig/observation/handle.py +170 -0
  89. browserrig/observation/images.py +243 -0
  90. browserrig/observation/marks.py +157 -0
  91. browserrig/observation/models.py +230 -0
  92. browserrig/observation/plugin.py +413 -0
  93. browserrig/observation/redirects.py +40 -0
  94. browserrig/observation/references.py +47 -0
  95. browserrig/observation/render.py +291 -0
  96. browserrig/observation/renderer.py +76 -0
  97. browserrig/observation/state.py +37 -0
  98. browserrig/pacing/__init__.py +5 -0
  99. browserrig/pacing/plugin.py +22 -0
  100. browserrig/py.typed +0 -0
  101. browserrig/readiness/__init__.py +25 -0
  102. browserrig/readiness/config.py +12 -0
  103. browserrig/readiness/dom.py +83 -0
  104. browserrig/readiness/downloads.py +30 -0
  105. browserrig/readiness/hooks.py +27 -0
  106. browserrig/readiness/models.py +13 -0
  107. browserrig/readiness/plugin.py +170 -0
  108. browserrig/readiness/policies.py +43 -0
  109. browserrig/readiness/state.py +20 -0
  110. browserrig/readiness/strategy.py +30 -0
  111. browserrig/readiness/wait.py +238 -0
  112. browserrig/scripts/README.md +39 -0
  113. browserrig/scripts/__init__.py +186 -0
  114. browserrig/scripts/content.js +526 -0
  115. browserrig/scripts/cursor.js +57 -0
  116. browserrig/scripts/element.js +82 -0
  117. browserrig/scripts/find.js +377 -0
  118. browserrig/scripts/focused.js +15 -0
  119. browserrig/scripts/lib/composed.js +21 -0
  120. browserrig/scripts/lib/elements.js +9 -0
  121. browserrig/scripts/lib/geometry.js +13 -0
  122. browserrig/scripts/lib/hit.js +84 -0
  123. browserrig/scripts/lib/markdown.js +32 -0
  124. browserrig/scripts/lib/scroll.js +226 -0
  125. browserrig/scripts/lib/secret.js +74 -0
  126. browserrig/scripts/lib/visibility.js +42 -0
  127. browserrig/scripts/models.py +153 -0
  128. browserrig/scripts/owner_hits.js +4 -0
  129. browserrig/scripts/owner_scroll.js +7 -0
  130. browserrig/scripts/owner_size.js +11 -0
  131. browserrig/scripts/page_facts.js +9 -0
  132. browserrig/scripts/reset.js +4 -0
  133. browserrig/scripts/scroll.js +5 -0
  134. browserrig/scripts/secret_field.js +7 -0
  135. browserrig/scripts/secret_focus.js +31 -0
  136. browserrig/scripts/seed.js +13 -0
  137. browserrig/scripts/stability.js +140 -0
  138. browserrig/scripts/tab_strip.js +27 -0
  139. browserrig/scripts/viewport.js +3 -0
  140. browserrig/secrets/__init__.py +5 -0
  141. browserrig/secrets/models.py +90 -0
  142. browserrig/secrets/origin.py +95 -0
  143. browserrig/secrets/plugin.py +397 -0
  144. browserrig/secrets/redaction.py +107 -0
  145. browserrig/secrets/state.py +6 -0
  146. browserrig/secrets/totp.py +62 -0
  147. browserrig/terminal.py +120 -0
  148. browserrig/tools/__init__.py +51 -0
  149. browserrig/tools/chains.py +55 -0
  150. browserrig/tools/dialogs.py +23 -0
  151. browserrig/tools/effects.py +8 -0
  152. browserrig/tools/execution.py +333 -0
  153. browserrig/tools/files.py +411 -0
  154. browserrig/tools/find.py +125 -0
  155. browserrig/tools/forms.py +76 -0
  156. browserrig/tools/intents.py +82 -0
  157. browserrig/tools/javascript.py +37 -0
  158. browserrig/tools/keyboard.py +126 -0
  159. browserrig/tools/mouse.py +224 -0
  160. browserrig/tools/navigation.py +189 -0
  161. browserrig/tools/scrolling.py +39 -0
  162. browserrig/tools/tabs.py +81 -0
  163. browserrig/tools/targeting.py +59 -0
  164. browserrig-0.1.0.dist-info/METADATA +174 -0
  165. browserrig-0.1.0.dist-info/RECORD +167 -0
  166. browserrig-0.1.0.dist-info/WHEEL +4 -0
  167. 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,5 @@
1
+ from browserrig.core import ActivityKind, ActivityOutcome
2
+
3
+ from .plugin import Activity
4
+
5
+ __all__ = ["Activity", "ActivityKind", "ActivityOutcome"]
@@ -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