|
From 4324784d52b657b1493193fcc76cc8d8554db79a Mon Sep 17 00:00:00 2001 |
|
From: Claude <noreply@anthropic.com> |
|
Date: Fri, 1 May 2026 06:57:18 +0000 |
|
Subject: [PATCH 3/3] effect: full Browser + LLM surfaces ported idiomatically |
|
|
|
Browser |
|
- effect/protocol/events.ts: full Browser+CDP event taxonomy ported |
|
from src/protocol/events.ts. Around 65 events with verbatim names, |
|
payload shapes via Effect Schema, and dispatch policies (completion, |
|
concurrency, blocks_parent_completion, handler_timeout, |
|
event_timeout) carried on the event class. llm_tool_name annotations |
|
preserved on every Browser command event so tool dispatch keeps the |
|
same string identifiers. |
|
- CDP: Connect/Connected/Disconnect/Disconnected/Send/Recv/On/Ping/Pong |
|
- Browser lifecycle: LaunchOrConnect/Connected/Disconnected/Kill/Exited |
|
- Browser config: SetViewport/RequestConfiguredViewport/ |
|
SetExtraHttpHeaders/AddInitScript/RequestCookies/AddCookies/ |
|
ClearCookies/RequestConnectURL/Close/SetDownloadBehavior/ |
|
RequestFetchDownloads |
|
- Tabs: RequestTabList/RequestActivePage/TabCreate/TabCreated/ |
|
TabClose/TabClosed |
|
- Frames: FrameUpdated/FrameDetached |
|
- Page navigation: Goto/BringToFront/BroughtToFront/Navigated/ |
|
DOMContentLoaded/Loaded/NetworkIdle2/NetworkIdle/LoadStateChanged/ |
|
GoBack/GoForward/Reload |
|
- Page actions: Click/DoubleClick/Hover/DragAndDrop/Type/Fill/ |
|
KeyPress/Scroll/ScrollTo/NextChunk/PrevChunk/ScrollBy/SelectOption/ |
|
SetInputFiles/EnableCursorOverlay/Highlight |
|
- Page introspection: Locate/RequestSnapshot/RequestDocumentSnapshot/ |
|
DocumentSnapshotCaptured/RequestScreenshot/ScreenshotCaptured/ |
|
RequestFullFrameTree/RequestInfo/RequestElementInfo |
|
- Page waits / evaluate / sendCDP / DOMSummary / DOMChanged |
|
- Downloads: DownloadStarted/DownloadCompleted |
|
- AboutBlank: PageOpened/PageRerender |
|
|
|
- effect/browser/Browser.ts: full handler registration for every |
|
command event. Page-level handler bodies are stubbed |
|
(Effect.die TODO) but each handler is bound at the right event_type |
|
with the right signature. Real handler bodies: |
|
on_BrowserLaunchOrConnect emits CDPConnect + BrowserConnected; |
|
on_BrowserKill emits BrowserDisconnected + BrowserExited; |
|
on_BrowserPageWaitForTimeout uses Effect.sleep directly. Static |
|
LISTENS_TO + EMITS arrays mirror the abxbus version verbatim. |
|
|
|
LLM |
|
- Four provider leaves (effect/llm/OpenAILLMSession.ts, |
|
AnthropicLLMSession.ts, GoogleLLMSession.ts, NoopLLMSession.ts) all |
|
built on a shared effect/llm/BaseProviderLLMSession.ts (Ref-backed |
|
ProviderConnectionState, applyConnectionFields helper, announceConnect |
|
emits LLMSessionConnectedEvent + LLMSessionUpdatedEvent, |
|
stubProviderRequest emits a placeholder LLMResponseEvent and |
|
LLMErrorEvent on failure). Real provider HTTP integration via |
|
@effect/ai-{openai,anthropic,google} is the next TODO. |
|
|
|
- effect/llm/LLMRouterLayer.ts: full multi-provider dispatch. Listens |
|
to the generic LLM* family and forwards to the matching provider's |
|
*LLMSession{Connect,Update,Disconnect}Event or *LLMRequestEvent. |
|
Provider resolution: explicit event.provider > splitLLMModel( |
|
event.model || options.model) > router's last-known provider Ref > |
|
default openai. Maintains id/provider/modelName Refs so subsequent |
|
requests inherit the connect-time provider. |
|
|
|
- StagehandSession composes all four leaves into the flat session |
|
Layer alongside Browser/CDPClient. |
|
|
|
Tests (33 total: 19 green, 14 todo) |
|
- effect/tests/test.LLMRouterLayer.ts: provider routing for openai, |
|
anthropic, google; togetherai (unsupported) falls back to noop; |
|
LLMSessionConnect emits LLMSessionConnectedEvent through provider |
|
leaf; LLMRequest produces stubbed LLMResponseEvent. |
|
- effect/tests/test.Browser.ts: launch -> CDPConnect+BrowserConnected, |
|
kill -> BrowserDisconnected, BrowserPageWaitForTimeout sleep |
|
semantics, BrowserPageGoto emits Page.navigate CDPSend. |
|
- it.todo entries cover real provider HTTP, Page.captureScreenshot, |
|
Target.getTargets, Emulation.setDeviceMetricsOverride, |
|
Runtime.evaluate, alternate launch branches. |
|
|
|
https://claude.ai/code/session_01TFHsTPkxNfqUQjS6xLByft |
|
--- |
|
effect/browser/Browser.ts | 451 ++++++++-- |
|
effect/cdp/CDPClient.ts | 13 +- |
|
effect/llm/AnthropicLLMSession.ts | 77 ++ |
|
effect/llm/BaseProviderLLMSession.ts | 111 +++ |
|
effect/llm/GoogleLLMSession.ts | 75 ++ |
|
effect/llm/LLMRouterLayer.ts | 232 ++++- |
|
effect/llm/NoopLLMSession.ts | 82 ++ |
|
effect/llm/OpenAILLMSession.ts | 111 ++- |
|
effect/protocol/events.ts | 1200 ++++++++++++++++++++++++-- |
|
effect/stagehand/StagehandSession.ts | 16 +- |
|
effect/tests/test.Browser.ts | 82 +- |
|
effect/tests/test.LLMRouterLayer.ts | 143 ++- |
|
12 files changed, 2327 insertions(+), 266 deletions(-) |
|
create mode 100644 effect/llm/AnthropicLLMSession.ts |
|
create mode 100644 effect/llm/BaseProviderLLMSession.ts |
|
create mode 100644 effect/llm/GoogleLLMSession.ts |
|
create mode 100644 effect/llm/NoopLLMSession.ts |
|
|
|
diff --git a/effect/browser/Browser.ts b/effect/browser/Browser.ts |
|
index b27339e..2da15f9 100644 |
|
--- a/effect/browser/Browser.ts |
|
+++ b/effect/browser/Browser.ts |
|
@@ -3,13 +3,77 @@ import { Effect, Ref } from "effect"; |
|
import { CDPClient } from "../cdp/CDPClient.js"; |
|
import { Bus } from "../protocol/Bus.js"; |
|
import { |
|
+ AboutBlankPageOpenedEvent, |
|
+ BrowserAddCookiesEvent, |
|
+ BrowserAddInitScriptEvent, |
|
+ BrowserClearCookiesEvent, |
|
+ BrowserCloseEvent, |
|
BrowserConnectedEvent, |
|
+ BrowserDisconnectedEvent, |
|
+ BrowserDownloadCompletedEvent, |
|
+ BrowserDownloadStartedEvent, |
|
+ BrowserExitedEvent, |
|
+ BrowserFrameDetachedEvent, |
|
+ BrowserFrameUpdatedEvent, |
|
BrowserKillEvent, |
|
BrowserLaunchOrConnectEvent, |
|
+ BrowserPageBringToFrontEvent, |
|
+ BrowserPageBroughtToFrontEvent, |
|
+ BrowserPageClickEvent, |
|
+ BrowserPageDOMChangedEvent, |
|
+ BrowserPageDOMContentLoadedEvent, |
|
+ BrowserPageDOMSummaryEvent, |
|
+ BrowserPageDocumentSnapshotCapturedEvent, |
|
+ BrowserPageDoubleClickEvent, |
|
+ BrowserPageDragAndDropEvent, |
|
+ BrowserPageEnableCursorOverlayEvent, |
|
+ BrowserPageEvaluateEvent, |
|
+ BrowserPageFillEvent, |
|
+ BrowserPageGoBackEvent, |
|
+ BrowserPageGoForwardEvent, |
|
BrowserPageGotoEvent, |
|
+ BrowserPageHighlightEvent, |
|
+ BrowserPageHoverEvent, |
|
+ BrowserPageKeyPressEvent, |
|
+ BrowserPageLoadStateChangedEvent, |
|
+ BrowserPageLoadedEvent, |
|
+ BrowserPageLocateEvent, |
|
BrowserPageNavigatedEvent, |
|
+ BrowserPageNetworkIdle2Event, |
|
+ BrowserPageNetworkIdleEvent, |
|
+ BrowserPageNextChunkEvent, |
|
+ BrowserPagePrevChunkEvent, |
|
+ BrowserPageReloadEvent, |
|
+ BrowserPageRequestDocumentSnapshotEvent, |
|
+ BrowserPageRequestElementInfoEvent, |
|
+ BrowserPageRequestFullFrameTreeEvent, |
|
+ BrowserPageRequestInfoEvent, |
|
BrowserPageRequestScreenshotEvent, |
|
+ BrowserPageRequestSnapshotEvent, |
|
+ BrowserPageScreenshotCapturedEvent, |
|
+ BrowserPageScrollByEvent, |
|
+ BrowserPageScrollEvent, |
|
+ BrowserPageScrollToEvent, |
|
+ BrowserPageSelectOptionEvent, |
|
+ BrowserPageSendCDPEvent, |
|
+ BrowserPageSetInputFilesEvent, |
|
+ BrowserPageTypeEvent, |
|
+ BrowserPageWaitForLoadStateEvent, |
|
+ BrowserPageWaitForSelectorEvent, |
|
+ BrowserPageWaitForTimeoutEvent, |
|
+ BrowserRequestActivePageEvent, |
|
+ BrowserRequestConfiguredViewportEvent, |
|
+ BrowserRequestConnectURLEvent, |
|
+ BrowserRequestCookiesEvent, |
|
+ BrowserRequestFetchDownloadsEvent, |
|
BrowserRequestTabListEvent, |
|
+ BrowserSetDownloadBehaviorEvent, |
|
+ BrowserSetExtraHttpHeadersEvent, |
|
+ BrowserSetViewportEvent, |
|
+ BrowserTabCloseEvent, |
|
+ BrowserTabClosedEvent, |
|
+ BrowserTabCreateEvent, |
|
+ BrowserTabCreatedEvent, |
|
CDPConnectEvent, |
|
CDPSendEvent, |
|
type SelectorObject, |
|
@@ -18,23 +82,24 @@ import { |
|
|
|
/** |
|
* Browser fact projector and browser-command owner. The single layer that listens to high-level browser commands |
|
- * (`BrowserPageGotoEvent`, `BrowserPageRequestScreenshotEvent`, `BrowserRequestTabListEvent`, ...) and translates them |
|
+ * (`BrowserPage*Event`, `BrowserTab*Event`, `BrowserRequest*Event`, `BrowserSet*Event`, ...) and translates them |
|
* into raw `CDPSendEvent` traffic. Must not know about LLMs, agents, or Browserbase-specific orchestration. |
|
* |
|
- * State (held in Refs because Browser facts are projected from event history, not durable): |
|
- * - `id` (uuid v7) — set once at startup |
|
- * - `browserEnv` ("local" | "bb" | "remote" | "extension") — settable by the launch branch that wins |
|
- * - `connected` — flips on `CDPConnectedEvent` |
|
+ * The full command surface is registered with `Bus.on(...)` so every event has a handler bound at the right |
|
+ * `event_type`. Most page-level command bodies are stubbed with `Effect.die("TODO: ...")` because their CDP |
|
+ * encodings are non-trivial (Page.captureScreenshot, Runtime.evaluate, Target.getTargets accessibility-tree |
|
+ * builders, etc.) and porting each one is a separate work unit. The signatures, dispatch policies, and event-class |
|
+ * registrations are real — adding a real handler body is a one-file change. |
|
* |
|
- * Most of Browser's surface is per-page commands. Those handlers are stubbed below with `Effect.die("TODO: ...")` so |
|
- * the layer registers handlers for the right events and the type signatures stay correct, but without committing to |
|
- * a specific CDP encoding yet. |
|
+ * State (Refs, because Browser facts are projected from event history, not durable): |
|
+ * - `id` — uuid v7, set once at startup |
|
+ * - `browserEnv` — "local" | "bb" | "remote" | "extension", set by whichever launch branch wins |
|
+ * - `connected` — flips on `CDPConnectedEvent` |
|
+ * - `cdpUrl` — the connect URL for the live websocket (mirrored from the launch event) |
|
* |
|
- * Naming parity preserved: |
|
- * - `Browser` (class), `BrowserService` shape mirrors abxbus `Browser.state` keys. |
|
- * - Handlers: `on_BrowserLaunchOrConnect`, `on_BrowserPageGoto`, `on_BrowserPageRequestScreenshot`, |
|
- * `on_BrowserRequestTabList`, `on_BrowserKill`. |
|
- * - Static `LISTENS_TO` / `EMITS` arrays. |
|
+ * Naming parity preserved: every handler is `on_<EventNameWithoutEventSuffix>`, the static `LISTENS_TO` / `EMITS` |
|
+ * arrays mirror the abxbus version, and `BrowserPageBroughtToFrontEvent` / `BrowserPageNavigatedEvent` / |
|
+ * `BrowserDownload*Event` etc. are emitted with the same payload shapes. |
|
*/ |
|
export type BrowserEnv = "local" | "bb" | "remote" | "extension"; |
|
|
|
@@ -45,12 +110,12 @@ export interface BrowserState { |
|
readonly cdpUrl: string | null; |
|
} |
|
|
|
+const todo = (where: string) => Effect.die(`TODO: port Browser.${where} to CDP-driven Effect`); |
|
+ |
|
export class Browser extends Effect.Service<Browser>()("Browser", { |
|
scoped: Effect.gen(function* () { |
|
const bus = yield* Bus; |
|
- // Resolving CDPClient here makes Browser depend on CDPClient at the Layer level — composition order is enforced |
|
- // automatically by Layer.mergeAll, no manual sub-layer construction. |
|
- yield* CDPClient; |
|
+ yield* CDPClient; // declared dependency so Layer.mergeAll wires the CDPClient ahead of Browser |
|
|
|
const state = yield* Ref.make<BrowserState>({ |
|
id: bus.id, |
|
@@ -59,35 +124,86 @@ export class Browser extends Effect.Service<Browser>()("Browser", { |
|
cdpUrl: null, |
|
}); |
|
|
|
+ // ----- lifecycle / connection ------------------------------------------------------- |
|
+ |
|
const on_BrowserLaunchOrConnect = (event: BrowserLaunchOrConnectEvent) => |
|
Effect.gen(function* () { |
|
- // Single-branch launch path: ask CDPClient to connect to the supplied URL. In the abxbus version this event |
|
- // had multiple competing handlers (LocalBrowserLayer / BBBrowserLayer / RemoteBrowserLayer / ExtensionUILayer) |
|
- // and `event_handler_completion: "first"` chose one. The Effect-native model selects the runtime branch at |
|
- // Layer composition time instead — only one launch handler is registered, so there is no contention. |
|
- // TODO: re-introduce branch selection (local vs Browserbase vs remote vs extension) by providing alternate |
|
- // Browser layers that each register their own `on_BrowserLaunchOrConnect`. |
|
- yield* bus.emit(CDPConnectEvent({ cdpUrl: event.cdpUrl })); |
|
- yield* Ref.update(state, (s) => ({ ...s, connected: true, cdpUrl: event.cdpUrl ?? s.cdpUrl })); |
|
- yield* bus.emit(BrowserConnectedEvent({ browser_id: bus.id, cdpUrl: event.cdpUrl })); |
|
- return { browser_id: bus.id, cdpUrl: event.cdpUrl ?? null }; |
|
+ // Single-branch launch path — Effect-native model selects the runtime branch at Layer composition time |
|
+ // (alternate Browser layers each register their own on_BrowserLaunchOrConnect; only one is loaded). |
|
+ // TODO: add LocalBrowserLayer / BBBrowserLayer / RemoteBrowserLayer / ExtensionUILayer alternate branches. |
|
+ const cdpUrl = event.cdpUrl ?? ""; |
|
+ if (cdpUrl.length > 0) yield* bus.emit(CDPConnectEvent({ cdpUrl })); |
|
+ yield* Ref.update(state, (s) => ({ ...s, connected: true, cdpUrl: cdpUrl || s.cdpUrl })); |
|
+ yield* bus.emit(BrowserConnectedEvent({ browser_id: bus.id, cdpUrl: cdpUrl || undefined })); |
|
+ return { browser_id: bus.id, cdpUrl: cdpUrl || null, env: "local" as const }; |
|
}); |
|
|
|
const on_BrowserKill = (_event: BrowserKillEvent) => |
|
Effect.gen(function* () { |
|
yield* Ref.update(state, (s) => ({ ...s, connected: false })); |
|
- // Browser does not own the websocket — emitting a CDP disconnect lets CDPClient tear down cleanly. |
|
- // TODO: wire CDPDisconnectEvent emit once Browser-side teardown is fully ported. |
|
- return {}; |
|
+ yield* bus.emit(BrowserDisconnectedEvent({ browser_id: bus.id })); |
|
+ yield* bus.emit(BrowserExitedEvent({})); |
|
+ return { killed: true }; |
|
+ }); |
|
+ |
|
+ // ----- viewport / headers / scripts / cookies --------------------------------------- |
|
+ |
|
+ const on_BrowserSetViewport = (_event: BrowserSetViewportEvent) => todo("on_BrowserSetViewport"); |
|
+ const on_BrowserRequestConfiguredViewport = (_event: BrowserRequestConfiguredViewportEvent) => |
|
+ todo("on_BrowserRequestConfiguredViewport"); |
|
+ const on_BrowserSetExtraHttpHeaders = (_event: BrowserSetExtraHttpHeadersEvent) => |
|
+ todo("on_BrowserSetExtraHttpHeaders"); |
|
+ const on_BrowserAddInitScript = (_event: BrowserAddInitScriptEvent) => todo("on_BrowserAddInitScript"); |
|
+ const on_BrowserRequestCookies = (_event: BrowserRequestCookiesEvent) => todo("on_BrowserRequestCookies"); |
|
+ const on_BrowserAddCookies = (_event: BrowserAddCookiesEvent) => todo("on_BrowserAddCookies"); |
|
+ const on_BrowserClearCookies = (_event: BrowserClearCookiesEvent) => todo("on_BrowserClearCookies"); |
|
+ const on_BrowserRequestConnectURL = (_event: BrowserRequestConnectURLEvent) => todo("on_BrowserRequestConnectURL"); |
|
+ const on_BrowserClose = (_event: BrowserCloseEvent) => todo("on_BrowserClose"); |
|
+ const on_BrowserSetDownloadBehavior = (_event: BrowserSetDownloadBehaviorEvent) => |
|
+ todo("on_BrowserSetDownloadBehavior"); |
|
+ const on_BrowserRequestFetchDownloads = (_event: BrowserRequestFetchDownloadsEvent) => |
|
+ todo("on_BrowserRequestFetchDownloads"); |
|
+ |
|
+ // ----- tabs / pages query -------------------------------------------------------- |
|
+ |
|
+ const on_BrowserRequestTabList = (_event: BrowserRequestTabListEvent): Effect.Effect<ReadonlyArray<TargetInfo>> => |
|
+ // TODO: CDP Target.getTargets, filter type === "page", project TargetInfo[]. |
|
+ todo("on_BrowserRequestTabList"); |
|
+ |
|
+ const on_BrowserRequestActivePage = (_event: BrowserRequestActivePageEvent) => todo("on_BrowserRequestActivePage"); |
|
+ |
|
+ const on_BrowserTabCreate = (event: BrowserTabCreateEvent) => |
|
+ Effect.gen(function* () { |
|
+ // TODO: CDP Target.createTarget { url: event.url ?? "about:blank" }, capture targetId, emit TabCreated. |
|
+ yield* bus.emit( |
|
+ CDPSendEvent({ |
|
+ method: "Target.createTarget", |
|
+ params: { url: event.url ?? "about:blank" }, |
|
+ }), |
|
+ ); |
|
+ return yield* todo("on_BrowserTabCreate"); |
|
+ }); |
|
+ |
|
+ const on_BrowserTabClose = (event: BrowserTabCloseEvent) => |
|
+ Effect.gen(function* () { |
|
+ // TODO: resolve targetId from selector, CDP Target.closeTarget, emit BrowserTabClosed. |
|
+ const targetId = event.selector.targetId ?? null; |
|
+ if (targetId != null) { |
|
+ yield* bus.emit(CDPSendEvent({ method: "Target.closeTarget", params: { targetId } })); |
|
+ yield* bus.emit(BrowserTabClosedEvent({ targetId })); |
|
+ } |
|
+ return yield* todo("on_BrowserTabClose"); |
|
}); |
|
|
|
+ // ----- page navigation ------------------------------------------------------------ |
|
+ |
|
const on_BrowserPageGoto = (event: BrowserPageGotoEvent) => |
|
Effect.gen(function* () { |
|
- // TODO: port full goto pipeline. Pre-port flow: |
|
- // 1. resolve targetId from selector (via projection of CDPRecvEvent history) |
|
+ // TODO: full goto pipeline: |
|
+ // 1. resolve targetId from selector |
|
// 2. CDPSendEvent { method: "Page.navigate", params: { url, frameId? } } |
|
// 3. wait for Page.frameStoppedLoading |
|
- // 4. emit BrowserPageNavigatedEvent |
|
+ // 4. emit BrowserPageNavigatedEvent and return shape |
|
yield* bus.emit( |
|
CDPSendEvent({ |
|
method: "Page.navigate", |
|
@@ -95,33 +211,203 @@ export class Browser extends Effect.Service<Browser>()("Browser", { |
|
targetId: event.selector?.targetId ?? undefined, |
|
}), |
|
); |
|
- yield* bus.emit(BrowserPageNavigatedEvent({ url: event.url, targetId: event.selector?.targetId ?? undefined })); |
|
- return Effect.die("TODO: port BrowserPageGoto wait-for-load and result shaping"); |
|
+ yield* bus.emit( |
|
+ BrowserPageNavigatedEvent({ |
|
+ url: event.url, |
|
+ selector: event.selector, |
|
+ }), |
|
+ ); |
|
+ return yield* todo("on_BrowserPageGoto"); |
|
}); |
|
|
|
+ const on_BrowserPageBringToFront = (_event: BrowserPageBringToFrontEvent) => todo("on_BrowserPageBringToFront"); |
|
+ const on_BrowserPageGoBack = (_event: BrowserPageGoBackEvent) => todo("on_BrowserPageGoBack"); |
|
+ const on_BrowserPageGoForward = (_event: BrowserPageGoForwardEvent) => todo("on_BrowserPageGoForward"); |
|
+ const on_BrowserPageReload = (_event: BrowserPageReloadEvent) => todo("on_BrowserPageReload"); |
|
+ |
|
+ // ----- page actions --------------------------------------------------------------- |
|
+ |
|
+ const on_BrowserPageClick = (_event: BrowserPageClickEvent) => todo("on_BrowserPageClick"); |
|
+ const on_BrowserPageDoubleClick = (_event: BrowserPageDoubleClickEvent) => todo("on_BrowserPageDoubleClick"); |
|
+ const on_BrowserPageHover = (_event: BrowserPageHoverEvent) => todo("on_BrowserPageHover"); |
|
+ const on_BrowserPageDragAndDrop = (_event: BrowserPageDragAndDropEvent) => todo("on_BrowserPageDragAndDrop"); |
|
+ const on_BrowserPageType = (_event: BrowserPageTypeEvent) => todo("on_BrowserPageType"); |
|
+ const on_BrowserPageFill = (_event: BrowserPageFillEvent) => todo("on_BrowserPageFill"); |
|
+ const on_BrowserPageKeyPress = (_event: BrowserPageKeyPressEvent) => todo("on_BrowserPageKeyPress"); |
|
+ const on_BrowserPageScroll = (_event: BrowserPageScrollEvent) => todo("on_BrowserPageScroll"); |
|
+ const on_BrowserPageScrollTo = (_event: BrowserPageScrollToEvent) => todo("on_BrowserPageScrollTo"); |
|
+ const on_BrowserPageNextChunk = (_event: BrowserPageNextChunkEvent) => todo("on_BrowserPageNextChunk"); |
|
+ const on_BrowserPagePrevChunk = (_event: BrowserPagePrevChunkEvent) => todo("on_BrowserPagePrevChunk"); |
|
+ const on_BrowserPageScrollBy = (_event: BrowserPageScrollByEvent) => todo("on_BrowserPageScrollBy"); |
|
+ const on_BrowserPageSelectOption = (_event: BrowserPageSelectOptionEvent) => todo("on_BrowserPageSelectOption"); |
|
+ const on_BrowserPageSetInputFiles = (_event: BrowserPageSetInputFilesEvent) => todo("on_BrowserPageSetInputFiles"); |
|
+ const on_BrowserPageEnableCursorOverlay = (_event: BrowserPageEnableCursorOverlayEvent) => |
|
+ todo("on_BrowserPageEnableCursorOverlay"); |
|
+ const on_BrowserPageHighlight = (_event: BrowserPageHighlightEvent) => todo("on_BrowserPageHighlight"); |
|
+ |
|
+ // ----- page introspection / waits / evaluate / sendCDP ---------------------------- |
|
+ |
|
+ const on_BrowserPageLocate = (_event: BrowserPageLocateEvent) => todo("on_BrowserPageLocate"); |
|
+ const on_BrowserPageRequestSnapshot = (_event: BrowserPageRequestSnapshotEvent) => |
|
+ todo("on_BrowserPageRequestSnapshot"); |
|
+ const on_BrowserPageRequestDocumentSnapshot = (_event: BrowserPageRequestDocumentSnapshotEvent) => |
|
+ todo("on_BrowserPageRequestDocumentSnapshot"); |
|
const on_BrowserPageRequestScreenshot = (_event: BrowserPageRequestScreenshotEvent) => |
|
- // TODO: port. Pre-port flow: |
|
- // 1. resolve targetId from selector |
|
- // 2. CDPSendEvent { method: "Page.captureScreenshot", params: { format: "png" } } |
|
- // 3. base64 result -> { screenshot: data } |
|
- Effect.die("TODO: port Browser.on_BrowserPageRequestScreenshot to Effect generator"); |
|
+ todo("on_BrowserPageRequestScreenshot"); |
|
+ const on_BrowserPageRequestFullFrameTree = (_event: BrowserPageRequestFullFrameTreeEvent) => |
|
+ todo("on_BrowserPageRequestFullFrameTree"); |
|
+ const on_BrowserPageRequestInfo = (_event: BrowserPageRequestInfoEvent) => todo("on_BrowserPageRequestInfo"); |
|
+ const on_BrowserPageRequestElementInfo = (_event: BrowserPageRequestElementInfoEvent) => |
|
+ todo("on_BrowserPageRequestElementInfo"); |
|
+ const on_BrowserPageWaitForLoadState = (_event: BrowserPageWaitForLoadStateEvent) => |
|
+ todo("on_BrowserPageWaitForLoadState"); |
|
+ const on_BrowserPageWaitForSelector = (_event: BrowserPageWaitForSelectorEvent) => |
|
+ todo("on_BrowserPageWaitForSelector"); |
|
+ const on_BrowserPageWaitForTimeout = (event: BrowserPageWaitForTimeoutEvent) => |
|
+ // Real implementation: just sleep. |
|
+ Effect.sleep(`${event.ms} millis`).pipe(Effect.as({ ms: event.ms })); |
|
+ const on_BrowserPageEvaluate = (_event: BrowserPageEvaluateEvent) => todo("on_BrowserPageEvaluate"); |
|
+ const on_BrowserPageSendCDP = (event: BrowserPageSendCDPEvent) => |
|
+ // Forward as CDPSendEvent and let CDPClient correlate the response. |
|
+ Effect.gen(function* () { |
|
+ yield* bus.emit( |
|
+ CDPSendEvent({ |
|
+ method: event.method, |
|
+ params: event.params, |
|
+ targetId: event.targetId, |
|
+ }), |
|
+ ); |
|
+ return yield* todo("on_BrowserPageSendCDP"); |
|
+ }); |
|
+ const on_BrowserPageDOMSummary = (_event: BrowserPageDOMSummaryEvent) => todo("on_BrowserPageDOMSummary"); |
|
|
|
- const on_BrowserRequestTabList = (_event: BrowserRequestTabListEvent): Effect.Effect<ReadonlyArray<TargetInfo>> => |
|
- // TODO: port. Pre-port flow: |
|
- // 1. CDPSendEvent { method: "Target.getTargets" } |
|
- // 2. filter by type === "page" and project TargetInfo |
|
- Effect.die("TODO: port Browser.on_BrowserRequestTabList to Effect generator"); |
|
+ // ----- registrations -------------------------------------------------------------- |
|
|
|
yield* bus.on(BrowserLaunchOrConnectEvent, on_BrowserLaunchOrConnect, { |
|
handler_name: "Browser.on_BrowserLaunchOrConnect", |
|
}); |
|
yield* bus.on(BrowserKillEvent, on_BrowserKill, { handler_name: "Browser.on_BrowserKill" }); |
|
+ |
|
+ yield* bus.on(BrowserSetViewportEvent, on_BrowserSetViewport, { handler_name: "Browser.on_BrowserSetViewport" }); |
|
+ yield* bus.on(BrowserRequestConfiguredViewportEvent, on_BrowserRequestConfiguredViewport, { |
|
+ handler_name: "Browser.on_BrowserRequestConfiguredViewport", |
|
+ }); |
|
+ yield* bus.on(BrowserSetExtraHttpHeadersEvent, on_BrowserSetExtraHttpHeaders, { |
|
+ handler_name: "Browser.on_BrowserSetExtraHttpHeaders", |
|
+ }); |
|
+ yield* bus.on(BrowserAddInitScriptEvent, on_BrowserAddInitScript, { |
|
+ handler_name: "Browser.on_BrowserAddInitScript", |
|
+ }); |
|
+ yield* bus.on(BrowserRequestCookiesEvent, on_BrowserRequestCookies, { |
|
+ handler_name: "Browser.on_BrowserRequestCookies", |
|
+ }); |
|
+ yield* bus.on(BrowserAddCookiesEvent, on_BrowserAddCookies, { handler_name: "Browser.on_BrowserAddCookies" }); |
|
+ yield* bus.on(BrowserClearCookiesEvent, on_BrowserClearCookies, { |
|
+ handler_name: "Browser.on_BrowserClearCookies", |
|
+ }); |
|
+ yield* bus.on(BrowserRequestConnectURLEvent, on_BrowserRequestConnectURL, { |
|
+ handler_name: "Browser.on_BrowserRequestConnectURL", |
|
+ }); |
|
+ yield* bus.on(BrowserCloseEvent, on_BrowserClose, { handler_name: "Browser.on_BrowserClose" }); |
|
+ yield* bus.on(BrowserSetDownloadBehaviorEvent, on_BrowserSetDownloadBehavior, { |
|
+ handler_name: "Browser.on_BrowserSetDownloadBehavior", |
|
+ }); |
|
+ yield* bus.on(BrowserRequestFetchDownloadsEvent, on_BrowserRequestFetchDownloads, { |
|
+ handler_name: "Browser.on_BrowserRequestFetchDownloads", |
|
+ }); |
|
+ |
|
+ yield* bus.on(BrowserRequestTabListEvent, on_BrowserRequestTabList, { |
|
+ handler_name: "Browser.on_BrowserRequestTabList", |
|
+ }); |
|
+ yield* bus.on(BrowserRequestActivePageEvent, on_BrowserRequestActivePage, { |
|
+ handler_name: "Browser.on_BrowserRequestActivePage", |
|
+ }); |
|
+ yield* bus.on(BrowserTabCreateEvent, on_BrowserTabCreate, { handler_name: "Browser.on_BrowserTabCreate" }); |
|
+ yield* bus.on(BrowserTabCloseEvent, on_BrowserTabClose, { handler_name: "Browser.on_BrowserTabClose" }); |
|
+ |
|
yield* bus.on(BrowserPageGotoEvent, on_BrowserPageGoto, { handler_name: "Browser.on_BrowserPageGoto" }); |
|
+ yield* bus.on(BrowserPageBringToFrontEvent, on_BrowserPageBringToFront, { |
|
+ handler_name: "Browser.on_BrowserPageBringToFront", |
|
+ }); |
|
+ yield* bus.on(BrowserPageGoBackEvent, on_BrowserPageGoBack, { handler_name: "Browser.on_BrowserPageGoBack" }); |
|
+ yield* bus.on(BrowserPageGoForwardEvent, on_BrowserPageGoForward, { |
|
+ handler_name: "Browser.on_BrowserPageGoForward", |
|
+ }); |
|
+ yield* bus.on(BrowserPageReloadEvent, on_BrowserPageReload, { handler_name: "Browser.on_BrowserPageReload" }); |
|
+ |
|
+ yield* bus.on(BrowserPageClickEvent, on_BrowserPageClick, { handler_name: "Browser.on_BrowserPageClick" }); |
|
+ yield* bus.on(BrowserPageDoubleClickEvent, on_BrowserPageDoubleClick, { |
|
+ handler_name: "Browser.on_BrowserPageDoubleClick", |
|
+ }); |
|
+ yield* bus.on(BrowserPageHoverEvent, on_BrowserPageHover, { handler_name: "Browser.on_BrowserPageHover" }); |
|
+ yield* bus.on(BrowserPageDragAndDropEvent, on_BrowserPageDragAndDrop, { |
|
+ handler_name: "Browser.on_BrowserPageDragAndDrop", |
|
+ }); |
|
+ yield* bus.on(BrowserPageTypeEvent, on_BrowserPageType, { handler_name: "Browser.on_BrowserPageType" }); |
|
+ yield* bus.on(BrowserPageFillEvent, on_BrowserPageFill, { handler_name: "Browser.on_BrowserPageFill" }); |
|
+ yield* bus.on(BrowserPageKeyPressEvent, on_BrowserPageKeyPress, { |
|
+ handler_name: "Browser.on_BrowserPageKeyPress", |
|
+ }); |
|
+ yield* bus.on(BrowserPageScrollEvent, on_BrowserPageScroll, { handler_name: "Browser.on_BrowserPageScroll" }); |
|
+ yield* bus.on(BrowserPageScrollToEvent, on_BrowserPageScrollTo, { |
|
+ handler_name: "Browser.on_BrowserPageScrollTo", |
|
+ }); |
|
+ yield* bus.on(BrowserPageNextChunkEvent, on_BrowserPageNextChunk, { |
|
+ handler_name: "Browser.on_BrowserPageNextChunk", |
|
+ }); |
|
+ yield* bus.on(BrowserPagePrevChunkEvent, on_BrowserPagePrevChunk, { |
|
+ handler_name: "Browser.on_BrowserPagePrevChunk", |
|
+ }); |
|
+ yield* bus.on(BrowserPageScrollByEvent, on_BrowserPageScrollBy, { |
|
+ handler_name: "Browser.on_BrowserPageScrollBy", |
|
+ }); |
|
+ yield* bus.on(BrowserPageSelectOptionEvent, on_BrowserPageSelectOption, { |
|
+ handler_name: "Browser.on_BrowserPageSelectOption", |
|
+ }); |
|
+ yield* bus.on(BrowserPageSetInputFilesEvent, on_BrowserPageSetInputFiles, { |
|
+ handler_name: "Browser.on_BrowserPageSetInputFiles", |
|
+ }); |
|
+ yield* bus.on(BrowserPageEnableCursorOverlayEvent, on_BrowserPageEnableCursorOverlay, { |
|
+ handler_name: "Browser.on_BrowserPageEnableCursorOverlay", |
|
+ }); |
|
+ yield* bus.on(BrowserPageHighlightEvent, on_BrowserPageHighlight, { |
|
+ handler_name: "Browser.on_BrowserPageHighlight", |
|
+ }); |
|
+ |
|
+ yield* bus.on(BrowserPageLocateEvent, on_BrowserPageLocate, { handler_name: "Browser.on_BrowserPageLocate" }); |
|
+ yield* bus.on(BrowserPageRequestSnapshotEvent, on_BrowserPageRequestSnapshot, { |
|
+ handler_name: "Browser.on_BrowserPageRequestSnapshot", |
|
+ }); |
|
+ yield* bus.on(BrowserPageRequestDocumentSnapshotEvent, on_BrowserPageRequestDocumentSnapshot, { |
|
+ handler_name: "Browser.on_BrowserPageRequestDocumentSnapshot", |
|
+ }); |
|
yield* bus.on(BrowserPageRequestScreenshotEvent, on_BrowserPageRequestScreenshot, { |
|
handler_name: "Browser.on_BrowserPageRequestScreenshot", |
|
}); |
|
- yield* bus.on(BrowserRequestTabListEvent, on_BrowserRequestTabList, { |
|
- handler_name: "Browser.on_BrowserRequestTabList", |
|
+ yield* bus.on(BrowserPageRequestFullFrameTreeEvent, on_BrowserPageRequestFullFrameTree, { |
|
+ handler_name: "Browser.on_BrowserPageRequestFullFrameTree", |
|
+ }); |
|
+ yield* bus.on(BrowserPageRequestInfoEvent, on_BrowserPageRequestInfo, { |
|
+ handler_name: "Browser.on_BrowserPageRequestInfo", |
|
+ }); |
|
+ yield* bus.on(BrowserPageRequestElementInfoEvent, on_BrowserPageRequestElementInfo, { |
|
+ handler_name: "Browser.on_BrowserPageRequestElementInfo", |
|
+ }); |
|
+ yield* bus.on(BrowserPageWaitForLoadStateEvent, on_BrowserPageWaitForLoadState, { |
|
+ handler_name: "Browser.on_BrowserPageWaitForLoadState", |
|
+ }); |
|
+ yield* bus.on(BrowserPageWaitForSelectorEvent, on_BrowserPageWaitForSelector, { |
|
+ handler_name: "Browser.on_BrowserPageWaitForSelector", |
|
+ }); |
|
+ yield* bus.on(BrowserPageWaitForTimeoutEvent, on_BrowserPageWaitForTimeout, { |
|
+ handler_name: "Browser.on_BrowserPageWaitForTimeout", |
|
+ }); |
|
+ yield* bus.on(BrowserPageEvaluateEvent, on_BrowserPageEvaluate, { |
|
+ handler_name: "Browser.on_BrowserPageEvaluate", |
|
+ }); |
|
+ yield* bus.on(BrowserPageSendCDPEvent, on_BrowserPageSendCDP, { handler_name: "Browser.on_BrowserPageSendCDP" }); |
|
+ yield* bus.on(BrowserPageDOMSummaryEvent, on_BrowserPageDOMSummary, { |
|
+ handler_name: "Browser.on_BrowserPageDOMSummary", |
|
}); |
|
|
|
/** Selector projection helper kept on the service for callers that need to resolve a `targetId` outside a handler. */ |
|
@@ -135,10 +421,79 @@ export class Browser extends Effect.Service<Browser>()("Browser", { |
|
static readonly LISTENS_TO = [ |
|
BrowserLaunchOrConnectEvent, |
|
BrowserKillEvent, |
|
+ BrowserSetViewportEvent, |
|
+ BrowserRequestConfiguredViewportEvent, |
|
+ BrowserSetExtraHttpHeadersEvent, |
|
+ BrowserAddInitScriptEvent, |
|
+ BrowserRequestCookiesEvent, |
|
+ BrowserAddCookiesEvent, |
|
+ BrowserClearCookiesEvent, |
|
+ BrowserRequestConnectURLEvent, |
|
+ BrowserCloseEvent, |
|
+ BrowserSetDownloadBehaviorEvent, |
|
+ BrowserRequestFetchDownloadsEvent, |
|
+ BrowserRequestTabListEvent, |
|
+ BrowserRequestActivePageEvent, |
|
+ BrowserTabCreateEvent, |
|
+ BrowserTabCloseEvent, |
|
BrowserPageGotoEvent, |
|
+ BrowserPageBringToFrontEvent, |
|
+ BrowserPageGoBackEvent, |
|
+ BrowserPageGoForwardEvent, |
|
+ BrowserPageReloadEvent, |
|
+ BrowserPageClickEvent, |
|
+ BrowserPageDoubleClickEvent, |
|
+ BrowserPageHoverEvent, |
|
+ BrowserPageDragAndDropEvent, |
|
+ BrowserPageTypeEvent, |
|
+ BrowserPageFillEvent, |
|
+ BrowserPageKeyPressEvent, |
|
+ BrowserPageScrollEvent, |
|
+ BrowserPageScrollToEvent, |
|
+ BrowserPageNextChunkEvent, |
|
+ BrowserPagePrevChunkEvent, |
|
+ BrowserPageScrollByEvent, |
|
+ BrowserPageSelectOptionEvent, |
|
+ BrowserPageSetInputFilesEvent, |
|
+ BrowserPageEnableCursorOverlayEvent, |
|
+ BrowserPageHighlightEvent, |
|
+ BrowserPageLocateEvent, |
|
+ BrowserPageRequestSnapshotEvent, |
|
+ BrowserPageRequestDocumentSnapshotEvent, |
|
BrowserPageRequestScreenshotEvent, |
|
- BrowserRequestTabListEvent, |
|
+ BrowserPageRequestFullFrameTreeEvent, |
|
+ BrowserPageRequestInfoEvent, |
|
+ BrowserPageRequestElementInfoEvent, |
|
+ BrowserPageWaitForLoadStateEvent, |
|
+ BrowserPageWaitForSelectorEvent, |
|
+ BrowserPageWaitForTimeoutEvent, |
|
+ BrowserPageEvaluateEvent, |
|
+ BrowserPageSendCDPEvent, |
|
+ BrowserPageDOMSummaryEvent, |
|
] as const; |
|
|
|
- static readonly EMITS = [BrowserConnectedEvent, BrowserPageNavigatedEvent, CDPConnectEvent, CDPSendEvent] as const; |
|
+ static readonly EMITS = [ |
|
+ BrowserConnectedEvent, |
|
+ BrowserDisconnectedEvent, |
|
+ BrowserExitedEvent, |
|
+ BrowserTabCreatedEvent, |
|
+ BrowserTabClosedEvent, |
|
+ BrowserPageBroughtToFrontEvent, |
|
+ BrowserPageNavigatedEvent, |
|
+ BrowserPageDOMContentLoadedEvent, |
|
+ BrowserPageLoadedEvent, |
|
+ BrowserPageNetworkIdle2Event, |
|
+ BrowserPageNetworkIdleEvent, |
|
+ BrowserPageLoadStateChangedEvent, |
|
+ BrowserPageDOMChangedEvent, |
|
+ BrowserPageDocumentSnapshotCapturedEvent, |
|
+ BrowserPageScreenshotCapturedEvent, |
|
+ BrowserFrameUpdatedEvent, |
|
+ BrowserFrameDetachedEvent, |
|
+ BrowserDownloadStartedEvent, |
|
+ BrowserDownloadCompletedEvent, |
|
+ AboutBlankPageOpenedEvent, |
|
+ CDPConnectEvent, |
|
+ CDPSendEvent, |
|
+ ] as const; |
|
} |
|
diff --git a/effect/cdp/CDPClient.ts b/effect/cdp/CDPClient.ts |
|
index 4a0f435..e92c26d 100644 |
|
--- a/effect/cdp/CDPClient.ts |
|
+++ b/effect/cdp/CDPClient.ts |
|
@@ -41,13 +41,13 @@ export class CDPClient extends Effect.Service<CDPClient>()("CDPClient", { |
|
|
|
const on_CDPConnect = (event: CDPConnectEvent) => |
|
Effect.gen(function* () { |
|
- if (event.cdpUrl != null) yield* Ref.set(cdpUrl, event.cdpUrl); |
|
+ yield* Ref.set(cdpUrl, event.cdpUrl); |
|
yield* Ref.set(status, "connecting"); |
|
// TODO: open websocket here, dispatch Target.setAutoAttach { autoAttach: true, flatten: true } once |
|
// connected, and pump frames into `bus.emit(CDPRecvEvent({...}))`. |
|
yield* Ref.set(status, "connected"); |
|
- yield* bus.emit(CDPConnectedEvent({ cdpUrl: event.cdpUrl ?? (yield* Ref.get(cdpUrl)) ?? "" })); |
|
- return { cdpUrl: event.cdpUrl ?? null }; |
|
+ yield* bus.emit(CDPConnectedEvent({ cdpUrl: event.cdpUrl })); |
|
+ return { cdpUrl: event.cdpUrl }; |
|
}); |
|
|
|
const on_CDPSend = (event: CDPSendEvent) => |
|
@@ -55,9 +55,9 @@ export class CDPClient extends Effect.Service<CDPClient>()("CDPClient", { |
|
// TODO: actually serialize and send the CDP frame over the websocket. For now we just allocate a request id |
|
// and stamp it into the recv-side mock so dependent flows do not block. |
|
const id = yield* Ref.updateAndGet(lastRequestId, (n) => n + 1); |
|
- // resolve sessionId from targetId map if not provided |
|
+ // resolve cdp_session_id from targetId map if not provided |
|
const resolvedSessionId = |
|
- event.sessionId ?? |
|
+ event.cdp_session_id ?? |
|
(event.targetId != null |
|
? yield* Ref.get(targetToSession).pipe( |
|
Effect.map((m) => HashMap.get(m, event.targetId!)), |
|
@@ -68,9 +68,8 @@ export class CDPClient extends Effect.Service<CDPClient>()("CDPClient", { |
|
yield* bus.emit( |
|
CDPRecvEvent({ |
|
method: event.method, |
|
- params: event.params, |
|
targetId: event.targetId, |
|
- sessionId: resolvedSessionId, |
|
+ cdp_session_id: resolvedSessionId, |
|
requestId: id, |
|
result: undefined, |
|
error: undefined, |
|
diff --git a/effect/llm/AnthropicLLMSession.ts b/effect/llm/AnthropicLLMSession.ts |
|
new file mode 100644 |
|
index 0000000..950be9d |
|
--- /dev/null |
|
+++ b/effect/llm/AnthropicLLMSession.ts |
|
@@ -0,0 +1,77 @@ |
|
+import { Effect, Ref } from "effect"; |
|
+ |
|
+import { Bus } from "../protocol/Bus.js"; |
|
+import { |
|
+ AnthropicLLMRequestEvent, |
|
+ AnthropicLLMSessionConnectEvent, |
|
+ AnthropicLLMSessionDisconnectEvent, |
|
+ AnthropicLLMSessionUpdateEvent, |
|
+} from "../protocol/events.js"; |
|
+import { |
|
+ announceConnect, |
|
+ applyConnectionFields, |
|
+ initialConnectionState, |
|
+ stubProviderRequest, |
|
+ type ProviderConnectionState, |
|
+} from "./BaseProviderLLMSession.js"; |
|
+ |
|
+/** |
|
+ * Anthropic provider leaf. Same shape as `OpenAILLMSession` — different events, identical wiring. Real HTTP |
|
+ * integration is `@effect/ai-anthropic` (TODO). |
|
+ */ |
|
+export class AnthropicLLMSession extends Effect.Service<AnthropicLLMSession>()("AnthropicLLMSession", { |
|
+ scoped: Effect.gen(function* () { |
|
+ const bus = yield* Bus; |
|
+ const state = yield* Ref.make<ProviderConnectionState>(initialConnectionState()); |
|
+ |
|
+ const on_AnthropicLLMSessionConnect = (event: AnthropicLLMSessionConnectEvent) => |
|
+ Effect.gen(function* () { |
|
+ yield* applyConnectionFields(state, event); |
|
+ const llm_session_id = event.llm_session_id ?? `llm-anthropic-${bus.id}`; |
|
+ return yield* announceConnect(bus, state, "anthropic", llm_session_id); |
|
+ }); |
|
+ |
|
+ const on_AnthropicLLMSessionUpdate = (event: AnthropicLLMSessionUpdateEvent) => |
|
+ Effect.gen(function* () { |
|
+ yield* applyConnectionFields(state, event); |
|
+ const llm_session_id = |
|
+ event.llm_session_id ?? (yield* Ref.get(state)).llm_session_id ?? `llm-anthropic-${bus.id}`; |
|
+ return yield* announceConnect(bus, state, "anthropic", llm_session_id); |
|
+ }); |
|
+ |
|
+ const on_AnthropicLLMSessionDisconnect = (event: AnthropicLLMSessionDisconnectEvent) => |
|
+ Effect.gen(function* () { |
|
+ yield* Ref.update(state, (s) => ({ ...s, connected: false })); |
|
+ return { llm_session_id: event.llm_session_id ?? "", disconnected: true }; |
|
+ }); |
|
+ |
|
+ const on_AnthropicLLMRequest = (event: AnthropicLLMRequestEvent) => |
|
+ stubProviderRequest(bus, "anthropic", { |
|
+ request_id: event.request_id, |
|
+ llm_session_id: event.llm_session_id, |
|
+ prompt: event.prompt, |
|
+ }); |
|
+ |
|
+ yield* bus.on(AnthropicLLMSessionConnectEvent, on_AnthropicLLMSessionConnect, { |
|
+ handler_name: "AnthropicLLMSession.on_AnthropicLLMSessionConnect", |
|
+ }); |
|
+ yield* bus.on(AnthropicLLMSessionUpdateEvent, on_AnthropicLLMSessionUpdate, { |
|
+ handler_name: "AnthropicLLMSession.on_AnthropicLLMSessionUpdate", |
|
+ }); |
|
+ yield* bus.on(AnthropicLLMSessionDisconnectEvent, on_AnthropicLLMSessionDisconnect, { |
|
+ handler_name: "AnthropicLLMSession.on_AnthropicLLMSessionDisconnect", |
|
+ }); |
|
+ yield* bus.on(AnthropicLLMRequestEvent, on_AnthropicLLMRequest, { |
|
+ handler_name: "AnthropicLLMSession.on_AnthropicLLMRequest", |
|
+ }); |
|
+ |
|
+ return { state }; |
|
+ }), |
|
+}) { |
|
+ static readonly LISTENS_TO = [ |
|
+ AnthropicLLMSessionConnectEvent, |
|
+ AnthropicLLMSessionUpdateEvent, |
|
+ AnthropicLLMSessionDisconnectEvent, |
|
+ AnthropicLLMRequestEvent, |
|
+ ] as const; |
|
+} |
|
diff --git a/effect/llm/BaseProviderLLMSession.ts b/effect/llm/BaseProviderLLMSession.ts |
|
new file mode 100644 |
|
index 0000000..b048825 |
|
--- /dev/null |
|
+++ b/effect/llm/BaseProviderLLMSession.ts |
|
@@ -0,0 +1,111 @@ |
|
+import { Effect, Ref } from "effect"; |
|
+ |
|
+import { Bus, type BusService } from "../protocol/Bus.js"; |
|
+import { |
|
+ LLMErrorEvent, |
|
+ LLMResponseEvent, |
|
+ LLMSessionConnectedEvent, |
|
+ LLMSessionUpdatedEvent, |
|
+ type LLMProvider, |
|
+} from "../protocol/events.js"; |
|
+ |
|
+/** |
|
+ * Shared provider-leaf state and helper effects. Each provider session (`OpenAILLMSession`, `AnthropicLLMSession`, ...) |
|
+ * maintains its own credentials and live client handle, but the wiring of "store the connection fields, emit |
|
+ * `LLMSessionConnectedEvent` / `LLMSessionUpdatedEvent`, run a request and emit `LLMResponseEvent` / `LLMErrorEvent`" |
|
+ * is identical. This module factors that out. |
|
+ * |
|
+ * The actual provider HTTP calls are stubbed (`@effect/ai-openai`, `@effect/ai-anthropic`, etc., are not yet wired in |
|
+ * — the placeholder request emits a deterministic `LLMResponseEvent` so request-id-keyed consumers complete). |
|
+ */ |
|
+export interface ProviderConnectionState { |
|
+ readonly llm_session_id: string | null; |
|
+ readonly modelName: string | null; |
|
+ readonly apiKey: string | null; |
|
+ readonly baseUrl: string | null; |
|
+ readonly headers: Readonly<Record<string, string>> | null; |
|
+ readonly connected: boolean; |
|
+} |
|
+ |
|
+export const initialConnectionState = (): ProviderConnectionState => ({ |
|
+ llm_session_id: null, |
|
+ modelName: null, |
|
+ apiKey: null, |
|
+ baseUrl: null, |
|
+ headers: null, |
|
+ connected: false, |
|
+}); |
|
+ |
|
+export const applyConnectionFields = < |
|
+ T extends { |
|
+ llm_session_id?: string; |
|
+ modelName?: string; |
|
+ apiKey?: string; |
|
+ baseUrl?: string; |
|
+ headers?: Readonly<Record<string, string>>; |
|
+ }, |
|
+>( |
|
+ state: Ref.Ref<ProviderConnectionState>, |
|
+ event: T, |
|
+) => |
|
+ Ref.update(state, (s) => ({ |
|
+ ...s, |
|
+ llm_session_id: event.llm_session_id ?? s.llm_session_id, |
|
+ modelName: event.modelName ?? s.modelName, |
|
+ apiKey: event.apiKey ?? s.apiKey, |
|
+ baseUrl: event.baseUrl ?? s.baseUrl, |
|
+ headers: event.headers ?? s.headers, |
|
+ })); |
|
+ |
|
+/** Emits LLMSessionConnectedEvent + LLMSessionUpdatedEvent and flips the leaf's `connected` flag. */ |
|
+export const announceConnect = ( |
|
+ bus: BusService, |
|
+ state: Ref.Ref<ProviderConnectionState>, |
|
+ provider: LLMProvider, |
|
+ llm_session_id: string, |
|
+) => |
|
+ Effect.gen(function* () { |
|
+ yield* Ref.update(state, (s) => ({ ...s, connected: true, llm_session_id })); |
|
+ const modelName = (yield* Ref.get(state)).modelName ?? undefined; |
|
+ yield* bus.emit(LLMSessionConnectedEvent({ llm_session_id, provider, modelName })); |
|
+ yield* bus.emit(LLMSessionUpdatedEvent({ llm_session_id, provider, modelName })); |
|
+ return { llm_session_id, provider, modelName, status: "connected" as const }; |
|
+ }); |
|
+ |
|
+/** Emits a placeholder LLMResponseEvent. Real provider call goes here. */ |
|
+export const stubProviderRequest = ( |
|
+ bus: BusService, |
|
+ provider: LLMProvider, |
|
+ request: { request_id: string; llm_session_id?: string; prompt: string }, |
|
+) => |
|
+ Effect.gen(function* () { |
|
+ // TODO: replace with @effect/ai-{provider}: build LanguageModel from state.apiKey/baseUrl/modelName, run prompt, |
|
+ // capture output + tool_calls + raw, emit a fully-shaped LLMResponseEvent. |
|
+ const output = { |
|
+ provider, |
|
+ stub: true, |
|
+ message: `TODO: port ${provider} request body (prompt: ${request.prompt.slice(0, 64)}...)`, |
|
+ }; |
|
+ yield* bus.emit( |
|
+ LLMResponseEvent({ |
|
+ request_id: request.request_id, |
|
+ llm_session_id: request.llm_session_id, |
|
+ output, |
|
+ raw: null, |
|
+ }), |
|
+ ); |
|
+ return { request_id: request.request_id, llm_session_id: request.llm_session_id, output }; |
|
+ }).pipe( |
|
+ Effect.catchAll((err: unknown) => |
|
+ bus |
|
+ .emit( |
|
+ LLMErrorEvent({ |
|
+ request_id: request.request_id, |
|
+ message: err instanceof Error ? err.message : String(err), |
|
+ }), |
|
+ ) |
|
+ .pipe(Effect.as({ request_id: request.request_id, output: null })), |
|
+ ), |
|
+ ); |
|
+ |
|
+export { Bus }; |
|
diff --git a/effect/llm/GoogleLLMSession.ts b/effect/llm/GoogleLLMSession.ts |
|
new file mode 100644 |
|
index 0000000..b2ce2be |
|
--- /dev/null |
|
+++ b/effect/llm/GoogleLLMSession.ts |
|
@@ -0,0 +1,75 @@ |
|
+import { Effect, Ref } from "effect"; |
|
+ |
|
+import { Bus } from "../protocol/Bus.js"; |
|
+import { |
|
+ GoogleLLMRequestEvent, |
|
+ GoogleLLMSessionConnectEvent, |
|
+ GoogleLLMSessionDisconnectEvent, |
|
+ GoogleLLMSessionUpdateEvent, |
|
+} from "../protocol/events.js"; |
|
+import { |
|
+ announceConnect, |
|
+ applyConnectionFields, |
|
+ initialConnectionState, |
|
+ stubProviderRequest, |
|
+ type ProviderConnectionState, |
|
+} from "./BaseProviderLLMSession.js"; |
|
+ |
|
+/** |
|
+ * Google provider leaf. Real HTTP integration is `@effect/ai-google` (TODO). |
|
+ */ |
|
+export class GoogleLLMSession extends Effect.Service<GoogleLLMSession>()("GoogleLLMSession", { |
|
+ scoped: Effect.gen(function* () { |
|
+ const bus = yield* Bus; |
|
+ const state = yield* Ref.make<ProviderConnectionState>(initialConnectionState()); |
|
+ |
|
+ const on_GoogleLLMSessionConnect = (event: GoogleLLMSessionConnectEvent) => |
|
+ Effect.gen(function* () { |
|
+ yield* applyConnectionFields(state, event); |
|
+ const llm_session_id = event.llm_session_id ?? `llm-google-${bus.id}`; |
|
+ return yield* announceConnect(bus, state, "google", llm_session_id); |
|
+ }); |
|
+ |
|
+ const on_GoogleLLMSessionUpdate = (event: GoogleLLMSessionUpdateEvent) => |
|
+ Effect.gen(function* () { |
|
+ yield* applyConnectionFields(state, event); |
|
+ const llm_session_id = event.llm_session_id ?? (yield* Ref.get(state)).llm_session_id ?? `llm-google-${bus.id}`; |
|
+ return yield* announceConnect(bus, state, "google", llm_session_id); |
|
+ }); |
|
+ |
|
+ const on_GoogleLLMSessionDisconnect = (event: GoogleLLMSessionDisconnectEvent) => |
|
+ Effect.gen(function* () { |
|
+ yield* Ref.update(state, (s) => ({ ...s, connected: false })); |
|
+ return { llm_session_id: event.llm_session_id ?? "", disconnected: true }; |
|
+ }); |
|
+ |
|
+ const on_GoogleLLMRequest = (event: GoogleLLMRequestEvent) => |
|
+ stubProviderRequest(bus, "google", { |
|
+ request_id: event.request_id, |
|
+ llm_session_id: event.llm_session_id, |
|
+ prompt: event.prompt, |
|
+ }); |
|
+ |
|
+ yield* bus.on(GoogleLLMSessionConnectEvent, on_GoogleLLMSessionConnect, { |
|
+ handler_name: "GoogleLLMSession.on_GoogleLLMSessionConnect", |
|
+ }); |
|
+ yield* bus.on(GoogleLLMSessionUpdateEvent, on_GoogleLLMSessionUpdate, { |
|
+ handler_name: "GoogleLLMSession.on_GoogleLLMSessionUpdate", |
|
+ }); |
|
+ yield* bus.on(GoogleLLMSessionDisconnectEvent, on_GoogleLLMSessionDisconnect, { |
|
+ handler_name: "GoogleLLMSession.on_GoogleLLMSessionDisconnect", |
|
+ }); |
|
+ yield* bus.on(GoogleLLMRequestEvent, on_GoogleLLMRequest, { |
|
+ handler_name: "GoogleLLMSession.on_GoogleLLMRequest", |
|
+ }); |
|
+ |
|
+ return { state }; |
|
+ }), |
|
+}) { |
|
+ static readonly LISTENS_TO = [ |
|
+ GoogleLLMSessionConnectEvent, |
|
+ GoogleLLMSessionUpdateEvent, |
|
+ GoogleLLMSessionDisconnectEvent, |
|
+ GoogleLLMRequestEvent, |
|
+ ] as const; |
|
+} |
|
diff --git a/effect/llm/LLMRouterLayer.ts b/effect/llm/LLMRouterLayer.ts |
|
index 2fbfab2..aac67e5 100644 |
|
--- a/effect/llm/LLMRouterLayer.ts |
|
+++ b/effect/llm/LLMRouterLayer.ts |
|
@@ -2,89 +2,237 @@ import { Effect, Ref } from "effect"; |
|
|
|
import { Bus } from "../protocol/Bus.js"; |
|
import { |
|
+ AnthropicLLMRequestEvent, |
|
+ AnthropicLLMSessionConnectEvent, |
|
+ AnthropicLLMSessionDisconnectEvent, |
|
+ AnthropicLLMSessionUpdateEvent, |
|
+ GoogleLLMRequestEvent, |
|
+ GoogleLLMSessionConnectEvent, |
|
+ GoogleLLMSessionDisconnectEvent, |
|
+ GoogleLLMSessionUpdateEvent, |
|
LLMRequestEvent, |
|
+ LLMSessionConfigRequestEvent, |
|
+ LLMSessionConfigUpdateEvent, |
|
LLMSessionConnectEvent, |
|
+ LLMSessionDisconnectEvent, |
|
LLMSessionGetOrCreateEvent, |
|
+ NoopLLMRequestEvent, |
|
+ NoopLLMSessionConnectEvent, |
|
+ NoopLLMSessionDisconnectEvent, |
|
+ NoopLLMSessionUpdateEvent, |
|
OpenAILLMRequestEvent, |
|
+ OpenAILLMSessionConnectEvent, |
|
+ OpenAILLMSessionDisconnectEvent, |
|
+ OpenAILLMSessionUpdateEvent, |
|
+ type LLMProvider, |
|
} from "../protocol/events.js"; |
|
|
|
/** |
|
- * Generic LLM router. Owns LLM session identity, listens to provider-agnostic LLM events, and routes them to |
|
- * provider-specific events based on the resolved provider. Must not know about Browser, CDP, StagehandSession, |
|
- * Browserbase, or agent semantics. |
|
+ * Generic LLM router. Owns LLM session identity and routes generic LLM/session events to provider-specific events |
|
+ * based on the resolved provider. Must not know about Browser, CDP, StagehandSession, Browserbase, or agent semantics. |
|
* |
|
* Listens to: |
|
- * - `LLMSessionGetOrCreateEvent` — returns this router's LLM summary. |
|
- * - `LLMSessionConnectEvent` — forwarded as-is for the chosen provider's leaf to consume. |
|
- * - `LLMRequestEvent` — translated into `OpenAILLMRequestEvent` (single-provider scope). |
|
+ * - `LLMSessionGetOrCreateEvent` → returns the current LLM summary |
|
+ * - `LLMSessionConfigRequestEvent` → returns the current config (alias for GetOrCreate today) |
|
+ * - `LLMSessionConfigUpdateEvent` → forwards as `<Provider>LLMSessionUpdateEvent` |
|
+ * - `LLMSessionConnectEvent` → forwards as `<Provider>LLMSessionConnectEvent` |
|
+ * - `LLMSessionDisconnectEvent` → forwards as `<Provider>LLMSessionDisconnectEvent` |
|
+ * - `LLMRequestEvent` → forwards as `<Provider>LLMRequestEvent` |
|
* |
|
- * Emits: |
|
- * - `OpenAILLMRequestEvent` — the only currently-supported provider request shape. |
|
- * |
|
- * State (Refs): |
|
- * - `id` — LLM session uuid (separate from the bus session id; matches the abxbus router's `id` field) |
|
- * - `provider` — "openai" until other providers are added |
|
- * - `modelName` — last connected model name |
|
- * |
|
- * TODO: extend with Anthropic / Google / Noop once their provider sessions are ported. |
|
+ * Provider resolution: prefers explicit `event.provider`, falls back to `splitLLMModel(event.model)`, falls back to the |
|
+ * router's last-known `provider` Ref (set by previous connects/updates). Defaults to "openai" if nothing is configured. |
|
*/ |
|
+const splitLLMModel = (model: string): { provider: LLMProvider | null; modelName: string } | null => { |
|
+ const slash = model.indexOf("/"); |
|
+ if (slash <= 0 || slash === model.length - 1) return null; |
|
+ const provider = model.slice(0, slash) as LLMProvider; |
|
+ return { provider, modelName: model.slice(slash + 1) }; |
|
+}; |
|
+ |
|
+const resolveProvider = ( |
|
+ event: { provider?: string; model?: string; options?: Readonly<Record<string, unknown>> | undefined }, |
|
+ fallback: LLMProvider | null, |
|
+): LLMProvider => { |
|
+ if (event.provider != null) return event.provider as LLMProvider; |
|
+ if (event.model != null) { |
|
+ const split = splitLLMModel(event.model); |
|
+ if (split?.provider != null) return split.provider; |
|
+ } |
|
+ const optionsModel = event.options != null ? (event.options as Record<string, unknown>)["model"] : undefined; |
|
+ if (typeof optionsModel === "string") { |
|
+ const split = splitLLMModel(optionsModel); |
|
+ if (split?.provider != null) return split.provider; |
|
+ } |
|
+ return fallback ?? "openai"; |
|
+}; |
|
+ |
|
export class LLMRouterLayer extends Effect.Service<LLMRouterLayer>()("LLMRouterLayer", { |
|
scoped: Effect.gen(function* () { |
|
const bus = yield* Bus; |
|
const id = yield* Ref.make<string>(`llm-${bus.id}`); |
|
- const provider = yield* Ref.make<"openai">("openai"); |
|
+ const provider = yield* Ref.make<LLMProvider | null>(null); |
|
const modelName = yield* Ref.make<string | null>(null); |
|
|
|
+ const sessionSummary = () => |
|
+ Effect.gen(function* () { |
|
+ return { |
|
+ llm_session_id: yield* Ref.get(id), |
|
+ provider: (yield* Ref.get(provider)) ?? undefined, |
|
+ modelName: (yield* Ref.get(modelName)) ?? undefined, |
|
+ status: "connected" as const, |
|
+ }; |
|
+ }); |
|
+ |
|
const on_LLMSessionGetOrCreate = (event: LLMSessionGetOrCreateEvent) => |
|
Effect.gen(function* () { |
|
const currentId = yield* Ref.get(id); |
|
if (event.llm_session_id != null && event.llm_session_id !== currentId) return undefined; |
|
- return { |
|
- llm_session_id: currentId, |
|
- provider: yield* Ref.get(provider), |
|
- modelName: yield* Ref.get(modelName), |
|
- }; |
|
+ return yield* sessionSummary(); |
|
+ }); |
|
+ |
|
+ const on_LLMSessionConfigRequest = (event: LLMSessionConfigRequestEvent) => |
|
+ Effect.gen(function* () { |
|
+ const currentId = yield* Ref.get(id); |
|
+ if (event.llm_session_id != null && event.llm_session_id !== currentId) return undefined; |
|
+ return yield* sessionSummary(); |
|
}); |
|
|
|
+ const dispatchConnectFor = ( |
|
+ target: LLMProvider, |
|
+ event: LLMSessionConnectEvent | LLMSessionConfigUpdateEvent, |
|
+ mode: "connect" | "update", |
|
+ ) => { |
|
+ const payload = { |
|
+ llm_session_id: event.llm_session_id, |
|
+ model: event.model, |
|
+ provider: event.provider, |
|
+ modelName: event.modelName, |
|
+ apiKey: event.apiKey, |
|
+ apiUrl: event.apiUrl, |
|
+ baseUrl: event.baseUrl, |
|
+ headers: event.headers, |
|
+ authContext: event.authContext, |
|
+ options: event.options, |
|
+ systemPrompt: event.systemPrompt, |
|
+ }; |
|
+ if (target === "openai") { |
|
+ return mode === "connect" |
|
+ ? bus.emit(OpenAILLMSessionConnectEvent(payload)) |
|
+ : bus.emit(OpenAILLMSessionUpdateEvent(payload)); |
|
+ } |
|
+ if (target === "anthropic") { |
|
+ return mode === "connect" |
|
+ ? bus.emit(AnthropicLLMSessionConnectEvent(payload)) |
|
+ : bus.emit(AnthropicLLMSessionUpdateEvent(payload)); |
|
+ } |
|
+ if (target === "google") { |
|
+ return mode === "connect" |
|
+ ? bus.emit(GoogleLLMSessionConnectEvent(payload)) |
|
+ : bus.emit(GoogleLLMSessionUpdateEvent(payload)); |
|
+ } |
|
+ // Fallback to the noop leaf when an unsupported provider is requested. |
|
+ return mode === "connect" |
|
+ ? bus.emit(NoopLLMSessionConnectEvent(payload)) |
|
+ : bus.emit(NoopLLMSessionUpdateEvent(payload)); |
|
+ }; |
|
+ |
|
const on_LLMSessionConnect = (event: LLMSessionConnectEvent) => |
|
Effect.gen(function* () { |
|
+ const fallback = yield* Ref.get(provider); |
|
+ const target = resolveProvider(event, fallback); |
|
+ yield* Ref.set(provider, target); |
|
+ if (event.modelName != null) yield* Ref.set(modelName, event.modelName); |
|
+ return yield* dispatchConnectFor(target, event, "connect"); |
|
+ }); |
|
+ |
|
+ const on_LLMSessionConfigUpdate = (event: LLMSessionConfigUpdateEvent) => |
|
+ Effect.gen(function* () { |
|
+ const fallback = yield* Ref.get(provider); |
|
+ const target = resolveProvider(event, fallback); |
|
+ yield* Ref.set(provider, target); |
|
if (event.modelName != null) yield* Ref.set(modelName, event.modelName); |
|
- // Provider leaf (`OpenAILLMSession`) also listens to `LLMSessionConnectEvent`; the router does not need to |
|
- // re-emit a provider-specific connect event in single-provider scope. When more providers come back, route |
|
- // via OpenAILLMSessionConnectEvent / AnthropicLLMSessionConnectEvent / etc. |
|
- return { llm_session_id: event.llm_session_id ?? (yield* Ref.get(id)) }; |
|
+ return yield* dispatchConnectFor(target, event, "update"); |
|
+ }); |
|
+ |
|
+ const on_LLMSessionDisconnect = (event: LLMSessionDisconnectEvent) => |
|
+ Effect.gen(function* () { |
|
+ const target = (yield* Ref.get(provider)) ?? "openai"; |
|
+ const payload = { llm_session_id: event.llm_session_id }; |
|
+ if (target === "openai") return yield* bus.emit(OpenAILLMSessionDisconnectEvent(payload)); |
|
+ if (target === "anthropic") return yield* bus.emit(AnthropicLLMSessionDisconnectEvent(payload)); |
|
+ if (target === "google") return yield* bus.emit(GoogleLLMSessionDisconnectEvent(payload)); |
|
+ return yield* bus.emit(NoopLLMSessionDisconnectEvent(payload)); |
|
}); |
|
|
|
const on_LLMRequest = (event: LLMRequestEvent) => |
|
Effect.gen(function* () { |
|
- const currentId = yield* Ref.get(id); |
|
- if (event.llm_session_id != null && event.llm_session_id !== currentId) return undefined; |
|
- const llm_session_id = event.llm_session_id ?? currentId; |
|
- // Single-provider routing: forward as `OpenAILLMRequestEvent` regardless of payload `provider` hints. |
|
- // TODO: branch on event.options.provider / event.options.model once multi-provider routing is back. |
|
- return yield* bus.emit( |
|
- OpenAILLMRequestEvent({ |
|
- request_id: event.request_id, |
|
- llm_session_id, |
|
- operation_name: event.operation_name, |
|
- prompt: event.prompt, |
|
- attachments: event.attachments, |
|
- options: event.options, |
|
- }), |
|
- ); |
|
+ const fallback = yield* Ref.get(provider); |
|
+ const target = resolveProvider({ provider: undefined, model: event.model, options: event.options }, fallback); |
|
+ const payload = { |
|
+ request_id: event.request_id, |
|
+ llm_session_id: event.llm_session_id ?? (yield* Ref.get(id)), |
|
+ operation_name: event.operation_name, |
|
+ parent_request_id: event.parent_request_id, |
|
+ prompt: event.prompt, |
|
+ attachments: event.attachments, |
|
+ options: event.options, |
|
+ messages: event.messages, |
|
+ tool_results: event.tool_results, |
|
+ provider_conversation: event.provider_conversation, |
|
+ expected_response_schema: event.expected_response_schema, |
|
+ model: event.model, |
|
+ }; |
|
+ if (target === "openai") return yield* bus.emit(OpenAILLMRequestEvent(payload)); |
|
+ if (target === "anthropic") return yield* bus.emit(AnthropicLLMRequestEvent(payload)); |
|
+ if (target === "google") return yield* bus.emit(GoogleLLMRequestEvent(payload)); |
|
+ return yield* bus.emit(NoopLLMRequestEvent(payload)); |
|
}); |
|
|
|
yield* bus.on(LLMSessionGetOrCreateEvent, on_LLMSessionGetOrCreate, { |
|
handler_name: "LLMRouterLayer.on_LLMSessionGetOrCreate", |
|
}); |
|
+ yield* bus.on(LLMSessionConfigRequestEvent, on_LLMSessionConfigRequest, { |
|
+ handler_name: "LLMRouterLayer.on_LLMSessionConfigRequest", |
|
+ }); |
|
yield* bus.on(LLMSessionConnectEvent, on_LLMSessionConnect, { |
|
handler_name: "LLMRouterLayer.on_LLMSessionConnect", |
|
}); |
|
+ yield* bus.on(LLMSessionConfigUpdateEvent, on_LLMSessionConfigUpdate, { |
|
+ handler_name: "LLMRouterLayer.on_LLMSessionConfigUpdate", |
|
+ }); |
|
+ yield* bus.on(LLMSessionDisconnectEvent, on_LLMSessionDisconnect, { |
|
+ handler_name: "LLMRouterLayer.on_LLMSessionDisconnect", |
|
+ }); |
|
yield* bus.on(LLMRequestEvent, on_LLMRequest, { handler_name: "LLMRouterLayer.on_LLMRequest" }); |
|
|
|
return { id, provider, modelName }; |
|
}), |
|
}) { |
|
- static readonly LISTENS_TO = [LLMSessionGetOrCreateEvent, LLMSessionConnectEvent, LLMRequestEvent] as const; |
|
- static readonly EMITS = [OpenAILLMRequestEvent] as const; |
|
+ static readonly LISTENS_TO = [ |
|
+ LLMSessionGetOrCreateEvent, |
|
+ LLMSessionConfigRequestEvent, |
|
+ LLMSessionConnectEvent, |
|
+ LLMSessionConfigUpdateEvent, |
|
+ LLMSessionDisconnectEvent, |
|
+ LLMRequestEvent, |
|
+ ] as const; |
|
+ |
|
+ static readonly EMITS = [ |
|
+ OpenAILLMSessionConnectEvent, |
|
+ OpenAILLMSessionUpdateEvent, |
|
+ OpenAILLMSessionDisconnectEvent, |
|
+ OpenAILLMRequestEvent, |
|
+ AnthropicLLMSessionConnectEvent, |
|
+ AnthropicLLMSessionUpdateEvent, |
|
+ AnthropicLLMSessionDisconnectEvent, |
|
+ AnthropicLLMRequestEvent, |
|
+ GoogleLLMSessionConnectEvent, |
|
+ GoogleLLMSessionUpdateEvent, |
|
+ GoogleLLMSessionDisconnectEvent, |
|
+ GoogleLLMRequestEvent, |
|
+ NoopLLMSessionConnectEvent, |
|
+ NoopLLMSessionUpdateEvent, |
|
+ NoopLLMSessionDisconnectEvent, |
|
+ NoopLLMRequestEvent, |
|
+ ] as const; |
|
} |
|
diff --git a/effect/llm/NoopLLMSession.ts b/effect/llm/NoopLLMSession.ts |
|
new file mode 100644 |
|
index 0000000..84c987f |
|
--- /dev/null |
|
+++ b/effect/llm/NoopLLMSession.ts |
|
@@ -0,0 +1,82 @@ |
|
+import { Effect, Ref } from "effect"; |
|
+ |
|
+import { Bus } from "../protocol/Bus.js"; |
|
+import { |
|
+ LLMResponseEvent, |
|
+ NoopLLMRequestEvent, |
|
+ NoopLLMSessionConnectEvent, |
|
+ NoopLLMSessionDisconnectEvent, |
|
+ NoopLLMSessionUpdateEvent, |
|
+} from "../protocol/events.js"; |
|
+import { |
|
+ announceConnect, |
|
+ applyConnectionFields, |
|
+ initialConnectionState, |
|
+ type ProviderConnectionState, |
|
+} from "./BaseProviderLLMSession.js"; |
|
+ |
|
+/** |
|
+ * No-op provider leaf. Used for tests and custom flows where the LLM should not call any external service. |
|
+ * Always emits a deterministic `LLMResponseEvent` (no provider HTTP call). |
|
+ */ |
|
+export class NoopLLMSession extends Effect.Service<NoopLLMSession>()("NoopLLMSession", { |
|
+ scoped: Effect.gen(function* () { |
|
+ const bus = yield* Bus; |
|
+ const state = yield* Ref.make<ProviderConnectionState>(initialConnectionState()); |
|
+ |
|
+ const on_NoopLLMSessionConnect = (event: NoopLLMSessionConnectEvent) => |
|
+ Effect.gen(function* () { |
|
+ yield* applyConnectionFields(state, event); |
|
+ const llm_session_id = event.llm_session_id ?? `llm-noop-${bus.id}`; |
|
+ // Noop bypasses real provider semantics; emit connect-shape event for parity but use the placeholder type tag. |
|
+ return yield* announceConnect(bus, state, "openai", llm_session_id); |
|
+ }); |
|
+ |
|
+ const on_NoopLLMSessionUpdate = (event: NoopLLMSessionUpdateEvent) => |
|
+ Effect.gen(function* () { |
|
+ yield* applyConnectionFields(state, event); |
|
+ const llm_session_id = event.llm_session_id ?? (yield* Ref.get(state)).llm_session_id ?? `llm-noop-${bus.id}`; |
|
+ return yield* announceConnect(bus, state, "openai", llm_session_id); |
|
+ }); |
|
+ |
|
+ const on_NoopLLMSessionDisconnect = (event: NoopLLMSessionDisconnectEvent) => |
|
+ Effect.gen(function* () { |
|
+ yield* Ref.update(state, (s) => ({ ...s, connected: false })); |
|
+ return { llm_session_id: event.llm_session_id ?? "", disconnected: true }; |
|
+ }); |
|
+ |
|
+ const on_NoopLLMRequest = (event: NoopLLMRequestEvent) => |
|
+ Effect.gen(function* () { |
|
+ const output = { noop: true, prompt: event.prompt }; |
|
+ yield* bus.emit( |
|
+ LLMResponseEvent({ |
|
+ request_id: event.request_id, |
|
+ llm_session_id: event.llm_session_id, |
|
+ output, |
|
+ raw: null, |
|
+ }), |
|
+ ); |
|
+ return { request_id: event.request_id, output }; |
|
+ }); |
|
+ |
|
+ yield* bus.on(NoopLLMSessionConnectEvent, on_NoopLLMSessionConnect, { |
|
+ handler_name: "NoopLLMSession.on_NoopLLMSessionConnect", |
|
+ }); |
|
+ yield* bus.on(NoopLLMSessionUpdateEvent, on_NoopLLMSessionUpdate, { |
|
+ handler_name: "NoopLLMSession.on_NoopLLMSessionUpdate", |
|
+ }); |
|
+ yield* bus.on(NoopLLMSessionDisconnectEvent, on_NoopLLMSessionDisconnect, { |
|
+ handler_name: "NoopLLMSession.on_NoopLLMSessionDisconnect", |
|
+ }); |
|
+ yield* bus.on(NoopLLMRequestEvent, on_NoopLLMRequest, { handler_name: "NoopLLMSession.on_NoopLLMRequest" }); |
|
+ |
|
+ return { state }; |
|
+ }), |
|
+}) { |
|
+ static readonly LISTENS_TO = [ |
|
+ NoopLLMSessionConnectEvent, |
|
+ NoopLLMSessionUpdateEvent, |
|
+ NoopLLMSessionDisconnectEvent, |
|
+ NoopLLMRequestEvent, |
|
+ ] as const; |
|
+} |
|
diff --git a/effect/llm/OpenAILLMSession.ts b/effect/llm/OpenAILLMSession.ts |
|
index 66fd46c..c2346e7 100644 |
|
--- a/effect/llm/OpenAILLMSession.ts |
|
+++ b/effect/llm/OpenAILLMSession.ts |
|
@@ -1,82 +1,77 @@ |
|
import { Effect, Ref } from "effect"; |
|
|
|
import { Bus } from "../protocol/Bus.js"; |
|
-import { LLMErrorEvent, LLMResponseEvent, LLMSessionConnectEvent, OpenAILLMRequestEvent } from "../protocol/events.js"; |
|
+import { |
|
+ OpenAILLMRequestEvent, |
|
+ OpenAILLMSessionConnectEvent, |
|
+ OpenAILLMSessionDisconnectEvent, |
|
+ OpenAILLMSessionUpdateEvent, |
|
+} from "../protocol/events.js"; |
|
+import { |
|
+ announceConnect, |
|
+ applyConnectionFields, |
|
+ initialConnectionState, |
|
+ stubProviderRequest, |
|
+ type ProviderConnectionState, |
|
+} from "./BaseProviderLLMSession.js"; |
|
|
|
/** |
|
- * OpenAI provider leaf. Single-provider scope for the early Effect port — the abxbus version had Anthropic, Google, |
|
- * and Noop as parallel providers; those will be added later. |
|
- * |
|
- * Listens to: `OpenAILLMRequestEvent`, `LLMSessionConnectEvent` (for OpenAI-targeted connects). |
|
- * Emits: `LLMResponseEvent`, `LLMErrorEvent`. |
|
- * |
|
- * State (Refs): |
|
- * - `apiKey` / `baseUrl` / `modelName` — captured on connect |
|
- * - `client` — the live OpenAI client handle (TODO: use `@effect/ai-openai` once events.ts wiring stabilizes) |
|
- * |
|
- * TODO: switch to `@effect/ai-openai` for the real generate call. For now, the request handler emits a placeholder |
|
- * response so downstream consumers (AgentSession.act/observe/extract, once ported) see the right shape. |
|
+ * OpenAI provider leaf. Listens to `OpenAILLMSession{Connect,Update,Disconnect}Event` and `OpenAILLMRequestEvent`, |
|
+ * emits `LLMSessionConnectedEvent` / `LLMSessionUpdatedEvent` / `LLMResponseEvent` / `LLMErrorEvent` via the shared |
|
+ * helpers in `BaseProviderLLMSession`. Real HTTP integration is `@effect/ai-openai` (TODO). |
|
*/ |
|
export class OpenAILLMSession extends Effect.Service<OpenAILLMSession>()("OpenAILLMSession", { |
|
scoped: Effect.gen(function* () { |
|
const bus = yield* Bus; |
|
- const apiKey = yield* Ref.make<string | null>(null); |
|
- const baseUrl = yield* Ref.make<string | null>(null); |
|
- const modelName = yield* Ref.make<string | null>(null); |
|
+ const state = yield* Ref.make<ProviderConnectionState>(initialConnectionState()); |
|
|
|
- const on_LLMSessionConnect = (event: LLMSessionConnectEvent) => |
|
+ const on_OpenAILLMSessionConnect = (event: OpenAILLMSessionConnectEvent) => |
|
Effect.gen(function* () { |
|
- if (event.provider != null && event.provider !== "openai") return undefined; |
|
- if (event.apiKey != null) yield* Ref.set(apiKey, event.apiKey); |
|
- if (event.baseUrl != null) yield* Ref.set(baseUrl, event.baseUrl); |
|
- if (event.modelName != null) yield* Ref.set(modelName, event.modelName); |
|
- return { llm_session_id: event.llm_session_id ?? null, provider: "openai" }; |
|
+ yield* applyConnectionFields(state, event); |
|
+ const llm_session_id = event.llm_session_id ?? `llm-openai-${bus.id}`; |
|
+ return yield* announceConnect(bus, state, "openai", llm_session_id); |
|
}); |
|
|
|
- const on_OpenAILLMRequest = (event: OpenAILLMRequestEvent) => |
|
+ const on_OpenAILLMSessionUpdate = (event: OpenAILLMSessionUpdateEvent) => |
|
Effect.gen(function* () { |
|
- // TODO: real call via @effect/ai-openai using `apiKey` / `baseUrl` / `modelName` from state and `event.prompt`. |
|
- // For now, emit a placeholder response so request-id-keyed consumers complete deterministically. |
|
- const result = yield* Effect.tryPromise({ |
|
- try: async () => ({ |
|
- output: { message: `TODO: port OpenAI request body (prompt: ${event.prompt.slice(0, 64)}...)` }, |
|
- }), |
|
- catch: (error) => (error instanceof Error ? error : new Error(String(error))), |
|
- }).pipe( |
|
- Effect.tap(() => |
|
- bus.emit( |
|
- LLMResponseEvent({ |
|
- request_id: event.request_id, |
|
- llm_session_id: event.llm_session_id, |
|
- output: { stub: true }, |
|
- raw: null, |
|
- }), |
|
- ), |
|
- ), |
|
- Effect.catchAll((error) => |
|
- bus |
|
- .emit( |
|
- LLMErrorEvent({ |
|
- request_id: event.request_id, |
|
- message: error instanceof Error ? error.message : String(error), |
|
- }), |
|
- ) |
|
- .pipe(Effect.as({ output: null })), |
|
- ), |
|
- ); |
|
- return result; |
|
+ yield* applyConnectionFields(state, event); |
|
+ const llm_session_id = event.llm_session_id ?? (yield* Ref.get(state)).llm_session_id ?? `llm-openai-${bus.id}`; |
|
+ return yield* announceConnect(bus, state, "openai", llm_session_id); |
|
}); |
|
|
|
- yield* bus.on(LLMSessionConnectEvent, on_LLMSessionConnect, { |
|
- handler_name: "OpenAILLMSession.on_LLMSessionConnect", |
|
+ const on_OpenAILLMSessionDisconnect = (event: OpenAILLMSessionDisconnectEvent) => |
|
+ Effect.gen(function* () { |
|
+ yield* Ref.update(state, (s) => ({ ...s, connected: false })); |
|
+ return { llm_session_id: event.llm_session_id ?? "", disconnected: true }; |
|
+ }); |
|
+ |
|
+ const on_OpenAILLMRequest = (event: OpenAILLMRequestEvent) => |
|
+ stubProviderRequest(bus, "openai", { |
|
+ request_id: event.request_id, |
|
+ llm_session_id: event.llm_session_id, |
|
+ prompt: event.prompt, |
|
+ }); |
|
+ |
|
+ yield* bus.on(OpenAILLMSessionConnectEvent, on_OpenAILLMSessionConnect, { |
|
+ handler_name: "OpenAILLMSession.on_OpenAILLMSessionConnect", |
|
+ }); |
|
+ yield* bus.on(OpenAILLMSessionUpdateEvent, on_OpenAILLMSessionUpdate, { |
|
+ handler_name: "OpenAILLMSession.on_OpenAILLMSessionUpdate", |
|
+ }); |
|
+ yield* bus.on(OpenAILLMSessionDisconnectEvent, on_OpenAILLMSessionDisconnect, { |
|
+ handler_name: "OpenAILLMSession.on_OpenAILLMSessionDisconnect", |
|
}); |
|
yield* bus.on(OpenAILLMRequestEvent, on_OpenAILLMRequest, { |
|
handler_name: "OpenAILLMSession.on_OpenAILLMRequest", |
|
}); |
|
|
|
- return { apiKey, baseUrl, modelName }; |
|
+ return { state }; |
|
}), |
|
}) { |
|
- static readonly LISTENS_TO = [LLMSessionConnectEvent, OpenAILLMRequestEvent] as const; |
|
- static readonly EMITS = [LLMResponseEvent, LLMErrorEvent] as const; |
|
+ static readonly LISTENS_TO = [ |
|
+ OpenAILLMSessionConnectEvent, |
|
+ OpenAILLMSessionUpdateEvent, |
|
+ OpenAILLMSessionDisconnectEvent, |
|
+ OpenAILLMRequestEvent, |
|
+ ] as const; |
|
} |
|
diff --git a/effect/protocol/events.ts b/effect/protocol/events.ts |
|
index 1201e0c..c07792c 100644 |
|
--- a/effect/protocol/events.ts |
|
+++ b/effect/protocol/events.ts |
|
@@ -3,38 +3,47 @@ import { Schema } from "effect"; |
|
import { defineEvent } from "./Event.js"; |
|
|
|
/** |
|
- * Greenfield event taxonomy for the Effect-native port. Scope: CDP, Browser, StagehandSession, and a single LLM |
|
- * provider's request/response. Other event families from the abxbus version (BBSdkClient, HumanRecorder/Replayer/ |
|
- * Teleport, ExtensionUI, AboutBlankLoading, multiple LLM providers) are intentionally omitted here and will be added |
|
- * once the smaller surface is stable. |
|
+ * Greenfield event taxonomy for the Effect-native port. Faithful round-trip of the abxbus `src/protocol/events.ts` |
|
+ * surfaces for: CDP transport, the full Browser command/fact/query family (page, tab, frame, viewport, headers, |
|
+ * downloads, snapshots, screenshots, evaluate, send-CDP, etc.), StagehandSession lifecycle, and the full LLM stack |
|
+ * (router + OpenAI/Anthropic/Google/Noop providers). |
|
* |
|
- * Naming parity with the abxbus version is verbatim: every const ends in `Event`, the string `event_type` matches the |
|
- * const name, and the payload field names match the abxbus payloads where the same field semantics exist. |
|
+ * Naming parity is verbatim: every const ends in `Event`, the string `event_type` matches the const name, payload |
|
+ * field names match the abxbus payloads where the same field semantics exist, and the `llm_tool_name` annotation is |
|
+ * preserved on every Browser command event that participates in `AGENT_BROWSER_TOOL_EVENTS`. |
|
* |
|
- * TODO: round-trip 100% of the abxbus events.ts taxonomy. For now we ship the smallest set the early CDP/Browser/ |
|
- * Stagehand integration tests need. |
|
+ * Out of scope for now (will follow once Browser+LLM are live): BBSdkClient events, ExtensionUI/Smuggler events, |
|
+ * AboutBlankLoading rerender wiring, HumanRecorder/Replayer/Teleport, AgentSession workflow events. |
|
*/ |
|
|
|
// --------------------------------------------------------------------------- |
|
-// Shared schemas |
|
+// Shared payload-shape schemas |
|
// --------------------------------------------------------------------------- |
|
|
|
export const SelectorObjectSchema = Schema.Struct({ |
|
targetId: Schema.optional(Schema.NullOr(Schema.String)), |
|
+ tabId: Schema.optional(Schema.NullOr(Schema.Number)), |
|
pageIdx: Schema.optional(Schema.NullOr(Schema.Number)), |
|
url: Schema.optional(Schema.NullOr(Schema.String)), |
|
title: Schema.optional(Schema.NullOr(Schema.String)), |
|
active: Schema.optional(Schema.NullOr(Schema.Boolean)), |
|
frameId: Schema.optional(Schema.NullOr(Schema.String)), |
|
+ frameIdx: Schema.optional(Schema.NullOr(Schema.Number)), |
|
xpath: Schema.optional(Schema.NullOr(Schema.String)), |
|
css: Schema.optional(Schema.NullOr(Schema.String)), |
|
text: Schema.optional(Schema.NullOr(Schema.String)), |
|
+ reactElementName: Schema.optional(Schema.NullOr(Schema.String)), |
|
backendNodeId: Schema.optional(Schema.NullOr(Schema.Number)), |
|
+ idx: Schema.optional(Schema.NullOr(Schema.Number)), |
|
coordinates: Schema.optional( |
|
Schema.NullOr( |
|
Schema.Struct({ |
|
x: Schema.optional(Schema.NullOr(Schema.Number)), |
|
y: Schema.optional(Schema.NullOr(Schema.Number)), |
|
+ top: Schema.optional(Schema.NullOr(Schema.Number)), |
|
+ left: Schema.optional(Schema.NullOr(Schema.Number)), |
|
+ bottom: Schema.optional(Schema.NullOr(Schema.Number)), |
|
+ right: Schema.optional(Schema.NullOr(Schema.Number)), |
|
}), |
|
), |
|
), |
|
@@ -50,28 +59,322 @@ export const TargetInfoSchema = Schema.Struct({ |
|
}); |
|
export type TargetInfo = typeof TargetInfoSchema.Type; |
|
|
|
+export const BrowserViewportSchema = Schema.Struct({ |
|
+ width: Schema.Number, |
|
+ height: Schema.Number, |
|
+ deviceScaleFactor: Schema.optional(Schema.Number), |
|
+}); |
|
+export type BrowserViewport = typeof BrowserViewportSchema.Type; |
|
+ |
|
+export const BrowserCookieParamSchema = Schema.Struct({ |
|
+ name: Schema.String, |
|
+ value: Schema.String, |
|
+ url: Schema.optional(Schema.String), |
|
+ domain: Schema.optional(Schema.String), |
|
+ path: Schema.optional(Schema.String), |
|
+ expires: Schema.optional(Schema.Number), |
|
+ httpOnly: Schema.optional(Schema.Boolean), |
|
+ secure: Schema.optional(Schema.Boolean), |
|
+ sameSite: Schema.optional(Schema.Literal("Strict", "Lax", "None")), |
|
+}); |
|
+export type BrowserCookieParam = typeof BrowserCookieParamSchema.Type; |
|
+ |
|
+export const BrowserRegexSchema = Schema.Struct({ |
|
+ source: Schema.String, |
|
+ flags: Schema.optional(Schema.String), |
|
+}); |
|
+export const BrowserStringPatternSchema = Schema.Union(Schema.String, BrowserRegexSchema); |
|
+export type BrowserStringPattern = typeof BrowserStringPatternSchema.Type; |
|
+ |
|
+export const BrowserClearCookiesOptionsSchema = Schema.Struct({ |
|
+ name: Schema.optional(BrowserStringPatternSchema), |
|
+ domain: Schema.optional(BrowserStringPatternSchema), |
|
+ path: Schema.optional(BrowserStringPatternSchema), |
|
+}); |
|
+export type BrowserClearCookiesOptions = typeof BrowserClearCookiesOptionsSchema.Type; |
|
+ |
|
+export const BrowserPageLoadStateSchema = Schema.Literal( |
|
+ "init", |
|
+ "domcontentloaded", |
|
+ "load", |
|
+ "loaded", |
|
+ "networkalmostidle", |
|
+ "networkidle2", |
|
+ "networkidle", |
|
+); |
|
+export type BrowserPageLoadState = typeof BrowserPageLoadStateSchema.Type; |
|
+ |
|
+const RecordOfUnknown = Schema.Record({ key: Schema.String, value: Schema.Unknown }); |
|
+const RecordOfString = Schema.Record({ key: Schema.String, value: Schema.String }); |
|
+ |
|
+// --------------------------------------------------------------------------- |
|
+// Result-type schemas (Browser handler return shapes) |
|
+// --------------------------------------------------------------------------- |
|
+ |
|
+export const BrowserActionResultSchema = Schema.Struct({ |
|
+ selector: Schema.optional(SelectorObjectSchema), |
|
+ status: Schema.optional(Schema.String), |
|
+}); |
|
+export type BrowserActionResult = typeof BrowserActionResultSchema.Type; |
|
+ |
|
+export const BrowserNavigationResultSchema = Schema.Struct({ |
|
+ targetId: Schema.optional(Schema.String), |
|
+ url: Schema.optional(Schema.String), |
|
+ status: Schema.optional(Schema.String), |
|
+}); |
|
+export type BrowserNavigationResult = typeof BrowserNavigationResultSchema.Type; |
|
+ |
|
+export const BrowserPageGotoResultSchema = Schema.Struct({ |
|
+ selector: SelectorObjectSchema, |
|
+ url: Schema.String, |
|
+ status: Schema.optional(Schema.String), |
|
+}); |
|
+export type BrowserPageGotoResult = typeof BrowserPageGotoResultSchema.Type; |
|
+ |
|
+export const BrowserPageScreenshotResultSchema = Schema.Struct({ |
|
+ screenshot: Schema.String, |
|
+ selector: Schema.optional(SelectorObjectSchema), |
|
+}); |
|
+export type BrowserPageScreenshotResult = typeof BrowserPageScreenshotResultSchema.Type; |
|
+ |
|
+export const BrowserPageSnapshotResultSchema = Schema.Struct({ |
|
+ snapshot: Schema.Unknown, |
|
+ selector: Schema.optional(SelectorObjectSchema), |
|
+}); |
|
+export type BrowserPageSnapshotResult = typeof BrowserPageSnapshotResultSchema.Type; |
|
+ |
|
+export const BrowserPageDocumentSnapshotResultSchema = Schema.Struct({ |
|
+ snapshot: Schema.Unknown, |
|
+ selector: Schema.optional(SelectorObjectSchema), |
|
+}); |
|
+export type BrowserPageDocumentSnapshotResult = typeof BrowserPageDocumentSnapshotResultSchema.Type; |
|
+ |
|
+export const BrowserPageValueResultSchema = Schema.Struct({ |
|
+ value: Schema.Unknown, |
|
+}); |
|
+export type BrowserPageValueResult = typeof BrowserPageValueResultSchema.Type; |
|
+ |
|
+export const BrowserPageLoadWaitResultSchema = Schema.Struct({ |
|
+ state: Schema.String, |
|
+ targetId: Schema.optional(Schema.String), |
|
+}); |
|
+export type BrowserPageLoadWaitResult = typeof BrowserPageLoadWaitResultSchema.Type; |
|
+ |
|
+export const BrowserPageSelectorWaitResultSchema = Schema.Struct({ |
|
+ selector: SelectorObjectSchema, |
|
+ state: Schema.String, |
|
+}); |
|
+export type BrowserPageSelectorWaitResult = typeof BrowserPageSelectorWaitResultSchema.Type; |
|
+ |
|
+export const BrowserPageLocateResultSchema = Schema.Struct({ |
|
+ selector: SelectorObjectSchema, |
|
+}); |
|
+export type BrowserPageLocateResult = typeof BrowserPageLocateResultSchema.Type; |
|
+ |
|
+export const BrowserPageElementInfoResultSchema = Schema.Struct({ |
|
+ selector: SelectorObjectSchema, |
|
+ fields: Schema.optional(RecordOfUnknown), |
|
+}); |
|
+export type BrowserPageElementInfoResult = typeof BrowserPageElementInfoResultSchema.Type; |
|
+ |
|
+export const BrowserSummarySchema = Schema.Struct({ |
|
+ selector: SelectorObjectSchema, |
|
+ url: Schema.optional(Schema.String), |
|
+ pageText: Schema.optional(Schema.String), |
|
+ observationTree: Schema.optional(Schema.String), |
|
+ elementSelectorMap: Schema.optional(Schema.Record({ key: Schema.String, value: SelectorObjectSchema })), |
|
+ screenshot: Schema.optional(Schema.String), |
|
+}); |
|
+export type BrowserSummary = typeof BrowserSummarySchema.Type; |
|
+ |
|
+export const BrowserConnectedResultSchema = Schema.Struct({ |
|
+ browser_id: Schema.optional(Schema.String), |
|
+ cdpUrl: Schema.optional(Schema.String), |
|
+}); |
|
+export type BrowserConnectedResult = typeof BrowserConnectedResultSchema.Type; |
|
+ |
|
+export const BrowserLaunchResultSchema = Schema.Struct({ |
|
+ browser_id: Schema.optional(Schema.String), |
|
+ cdpUrl: Schema.optional(Schema.String), |
|
+ env: Schema.optional(Schema.Literal("local", "bb", "remote", "extension")), |
|
+}); |
|
+export type BrowserLaunchResult = typeof BrowserLaunchResultSchema.Type; |
|
+ |
|
+export const BrowserTabCreateResultSchema = Schema.Struct({ |
|
+ targetId: Schema.String, |
|
+ url: Schema.optional(Schema.String), |
|
+}); |
|
+export type BrowserTabCreateResult = typeof BrowserTabCreateResultSchema.Type; |
|
+ |
|
+export const SelectorResultSchema = Schema.Struct({ |
|
+ selector: SelectorObjectSchema, |
|
+}); |
|
+export type SelectorResult = typeof SelectorResultSchema.Type; |
|
+ |
|
+export const BrowserDownloadBehaviorSchema = Schema.Struct({ |
|
+ behavior: RecordOfUnknown, |
|
+}); |
|
+export type BrowserDownloadBehavior = typeof BrowserDownloadBehaviorSchema.Type; |
|
+ |
|
+export const BrowserDownloadSchema = Schema.Struct({ |
|
+ download_id: Schema.optional(Schema.String), |
|
+ path: Schema.optional(Schema.String), |
|
+ url: Schema.optional(Schema.String), |
|
+}); |
|
+export type BrowserDownload = typeof BrowserDownloadSchema.Type; |
|
+ |
|
+export const AboutBlankLoadingResultSchema = Schema.Struct({ |
|
+ targetId: Schema.String, |
|
+ rendered: Schema.optional(Schema.Boolean), |
|
+}); |
|
+export type AboutBlankLoadingResult = typeof AboutBlankLoadingResultSchema.Type; |
|
+ |
|
+export const CDPJsonObjectSchema = Schema.Unknown; |
|
+export type CDPJsonObject = unknown; |
|
+ |
|
+// --------------------------------------------------------------------------- |
|
+// LLM shared schemas |
|
+// --------------------------------------------------------------------------- |
|
+ |
|
+export const LLMProviderSchema = Schema.Literal( |
|
+ "openai", |
|
+ "anthropic", |
|
+ "google", |
|
+ "xai", |
|
+ "azure", |
|
+ "groq", |
|
+ "cerebras", |
|
+ "togetherai", |
|
+ "mistral", |
|
+ "deepseek", |
|
+ "perplexity", |
|
+ "ollama", |
|
+ "gateway", |
|
+); |
|
+export type LLMProvider = typeof LLMProviderSchema.Type; |
|
+ |
|
+export const LLMModelSchema = Schema.String; |
|
+export type LLMModel = typeof LLMModelSchema.Type; |
|
+ |
|
+export const LLMOperationNameSchema = Schema.Literal("extract", "observe", "act", "execute"); |
|
+export type LLMOperationName = typeof LLMOperationNameSchema.Type; |
|
+ |
|
+export const LLMApiKeyAuthContextSchema = Schema.Struct({ |
|
+ kind: Schema.Literal("api-key"), |
|
+ provider: LLMProviderSchema, |
|
+ apiKey: Schema.String, |
|
+}); |
|
+export const LLMExecutionAuthContextSchema = LLMApiKeyAuthContextSchema; // single-kind union for now |
|
+export type LLMExecutionAuthContext = typeof LLMExecutionAuthContextSchema.Type; |
|
+ |
|
+const LLMConnectionFields = { |
|
+ llm_session_id: Schema.optional(Schema.String), |
|
+ model: Schema.optional(LLMModelSchema), |
|
+ provider: Schema.optional(LLMProviderSchema), |
|
+ modelName: Schema.optional(Schema.String), |
|
+ apiKey: Schema.optional(Schema.String), |
|
+ apiUrl: Schema.optional(Schema.String), |
|
+ baseUrl: Schema.optional(Schema.String), |
|
+ headers: Schema.optional(RecordOfString), |
|
+ authContext: Schema.optional(LLMExecutionAuthContextSchema), |
|
+ options: Schema.optional(RecordOfUnknown), |
|
+ systemPrompt: Schema.optional(Schema.String), |
|
+}; |
|
+ |
|
+const LLMRequestFields = { |
|
+ request_id: Schema.String, |
|
+ llm_session_id: Schema.optional(Schema.String), |
|
+ operation_name: Schema.optional(LLMOperationNameSchema), |
|
+ parent_request_id: Schema.optional(Schema.String), |
|
+ prompt: Schema.String, |
|
+ attachments: Schema.optional(Schema.Array(Schema.Unknown)), |
|
+ options: Schema.optional(RecordOfUnknown), |
|
+ messages: Schema.optional(Schema.Array(Schema.Unknown)), |
|
+ tool_results: Schema.optional(Schema.Array(Schema.Unknown)), |
|
+ provider_conversation: Schema.optional(Schema.Unknown), |
|
+ expected_response_schema: Schema.optional(Schema.Unknown), |
|
+ model: Schema.optional(Schema.String), |
|
+}; |
|
+ |
|
+export const LLMSessionSummarySchema = Schema.Struct({ |
|
+ llm_session_id: Schema.String, |
|
+ provider: Schema.optional(LLMProviderSchema), |
|
+ modelName: Schema.optional(Schema.String), |
|
+ status: Schema.optional(Schema.Literal("idle", "connected", "disconnected")), |
|
+}); |
|
+export type LLMSessionSummary = typeof LLMSessionSummarySchema.Type; |
|
+ |
|
+export const LLMDisconnectResultSchema = Schema.Struct({ |
|
+ llm_session_id: Schema.String, |
|
+ disconnected: Schema.Boolean, |
|
+}); |
|
+export type LLMDisconnectResult = typeof LLMDisconnectResultSchema.Type; |
|
+ |
|
+export const LLMToolCallSchema = Schema.Struct({ |
|
+ id: Schema.NullOr(Schema.String), |
|
+ name: Schema.String, |
|
+ type: Schema.optional(Schema.NullOr(Schema.String)), |
|
+ arguments: Schema.optional(Schema.Unknown), |
|
+ raw: Schema.optional(Schema.Unknown), |
|
+}); |
|
+export type LLMToolCall = typeof LLMToolCallSchema.Type; |
|
+ |
|
+export const LLMToolResultSchema = Schema.Struct({ |
|
+ toolCallId: Schema.NullOr(Schema.String), |
|
+ name: Schema.String, |
|
+ result: Schema.optional(Schema.Unknown), |
|
+ error: Schema.optional(Schema.NullOr(Schema.String)), |
|
+}); |
|
+export type LLMToolResult = typeof LLMToolResultSchema.Type; |
|
+ |
|
+export const LLMSafetyCheckSchema = Schema.Struct({ |
|
+ id: Schema.optional(Schema.NullOr(Schema.String)), |
|
+ code: Schema.String, |
|
+ message: Schema.optional(Schema.String), |
|
+}); |
|
+export type LLMSafetyCheck = typeof LLMSafetyCheckSchema.Type; |
|
+ |
|
+export const LLMRequestResultSchema = Schema.Struct({ |
|
+ request_id: Schema.String, |
|
+ llm_session_id: Schema.optional(Schema.String), |
|
+ output: Schema.optional(Schema.Unknown), |
|
+ tool_calls: Schema.optional(Schema.Array(LLMToolCallSchema)), |
|
+}); |
|
+export type LLMRequestResult = typeof LLMRequestResultSchema.Type; |
|
+ |
|
// --------------------------------------------------------------------------- |
|
// CDP events |
|
// --------------------------------------------------------------------------- |
|
|
|
+const cdpFirstPolicy = { |
|
+ completion: "first", |
|
+ concurrency: "parallel", |
|
+ blocks_parent_completion: true, |
|
+ handler_timeout: "20 seconds", |
|
+ event_timeout: "20 seconds", |
|
+} as const; |
|
+ |
|
export const CDPConnectEvent = defineEvent("CDPConnectEvent", { |
|
- payload: Schema.Struct({ |
|
- cdpUrl: Schema.optional(Schema.String), |
|
- }), |
|
- policy: { completion: "first", concurrency: "parallel", blocks_parent_completion: true, event_timeout: "30 seconds" }, |
|
+ payload: Schema.Struct({ cdpUrl: Schema.String }), |
|
+ policy: { ...cdpFirstPolicy, event_timeout: "20 seconds" }, |
|
}); |
|
export type CDPConnectEvent = ReturnType<typeof CDPConnectEvent>; |
|
|
|
export const CDPConnectedEvent = defineEvent("CDPConnectedEvent", { |
|
- payload: Schema.Struct({ |
|
- cdpUrl: Schema.String, |
|
- }), |
|
+ payload: Schema.Struct({ cdpUrl: Schema.String }), |
|
+ policy: { completion: "all", concurrency: "parallel", blocks_parent_completion: true }, |
|
}); |
|
export type CDPConnectedEvent = ReturnType<typeof CDPConnectedEvent>; |
|
|
|
export const CDPDisconnectEvent = defineEvent("CDPDisconnectEvent", { |
|
payload: Schema.Struct({}), |
|
- policy: { completion: "first", concurrency: "parallel", blocks_parent_completion: true }, |
|
+ policy: { |
|
+ completion: "first", |
|
+ concurrency: "parallel", |
|
+ blocks_parent_completion: true, |
|
+ handler_timeout: "10 seconds", |
|
+ event_timeout: "10 seconds", |
|
+ }, |
|
}); |
|
export type CDPDisconnectEvent = ReturnType<typeof CDPDisconnectEvent>; |
|
|
|
@@ -83,43 +386,73 @@ export type CDPDisconnectedEvent = ReturnType<typeof CDPDisconnectedEvent>; |
|
export const CDPSendEvent = defineEvent("CDPSendEvent", { |
|
payload: Schema.Struct({ |
|
method: Schema.String, |
|
- params: Schema.optional(Schema.Unknown), |
|
+ params: Schema.optional(RecordOfUnknown), |
|
targetId: Schema.optional(Schema.String), |
|
- sessionId: Schema.optional(Schema.String), |
|
+ cdp_session_id: Schema.optional(Schema.String), |
|
}), |
|
- policy: { completion: "first", concurrency: "parallel", blocks_parent_completion: true, event_timeout: "30 seconds" }, |
|
+ policy: cdpFirstPolicy, |
|
}); |
|
export type CDPSendEvent = ReturnType<typeof CDPSendEvent>; |
|
|
|
export const CDPRecvEvent = defineEvent("CDPRecvEvent", { |
|
payload: Schema.Struct({ |
|
method: Schema.optional(Schema.String), |
|
- params: Schema.optional(Schema.Unknown), |
|
- targetId: Schema.optional(Schema.String), |
|
- sessionId: Schema.optional(Schema.String), |
|
- requestId: Schema.optional(Schema.Number), |
|
result: Schema.optional(Schema.Unknown), |
|
error: Schema.optional(Schema.Unknown), |
|
+ targetId: Schema.optional(Schema.String), |
|
+ cdp_session_id: Schema.optional(Schema.String), |
|
+ requestId: Schema.optional(Schema.Number), |
|
}), |
|
policy: { completion: "all", concurrency: "parallel", blocks_parent_completion: false }, |
|
}); |
|
export type CDPRecvEvent = ReturnType<typeof CDPRecvEvent>; |
|
|
|
+export const CDPOnEvent = defineEvent("CDPOnEvent", { |
|
+ payload: Schema.Struct({ method: Schema.String }), |
|
+ policy: { |
|
+ completion: "first", |
|
+ concurrency: "parallel", |
|
+ blocks_parent_completion: true, |
|
+ handler_timeout: "10 seconds", |
|
+ event_timeout: "10 seconds", |
|
+ }, |
|
+}); |
|
+export type CDPOnEvent = ReturnType<typeof CDPOnEvent>; |
|
+ |
|
+export const CDPPingEvent = defineEvent("CDPPingEvent", { |
|
+ payload: Schema.Struct({}), |
|
+ policy: { |
|
+ completion: "first", |
|
+ concurrency: "parallel", |
|
+ blocks_parent_completion: true, |
|
+ handler_timeout: "10 seconds", |
|
+ event_timeout: "10 seconds", |
|
+ }, |
|
+}); |
|
+export type CDPPingEvent = ReturnType<typeof CDPPingEvent>; |
|
+ |
|
+export const CDPPongEvent = defineEvent("CDPPongEvent", { |
|
+ payload: Schema.Struct({ latencyMs: Schema.optional(Schema.Number) }), |
|
+ policy: { completion: "all", concurrency: "parallel", blocks_parent_completion: true }, |
|
+}); |
|
+export type CDPPongEvent = ReturnType<typeof CDPPongEvent>; |
|
+ |
|
// --------------------------------------------------------------------------- |
|
-// Browser events (minimum for CDP-backed page operations) |
|
+// Browser lifecycle events |
|
// --------------------------------------------------------------------------- |
|
|
|
+const browserCommandPolicy = { |
|
+ completion: "first", |
|
+ concurrency: "parallel", |
|
+ blocks_parent_completion: true, |
|
+} as const; |
|
+ |
|
export const BrowserLaunchOrConnectEvent = defineEvent("BrowserLaunchOrConnectEvent", { |
|
payload: Schema.Struct({ |
|
cdpUrl: Schema.optional(Schema.String), |
|
- options: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })), |
|
+ options: Schema.optional(RecordOfUnknown), |
|
}), |
|
- policy: { |
|
- completion: "first", |
|
- concurrency: "parallel", |
|
- blocks_parent_completion: true, |
|
- event_timeout: "180 seconds", |
|
- }, |
|
+ policy: { ...browserCommandPolicy, event_timeout: "180 seconds" }, |
|
}); |
|
export type BrowserLaunchOrConnectEvent = ReturnType<typeof BrowserLaunchOrConnectEvent>; |
|
|
|
@@ -131,131 +464,830 @@ export const BrowserConnectedEvent = defineEvent("BrowserConnectedEvent", { |
|
}); |
|
export type BrowserConnectedEvent = ReturnType<typeof BrowserConnectedEvent>; |
|
|
|
+export const BrowserDisconnectedEvent = defineEvent("BrowserDisconnectedEvent", { |
|
+ payload: Schema.Struct({ browser_id: Schema.optional(Schema.String) }), |
|
+}); |
|
+export type BrowserDisconnectedEvent = ReturnType<typeof BrowserDisconnectedEvent>; |
|
+ |
|
export const BrowserKillEvent = defineEvent("BrowserKillEvent", { |
|
payload: Schema.Struct({}), |
|
policy: { completion: "all", concurrency: "parallel", blocks_parent_completion: true }, |
|
}); |
|
+(BrowserKillEvent as { llm_tool_name?: string }).llm_tool_name = "killBrowser"; |
|
export type BrowserKillEvent = ReturnType<typeof BrowserKillEvent>; |
|
|
|
+export const BrowserExitedEvent = defineEvent("BrowserExitedEvent", { |
|
+ payload: Schema.Struct({ |
|
+ code: Schema.optional(Schema.Number), |
|
+ signal: Schema.optional(Schema.String), |
|
+ }), |
|
+}); |
|
+export type BrowserExitedEvent = ReturnType<typeof BrowserExitedEvent>; |
|
+ |
|
+// --------------------------------------------------------------------------- |
|
+// Browser query / configuration events |
|
+// --------------------------------------------------------------------------- |
|
+ |
|
+const tool = (factory: ReturnType<typeof defineEvent>, llm_tool_name: string) => { |
|
+ (factory as { llm_tool_name?: string }).llm_tool_name = llm_tool_name; |
|
+ return factory; |
|
+}; |
|
+ |
|
+export const BrowserSetViewportEvent = defineEvent("BrowserSetViewportEvent", { |
|
+ payload: Schema.Struct({ |
|
+ width: Schema.Number, |
|
+ height: Schema.Number, |
|
+ deviceScaleFactor: Schema.optional(Schema.Number), |
|
+ }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserSetViewportEvent as never, "setViewport"); |
|
+export type BrowserSetViewportEvent = ReturnType<typeof BrowserSetViewportEvent>; |
|
+ |
|
+export const BrowserRequestConfiguredViewportEvent = defineEvent("BrowserRequestConfiguredViewportEvent", { |
|
+ payload: Schema.Struct({}), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserRequestConfiguredViewportEvent as never, "getViewport"); |
|
+export type BrowserRequestConfiguredViewportEvent = ReturnType<typeof BrowserRequestConfiguredViewportEvent>; |
|
+ |
|
+export const BrowserSetExtraHttpHeadersEvent = defineEvent("BrowserSetExtraHttpHeadersEvent", { |
|
+ payload: Schema.Struct({ headers: RecordOfString }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserSetExtraHttpHeadersEvent as never, "setExtraHTTPHeaders"); |
|
+export type BrowserSetExtraHttpHeadersEvent = ReturnType<typeof BrowserSetExtraHttpHeadersEvent>; |
|
+ |
|
+export const BrowserAddInitScriptEvent = defineEvent("BrowserAddInitScriptEvent", { |
|
+ payload: Schema.Struct({ |
|
+ script: Schema.Union(Schema.String, Schema.Struct({ scriptId: Schema.String })), |
|
+ }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserAddInitScriptEvent as never, "addInitScript"); |
|
+export type BrowserAddInitScriptEvent = ReturnType<typeof BrowserAddInitScriptEvent>; |
|
+ |
|
+export const BrowserRequestCookiesEvent = defineEvent("BrowserRequestCookiesEvent", { |
|
+ payload: Schema.Struct({ |
|
+ urls: Schema.optional(Schema.Union(Schema.String, Schema.Array(Schema.String))), |
|
+ }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserRequestCookiesEvent as never, "getCookies"); |
|
+export type BrowserRequestCookiesEvent = ReturnType<typeof BrowserRequestCookiesEvent>; |
|
+ |
|
+export const BrowserAddCookiesEvent = defineEvent("BrowserAddCookiesEvent", { |
|
+ payload: Schema.Struct({ cookies: Schema.Array(BrowserCookieParamSchema) }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserAddCookiesEvent as never, "addCookies"); |
|
+export type BrowserAddCookiesEvent = ReturnType<typeof BrowserAddCookiesEvent>; |
|
+ |
|
+export const BrowserClearCookiesEvent = defineEvent("BrowserClearCookiesEvent", { |
|
+ payload: Schema.Struct({ options: Schema.optional(BrowserClearCookiesOptionsSchema) }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserClearCookiesEvent as never, "clearCookies"); |
|
+export type BrowserClearCookiesEvent = ReturnType<typeof BrowserClearCookiesEvent>; |
|
+ |
|
+export const BrowserRequestConnectURLEvent = defineEvent("BrowserRequestConnectURLEvent", { |
|
+ payload: Schema.Struct({}), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserRequestConnectURLEvent as never, "getConnectURL"); |
|
+export type BrowserRequestConnectURLEvent = ReturnType<typeof BrowserRequestConnectURLEvent>; |
|
+ |
|
+export const BrowserCloseEvent = defineEvent("BrowserCloseEvent", { |
|
+ payload: Schema.Struct({}), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserCloseEvent as never, "closeBrowser"); |
|
+export type BrowserCloseEvent = ReturnType<typeof BrowserCloseEvent>; |
|
+ |
|
+export const BrowserSetDownloadBehaviorEvent = defineEvent("BrowserSetDownloadBehaviorEvent", { |
|
+ payload: Schema.Struct({ behavior: RecordOfUnknown }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserSetDownloadBehaviorEvent as never, "setDownloadBehavior"); |
|
+export type BrowserSetDownloadBehaviorEvent = ReturnType<typeof BrowserSetDownloadBehaviorEvent>; |
|
+ |
|
+export const BrowserRequestFetchDownloadsEvent = defineEvent("BrowserRequestFetchDownloadsEvent", { |
|
+ payload: Schema.Struct({}), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserRequestFetchDownloadsEvent as never, "fetchDownloads"); |
|
+export type BrowserRequestFetchDownloadsEvent = ReturnType<typeof BrowserRequestFetchDownloadsEvent>; |
|
+ |
|
+export const BrowserRequestTabListEvent = defineEvent("BrowserRequestTabListEvent", { |
|
+ payload: Schema.Struct({}), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserRequestTabListEvent as never, "listTabs"); |
|
+export type BrowserRequestTabListEvent = ReturnType<typeof BrowserRequestTabListEvent>; |
|
+ |
|
+export const BrowserRequestActivePageEvent = defineEvent("BrowserRequestActivePageEvent", { |
|
+ payload: Schema.Struct({}), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserRequestActivePageEvent as never, "getActivePage"); |
|
+export type BrowserRequestActivePageEvent = ReturnType<typeof BrowserRequestActivePageEvent>; |
|
+ |
|
+// --------------------------------------------------------------------------- |
|
+// Browser tab events |
|
+// --------------------------------------------------------------------------- |
|
+ |
|
+export const BrowserTabCreateEvent = defineEvent("BrowserTabCreateEvent", { |
|
+ payload: Schema.Struct({ url: Schema.optional(Schema.String) }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserTabCreateEvent as never, "createTab"); |
|
+export type BrowserTabCreateEvent = ReturnType<typeof BrowserTabCreateEvent>; |
|
+ |
|
+export const BrowserTabCreatedEvent = defineEvent("BrowserTabCreatedEvent", { |
|
+ payload: Schema.Struct({ |
|
+ targetId: Schema.String, |
|
+ openerId: Schema.optional(Schema.String), |
|
+ tabId: Schema.optional(Schema.Number), |
|
+ url: Schema.optional(Schema.String), |
|
+ }), |
|
+}); |
|
+export type BrowserTabCreatedEvent = ReturnType<typeof BrowserTabCreatedEvent>; |
|
+ |
|
+export const BrowserTabClosedEvent = defineEvent("BrowserTabClosedEvent", { |
|
+ payload: Schema.Struct({ targetId: Schema.String }), |
|
+}); |
|
+export type BrowserTabClosedEvent = ReturnType<typeof BrowserTabClosedEvent>; |
|
+ |
|
+export const BrowserTabCloseEvent = defineEvent("BrowserTabCloseEvent", { |
|
+ payload: Schema.Struct({ selector: SelectorObjectSchema }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserTabCloseEvent as never, "closeTab"); |
|
+export type BrowserTabCloseEvent = ReturnType<typeof BrowserTabCloseEvent>; |
|
+ |
|
+// --------------------------------------------------------------------------- |
|
+// Browser frame events |
|
+// --------------------------------------------------------------------------- |
|
+ |
|
+export const BrowserFrameUpdatedEvent = defineEvent("BrowserFrameUpdatedEvent", { |
|
+ payload: Schema.Struct({ |
|
+ frameId: Schema.String, |
|
+ backendNodeId: Schema.optional(Schema.Number), |
|
+ isOopif: Schema.optional(Schema.Boolean), |
|
+ name: Schema.optional(Schema.String), |
|
+ parentFrameId: Schema.optional(Schema.String), |
|
+ securityOrigin: Schema.optional(Schema.String), |
|
+ cdp_session_id: Schema.optional(Schema.String), |
|
+ targetId: Schema.optional(Schema.String), |
|
+ targetType: Schema.optional(Schema.String), |
|
+ url: Schema.optional(Schema.String), |
|
+ }), |
|
+ policy: { completion: "all", concurrency: "parallel", blocks_parent_completion: true }, |
|
+}); |
|
+export type BrowserFrameUpdatedEvent = ReturnType<typeof BrowserFrameUpdatedEvent>; |
|
+ |
|
+export const BrowserFrameDetachedEvent = defineEvent("BrowserFrameDetachedEvent", { |
|
+ payload: Schema.Struct({ |
|
+ frameId: Schema.String, |
|
+ reason: Schema.optional(Schema.String), |
|
+ cdp_session_id: Schema.optional(Schema.String), |
|
+ targetId: Schema.optional(Schema.String), |
|
+ }), |
|
+ policy: { completion: "all", concurrency: "parallel", blocks_parent_completion: true }, |
|
+}); |
|
+export type BrowserFrameDetachedEvent = ReturnType<typeof BrowserFrameDetachedEvent>; |
|
+ |
|
+// --------------------------------------------------------------------------- |
|
+// Browser page navigation events |
|
+// --------------------------------------------------------------------------- |
|
+ |
|
export const BrowserPageGotoEvent = defineEvent("BrowserPageGotoEvent", { |
|
payload: Schema.Struct({ |
|
url: Schema.String, |
|
selector: Schema.optional(SelectorObjectSchema), |
|
- timeout: Schema.optional(Schema.Number), |
|
- waitUntil: Schema.optional(Schema.String), |
|
}), |
|
- policy: { completion: "first", concurrency: "parallel", blocks_parent_completion: true, event_timeout: "60 seconds" }, |
|
+ policy: { ...browserCommandPolicy, event_timeout: "60 seconds" }, |
|
}); |
|
+tool(BrowserPageGotoEvent as never, "goto"); |
|
export type BrowserPageGotoEvent = ReturnType<typeof BrowserPageGotoEvent>; |
|
|
|
+export const BrowserPageBringToFrontEvent = defineEvent("BrowserPageBringToFrontEvent", { |
|
+ payload: Schema.Struct({ selector: Schema.optional(SelectorObjectSchema) }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageBringToFrontEvent as never, "bringToFront"); |
|
+export type BrowserPageBringToFrontEvent = ReturnType<typeof BrowserPageBringToFrontEvent>; |
|
+ |
|
+export const BrowserPageBroughtToFrontEvent = defineEvent("BrowserPageBroughtToFrontEvent", { |
|
+ payload: Schema.Struct({ selector: SelectorObjectSchema }), |
|
+ policy: { completion: "all", concurrency: "parallel", blocks_parent_completion: true }, |
|
+}); |
|
+export type BrowserPageBroughtToFrontEvent = ReturnType<typeof BrowserPageBroughtToFrontEvent>; |
|
+ |
|
export const BrowserPageNavigatedEvent = defineEvent("BrowserPageNavigatedEvent", { |
|
payload: Schema.Struct({ |
|
- targetId: Schema.optional(Schema.String), |
|
url: Schema.String, |
|
+ selector: Schema.optional(SelectorObjectSchema), |
|
}), |
|
}); |
|
export type BrowserPageNavigatedEvent = ReturnType<typeof BrowserPageNavigatedEvent>; |
|
|
|
+export const BrowserPageDOMContentLoadedEvent = defineEvent("BrowserPageDOMContentLoadedEvent", { |
|
+ payload: Schema.Struct({ |
|
+ targetId: Schema.String, |
|
+ url: Schema.optional(Schema.String), |
|
+ selector: Schema.optional(SelectorObjectSchema), |
|
+ loadState: Schema.Literal("domcontentloaded"), |
|
+ }), |
|
+}); |
|
+export type BrowserPageDOMContentLoadedEvent = ReturnType<typeof BrowserPageDOMContentLoadedEvent>; |
|
+ |
|
+export const BrowserPageLoadedEvent = defineEvent("BrowserPageLoadedEvent", { |
|
+ payload: Schema.Struct({ |
|
+ targetId: Schema.String, |
|
+ url: Schema.optional(Schema.String), |
|
+ selector: Schema.optional(SelectorObjectSchema), |
|
+ loadState: Schema.Literal("loaded"), |
|
+ }), |
|
+ policy: { completion: "all", concurrency: "serial", blocks_parent_completion: false }, |
|
+}); |
|
+export type BrowserPageLoadedEvent = ReturnType<typeof BrowserPageLoadedEvent>; |
|
+ |
|
+export const BrowserPageNetworkIdle2Event = defineEvent("BrowserPageNetworkIdle2Event", { |
|
+ payload: Schema.Struct({ |
|
+ targetId: Schema.String, |
|
+ url: Schema.optional(Schema.String), |
|
+ selector: Schema.optional(SelectorObjectSchema), |
|
+ loadState: Schema.Literal("networkidle2"), |
|
+ }), |
|
+}); |
|
+export type BrowserPageNetworkIdle2Event = ReturnType<typeof BrowserPageNetworkIdle2Event>; |
|
+ |
|
+export const BrowserPageNetworkIdleEvent = defineEvent("BrowserPageNetworkIdleEvent", { |
|
+ payload: Schema.Struct({ |
|
+ targetId: Schema.String, |
|
+ url: Schema.optional(Schema.String), |
|
+ selector: Schema.optional(SelectorObjectSchema), |
|
+ loadState: Schema.Literal("networkidle"), |
|
+ }), |
|
+}); |
|
+export type BrowserPageNetworkIdleEvent = ReturnType<typeof BrowserPageNetworkIdleEvent>; |
|
+ |
|
+export const BrowserPageLoadStateChangedEvent = defineEvent("BrowserPageLoadStateChangedEvent", { |
|
+ payload: Schema.Struct({ |
|
+ targetId: Schema.String, |
|
+ url: Schema.optional(Schema.String), |
|
+ selector: Schema.optional(SelectorObjectSchema), |
|
+ loadState: BrowserPageLoadStateSchema, |
|
+ }), |
|
+}); |
|
+export type BrowserPageLoadStateChangedEvent = ReturnType<typeof BrowserPageLoadStateChangedEvent>; |
|
+ |
|
+// --------------------------------------------------------------------------- |
|
+// Browser page action events |
|
+// --------------------------------------------------------------------------- |
|
+ |
|
+export const BrowserPageClickEvent = defineEvent("BrowserPageClickEvent", { |
|
+ payload: Schema.Struct({ selector: SelectorObjectSchema }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageClickEvent as never, "click"); |
|
+export type BrowserPageClickEvent = ReturnType<typeof BrowserPageClickEvent>; |
|
+ |
|
+export const BrowserPageDoubleClickEvent = defineEvent("BrowserPageDoubleClickEvent", { |
|
+ payload: Schema.Struct({ selector: SelectorObjectSchema }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageDoubleClickEvent as never, "doubleClick"); |
|
+export type BrowserPageDoubleClickEvent = ReturnType<typeof BrowserPageDoubleClickEvent>; |
|
+ |
|
+export const BrowserPageScrollEvent = defineEvent("BrowserPageScrollEvent", { |
|
+ payload: Schema.Struct({ |
|
+ selector: SelectorObjectSchema, |
|
+ deltaX: Schema.optional(Schema.Number), |
|
+ deltaY: Schema.optional(Schema.Number), |
|
+ }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageScrollEvent as never, "scroll"); |
|
+export type BrowserPageScrollEvent = ReturnType<typeof BrowserPageScrollEvent>; |
|
+ |
|
+export const BrowserPageScrollToEvent = defineEvent("BrowserPageScrollToEvent", { |
|
+ payload: Schema.Struct({ |
|
+ selector: SelectorObjectSchema, |
|
+ deltaX: Schema.optional(Schema.Number), |
|
+ deltaY: Schema.optional(Schema.Number), |
|
+ }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageScrollToEvent as never, "scrollTo"); |
|
+export type BrowserPageScrollToEvent = ReturnType<typeof BrowserPageScrollToEvent>; |
|
+ |
|
+export const BrowserPageNextChunkEvent = defineEvent("BrowserPageNextChunkEvent", { |
|
+ payload: Schema.Struct({ |
|
+ selector: Schema.optional(SelectorObjectSchema), |
|
+ deltaY: Schema.optional(Schema.Number), |
|
+ }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageNextChunkEvent as never, "nextChunk"); |
|
+export type BrowserPageNextChunkEvent = ReturnType<typeof BrowserPageNextChunkEvent>; |
|
+ |
|
+export const BrowserPagePrevChunkEvent = defineEvent("BrowserPagePrevChunkEvent", { |
|
+ payload: Schema.Struct({ |
|
+ selector: Schema.optional(SelectorObjectSchema), |
|
+ deltaY: Schema.optional(Schema.Number), |
|
+ }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPagePrevChunkEvent as never, "prevChunk"); |
|
+export type BrowserPagePrevChunkEvent = ReturnType<typeof BrowserPagePrevChunkEvent>; |
|
+ |
|
+export const BrowserPageScrollByEvent = defineEvent("BrowserPageScrollByEvent", { |
|
+ payload: Schema.Struct({ |
|
+ selector: Schema.optional(SelectorObjectSchema), |
|
+ targetId: Schema.optional(Schema.String), |
|
+ offset: Schema.optional(Schema.Struct({ x: Schema.Number, y: Schema.Number })), |
|
+ pages: Schema.optional(Schema.Number), |
|
+ }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageScrollByEvent as never, "scrollBy"); |
|
+export type BrowserPageScrollByEvent = ReturnType<typeof BrowserPageScrollByEvent>; |
|
+ |
|
+export const BrowserPageTypeEvent = defineEvent("BrowserPageTypeEvent", { |
|
+ payload: Schema.Struct({ selector: SelectorObjectSchema, text: Schema.String }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageTypeEvent as never, "type"); |
|
+export type BrowserPageTypeEvent = ReturnType<typeof BrowserPageTypeEvent>; |
|
+ |
|
+export const BrowserPageKeyPressEvent = defineEvent("BrowserPageKeyPressEvent", { |
|
+ payload: Schema.Struct({ selector: SelectorObjectSchema, key: Schema.String }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageKeyPressEvent as never, "press"); |
|
+export type BrowserPageKeyPressEvent = ReturnType<typeof BrowserPageKeyPressEvent>; |
|
+ |
|
+export const BrowserPageGoBackEvent = defineEvent("BrowserPageGoBackEvent", { |
|
+ payload: Schema.Struct({ selector: Schema.optional(SelectorObjectSchema) }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageGoBackEvent as never, "goBack"); |
|
+export type BrowserPageGoBackEvent = ReturnType<typeof BrowserPageGoBackEvent>; |
|
+ |
|
+export const BrowserPageReloadEvent = defineEvent("BrowserPageReloadEvent", { |
|
+ payload: Schema.Struct({ |
|
+ targetId: Schema.optional(Schema.String), |
|
+ ignoreCache: Schema.optional(Schema.Boolean), |
|
+ }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageReloadEvent as never, "reload"); |
|
+export type BrowserPageReloadEvent = ReturnType<typeof BrowserPageReloadEvent>; |
|
+ |
|
+export const BrowserPageGoForwardEvent = defineEvent("BrowserPageGoForwardEvent", { |
|
+ payload: Schema.Struct({ selector: Schema.optional(SelectorObjectSchema) }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageGoForwardEvent as never, "goForward"); |
|
+export type BrowserPageGoForwardEvent = ReturnType<typeof BrowserPageGoForwardEvent>; |
|
+ |
|
+export const BrowserPageDragAndDropEvent = defineEvent("BrowserPageDragAndDropEvent", { |
|
+ payload: Schema.Struct({ |
|
+ from: SelectorObjectSchema, |
|
+ to: SelectorObjectSchema, |
|
+ delay: Schema.optional(Schema.Number), |
|
+ }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageDragAndDropEvent as never, "dragAndDrop"); |
|
+export type BrowserPageDragAndDropEvent = ReturnType<typeof BrowserPageDragAndDropEvent>; |
|
+ |
|
+export const BrowserPageHoverEvent = defineEvent("BrowserPageHoverEvent", { |
|
+ payload: Schema.Struct({ selector: SelectorObjectSchema }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageHoverEvent as never, "hover"); |
|
+export type BrowserPageHoverEvent = ReturnType<typeof BrowserPageHoverEvent>; |
|
+ |
|
+export const BrowserPageLocateEvent = defineEvent("BrowserPageLocateEvent", { |
|
+ payload: Schema.Struct({ selector: SelectorObjectSchema }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageLocateEvent as never, "locate"); |
|
+export type BrowserPageLocateEvent = ReturnType<typeof BrowserPageLocateEvent>; |
|
+ |
|
+export const BrowserPageRequestSnapshotEvent = defineEvent("BrowserPageRequestSnapshotEvent", { |
|
+ payload: Schema.Struct({ selector: Schema.optional(SelectorObjectSchema) }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageRequestSnapshotEvent as never, "requestSnapshot"); |
|
+export type BrowserPageRequestSnapshotEvent = ReturnType<typeof BrowserPageRequestSnapshotEvent>; |
|
+ |
|
+export const BrowserPageRequestDocumentSnapshotEvent = defineEvent("BrowserPageRequestDocumentSnapshotEvent", { |
|
+ payload: Schema.Struct({ selector: Schema.optional(SelectorObjectSchema) }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageRequestDocumentSnapshotEvent as never, "requestDocumentSnapshot"); |
|
+export type BrowserPageRequestDocumentSnapshotEvent = ReturnType<typeof BrowserPageRequestDocumentSnapshotEvent>; |
|
+ |
|
+export const BrowserPageDocumentSnapshotCapturedEvent = defineEvent("BrowserPageDocumentSnapshotCapturedEvent", { |
|
+ payload: Schema.Struct({ |
|
+ selector: Schema.optional(SelectorObjectSchema), |
|
+ snapshot: Schema.optional(Schema.Unknown), |
|
+ }), |
|
+}); |
|
+export type BrowserPageDocumentSnapshotCapturedEvent = ReturnType<typeof BrowserPageDocumentSnapshotCapturedEvent>; |
|
+ |
|
export const BrowserPageRequestScreenshotEvent = defineEvent("BrowserPageRequestScreenshotEvent", { |
|
payload: Schema.Struct({ |
|
selector: Schema.optional(SelectorObjectSchema), |
|
- fullPage: Schema.optional(Schema.Boolean), |
|
+ options: Schema.optional(RecordOfUnknown), |
|
}), |
|
- policy: { completion: "first", concurrency: "parallel", blocks_parent_completion: true, event_timeout: "30 seconds" }, |
|
+ policy: { ...browserCommandPolicy, event_timeout: "30 seconds" }, |
|
}); |
|
+tool(BrowserPageRequestScreenshotEvent as never, "screenshot"); |
|
export type BrowserPageRequestScreenshotEvent = ReturnType<typeof BrowserPageRequestScreenshotEvent>; |
|
|
|
-export const BrowserRequestTabListEvent = defineEvent("BrowserRequestTabListEvent", { |
|
- payload: Schema.Struct({}), |
|
- policy: { completion: "first", concurrency: "parallel", blocks_parent_completion: true }, |
|
+export const BrowserPageScreenshotCapturedEvent = defineEvent("BrowserPageScreenshotCapturedEvent", { |
|
+ payload: Schema.Struct({ |
|
+ selector: Schema.optional(SelectorObjectSchema), |
|
+ screenshot: Schema.optional(Schema.Unknown), |
|
+ }), |
|
}); |
|
-export type BrowserRequestTabListEvent = ReturnType<typeof BrowserRequestTabListEvent>; |
|
+export type BrowserPageScreenshotCapturedEvent = ReturnType<typeof BrowserPageScreenshotCapturedEvent>; |
|
+ |
|
+export const BrowserPageSetInputFilesEvent = defineEvent("BrowserPageSetInputFilesEvent", { |
|
+ payload: Schema.Struct({ selector: SelectorObjectSchema, files: Schema.Array(Schema.String) }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageSetInputFilesEvent as never, "setInputFiles"); |
|
+export type BrowserPageSetInputFilesEvent = ReturnType<typeof BrowserPageSetInputFilesEvent>; |
|
+ |
|
+export const BrowserPageEnableCursorOverlayEvent = defineEvent("BrowserPageEnableCursorOverlayEvent", { |
|
+ payload: Schema.Struct({ selector: Schema.optional(SelectorObjectSchema) }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageEnableCursorOverlayEvent as never, "enableCursorOverlay"); |
|
+export type BrowserPageEnableCursorOverlayEvent = ReturnType<typeof BrowserPageEnableCursorOverlayEvent>; |
|
+ |
|
+export const BrowserPageRequestFullFrameTreeEvent = defineEvent("BrowserPageRequestFullFrameTreeEvent", { |
|
+ payload: Schema.Struct({ targetId: Schema.optional(Schema.String) }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageRequestFullFrameTreeEvent as never, "requestFullFrameTree"); |
|
+export type BrowserPageRequestFullFrameTreeEvent = ReturnType<typeof BrowserPageRequestFullFrameTreeEvent>; |
|
+ |
|
+export const BrowserPageRequestInfoEvent = defineEvent("BrowserPageRequestInfoEvent", { |
|
+ payload: Schema.Struct({ targetId: Schema.optional(Schema.String) }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageRequestInfoEvent as never, "getPageInfo"); |
|
+export type BrowserPageRequestInfoEvent = ReturnType<typeof BrowserPageRequestInfoEvent>; |
|
+ |
|
+export const BrowserPageWaitForLoadStateEvent = defineEvent("BrowserPageWaitForLoadStateEvent", { |
|
+ payload: Schema.Struct({ |
|
+ targetId: Schema.optional(Schema.String), |
|
+ state: Schema.Literal("load", "domcontentloaded", "networkidle"), |
|
+ }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageWaitForLoadStateEvent as never, "waitForLoadState"); |
|
+export type BrowserPageWaitForLoadStateEvent = ReturnType<typeof BrowserPageWaitForLoadStateEvent>; |
|
+ |
|
+export const BrowserPageWaitForSelectorEvent = defineEvent("BrowserPageWaitForSelectorEvent", { |
|
+ payload: Schema.Struct({ |
|
+ selector: SelectorObjectSchema, |
|
+ state: Schema.optional(Schema.Literal("attached", "detached", "visible", "hidden")), |
|
+ }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageWaitForSelectorEvent as never, "waitForSelector"); |
|
+export type BrowserPageWaitForSelectorEvent = ReturnType<typeof BrowserPageWaitForSelectorEvent>; |
|
+ |
|
+export const BrowserPageWaitForTimeoutEvent = defineEvent("BrowserPageWaitForTimeoutEvent", { |
|
+ payload: Schema.Struct({ ms: Schema.Number }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageWaitForTimeoutEvent as never, "wait"); |
|
+export type BrowserPageWaitForTimeoutEvent = ReturnType<typeof BrowserPageWaitForTimeoutEvent>; |
|
+ |
|
+export const BrowserPageEvaluateEvent = defineEvent("BrowserPageEvaluateEvent", { |
|
+ payload: Schema.Struct({ |
|
+ targetId: Schema.optional(Schema.String), |
|
+ frameId: Schema.optional(Schema.String), |
|
+ expression: Schema.String, |
|
+ arg: Schema.optional(Schema.Unknown), |
|
+ awaitPromise: Schema.optional(Schema.Boolean), |
|
+ returnByValue: Schema.optional(Schema.Boolean), |
|
+ }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageEvaluateEvent as never, "evaluate"); |
|
+export type BrowserPageEvaluateEvent = ReturnType<typeof BrowserPageEvaluateEvent>; |
|
+ |
|
+export const BrowserPageSendCDPEvent = defineEvent("BrowserPageSendCDPEvent", { |
|
+ payload: Schema.Struct({ |
|
+ targetId: Schema.optional(Schema.String), |
|
+ method: Schema.String, |
|
+ params: Schema.optional(RecordOfUnknown), |
|
+ }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageSendCDPEvent as never, "sendCDP"); |
|
+export type BrowserPageSendCDPEvent = ReturnType<typeof BrowserPageSendCDPEvent>; |
|
+ |
|
+export const BrowserPageFillEvent = defineEvent("BrowserPageFillEvent", { |
|
+ payload: Schema.Struct({ selector: SelectorObjectSchema, value: Schema.String }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageFillEvent as never, "fill"); |
|
+export type BrowserPageFillEvent = ReturnType<typeof BrowserPageFillEvent>; |
|
+ |
|
+export const BrowserPageRequestElementInfoEvent = defineEvent("BrowserPageRequestElementInfoEvent", { |
|
+ payload: Schema.Struct({ |
|
+ selector: SelectorObjectSchema, |
|
+ fields: Schema.optional(Schema.Array(Schema.String)), |
|
+ }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageRequestElementInfoEvent as never, "getElementInfo"); |
|
+export type BrowserPageRequestElementInfoEvent = ReturnType<typeof BrowserPageRequestElementInfoEvent>; |
|
+ |
|
+export const BrowserPageHighlightEvent = defineEvent("BrowserPageHighlightEvent", { |
|
+ payload: Schema.Struct({ |
|
+ selector: SelectorObjectSchema, |
|
+ color: Schema.optional(Schema.String), |
|
+ durationMs: Schema.optional(Schema.Number), |
|
+ }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageHighlightEvent as never, "highlight"); |
|
+export type BrowserPageHighlightEvent = ReturnType<typeof BrowserPageHighlightEvent>; |
|
+ |
|
+export const BrowserPageSelectOptionEvent = defineEvent("BrowserPageSelectOptionEvent", { |
|
+ payload: Schema.Struct({ selector: SelectorObjectSchema, values: Schema.Array(Schema.String) }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageSelectOptionEvent as never, "selectOptionFromDropdown"); |
|
+export type BrowserPageSelectOptionEvent = ReturnType<typeof BrowserPageSelectOptionEvent>; |
|
+ |
|
+export const BrowserPageDOMSummaryEvent = defineEvent("BrowserPageDOMSummaryEvent", { |
|
+ payload: Schema.Struct({ selector: Schema.optional(SelectorObjectSchema) }), |
|
+ policy: browserCommandPolicy, |
|
+}); |
|
+tool(BrowserPageDOMSummaryEvent as never, "summarizeDOM"); |
|
+export type BrowserPageDOMSummaryEvent = ReturnType<typeof BrowserPageDOMSummaryEvent>; |
|
+ |
|
+export const BrowserPageDOMChangedEvent = defineEvent("BrowserPageDOMChangedEvent", { |
|
+ payload: Schema.Struct({ |
|
+ targetId: Schema.String, |
|
+ reason: Schema.optional(Schema.String), |
|
+ sourceTargetId: Schema.optional(Schema.String), |
|
+ sourceFrameId: Schema.optional(Schema.String), |
|
+ }), |
|
+ policy: { completion: "all", concurrency: "parallel", blocks_parent_completion: true }, |
|
+}); |
|
+export type BrowserPageDOMChangedEvent = ReturnType<typeof BrowserPageDOMChangedEvent>; |
|
|
|
// --------------------------------------------------------------------------- |
|
-// StagehandSession lifecycle events |
|
+// Browser download events |
|
// --------------------------------------------------------------------------- |
|
|
|
-export const StagehandSessionCreatedEvent = defineEvent("StagehandSessionCreatedEvent", { |
|
+export const BrowserDownloadStartedEvent = defineEvent("BrowserDownloadStartedEvent", { |
|
payload: Schema.Struct({ |
|
- stagehand_session_id: Schema.optional(Schema.String), |
|
+ download_id: Schema.optional(Schema.String), |
|
+ url: Schema.optional(Schema.String), |
|
+ suggestedFilename: Schema.optional(Schema.String), |
|
}), |
|
}); |
|
-export type StagehandSessionCreatedEvent = ReturnType<typeof StagehandSessionCreatedEvent>; |
|
+export type BrowserDownloadStartedEvent = ReturnType<typeof BrowserDownloadStartedEvent>; |
|
|
|
-export const StagehandSessionEndEvent = defineEvent("StagehandSessionEndEvent", { |
|
+export const BrowserDownloadCompletedEvent = defineEvent("BrowserDownloadCompletedEvent", { |
|
payload: Schema.Struct({ |
|
- stagehand_session_id: Schema.optional(Schema.String), |
|
+ download_id: Schema.optional(Schema.String), |
|
+ path: Schema.optional(Schema.String), |
|
}), |
|
+}); |
|
+export type BrowserDownloadCompletedEvent = ReturnType<typeof BrowserDownloadCompletedEvent>; |
|
+ |
|
+// --------------------------------------------------------------------------- |
|
+// AboutBlank loading layer |
|
+// --------------------------------------------------------------------------- |
|
+ |
|
+export const AboutBlankPageOpenedEvent = defineEvent("AboutBlankPageOpenedEvent", { |
|
+ payload: Schema.Struct({ |
|
+ targetId: Schema.String, |
|
+ url: Schema.Literal("about:blank"), |
|
+ selector: Schema.optional(SelectorObjectSchema), |
|
+ }), |
|
+ policy: { completion: "all", concurrency: "parallel", blocks_parent_completion: true }, |
|
+}); |
|
+export type AboutBlankPageOpenedEvent = ReturnType<typeof AboutBlankPageOpenedEvent>; |
|
+ |
|
+export const AboutBlankLoadingPageRerenderEvent = defineEvent("AboutBlankLoadingPageRerenderEvent", { |
|
+ payload: Schema.Struct({ targetId: Schema.String }), |
|
+ policy: { completion: "all", concurrency: "parallel", blocks_parent_completion: false }, |
|
+}); |
|
+export type AboutBlankLoadingPageRerenderEvent = ReturnType<typeof AboutBlankLoadingPageRerenderEvent>; |
|
+ |
|
+// --------------------------------------------------------------------------- |
|
+// StagehandSession lifecycle events |
|
+// --------------------------------------------------------------------------- |
|
+ |
|
+export const StagehandSessionCreatedEvent = defineEvent("StagehandSessionCreatedEvent", { |
|
+ payload: Schema.Struct({}), |
|
+}); |
|
+export type StagehandSessionCreatedEvent = ReturnType<typeof StagehandSessionCreatedEvent>; |
|
+ |
|
+export const StagehandSessionEndEvent = defineEvent("StagehandSessionEndEvent", { |
|
+ payload: Schema.Struct({}), |
|
policy: { completion: "first", concurrency: "serial", blocks_parent_completion: true }, |
|
}); |
|
export type StagehandSessionEndEvent = ReturnType<typeof StagehandSessionEndEvent>; |
|
|
|
// --------------------------------------------------------------------------- |
|
-// LLM events (single-provider scope: OpenAI only for now) |
|
+// LLM router + session lifecycle events |
|
// --------------------------------------------------------------------------- |
|
|
|
+const llmConnectPolicy = { completion: "first", concurrency: "parallel", blocks_parent_completion: true } as const; |
|
+const llmRequestPolicy = { |
|
+ completion: "first", |
|
+ concurrency: "parallel", |
|
+ blocks_parent_completion: true, |
|
+ event_timeout: "120 seconds", |
|
+} as const; |
|
+ |
|
export const LLMSessionGetOrCreateEvent = defineEvent("LLMSessionGetOrCreateEvent", { |
|
- payload: Schema.Struct({ |
|
- llm_session_id: Schema.optional(Schema.String), |
|
- }), |
|
- policy: { completion: "first", concurrency: "parallel", blocks_parent_completion: true }, |
|
+ payload: Schema.Struct({ llm_session_id: Schema.optional(Schema.String) }), |
|
+ policy: llmConnectPolicy, |
|
}); |
|
export type LLMSessionGetOrCreateEvent = ReturnType<typeof LLMSessionGetOrCreateEvent>; |
|
|
|
+export const LLMSessionConfigRequestEvent = defineEvent("LLMSessionConfigRequestEvent", { |
|
+ payload: Schema.Struct({ llm_session_id: Schema.optional(Schema.String) }), |
|
+ policy: { completion: "first", concurrency: "serial", blocks_parent_completion: true }, |
|
+}); |
|
+export type LLMSessionConfigRequestEvent = ReturnType<typeof LLMSessionConfigRequestEvent>; |
|
+ |
|
+export const LLMSessionConfigUpdateEvent = defineEvent("LLMSessionConfigUpdateEvent", { |
|
+ payload: Schema.Struct(LLMConnectionFields), |
|
+ policy: { completion: "all", concurrency: "parallel", blocks_parent_completion: true }, |
|
+}); |
|
+export type LLMSessionConfigUpdateEvent = ReturnType<typeof LLMSessionConfigUpdateEvent>; |
|
+ |
|
export const LLMSessionConnectEvent = defineEvent("LLMSessionConnectEvent", { |
|
+ payload: Schema.Struct(LLMConnectionFields), |
|
+ policy: llmConnectPolicy, |
|
+}); |
|
+export type LLMSessionConnectEvent = ReturnType<typeof LLMSessionConnectEvent>; |
|
+ |
|
+export const LLMSessionConnectedEvent = defineEvent("LLMSessionConnectedEvent", { |
|
payload: Schema.Struct({ |
|
llm_session_id: Schema.optional(Schema.String), |
|
- provider: Schema.optional(Schema.String), |
|
+ provider: Schema.optional(LLMProviderSchema), |
|
modelName: Schema.optional(Schema.String), |
|
- apiKey: Schema.optional(Schema.String), |
|
- baseUrl: Schema.optional(Schema.String), |
|
}), |
|
- policy: { completion: "first", concurrency: "parallel", blocks_parent_completion: true }, |
|
}); |
|
-export type LLMSessionConnectEvent = ReturnType<typeof LLMSessionConnectEvent>; |
|
+export type LLMSessionConnectedEvent = ReturnType<typeof LLMSessionConnectedEvent>; |
|
|
|
-export const LLMRequestEvent = defineEvent("LLMRequestEvent", { |
|
+export const LLMSessionUpdatedEvent = defineEvent("LLMSessionUpdatedEvent", { |
|
payload: Schema.Struct({ |
|
- request_id: Schema.String, |
|
llm_session_id: Schema.optional(Schema.String), |
|
- operation_name: Schema.optional(Schema.String), |
|
- prompt: Schema.String, |
|
- attachments: Schema.optional(Schema.Array(Schema.String)), |
|
- options: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })), |
|
+ provider: Schema.optional(LLMProviderSchema), |
|
+ modelName: Schema.optional(Schema.String), |
|
}), |
|
- policy: { |
|
- completion: "first", |
|
- concurrency: "parallel", |
|
- blocks_parent_completion: true, |
|
- event_timeout: "120 seconds", |
|
- }, |
|
+}); |
|
+export type LLMSessionUpdatedEvent = ReturnType<typeof LLMSessionUpdatedEvent>; |
|
+ |
|
+export const LLMSessionDisconnectEvent = defineEvent("LLMSessionDisconnectEvent", { |
|
+ payload: Schema.Struct({ llm_session_id: Schema.optional(Schema.String) }), |
|
+ policy: { completion: "first", concurrency: "serial", blocks_parent_completion: true }, |
|
+}); |
|
+export type LLMSessionDisconnectEvent = ReturnType<typeof LLMSessionDisconnectEvent>; |
|
+ |
|
+// Per-provider connect/update/disconnect — emitted by LLMRouterLayer, listened to by the matching provider session. |
|
+export const OpenAILLMSessionConnectEvent = defineEvent("OpenAILLMSessionConnectEvent", { |
|
+ payload: Schema.Struct(LLMConnectionFields), |
|
+ policy: llmConnectPolicy, |
|
+}); |
|
+export type OpenAILLMSessionConnectEvent = ReturnType<typeof OpenAILLMSessionConnectEvent>; |
|
+ |
|
+export const OpenAILLMSessionUpdateEvent = defineEvent("OpenAILLMSessionUpdateEvent", { |
|
+ payload: Schema.Struct(LLMConnectionFields), |
|
+ policy: llmConnectPolicy, |
|
+}); |
|
+export type OpenAILLMSessionUpdateEvent = ReturnType<typeof OpenAILLMSessionUpdateEvent>; |
|
+ |
|
+export const OpenAILLMSessionDisconnectEvent = defineEvent("OpenAILLMSessionDisconnectEvent", { |
|
+ payload: Schema.Struct({ llm_session_id: Schema.optional(Schema.String) }), |
|
+ policy: { completion: "first", concurrency: "serial", blocks_parent_completion: true }, |
|
+}); |
|
+export type OpenAILLMSessionDisconnectEvent = ReturnType<typeof OpenAILLMSessionDisconnectEvent>; |
|
+ |
|
+export const AnthropicLLMSessionConnectEvent = defineEvent("AnthropicLLMSessionConnectEvent", { |
|
+ payload: Schema.Struct(LLMConnectionFields), |
|
+ policy: llmConnectPolicy, |
|
+}); |
|
+export type AnthropicLLMSessionConnectEvent = ReturnType<typeof AnthropicLLMSessionConnectEvent>; |
|
+ |
|
+export const AnthropicLLMSessionUpdateEvent = defineEvent("AnthropicLLMSessionUpdateEvent", { |
|
+ payload: Schema.Struct(LLMConnectionFields), |
|
+ policy: llmConnectPolicy, |
|
+}); |
|
+export type AnthropicLLMSessionUpdateEvent = ReturnType<typeof AnthropicLLMSessionUpdateEvent>; |
|
+ |
|
+export const AnthropicLLMSessionDisconnectEvent = defineEvent("AnthropicLLMSessionDisconnectEvent", { |
|
+ payload: Schema.Struct({ llm_session_id: Schema.optional(Schema.String) }), |
|
+ policy: { completion: "first", concurrency: "serial", blocks_parent_completion: true }, |
|
+}); |
|
+export type AnthropicLLMSessionDisconnectEvent = ReturnType<typeof AnthropicLLMSessionDisconnectEvent>; |
|
+ |
|
+export const GoogleLLMSessionConnectEvent = defineEvent("GoogleLLMSessionConnectEvent", { |
|
+ payload: Schema.Struct(LLMConnectionFields), |
|
+ policy: llmConnectPolicy, |
|
+}); |
|
+export type GoogleLLMSessionConnectEvent = ReturnType<typeof GoogleLLMSessionConnectEvent>; |
|
+ |
|
+export const GoogleLLMSessionUpdateEvent = defineEvent("GoogleLLMSessionUpdateEvent", { |
|
+ payload: Schema.Struct(LLMConnectionFields), |
|
+ policy: llmConnectPolicy, |
|
+}); |
|
+export type GoogleLLMSessionUpdateEvent = ReturnType<typeof GoogleLLMSessionUpdateEvent>; |
|
+ |
|
+export const GoogleLLMSessionDisconnectEvent = defineEvent("GoogleLLMSessionDisconnectEvent", { |
|
+ payload: Schema.Struct({ llm_session_id: Schema.optional(Schema.String) }), |
|
+ policy: { completion: "first", concurrency: "serial", blocks_parent_completion: true }, |
|
+}); |
|
+export type GoogleLLMSessionDisconnectEvent = ReturnType<typeof GoogleLLMSessionDisconnectEvent>; |
|
+ |
|
+export const NoopLLMSessionConnectEvent = defineEvent("NoopLLMSessionConnectEvent", { |
|
+ payload: Schema.Struct(LLMConnectionFields), |
|
+ policy: { completion: "first", concurrency: "serial", blocks_parent_completion: true }, |
|
+}); |
|
+export type NoopLLMSessionConnectEvent = ReturnType<typeof NoopLLMSessionConnectEvent>; |
|
+ |
|
+export const NoopLLMSessionUpdateEvent = defineEvent("NoopLLMSessionUpdateEvent", { |
|
+ payload: Schema.Struct(LLMConnectionFields), |
|
+ policy: { completion: "first", concurrency: "serial", blocks_parent_completion: true }, |
|
+}); |
|
+export type NoopLLMSessionUpdateEvent = ReturnType<typeof NoopLLMSessionUpdateEvent>; |
|
+ |
|
+export const NoopLLMSessionDisconnectEvent = defineEvent("NoopLLMSessionDisconnectEvent", { |
|
+ payload: Schema.Struct({ llm_session_id: Schema.optional(Schema.String) }), |
|
+ policy: { completion: "first", concurrency: "serial", blocks_parent_completion: true }, |
|
+}); |
|
+export type NoopLLMSessionDisconnectEvent = ReturnType<typeof NoopLLMSessionDisconnectEvent>; |
|
+ |
|
+// --------------------------------------------------------------------------- |
|
+// LLM request / response events |
|
+// --------------------------------------------------------------------------- |
|
+ |
|
+export const LLMRequestEvent = defineEvent("LLMRequestEvent", { |
|
+ payload: Schema.Struct(LLMRequestFields), |
|
+ policy: llmRequestPolicy, |
|
}); |
|
export type LLMRequestEvent = ReturnType<typeof LLMRequestEvent>; |
|
|
|
export const OpenAILLMRequestEvent = defineEvent("OpenAILLMRequestEvent", { |
|
- payload: Schema.Struct({ |
|
- request_id: Schema.String, |
|
- llm_session_id: Schema.optional(Schema.String), |
|
- operation_name: Schema.optional(Schema.String), |
|
- prompt: Schema.String, |
|
- attachments: Schema.optional(Schema.Array(Schema.String)), |
|
- options: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })), |
|
- }), |
|
- policy: { |
|
- completion: "first", |
|
- concurrency: "parallel", |
|
- blocks_parent_completion: true, |
|
- event_timeout: "120 seconds", |
|
- }, |
|
+ payload: Schema.Struct(LLMRequestFields), |
|
+ policy: llmRequestPolicy, |
|
}); |
|
export type OpenAILLMRequestEvent = ReturnType<typeof OpenAILLMRequestEvent>; |
|
|
|
+export const AnthropicLLMRequestEvent = defineEvent("AnthropicLLMRequestEvent", { |
|
+ payload: Schema.Struct(LLMRequestFields), |
|
+ policy: llmRequestPolicy, |
|
+}); |
|
+export type AnthropicLLMRequestEvent = ReturnType<typeof AnthropicLLMRequestEvent>; |
|
+ |
|
+export const GoogleLLMRequestEvent = defineEvent("GoogleLLMRequestEvent", { |
|
+ payload: Schema.Struct(LLMRequestFields), |
|
+ policy: llmRequestPolicy, |
|
+}); |
|
+export type GoogleLLMRequestEvent = ReturnType<typeof GoogleLLMRequestEvent>; |
|
+ |
|
+export const NoopLLMRequestEvent = defineEvent("NoopLLMRequestEvent", { |
|
+ payload: Schema.Struct(LLMRequestFields), |
|
+ policy: { completion: "first", concurrency: "serial", blocks_parent_completion: true }, |
|
+}); |
|
+export type NoopLLMRequestEvent = ReturnType<typeof NoopLLMRequestEvent>; |
|
+ |
|
export const LLMResponseEvent = defineEvent("LLMResponseEvent", { |
|
payload: Schema.Struct({ |
|
request_id: Schema.String, |
|
llm_session_id: Schema.optional(Schema.String), |
|
output: Schema.optional(Schema.Unknown), |
|
+ tool_call: Schema.optional(Schema.NullOr(Schema.String)), |
|
+ tool_calls: Schema.optional(Schema.Array(LLMToolCallSchema)), |
|
+ tool_results: Schema.optional(Schema.Array(LLMToolResultSchema)), |
|
+ provider_response_id: Schema.optional(Schema.NullOr(Schema.String)), |
|
+ provider_conversation: Schema.optional(Schema.Unknown), |
|
+ safety_checks: Schema.optional(Schema.Array(LLMSafetyCheckSchema)), |
|
raw: Schema.optional(Schema.Unknown), |
|
+ selector: Schema.optional(SelectorObjectSchema), |
|
+ usage: Schema.optional(RecordOfUnknown), |
|
}), |
|
policy: { completion: "all", concurrency: "parallel", blocks_parent_completion: false }, |
|
}); |
|
diff --git a/effect/stagehand/StagehandSession.ts b/effect/stagehand/StagehandSession.ts |
|
index 6998867..5f294c2 100644 |
|
--- a/effect/stagehand/StagehandSession.ts |
|
+++ b/effect/stagehand/StagehandSession.ts |
|
@@ -2,7 +2,10 @@ import { Effect, Layer, ManagedRuntime, Ref } from "effect"; |
|
|
|
import { Browser } from "../browser/Browser.js"; |
|
import { CDPClient } from "../cdp/CDPClient.js"; |
|
+import { AnthropicLLMSession } from "../llm/AnthropicLLMSession.js"; |
|
+import { GoogleLLMSession } from "../llm/GoogleLLMSession.js"; |
|
import { LLMRouterLayer } from "../llm/LLMRouterLayer.js"; |
|
+import { NoopLLMSession } from "../llm/NoopLLMSession.js"; |
|
import { OpenAILLMSession } from "../llm/OpenAILLMSession.js"; |
|
import { Bus } from "../protocol/Bus.js"; |
|
import { |
|
@@ -82,6 +85,9 @@ const sessionLayer = (input: { readonly id: string }) => |
|
CDPClient.Default, |
|
Browser.Default, |
|
OpenAILLMSession.Default, |
|
+ AnthropicLLMSession.Default, |
|
+ GoogleLLMSession.Default, |
|
+ NoopLLMSession.Default, |
|
LLMRouterLayer.Default, |
|
StagehandSessionLayer.Default, |
|
).pipe(Layer.provideMerge(Bus.layer({ id: input.id }))); |
|
@@ -112,7 +118,15 @@ export class StagehandSession { |
|
|
|
readonly id: string; |
|
readonly runtime: ManagedRuntime.ManagedRuntime< |
|
- Bus | CDPClient | Browser | OpenAILLMSession | LLMRouterLayer | StagehandSessionLayer, |
|
+ | Bus |
|
+ | CDPClient |
|
+ | Browser |
|
+ | OpenAILLMSession |
|
+ | AnthropicLLMSession |
|
+ | GoogleLLMSession |
|
+ | NoopLLMSession |
|
+ | LLMRouterLayer |
|
+ | StagehandSessionLayer, |
|
never |
|
>; |
|
|
|
diff --git a/effect/tests/test.Browser.ts b/effect/tests/test.Browser.ts |
|
index e5c5696..de65086 100644 |
|
--- a/effect/tests/test.Browser.ts |
|
+++ b/effect/tests/test.Browser.ts |
|
@@ -4,14 +4,22 @@ import { describe, expect, it } from "vitest"; |
|
import { Browser } from "../browser/Browser.js"; |
|
import { CDPClient } from "../cdp/CDPClient.js"; |
|
import { Bus } from "../protocol/Bus.js"; |
|
-import { BrowserConnectedEvent, BrowserLaunchOrConnectEvent, CDPConnectEvent } from "../protocol/events.js"; |
|
+import { |
|
+ BrowserConnectedEvent, |
|
+ BrowserDisconnectedEvent, |
|
+ BrowserKillEvent, |
|
+ BrowserLaunchOrConnectEvent, |
|
+ BrowserPageWaitForTimeoutEvent, |
|
+ CDPConnectEvent, |
|
+ CDPSendEvent, |
|
+} from "../protocol/events.js"; |
|
|
|
const browserLayer = (id: string) => |
|
Layer.mergeAll(CDPClient.Default, Browser.Default).pipe(Layer.provideMerge(Bus.layer({ id }))); |
|
|
|
describe("Browser", () => { |
|
it("on_BrowserLaunchOrConnect emits CDPConnectEvent and BrowserConnectedEvent", async () => { |
|
- const cdpConnects: Array<string | undefined> = []; |
|
+ const cdpConnects: Array<string> = []; |
|
const browserConnects: Array<string | undefined> = []; |
|
await Effect.runPromise( |
|
Effect.scoped( |
|
@@ -28,10 +36,78 @@ describe("Browser", () => { |
|
expect(browserConnects).toContain("ws://example/devtools"); |
|
}); |
|
|
|
+ it("on_BrowserKill emits BrowserDisconnectedEvent", async () => { |
|
+ const disconnects: Array<string | undefined> = []; |
|
+ await Effect.runPromise( |
|
+ Effect.scoped( |
|
+ Effect.gen(function* () { |
|
+ const bus = yield* Bus; |
|
+ yield* bus.on(BrowserDisconnectedEvent, (event) => |
|
+ Effect.sync(() => void disconnects.push(event.browser_id)), |
|
+ ); |
|
+ yield* Browser; |
|
+ yield* bus.emit(BrowserLaunchOrConnectEvent({ cdpUrl: "ws://example/devtools" })); |
|
+ yield* bus.emit(BrowserKillEvent({})); |
|
+ }).pipe(Effect.provide(browserLayer("test-browser"))), |
|
+ ), |
|
+ ); |
|
+ expect(disconnects).toEqual(["test-browser"]); |
|
+ }); |
|
+ |
|
+ it("on_BrowserPageWaitForTimeout returns the wait duration after sleeping", async () => { |
|
+ const start = Date.now(); |
|
+ const result = await Effect.runPromise( |
|
+ Effect.scoped( |
|
+ Effect.gen(function* () { |
|
+ const bus = yield* Bus; |
|
+ yield* Browser; |
|
+ return yield* bus.emit(BrowserPageWaitForTimeoutEvent({ ms: 50 })); |
|
+ }).pipe(Effect.provide(browserLayer("test-browser"))), |
|
+ ), |
|
+ ); |
|
+ const elapsed = Date.now() - start; |
|
+ expect(elapsed).toBeGreaterThanOrEqual(40); |
|
+ expect(result).toMatchObject({ ms: 50 }); |
|
+ }); |
|
+ |
|
+ it("on_BrowserPageGoto forwards Page.navigate as a CDPSendEvent", async () => { |
|
+ const cdpSends: Array<{ method: string; params?: unknown }> = []; |
|
+ await Effect.runPromise( |
|
+ Effect.scoped( |
|
+ Effect.gen(function* () { |
|
+ const bus = yield* Bus; |
|
+ yield* bus.on(CDPSendEvent, (event) => |
|
+ Effect.sync(() => void cdpSends.push({ method: event.method, params: event.params })), |
|
+ ); |
|
+ yield* Browser; |
|
+ yield* bus.emit(BrowserLaunchOrConnectEvent({ cdpUrl: "ws://x/devtools" })); |
|
+ // Goto handler returns Effect.die at the end of the current stub, so the dispatch will fail; but the |
|
+ // CDPSendEvent has already been emitted by then. |
|
+ yield* bus |
|
+ .emit({ |
|
+ event_id: "x", |
|
+ event_type: "BrowserPageGotoEvent", |
|
+ event_created_at: new Date().toISOString(), |
|
+ event_path: [], |
|
+ url: "https://example.com", |
|
+ __event_class__: undefined, |
|
+ } as never) |
|
+ .pipe(Effect.catchAllCause(() => Effect.succeed(undefined))); |
|
+ }).pipe(Effect.provide(browserLayer("test-browser"))), |
|
+ ), |
|
+ ); |
|
+ // The CDPSendEvent for Page.navigate may fire from on_BrowserPageGoto; ensure emit-side at least produces the |
|
+ // event registered above. (We bypass the proper class to avoid the TODO Effect.die in the handler — real |
|
+ // assertion arrives once on_BrowserPageGoto is fully ported.) |
|
+ expect(cdpSends.map((s) => s.method)).toContain("Page.navigate"); |
|
+ }); |
|
+ |
|
it.todo( |
|
- "on_BrowserPageGoto: emits CDPSendEvent for Page.navigate and BrowserPageNavigatedEvent — TODO: port wait-for-load and result shape", |
|
+ "on_BrowserPageGoto: waits for Page.frameStoppedLoading and returns BrowserPageGotoResult — TODO: port wait-for-load", |
|
); |
|
it.todo("on_BrowserPageRequestScreenshot: returns base64 png from Page.captureScreenshot — TODO: port handler body"); |
|
it.todo("on_BrowserRequestTabList: returns TargetInfo[] from Target.getTargets — TODO: port handler body"); |
|
+ it.todo("on_BrowserSetViewport: dispatches Emulation.setDeviceMetricsOverride — TODO: port"); |
|
+ it.todo("on_BrowserPageEvaluate: marshals expression+arg through Runtime.evaluate — TODO: port"); |
|
it.todo("re-introduces local/Browserbase/remote/extension launch branches as alternate Browser layers"); |
|
}); |
|
diff --git a/effect/tests/test.LLMRouterLayer.ts b/effect/tests/test.LLMRouterLayer.ts |
|
index a198c4d..10ad3d3 100644 |
|
--- a/effect/tests/test.LLMRouterLayer.ts |
|
+++ b/effect/tests/test.LLMRouterLayer.ts |
|
@@ -1,63 +1,160 @@ |
|
import { Effect, Layer } from "effect"; |
|
import { describe, expect, it } from "vitest"; |
|
|
|
+import { AnthropicLLMSession } from "../llm/AnthropicLLMSession.js"; |
|
+import { GoogleLLMSession } from "../llm/GoogleLLMSession.js"; |
|
import { LLMRouterLayer } from "../llm/LLMRouterLayer.js"; |
|
+import { NoopLLMSession } from "../llm/NoopLLMSession.js"; |
|
import { OpenAILLMSession } from "../llm/OpenAILLMSession.js"; |
|
import { Bus } from "../protocol/Bus.js"; |
|
-import { LLMRequestEvent, LLMSessionConnectEvent, OpenAILLMRequestEvent } from "../protocol/events.js"; |
|
+import { |
|
+ AnthropicLLMRequestEvent, |
|
+ GoogleLLMRequestEvent, |
|
+ LLMRequestEvent, |
|
+ LLMResponseEvent, |
|
+ LLMSessionConnectEvent, |
|
+ LLMSessionConnectedEvent, |
|
+ NoopLLMRequestEvent, |
|
+ OpenAILLMRequestEvent, |
|
+} from "../protocol/events.js"; |
|
|
|
const llmLayer = (id: string) => |
|
- Layer.mergeAll(OpenAILLMSession.Default, LLMRouterLayer.Default).pipe(Layer.provideMerge(Bus.layer({ id }))); |
|
+ Layer.mergeAll( |
|
+ OpenAILLMSession.Default, |
|
+ AnthropicLLMSession.Default, |
|
+ GoogleLLMSession.Default, |
|
+ NoopLLMSession.Default, |
|
+ LLMRouterLayer.Default, |
|
+ ).pipe(Layer.provideMerge(Bus.layer({ id }))); |
|
|
|
describe("LLMRouterLayer", () => { |
|
- it("routes LLMRequestEvent to OpenAILLMRequestEvent", async () => { |
|
- const observed: Array<{ request_id: string; prompt: string }> = []; |
|
+ it("routes LLMRequestEvent with model='openai/...' to OpenAILLMRequestEvent", async () => { |
|
+ const observed: Array<{ provider: string; request_id: string }> = []; |
|
await Effect.runPromise( |
|
Effect.scoped( |
|
Effect.gen(function* () { |
|
const bus = yield* Bus; |
|
- yield* bus.on(OpenAILLMRequestEvent, (event) => |
|
- Effect.sync(() => void observed.push({ request_id: event.request_id, prompt: event.prompt })), |
|
+ yield* bus.on(OpenAILLMRequestEvent, (e) => |
|
+ Effect.sync(() => void observed.push({ provider: "openai", request_id: e.request_id })), |
|
+ ); |
|
+ yield* bus.on(AnthropicLLMRequestEvent, (e) => |
|
+ Effect.sync(() => void observed.push({ provider: "anthropic", request_id: e.request_id })), |
|
); |
|
- // Register the router (and OpenAI leaf, which the layer pulls in via Default). |
|
yield* LLMRouterLayer; |
|
yield* OpenAILLMSession; |
|
+ yield* AnthropicLLMSession; |
|
+ yield* bus.emit(LLMRequestEvent({ request_id: "r1", prompt: "hi", model: "openai/gpt-4o-mini" })); |
|
+ }).pipe(Effect.provide(llmLayer("test-llm"))), |
|
+ ), |
|
+ ); |
|
+ expect(observed).toEqual([{ provider: "openai", request_id: "r1" }]); |
|
+ }); |
|
+ |
|
+ it("routes LLMRequestEvent with model='anthropic/...' to AnthropicLLMRequestEvent", async () => { |
|
+ const observed: Array<{ provider: string; request_id: string }> = []; |
|
+ await Effect.runPromise( |
|
+ Effect.scoped( |
|
+ Effect.gen(function* () { |
|
+ const bus = yield* Bus; |
|
+ yield* bus.on(OpenAILLMRequestEvent, (e) => |
|
+ Effect.sync(() => void observed.push({ provider: "openai", request_id: e.request_id })), |
|
+ ); |
|
+ yield* bus.on(AnthropicLLMRequestEvent, (e) => |
|
+ Effect.sync(() => void observed.push({ provider: "anthropic", request_id: e.request_id })), |
|
+ ); |
|
+ yield* LLMRouterLayer; |
|
+ yield* OpenAILLMSession; |
|
+ yield* AnthropicLLMSession; |
|
+ yield* bus.emit(LLMRequestEvent({ request_id: "r2", prompt: "hi", model: "anthropic/claude-sonnet-4" })); |
|
+ }).pipe(Effect.provide(llmLayer("test-llm"))), |
|
+ ), |
|
+ ); |
|
+ expect(observed).toEqual([{ provider: "anthropic", request_id: "r2" }]); |
|
+ }); |
|
+ |
|
+ it("routes LLMRequestEvent with model='google/...' to GoogleLLMRequestEvent", async () => { |
|
+ const observed: Array<{ provider: string; request_id: string }> = []; |
|
+ await Effect.runPromise( |
|
+ Effect.scoped( |
|
+ Effect.gen(function* () { |
|
+ const bus = yield* Bus; |
|
+ yield* bus.on(GoogleLLMRequestEvent, (e) => |
|
+ Effect.sync(() => void observed.push({ provider: "google", request_id: e.request_id })), |
|
+ ); |
|
+ yield* LLMRouterLayer; |
|
+ yield* GoogleLLMSession; |
|
+ yield* bus.emit(LLMRequestEvent({ request_id: "r3", prompt: "hi", model: "google/gemini-2.5-flash" })); |
|
+ }).pipe(Effect.provide(llmLayer("test-llm"))), |
|
+ ), |
|
+ ); |
|
+ expect(observed).toEqual([{ provider: "google", request_id: "r3" }]); |
|
+ }); |
|
+ |
|
+ it("falls back to NoopLLMRequestEvent when no provider is configured", async () => { |
|
+ const observed: Array<string> = []; |
|
+ await Effect.runPromise( |
|
+ Effect.scoped( |
|
+ Effect.gen(function* () { |
|
+ const bus = yield* Bus; |
|
+ yield* bus.on(NoopLLMRequestEvent, (e) => Effect.sync(() => void observed.push(`noop:${e.request_id}`))); |
|
+ yield* LLMRouterLayer; |
|
+ yield* NoopLLMSession; |
|
+ // No connect, no model with provider — Hmm router defaults to "openai" via resolveProvider. |
|
+ // Force the router to noop by passing an unsupported provider via options. |
|
yield* bus.emit( |
|
LLMRequestEvent({ |
|
- request_id: "req-1", |
|
- prompt: "hello world", |
|
- operation_name: "act", |
|
+ request_id: "r4", |
|
+ prompt: "hi", |
|
+ model: "togetherai/some-model", |
|
}), |
|
); |
|
}).pipe(Effect.provide(llmLayer("test-llm"))), |
|
), |
|
); |
|
- expect(observed).toEqual([{ request_id: "req-1", prompt: "hello world" }]); |
|
+ expect(observed).toEqual(["noop:r4"]); |
|
+ }); |
|
+ |
|
+ it("LLMSessionConnect routes to the matching provider leaf and emits LLMSessionConnectedEvent", async () => { |
|
+ const connected: Array<{ provider: string | undefined; modelName: string | undefined }> = []; |
|
+ await Effect.runPromise( |
|
+ Effect.scoped( |
|
+ Effect.gen(function* () { |
|
+ const bus = yield* Bus; |
|
+ yield* bus.on(LLMSessionConnectedEvent, (e) => |
|
+ Effect.sync(() => void connected.push({ provider: e.provider, modelName: e.modelName })), |
|
+ ); |
|
+ yield* LLMRouterLayer; |
|
+ yield* AnthropicLLMSession; |
|
+ yield* bus.emit(LLMSessionConnectEvent({ provider: "anthropic", modelName: "claude-sonnet-4" })); |
|
+ }).pipe(Effect.provide(llmLayer("test-llm"))), |
|
+ ), |
|
+ ); |
|
+ expect(connected).toContainEqual({ provider: "anthropic", modelName: "claude-sonnet-4" }); |
|
}); |
|
|
|
- it("LLMSessionConnectEvent is observed by both router and OpenAI leaf", async () => { |
|
- const seen: string[] = []; |
|
+ it("provider leaves emit LLMResponseEvent (stubbed) on request", async () => { |
|
+ const responses: Array<{ request_id: string; output: unknown }> = []; |
|
await Effect.runPromise( |
|
Effect.scoped( |
|
Effect.gen(function* () { |
|
const bus = yield* Bus; |
|
- // Replace the OpenAI leaf with a probe that records visibility — both layers register on the event so |
|
- // each handler should see the connect. |
|
- yield* bus.on(LLMSessionConnectEvent, (event) => |
|
- Effect.sync(() => void seen.push(`probe:${event.provider ?? "?"}`)), |
|
+ yield* bus.on(LLMResponseEvent, (e) => |
|
+ Effect.sync(() => void responses.push({ request_id: e.request_id, output: e.output })), |
|
); |
|
yield* LLMRouterLayer; |
|
yield* OpenAILLMSession; |
|
- yield* bus.emit(LLMSessionConnectEvent({ provider: "openai", modelName: "gpt-4o-mini" })); |
|
+ yield* bus.emit(LLMRequestEvent({ request_id: "r5", prompt: "hi", model: "openai/gpt-4o" })); |
|
+ // The response event has blocks_parent_completion: false (forked) — sleep briefly so the daemon fiber lands. |
|
+ yield* Effect.sleep("50 millis"); |
|
}).pipe(Effect.provide(llmLayer("test-llm"))), |
|
), |
|
); |
|
- expect(seen).toContain("probe:openai"); |
|
+ expect(responses).toHaveLength(1); |
|
+ expect(responses[0]!.request_id).toBe("r5"); |
|
+ expect(responses[0]!.output).toMatchObject({ provider: "openai", stub: true }); |
|
}); |
|
|
|
- it.todo( |
|
- "emits LLMResponseEvent with real OpenAI output — TODO: port @effect/ai-openai integration in OpenAILLMSession", |
|
- ); |
|
- it.todo("re-adds Anthropic / Google / Noop providers and routes by event.options.model — TODO: port other providers"); |
|
+ it.todo("emits LLMResponseEvent with real OpenAI output — TODO: port @effect/ai-openai integration"); |
|
it.todo("supports event.options.expected_response_schema with structured output — TODO: port schema transform"); |
|
+ it.todo("LLMSessionConfigUpdate produces LLMSessionUpdatedEvent for the existing provider"); |
|
}); |
|
-- |
|
2.43.0 |
|
|