feat(operator): add Playwright MCP service for web automation#335
Conversation
📝 WalkthroughWalkthroughThis PR introduces the Tale Operator service, a new browser automation service using OpenCode MCP with Playwright, while removing the existing Search service. The changes include a comprehensive API refactoring renaming Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Comment |
There was a problem hiding this comment.
Actionable comments posted: 40
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
services/platform/convex/lib/action_cache/index.ts (1)
81-86:⚠️ Potential issue | 🟡 MinorInconsistent removal of ESLint directive while
as anycast remains.The ESLint disable directive for
@typescript-eslint/no-explicit-anywas removed, but line 86 still uses(internal.tone_of_voice as any). This will cause ESLint to flag theas anyusage.Either:
- Keep the ESLint directive if the
as anycast is still required (as the comment on line 81 suggests the types need regeneration), or- Remove both the directive and the
as anycast if the types have been properly generatedOption 1: Restore the ESLint directive
// Note: The internal_actions module needs to be regenerated (run `npx convex dev`) - +// eslint-disable-next-line `@typescript-eslint/no-explicit-any` export const toneOfVoiceCache: ActionCache<services/platform/convex/lib/context_management/check_and_summarize.ts (1)
66-73:⚠️ Potential issue | 🟡 MinorFix the comment to match
excludeToolMessages: true.The comment says tool messages are included, but the flag excludes them.
✏️ Suggested fix
- // Estimate context size with tool messages included + // Estimate context size with tool messages excludedservices/platform/convex/customers/bulk_create_customers.ts (1)
77-81:⚠️ Potential issue | 🟠 MajorReplace
metadata’sanycast with a guarded object cast.With the ESLint suppression removed,
metadata: customerData.metadata as anywill likely violateno-explicit-anyand still bypass type safety. Guard the value and cast toRecord<string, unknown>(or reject non-objects) before insert.Based on learnings: after confirming a v.any() field is a plain object, cast to Record at the usage site.🛠️ Proposed fix
+ let metadata: Record<string, unknown> | undefined; + if (customerData.metadata !== undefined) { + if ( + typeof customerData.metadata === 'object' && + customerData.metadata !== null && + !Array.isArray(customerData.metadata) + ) { + metadata = customerData.metadata as Record<string, unknown>; + } else { + throw new Error('Customer metadata must be an object'); + } + } + await ctx.db.insert('customers', { organizationId, ...customerData, - metadata: customerData.metadata as any, + metadata, });services/platform/convex/conversations/create_conversation_with_message.ts (1)
92-98: 🧹 Nitpick | 🔵 TrivialStray blank line from ESLint directive removal.
The
as anycast on the metadata object literal is appropriate for handling the dynamically-typed schema field. Remove the blank line on line 92 for consistency.🧹 Remove stray blank line
deliveredAt: args.initialMessage.deliveredAt ? args.initialMessage.deliveredAt : direction === 'inbound' && args.initialMessage.sentAt ? args.initialMessage.sentAt : undefined, - metadata: {services/platform/app/hooks/use-offline-entity-data.ts (1)
87-91: 🧹 Nitpick | 🔵 TrivialReplace
config.queryFn as anynow that lint suppression is removed.If
useQueryacceptsTQuerydirectly, drop the cast; otherwise use a narrow helper type instead ofanyto preserve type safety.Proposed change
- const liveData = useQuery( - config.queryFn as any, - isOnline ? { organizationId } : 'skip' - ); + const liveData = useQuery( + config.queryFn, + isOnline ? { organizationId } : 'skip' + );.github/workflows/release.yml (3)
434-434:⚠️ Potential issue | 🔴 CriticalCritical:
summaryjob depends on non-existentbuild-searchjob.The
needsarray referencesbuild-searchbut this job was renamed tobuild-operatoron line 358. This will cause the release workflow to fail.🐛 Proposed fix
summary: name: Release Summary needs: - prepare - build-platform - build-rag - build-crawler - build-db - build-graph-db - build-proxy - - build-search + - build-operator - trigger-cli
21-21: 🧹 Nitpick | 🔵 TrivialUpdate comment to reflect operator naming.
The header comment still references
tale-searchbut should referencetale-operatorto match the renamed service.📝 Proposed fix
-# - ghcr.io/tale-project/tale/tale-search:1.0.0 +# - ghcr.io/tale-project/tale/tale-operator:1.0.0
455-455: 🧹 Nitpick | 🔵 TrivialUpdate summary table to reference operator instead of search.
The summary output still displays
tale-searchbut should displaytale-operator.📝 Proposed fix
- echo "| Search | \`${{ env.REGISTRY }}/${{ github.repository }}/tale-search:${{ needs.prepare.outputs.version_number }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Operator | \`${{ env.REGISTRY }}/${{ github.repository }}/tale-operator:${{ needs.prepare.outputs.version_number }}\` |" >> $GITHUB_STEP_SUMMARY
🤖 Fix all issues with AI agents
In `@services/operator/app/__init__.py`:
- Around line 1-6: The module docstring "Tale Operator Service" incorrectly
mentions "browser-use"; update the module-level docstring at the top of the file
(the existing triple-quoted string) to accurately describe the implementation by
replacing the reference to "browser-use" with "OpenCode CLI with Playwright MCP"
and briefly state that the service provides a REST API for web search and
browser task automation using OpenCode CLI and Playwright MCP.
In `@services/operator/app/config.py`:
- Around line 18-19: The current permissive default allowed_origins: str =
Field(default="*") should be made restrictive and documented: change the Field
default for allowed_origins to a non-wildcard value (e.g., empty string or a
specific localhost origin) instead of "*" and add a short comment/docstring next
to the allowed_origins Field instructing operators to set
OPERATOR_ALLOWED_ORIGINS in production to a comma-separated list of allowed
origins; reference the allowed_origins Field and the OPERATOR_ALLOWED_ORIGINS
env var so reviewers can find and verify the change.
- Around line 71-76: The current field validator validate_api_key (decorated
with `@field_validator` on "openai_api_key") runs during Settings() instantiation
at module import and will crash startup if OPENAI_API_KEY is missing; change
this by removing or disabling the `@field_validator` and instead implement an
explicit instance method (e.g., Settings.validate() or validate_api_key_lazy)
that checks self.openai_api_key and raises a clear ValueError only when called,
or make the validator conditional (skip validation when running in
startup/health-check mode) so Settings() can be created without failing—update
all call-sites to invoke the new validate() after startup initialization before
making outbound OpenAI requests.
In `@services/operator/app/main.py`:
- Around line 76-82: The CORS setup using CORSMiddleware reads
settings.allowed_origins (OPERATOR_ALLOWED_ORIGINS) and is currently effectively
permissive; before production deploy, set OPERATOR_ALLOWED_ORIGINS to a
comma-separated list of trusted origins (e.g.,
"https://example.com,https://app.example.com") in your Dockerfile / deployment
environment so that settings.allowed_origins produces a restrictive
allow_origins list instead of "*" (ensure the deployment config or env does not
leave the default wildcard value and that the settings parsing for
allowed_origins still strips and filters empty entries).
In `@services/operator/app/mcp/vision_server.py`:
- Around line 100-101: The code returns
result["choices"][0]["message"]["content"] directly after response.json() which
can raise KeyError/IndexError on unexpected API shapes and hide the root cause
under the broad except; change the handling in the function that calls
response.json() (variable result) to defensively validate the JSON shape before
accessing nested keys: ensure result is a dict, that result.get("choices") is a
non-empty list, that choices[0] is a dict with a "message" dict containing a
"content" string, and if any check fails log or raise a descriptive error
(including the whole result payload) instead of letting the generic except
swallow it; optionally consolidate into a small helper to extract the content so
callers use a validated value.
- Around line 55-56: The code is doing synchronous file I/O with open(path,
"rb") in an async context which blocks the event loop; replace that block by
using aiofiles: add aiofiles to dependencies, open the file with await
aiofiles.open(path, "rb") as f, read bytes with data = await f.read(), then set
image_data = base64.b64encode(data).decode("utf-8"); update the async function
that currently references path and image_data to use the awaited read and ensure
imports include import aiofiles.
In `@services/operator/app/models.py`:
- Around line 18-22: The ChatRequest model currently sets max_turns using
settings.max_steps which is evaluated at import time; change max_turns to be
optional (e.g., Optional[int] with default None) or use a Field default_factory
so the value isn't fixed at import, and then apply the runtime default in the
endpoint handler that receives ChatRequest (e.g., in the handler that calls/uses
ChatRequest) by setting chat_request.max_turns = chat_request.max_turns or
settings.max_steps (and validate bounds there if needed); refer to ChatRequest,
the max_turns Field, and settings.max_steps when making this change.
In `@services/operator/app/services/browser_service.py`:
- Around line 79-81: The __init__ method in the BrowserService class lacks a
return type annotation; update the method signature for __init__ to include the
explicit return type None (i.e. def __init__(self) -> None:) so it follows
Python typing conventions and tools expecting special-method annotations (refer
to the __init__ method and BrowserService class in browser_service.py).
- Around line 180-183: The current wait_for call uses settings.timeout *
max_turns which can produce very large timeouts; clamp or cap the computed
timeout before passing to asyncio.wait_for to a sensible upper bound (e.g.,
MAX_TIMEOUT_SECONDS constant) or compute timeout = min(settings.timeout *
max_turns, MAX_TIMEOUT_SECONDS) and use that value in the proc.communicate()
call; update browser_service.py where proc.communicate() is awaited (the
asyncio.wait_for(...) call referencing settings.timeout and max_turns) and add a
documented constant or configuration entry for the upper bound.
In `@services/operator/app/services/workspace_manager.py`:
- Around line 141-156: The _remove_workspace method deletes the workspace
directory, which prevents callers from measuring its size after deletion; update
the _remove_workspace docstring to state that callers (e.g.,
_enforce_disk_limit) must compute workspace size before invoking
_remove_workspace and that the method only removes files and returns True/False
on success, so size/accounting should be handled externally by callers like
_enforce_disk_limit or any caller performing disk accounting.
- Around line 229-243: The loop that reclaims space should compute and subtract
a workspace's disk usage before calling await
self._remove_workspace(workspace_id) and only update total_size_mb if
_remove_workspace returns success; move the Path(info.path) and
workspace.exists()/size calculation to before the await, compute size =
sum(...), then call removed = await self._remove_workspace(workspace_id) and if
removed do total_size_mb -= size / (1024*1024); this avoids checking a path
after deletion and prevents subtracting size when removal failed.
In `@services/operator/docker-entrypoint.sh`:
- Around line 17-20: Add validation in the entrypoint to ensure required
environment variables are set before proceeding: check that OPENAI_BASE_URL (or
VISION_BASE_URL after fallback), OPENAI_API_KEY (or VISION_API_KEY), and
OPENAI_MODEL (or VISION_MODEL) are non-empty and if any are missing log a clear
error via stderr and exit non‑zero. Locate the env setup lines referencing
VISION_BASE_URL, VISION_API_KEY, and VISION_MODEL and perform the checks
immediately after those assignments, reporting which specific variable is
missing to aid debugging and prevent silent failures at runtime.
- Around line 5-11: The current docker-entrypoint.sh uses a fixed sleep after
starting Xvfb (when OPERATOR_HEADLESS is "false"), which is brittle; replace the
sleep 2 with a readiness loop that polls for Xvfb to be responsive (for example
check for the X server socket /tmp/.X11-unix/X99 or run a lightweight probe like
xdpyinfo/xset against DISPLAY) until success or a configurable timeout, export
DISPLAY only after success, and log and exit non-zero if the timeout is reached
so the container fails fast; reference OPERATOR_HEADLESS, Xvfb and DISPLAY in
the loop and ensure the loop cleans up the background Xvfb process on failure.
- Around line 27-67: The heredoc in docker-entrypoint.sh directly interpolates
env vars (OPENAI_BASE_URL, OPENAI_MODEL, VISION_BASE_URL, VISION_API_KEY,
VISION_MODEL) into JSON, risking injection/malformed JSON; replace the raw
heredoc with a safe JSON construction using jq (e.g., jq -n with
--arg/--argjson) or another JSON-escaping mechanism to build the object for the
"provider.custom.options", model entries and "mcp.vision.environment" fields so
that special characters and newlines in those env vars are properly escaped
before writing ~/.config/opencode/opencode.json.
In `@services/operator/Dockerfile`:
- Around line 116-117: Remove the HEALTHCHECK directive from the Dockerfile (the
line beginning with "HEALTHCHECK --interval=30s --timeout=10s --start-period=60s
--retries=3 \ CMD curl -f http://localhost:8004/health || exit 1") and instead
add an equivalent healthcheck block to the docker-compose.yml under the operator
service: use a healthcheck: section with interval, timeout, start_period,
retries and the same test command (curl -f http://localhost:8004/health || exit
1) so orchestration-level config controls the health probe and the Dockerfile no
longer contains the HEALTHCHECK instruction.
- Around line 52-54: The RUN pipeline invoking curl piped to bash should enable
pipefail so a failing curl propagates; update the RUN that contains "curl -fsSL
https://deb.nodesource.com/setup_20.x | bash - \ && apt-get install -y nodejs
..." to run the pipeline under a shell with pipefail enabled (e.g., run the
curl|bash in the same shell after setting "set -o pipefail" or invoke bash -c to
enable pipefail) so that any curl failure causes the RUN to fail and stops the
build.
In `@services/operator/pyproject.toml`:
- Around line 6-14: The dependency bounds in the dependencies list are overly
restrictive and may exclude current stable patches and required compatibility
(e.g., fastapi, httpx, pydantic); update the upper bounds for fastapi, httpx,
pydantic and related packages (symbols: "fastapi", "httpx", "pydantic",
"pydantic-settings", "uvicorn", "loguru", "mcp") to next-major limits such as
fastapi>=0.115.0,<1.0.0 (or <0.129.0 if you prefer minor locking),
httpx>=0.27.0,<1.0.0 (or <0.29.0), and pydantic>=2.0.0,<3.0.0, ensure uvicorn,
loguru and mcp have similar next-major upper bounds, and before changing fastapi
to >=0.126.0 verify the codebase is migrated to Pydantic >=2.7.0 (remove any v1
usage) so runtime compatibility is preserved.
In `@services/platform/app/features/chat/hooks/use-message-processing.ts`:
- Line 50: Remove the accidental whitespace-only line in the file for the hook
use-message-processing.ts (the useMessageProcessing hook module); open
services/platform/app/features/chat/hooks/use-message-processing.ts and delete
the blank/whitespace-only line at line 50 so there are no stray spaces-only
lines in the file, then save and run lint/format to ensure no trailing
whitespace remains.
In `@services/platform/convex/agent_tools/files/txt_tool.ts`:
- Line 108: The current line counting uses content.split('\n') which miscounts
when content is "" and doesn't handle CRLF/CR; change the logic that sets
lineCount to normalize line endings and treat an empty string as zero lines
(e.g., replace CRLF/CR with LF then if content === "" return 0 else split on
/\r\n|\r|\n/); update the variable where lineCount is assigned (the existing
lineCount variable computed from content.split('\n')) so it uses this robust
approach.
- Around line 103-125: The current handler in txt_tool.ts treats
ctx.storage.getUrl(fileId) possibly returning null as success; update the block
after calling ctx.storage.getUrl to check for null and fail loudly: if url is
null, call debugLog('tool:txt generate failed', {...filename, fileId}) and
either throw an Error referencing fileId and filename or return an object with
operation: 'generate', success: false and an explanatory message (matching the
failure pattern used in generate_pptx.ts and generate_docx_from_template.ts);
ensure downstream fields (url, fileId, filename, char_count, line_count) are
only returned on success and that ctx.storage.getUrl and fileId are the
referenced symbols to locate the code to change.
In `@services/platform/convex/agent_tools/sub_agents/document_assistant_tool.ts`:
- Around line 112-120: The durationSeconds assignment can produce NaN if
result.durationMs is undefined; update the return in document_assistant_tool.ts
where successResponse is called to guard or default the value (e.g., compute
durationSeconds as (typeof result.durationMs === 'number' ? result.durationMs /
1000 : 0) or omit the field when undefined) so that successResponse receives a
valid number; adjust the object spread around result.usage and the
durationSeconds property accordingly to ensure no NaN is passed.
In
`@services/platform/convex/agent_tools/sub_agents/integration_assistant_tool.ts`:
- Around line 159-162: The code spreads result.usage and computes
durationSeconds as result.durationMs / 1000 which can yield NaN if
result.durationMs is undefined or null; update the construction (around the
usage object where result.usage is spread) to guard the value — compute
durationSeconds only when result.durationMs is a finite number (e.g., use a
typeof/Number.isFinite check) and otherwise set durationSeconds to undefined or
0 as appropriate for downstream consumers so the response never contains NaN.
In `@services/platform/convex/agent_tools/sub_agents/web_assistant_tool.ts`:
- Around line 101-102: The code currently hardcodes the strings 'opencode' and
'operator' — extract them into named constants (e.g., MODEL_OPENCODE and
PROVIDER_OPERATOR) at the top of web_assistant_tool.ts and replace the inline
literals in the array/list with those constants; additionally update the logic
that constructs/validates model/provider selections (methods referencing these
values) to allow deriving the model/provider from the Operator API response when
available (fall back to the constants if the response has no model/provider
field).
In `@services/platform/convex/conversations/create_conversation_public.ts`:
- Around line 31-34: The cast "(args.metadata || {}) as any" will trigger
no-explicit-any; instead, update the metadata typing: either add a concrete
Metadata type (e.g., Metadata or Record<string, unknown>) to the
CreateConversationArgs/args definition and cast to that type when assigning the
metadata property, or if a deliberate escape is required, use a targeted eslint
suppression (// eslint-disable-next-line `@typescript-eslint/no-explicit-any`)
right above the metadata assignment. Locate the metadata assignment in
create_conversation_public.ts (the metadata: (args.metadata || {}) as any line)
and replace the any cast with a specific type or add the suppression, and update
any related function/type declarations (CreateConversationArgs,
createConversationPublic handler) to reflect the chosen Metadata type.
In `@services/platform/convex/conversations/create_conversation.ts`:
- Around line 24-25: Remove the stray blank line immediately above the object
property "metadata: args.metadata as any" in the create_conversation code so the
property sits directly with the surrounding object properties (keep the existing
as any cast unchanged); locate the object literal where "metadata" is set
(within the createConversation/create_conversation handler) and delete the empty
line to restore consistent formatting.
In `@services/platform/convex/customers/create_customer_public.ts`:
- Around line 63-64: There is a stray blank line before the return in
create_customer_public that was left after removing an ESLint directive; delete
the empty line immediately above the statement that calls
ctx.db.insert('customers', { ...args, metadata: args.metadata as any }) so the
return line is contiguous with the surrounding code while keeping the existing
metadata: args.metadata as any cast intact.
In `@services/platform/convex/customers/update_customer_metadata.ts`:
- Around line 66-69: The patch call currently uses an unnecessary `as any` cast
on `updatedMetadata` which will trigger `@typescript-eslint/no-explicit-any`;
update the `await ctx.db.patch(customerId, { metadata: updatedMetadata as any
})` usage by removing the `as any` cast so it becomes `metadata:
updatedMetadata` if the Convex schema accepts `Record<string, unknown>`, or if
the Convex schema truly requires `any` reintroduce the ESLint disable comment
(or narrow the cast to `as Record<string, unknown>`) to suppress the lint rule;
locate this in the `update_customer_metadata` logic where `updatedMetadata` is
constructed and adjust accordingly.
In `@services/platform/convex/documents/create_document.ts`:
- Around line 24-25: Remove the leftover whitespace on the metadata line and
replace the broad any cast with a more specific type: cast args.metadata to
Record<string, unknown> instead of any; update the assignment in create_document
(the metadata: args.metadata line) to use that explicit cast so metadata
maintains better type safety and explicitness.
In `@services/platform/convex/documents/update_document.ts`:
- Line 94: Replace the blanket "as any" cast on updateData when calling
ctx.db.patch(args.documentId) with a type-safe approach: remove the global cast
and either (A) cast only the metadata field when needed (e.g., spread updateData
and include metadata: updateData.metadata as any only if metadata is present) or
(B) create a targeted patch type alias (similar to StepDefPatch) that matches
the Convex patch shape and use that for updateData; update the call site in
update_document.ts so ctx.db.patch(args.documentId, ...) uses the
narrowed/aliased type instead of updateData as any.
In `@services/platform/convex/email_providers/create_provider_internal.ts`:
- Around line 89-90: The line setting the metadata property uses an unsafe cast
"args.metadata as any" which will trigger the no-explicit-any lint rule; change
the cast to a safer type by replacing the explicit any with an explicit
Record<string, unknown> cast at the usage site (i.e., set metadata from
args.metadata cast to Record<string, unknown>) so the metadata property uses a
typed shape instead of any; update the assignment where metadata: args.metadata
as any is used to metadata: args.metadata as Record<string, unknown>.
In `@services/platform/convex/email_providers/internal_mutations.ts`:
- Around line 55-58: The patch call is using a blanket `as any` on
`updatedMetadata` which loses type safety; instead define and use a narrow
concrete type (e.g., type Primitive = string|number|boolean|null; type Metadata
= Record<string, Primitive>) and cast `updatedMetadata` to that narrow type
before calling ctx.db.patch(args.providerId, { metadata: ... }); ensure the cast
is applied only at the call site (or validate/transform the object to conform)
so nested optional fields remain typed and aligned with the schema rather than
using `as any`.
In `@services/platform/convex/integrations/create_integration_logic.ts`:
- Around line 90-91: There is a stray blank line interrupting the inline comment
about the accepted "as any" cast; edit the integrationId assignment block (the
await (ctx.runMutation as any)(...) call that assigns integrationId) to remove
the blank line so the explanatory comment directly precedes the line with the
cast, keeping the existing "as any" cast and comment intact for clarity.
In `@services/platform/convex/lib/context_management/check_and_summarize.ts`:
- Around line 36-41: The CheckAndSummarizeResult interface currently documents
summarizationTriggered but the implementation always returns false; update the
implementation that builds and returns the CheckAndSummarizeResult to set
summarizationTriggered to the real computed boolean (the needsSummarization
result derived from usageRatio >= SUMMARIZATION_THRESHOLD) or change the
interface to match returning behavior; specifically locate where the
estimate/needsSummarization is computed (the variable/prop named
needsSummarization or estimate.usageRatio) and assign that value to
summarizationTriggered in the returned object from the function that returns
CheckAndSummarizeResult (or remove/rename the field if you prefer the alternate
contract).
In `@services/platform/convex/products/create_product_public.ts`:
- Around line 41-43: The code casts args.metadata with an any assertion
(args.metadata as any) which will violate `@typescript-eslint/no-explicit-any` if
the rule remains enabled; either tighten the type for args.metadata in the input
type/interface so you can use the actual type here (remove the cast) or replace
the broad any with a narrower type (e.g., unknown then validate/convert to the
expected shape) or add a single-line, targeted eslint suppression (//
eslint-disable-next-line `@typescript-eslint/no-explicit-any`) immediately above
the metadata assignment if changing types isn’t feasible; update the assignment
of metadata and any related parameter/interface (args and its metadata property)
accordingly.
In `@services/platform/convex/products/update_product.ts`:
- Around line 49-50: The line setting updates.metadata uses a blanket cast "as
any" which violates no-explicit-any; replace it by casting or typing with the
schema-derived metadata type instead: remove "as any" and use the exported
metadata type from your product schema (e.g., ProductMetadata or
ProductUpdateMetadata) when assigning args.metadata to updates.metadata, adding
the appropriate type import and/or adjusting the updates variable's type so
updates.metadata: ProductMetadata (or the matching schema type) is used instead
of any.
In `@services/platform/convex/products/upsert_product_translation.ts`:
- Around line 49-50: Define a concrete type matching your Convex schema (e.g.,
ProductTranslation with fields language, name?, description?, category?, tags?,
metadata?, lastUpdated, createdAt), change the translations array to be typed as
ProductTranslation[], and construct newTranslation objects to conform to that
type instead of using "as any"; replace the two "as any" casts on
translations[existingIndex] and any push/assign sites with properly-typed
ProductTranslation objects so TypeScript enforces the schema shape in
upsert_product_translation and related functions.
- Around line 41-42: Remove the unsafe cast "as any" on metadata: stop widening
args.metadata in the newTranslation object and preserve its typed form (leave
metadata: args.metadata). If Convex expects a concrete type, update the
UpsertProductTranslationArgs interface to the correct metadata shape instead of
keeping it as unknown, then assign that typed property into newTranslation;
locate the metadata assignment in the newTranslation construction in
upsert_product_translation.ts and adjust either the assignment or the
UpsertProductTranslationArgs type accordingly.
In `@services/platform/convex/tone_of_voice/add_example_message.ts`:
- Around line 43-44: Replace the unsafe any cast on metadata with the
project-standard type: change the cast of args.metadata (the metadata property
in the add_example_message code) from any to Record<string, unknown> | undefined
so optional metadata keeps correct typing; update the metadata assignment
(metadata: args.metadata as ...) to use this type instead to match other Convex
modules.
In
`@services/platform/convex/workflow_engine/helpers/engine/execute_workflow_start.ts`:
- Around line 137-138: Remove the stray blank line following the onComplete
assignment so the block is contiguous; specifically edit the area around
"onComplete: internal.workflow_engine.engine.onWorkflowComplete as any" to
delete the empty line left after removing the ESLint directive and ensure the
existing inline comment explaining the "as any" cast remains directly adjacent
to the code.
In
`@services/platform/convex/workflow_engine/helpers/step_execution/execute_step_by_type.ts`:
- Around line 60-61: Remove the orphaned blank line immediately above the line
containing "config: stepDef.config as any" in execute_step_by_type.ts; simply
delete that extra empty line so the configuration property is directly
contiguous with surrounding code and formatting remains consistent around the
stepDef.config assignment.
Replace browser-use with Playwright MCP Server for browser automation. Uses Agent LLM for decisions and Vision LLM for content extraction. - Add MCP client for Playwright browser control - Add Agent client with function calling for browser decisions - Add Vision client for screenshot content extraction - Add /api/v1/answer endpoint for Perplexity-like responses - Add /api/v1/search endpoint for structured search results - Configure Docker with non-root user and Chrome sandbox
- Increase max_steps from 10 to 30 for more complex tasks - Remove language parameter from search URL (let Google auto-detect) - Remove /search and /search-with-fallback endpoints - Keep only /answer and /task endpoints for cleaner API - Add synthesize_task_result to return user-friendly results - Enhance agent prompt with chatbot interaction and form filling - Remove SearchRequest, SearchResult, RichData models
Replace custom agent implementation with Claude Code CLI: - Add Claude Code CLI and LiteLLM to container - Configure LiteLLM to translate Anthropic API to OpenAI format - Configure Claude Code with Playwright MCP for browser automation - Refactor BrowserService to use Claude Code --print mode - Remove custom agent_client, mcp_client, vision_client modules - Update entrypoint to start LiteLLM proxy and configure Claude Code This provides a more robust and battle-tested agent loop while maintaining flexibility to use any OpenAI-compatible model.
- Replace Claude Code CLI with OpenCode CLI (open-source, no legal risk) - Remove LiteLLM proxy (OpenCode natively supports OpenAI-compatible APIs) - Add JSON output parsing for token usage and cost tracking - Add duration_seconds and token_usage fields to API response - Add Vision MCP server for image analysis capabilities - Configure OpenCode with custom provider at runtime via entrypoint
- Rename operator service to browser-agent for clearer naming - Update environment variable prefix from OPERATOR_ to BROWSER_AGENT_ - Update all internal references (main.py, config.py, Dockerfile, etc.) - Remove search service (SearXNG) - browser-agent handles web search - Update compose.yml with new service name and remove search dependency
Rename the browser-agent service to web for clarity and consistency.
- Prevent sub-agent spawning to keep tasks in current session - Require precise detail page URLs for all recommendations - Ensure complete research before responding
Rename services/web to services/operator for future extensibility. - Rename directory: services/web/ -> services/operator/ - Update package name: tale-web -> tale-operator - Update env var prefix: WEB_* -> OPERATOR_* - Update Docker image: tale-web -> tale-operator - Update compose.yml service name and paths
Replace sub-agent architecture with direct HTTP calls to the Operator browser automation service. The web assistant tool now uses Playwright via the Operator service for intelligent web interactions. Changes: - Refactor web_assistant_tool to call Operator HTTP API - Add URL source extraction from browser tool results - Add OPERATOR_URL environment variable configuration - Remove eslint-disable comments across platform codebase
…ult handling Remove the thread summarization system that was adding complexity without clear benefits. Context is now managed through message limits and token budgets alone. Also increase tool result truncation limit from 200 to 8000 chars to preserve meaningful content from sub-agents. Changes: - Remove contextSummary from hooks, context building, and response flow - Remove auto-summarization triggers from chat completion and error handling - Deprecate hasSummary field (kept optional for backward compatibility) - Update agent prompt to emphasize presenting tool results to users - Add debug logging for URL extraction in operator service
- Add Perplexica AI search engine service to docker compose - Add workspace manager to operator for concurrent request isolation - Refactor agent parameter naming: taskDescription → promptMessage - Improve context separation: use system for thread context, prompt for user message - Add sources field to operator/tool responses for web assistant - Add pendingToolResponse state for better UI loading indicators - Add copy button to context window dialog - Remove streaming empty text retry logic (simplified flow)
…tion - Add generate operation to txt tool for creating text files from content - Display sub-agent execution duration in message info dialog - Pass durationSeconds from all sub-agent tools (crm, document, integration, workflow) - Simplify context management by removing auto-summarization scheduling
Add null check after calling ctx.storage.getUrl() and throw an error if URL is unavailable instead of returning success with empty URL.
Update module docstring to mention OpenCode CLI with Playwright MCP instead of browser-use.
Add undefined check before dividing durationMs by 1000 to prevent NaN values when durationMs is undefined.
Remove whitespace artifacts left from ESLint directive removal.
7c4a7a5 to
ffddd02
Compare
CORS is browser-enforced and has no effect on server-to-server calls. Since the operator service is only called by backend services, CORS configuration is unnecessary.
Summary
Changes
New Operator Service
Platform Enhancements
Refactoring
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Infrastructure Updates
Code Quality