Browser dag - #222
Conversation
- Added a default transport type of "streamable-http" in the MCP server configuration schema for both browser and node clients. - Enhanced error handling in the `createMcpClient` function to close the client on a 405 Method Not Allowed error, providing clearer guidance on using the correct transport type instead of SSE.
- Added new browser automation tasks: BrowserNavigateTask, BrowserClickTask, BrowserExtractTask, BrowserEvaluateTask, BrowserTransformTask, BrowserWaitTask, BrowserCloseTask.
- Implemented RunCleanupRegistry for managing cleanup tasks after graph execution.
- Updated TaskGraphRunner to utilize the RunCleanupRegistry for resource management.
- Enhanced documentation to include new browser automation capabilities and usage examples.
Plan: DAG-Style Playwright Browser Nodes with Shared Context
Summary
Build Node/Bun-only Playwright task nodes for DAG workflows using @workglow/tasks, with explicit context passing (context object) and runtime session indirection (session_id) so dataflow remains serializable.
Required nodes: Navigate, Extract, Click, Wait, Evaluate, Transform.
Add Close as a lifecycle node, plus automatic run-end cleanup safety.
Public API Changes
New task classes in @workglow/tasks (Node/Bun entrypoints):
BrowserNavigateTask
BrowserExtractTask
BrowserClickTask
BrowserWaitTask
BrowserEvaluateTask
BrowserTransformTask
BrowserCloseTask (lifecycle support)
New workflow helpers (namespaced to avoid collisions):
workflow.browserNavigate(...)
workflow.browserExtract(...)
workflow.browserClick(...)
workflow.browserWait(...)
workflow.browserEvaluate(...)
workflow.browserTransform(...)
workflow.browserClose(...)
Shared context contract (all browser tasks):
Input contains optional context: Record<string, unknown> and optional session_id: string
Output always returns context
Reserved metadata path: context.__browser = { session_id: string, url?: string, title?: string }
Raw Playwright handles never enter task input/output dataflow
Core runtime cleanup API in @workglow/task-graph:
Add a generic run-scoped cleanup registry token/interface in task-graph
TaskGraph runner executes registered cleanup handlers on complete, error, abort, disable
Browser tasks register session close handlers through this generic cleanup registry
Detailed Implementation
Add generic run cleanup support in task-graph:
Files: RunCleanup.ts (new), index.ts, common.ts, TaskGraphRunner.ts
Introduce RunCleanupRegistry with deduped add(key, fn) and runAll()
In TaskGraphRunner.handleStart, register a fresh cleanup registry in run-scoped ServiceRegistry
In handleComplete, handleError, handleAbort, handleDisable, call cleanup execution (idempotent)
Add browser session manager and Playwright loader in tasks package:
Files: BrowserSessionManager.ts (new), loadPlaywright.ts (new), types.ts (new)
BrowserSessionManager responsibilities:
Map session_id -> { browser, context, page }
getOrCreateSession, getSessionOrThrow, closeSession, closeAll
per-session mutex (runExclusive) to serialize operations on same session while allowing parallelism across sessions
loadPlaywright.ts uses dynamic import and throws clear config error if missing (bun add playwright / npm i playwright)
Implement browser task nodes:
Files (new):
BrowserNavigateTask.ts
BrowserExtractTask.ts
BrowserClickTask.ts
BrowserWaitTask.ts
BrowserEvaluateTask.ts
BrowserTransformTask.ts
BrowserCloseTask.ts
All browser tasks set static cacheable = false, category "Browser", and strict JSON schemas
Required behavior by task:
Navigate: create/reuse session, goto URL, update context.__browser, return navigation metadata
Click: resolve session, click selector, optional wait-for-navigation/load-state, return updated context
Wait: support one mode per run (timeout | selector | url | load_state | function), validate mode-specific fields
Extract: selector-driven extraction specs (text/html/attr/property/count/exists/list variants), return data
Evaluate: trusted JS string executed in page with { args, context }, return result
Transform: trusted JS string executed host-side with { input, context, data }, return { context, data }
Close: close one session id and remove/clear context.__browser (or only session_id field if configured)
Runtime-specific exports and registration:
Update node.ts and bun.ts to export/register browser tasks
Do not export/register browser tasks in browser.ts or common.ts
Add shared browser task export barrel for node/bun only if useful (index.ts)
Package dependency model:
Update package.json:
Add playwright as optional peer dependency
Add peerDependenciesMeta.playwright.optional = true
Keep dynamic import path to avoid forcing install for non-browser users
Documentation and examples:
Update README.md with browser section:
context/session pattern
full chain example using required six nodes
explicit browserClose usage and note about auto-cleanup
security note: Evaluate/Transform run trusted JS; use existing JavaScriptTask for safer interpreted transformations when needed
Node I/O Shape (Decision-Complete)
Common input fields for browser nodes:
context?: object default {}
session_id?: string (overrides context.__browser.session_id when provided)
timeout_ms?: number default 30000 (task-specific defaults allowed)
Common output fields:
context: object always emitted
Task-specific payload:
Navigate: url, title, status?, ok?
Extract: data
Click: clicked: true, url
Wait: waited: true
Evaluate: result
Transform: data?
Close: closed: true
Testing Plan
Unit tests for tasks (mock Playwright):
BrowserNavigateTask.test.ts
BrowserExtractTask.test.ts
BrowserClickWaitEvaluateTask.test.ts
BrowserTransformTask.test.ts
BrowserCloseTask.test.ts
DAG integration tests:
BrowserWorkflowDag.test.ts
Validate auto-connection of context across chain
Validate explicit branch behavior with manual connect() when needed
Validate per-session mutex ordering under parallel scheduler
Cleanup tests:
RunCleanupRegistry.test.ts
Verify run-end cleanup on success/failure/abort closes open browser sessions
Verify explicit close + run-end cleanup remains idempotent
Dependency/error tests:
missing Playwright dependency emits actionable TaskConfigurationError
missing session id for non-navigate browser nodes yields clear validation error
Compatibility and Rollout
Backward compatibility:
Additive API only; existing tasks/workflows unchanged
Browser tasks available only in Node/Bun package entrypoints
Performance/runtime:
Sessions reused by session_id to avoid relaunch overhead
Per-session serialization avoids racey page mutations in parallel DAG branches
Security stance:
Evaluate and Transform intentionally execute trusted JS strings
Document trust boundary and recommended safer alternative via existing JavaScriptTask
Assumptions and Defaults
Runtime scope: Node + Bun only.
Context model: serializable context + session_id; no raw Playwright objects in dataflow.
API naming: namespaced browser* helpers.
Scripting model: trusted JS strings for Evaluate/Transform; JavaScriptTask remains available for interpreted transforms.
Lifecycle: explicit close node plus automatic run-end cleanup.
Dependency model: optional Playwright peer dependency with dynamic import.
Skills: none used (request does not match available skill-creator/skill-installer workflows).
There was a problem hiding this comment.
Pull request overview
This PR adds a comprehensive browser automation task suite using Playwright, along with a run-scoped cleanup registry to ensure proper resource management. The implementation includes seven new browser tasks (Navigate, Extract, Click, Wait, Evaluate, Transform, Close) with context-based session management, automatic cleanup, and mutual exclusion for same-session operations.
Changes:
- Added browser automation tasks with Playwright integration for Node.js and Bun runtimes
- Implemented RunCleanupRegistry for automatic resource cleanup on task graph completion, failure, abort, or disable
- Enhanced MCP client error messages and added default transport configuration
Reviewed changes
Copilot reviewed 31 out of 32 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/util/src/mcp/McpClientUtil.node.ts | Added default transport and improved 405 error handling with client cleanup |
| packages/util/src/mcp/McpClientUtil.browser.ts | Added default transport and improved 405 error handling with client cleanup |
| packages/test/src/test/task/browserTestRuntime.ts | Test harness for browser tasks with mock Playwright API |
| packages/test/src/test/task/BrowserTransformTask.test.ts | Unit tests for browser transform task |
| packages/test/src/test/task/BrowserNavigateTask.test.ts | Unit tests for browser navigate task |
| packages/test/src/test/task/BrowserExtractTask.test.ts | Unit tests for browser extract task |
| packages/test/src/test/task/BrowserDependencyErrors.test.ts | Tests for Playwright dependency error messages |
| packages/test/src/test/task/BrowserCloseTask.test.ts | Unit tests for browser close task |
| packages/test/src/test/task/BrowserClickWaitEvaluateTask.test.ts | Unit tests for click, wait, and evaluate tasks |
| packages/test/src/test/task-graph/RunCleanupRegistry.test.ts | Tests for run-scoped cleanup registry |
| packages/test/src/test/task-graph/BrowserWorkflowDag.test.ts | Integration tests for browser workflow chains |
| packages/tasks/src/types.ts | Exported browser task types |
| packages/tasks/src/task/browser/types.ts | Core types and utilities for browser task context management |
| packages/tasks/src/task/browser/loadPlaywright.ts | Lazy Playwright loader with helpful error messages |
| packages/tasks/src/task/browser/index.ts | Browser task exports |
| packages/tasks/src/task/browser/BrowserWaitTask.ts | Task for waiting on timeouts, selectors, URLs, load states, or functions |
| packages/tasks/src/task/browser/BrowserTransformTask.ts | Task for host-side JavaScript transformation of context and data |
| packages/tasks/src/task/browser/BrowserSessionManager.ts | Session manager with mutex for exclusive access |
| packages/tasks/src/task/browser/BrowserNavigateTask.ts | Task for navigating to URLs and creating sessions |
| packages/tasks/src/task/browser/BrowserExtractTask.ts | Task for extracting data from pages using selectors |
| packages/tasks/src/task/browser/BrowserEvaluateTask.ts | Task for executing JavaScript in page context |
| packages/tasks/src/task/browser/BrowserCloseTask.ts | Task for closing browser sessions |
| packages/tasks/src/task/browser/BrowserClickTask.ts | Task for clicking elements |
| packages/tasks/src/node.ts | Registered browser tasks for Node.js runtime |
| packages/tasks/src/bun.ts | Registered browser tasks for Bun runtime |
| packages/tasks/package.json | Added optional Playwright peer dependency |
| packages/tasks/README.md | Added comprehensive browser task documentation |
| packages/task-graph/src/task/index.ts | Exported RunCleanup module |
| packages/task-graph/src/task/RunCleanup.ts | Cleanup registry implementation |
| packages/task-graph/src/task-graph/TaskGraphRunner.ts | Integrated cleanup registry into graph lifecycle |
| packages/task-graph/src/task-graph/TaskGraph.ts | Added registry config passthrough |
| bun.lock | Updated lockfile with Playwright dependency |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (!sessionId) { | ||
| throw new TaskConfigurationError("No browser session id found for close operation"); | ||
| } |
There was a problem hiding this comment.
The resolveSessionId function is called with required=true but the result is then checked for null/undefined. This creates unreachable code since resolveSessionId will throw a TaskConfigurationError when required=true and no session_id is found. Either remove the null check or change required to false.
| if (!sessionId) { | |
| throw new TaskConfigurationError("No browser session id found for close operation"); | |
| } |
| async waitForFunction(fn: (arg: any) => unknown, arg: any) { | ||
| state.waitEvents.push("function"); | ||
| const value = fn(arg); | ||
| if (!value) { | ||
| throw new TaskConfigurationError("waitForFunction predicate returned false"); |
There was a problem hiding this comment.
The test runtime implementation doesn't accurately simulate the real Playwright waitForFunction behavior. In real Playwright, waitForFunction polls the predicate until it returns a truthy value or times out. This implementation only runs the predicate once and throws if it's falsy, which doesn't accurately test the retry/polling behavior that the actual task will encounter.
| async waitForFunction(fn: (arg: any) => unknown, arg: any) { | |
| state.waitEvents.push("function"); | |
| const value = fn(arg); | |
| if (!value) { | |
| throw new TaskConfigurationError("waitForFunction predicate returned false"); | |
| async waitForFunction( | |
| fn: (arg: any) => unknown | Promise<unknown>, | |
| arg: any, | |
| options?: { timeoutMs?: number; pollingIntervalMs?: number } | |
| ) { | |
| state.waitEvents.push("function"); | |
| const timeoutMs = options?.timeoutMs ?? 1000; | |
| const pollingIntervalMs = options?.pollingIntervalMs ?? 50; | |
| const start = Date.now(); | |
| // Poll the predicate until it becomes truthy or times out, similar to Playwright's behavior. | |
| // This allows tests to exercise retry logic instead of relying on a single evaluation. | |
| // eslint-disable-next-line no-constant-condition | |
| while (true) { | |
| const value = await fn(arg); | |
| if (value) { | |
| return; | |
| } | |
| if (Date.now() - start >= timeoutMs) { | |
| throw new TaskConfigurationError( | |
| "waitForFunction predicate did not become truthy within the timeout" | |
| ); | |
| } | |
| await sleep(pollingIntervalMs); |
No description provided.